|
| 1 | +package bootstrap |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "io" |
| 8 | + "os" |
| 9 | + "os/exec" |
| 10 | + "path/filepath" |
| 11 | + "strings" |
| 12 | + |
| 13 | + "github.com/voocel/agentcore" |
| 14 | + agentctx "github.com/voocel/agentcore/context" |
| 15 | + "github.com/voocel/codebot/internal/agent" |
| 16 | + "github.com/voocel/codebot/internal/config" |
| 17 | + "github.com/voocel/codebot/internal/storage" |
| 18 | + localtools "github.com/voocel/codebot/internal/tools" |
| 19 | +) |
| 20 | + |
| 21 | +func assembleRuntime(input *resolvedInput, services *bootServices, assembly *sessionAssembly) (*Runtime, error) { |
| 22 | + taskRT := agentcore.NewTaskRuntime() |
| 23 | + taskTools := localtools.NewTaskTools(services.taskStore, taskRT, assembly.hookRunner) |
| 24 | + tools := make([]agentcore.Tool, 0, len(assembly.tools)+len(taskTools)) |
| 25 | + tools = append(tools, assembly.tools...) |
| 26 | + tools = append(tools, taskTools...) |
| 27 | + baseTools := make([]agentcore.Tool, 0, len(assembly.baseTools)+len(taskTools)) |
| 28 | + baseTools = append(baseTools, assembly.baseTools...) |
| 29 | + baseTools = append(baseTools, taskTools...) |
| 30 | + |
| 31 | + contextEngine, summaryCompact := buildContextEngine(assembly.chatModel, assembly.settings.ContextWindow) |
| 32 | + agentCore, err := buildAgent(assembly, services, contextEngine, taskRT, tools) |
| 33 | + if err != nil { |
| 34 | + return nil, err |
| 35 | + } |
| 36 | + |
| 37 | + if err := restoreAgentState(input, assembly, tools, agentCore); err != nil { |
| 38 | + return nil, err |
| 39 | + } |
| 40 | + |
| 41 | + session := buildSession(input, services, assembly, contextEngine, agentCore, tools) |
| 42 | + wireSessionRuntime(input, assembly, services, session, baseTools, tools, agentCore, taskRT, contextEngine, summaryCompact) |
| 43 | + |
| 44 | + return &Runtime{ |
| 45 | + Cwd: input.cwd, |
| 46 | + GitBranch: detectGitBranch(input.cwd), |
| 47 | + ApprovalEngine: services.approvalEngine, |
| 48 | + TaskRuntime: taskRT, |
| 49 | + Settings: assembly.settings, |
| 50 | + Session: session, |
| 51 | + SessionStore: input.sessionStore, |
| 52 | + PluginCatalog: services.pluginCatalog, |
| 53 | + SkillCatalog: services.skillCatalog, |
| 54 | + MCPManager: services.mcpManager, |
| 55 | + MCPServers: services.mcpServers, |
| 56 | + HookRunner: assembly.hookRunner, |
| 57 | + EnvHint: input.envHint, |
| 58 | + PlanSlug: input.sessionSnapshot.PlanSlug, |
| 59 | + PlanTitle: input.sessionSnapshot.PlanTitle, |
| 60 | + PlanPhase: input.sessionSnapshot.PlanPhase, |
| 61 | + PlanPreMode: input.sessionSnapshot.PlanPreMode, |
| 62 | + PlanAllowedCommands: append([]storage.AllowedCommandEntry(nil), input.sessionSnapshot.PlanAllowedCommands...), |
| 63 | + }, nil |
| 64 | +} |
| 65 | + |
| 66 | +func buildContextEngine(chatModel agentcore.ChatModel, contextWindow int) (*agentctx.ContextEngine, *agentctx.FullSummaryStrategy) { |
| 67 | + toolCompact := agentctx.NewToolResultMicrocompact(agentctx.ToolResultMicrocompactConfig{ |
| 68 | + Classifier: agent.CodebotToolClassifier, |
| 69 | + KeepRecent: 5, |
| 70 | + }) |
| 71 | + trimCompact := agentctx.NewLightTrim(agentctx.LightTrimConfig{}) |
| 72 | + summaryCompact := agentctx.NewFullSummary(agentctx.FullSummaryConfig{ |
| 73 | + Model: chatModel, |
| 74 | + }) |
| 75 | + engine := agentctx.NewEngine(agentctx.EngineConfig{ |
| 76 | + ContextWindow: contextWindow, |
| 77 | + Strategies: []agentctx.Strategy{ |
| 78 | + toolCompact, |
| 79 | + trimCompact, |
| 80 | + summaryCompact, |
| 81 | + }, |
| 82 | + }) |
| 83 | + return engine, summaryCompact |
| 84 | +} |
| 85 | + |
| 86 | +func buildAgent(assembly *sessionAssembly, services *bootServices, contextEngine agentcore.ContextManager, taskRT *agentcore.TaskRuntime, tools []agentcore.Tool) (*agentcore.Agent, error) { |
| 87 | + opts := []agentcore.AgentOption{ |
| 88 | + agentcore.WithModel(assembly.chatModel), |
| 89 | + agentcore.WithSystemBlocks(assembly.systemBlocks), |
| 90 | + agentcore.WithTools(tools...), |
| 91 | + agentcore.WithMaxTurns(assembly.settings.MaxTurns), |
| 92 | + agentcore.WithMaxToolErrors(3), |
| 93 | + agentcore.WithMaxToolConcurrency(4), |
| 94 | + agentcore.WithContextManager(contextEngine), |
| 95 | + agentcore.WithConvertToLLM(agentctx.ContextConvertToLLM), |
| 96 | + agentcore.WithContextWindow(assembly.settings.ContextWindow), |
| 97 | + agentcore.WithContextEstimate(agentctx.ContextEstimateAdapter), |
| 98 | + agentcore.WithPermissionEngine(services.approvalEngine), |
| 99 | + agentcore.WithTaskRuntime(taskRT), |
| 100 | + } |
| 101 | + if assembly.hookMiddleware != nil { |
| 102 | + opts = append(opts, agentcore.WithMiddlewares(assembly.hookMiddleware)) |
| 103 | + } |
| 104 | + return agentcore.NewAgent(opts...), nil |
| 105 | +} |
| 106 | + |
| 107 | +func restoreAgentState(input *resolvedInput, assembly *sessionAssembly, tools []agentcore.Tool, ag *agentcore.Agent) error { |
| 108 | + if len(input.sessionSnapshot.Messages) > 0 { |
| 109 | + if err := ag.SetMessages(input.sessionSnapshot.Messages); err != nil { |
| 110 | + return fmt.Errorf("restore agent messages: %w", err) |
| 111 | + } |
| 112 | + agentcore.ReactivateDeferred(tools, input.sessionSnapshot.Messages) |
| 113 | + } |
| 114 | + if input.sessionSnapshot.Thinking != "" { |
| 115 | + ag.SetThinkingLevel(agentcore.ThinkingLevel(input.sessionSnapshot.Thinking)) |
| 116 | + assembly.settings.ThinkingLevel = input.sessionSnapshot.Thinking |
| 117 | + } else if assembly.settings.ThinkingLevel != "" { |
| 118 | + ag.SetThinkingLevel(agentcore.ThinkingLevel(assembly.settings.ThinkingLevel)) |
| 119 | + } |
| 120 | + return nil |
| 121 | +} |
| 122 | + |
| 123 | +func buildSession(input *resolvedInput, services *bootServices, assembly *sessionAssembly, contextEngine agentcore.ContextManager, ag *agentcore.Agent, tools []agentcore.Tool) *agent.Session { |
| 124 | + return agent.NewSession(agent.SessionConfig{ |
| 125 | + Agent: ag, |
| 126 | + ContextManager: contextEngine, |
| 127 | + Store: input.sessionStore, |
| 128 | + Manager: input.sessionManager, |
| 129 | + Registry: input.registry, |
| 130 | + Settings: assembly.settings, |
| 131 | + Cwd: input.cwd, |
| 132 | + CreateModel: input.modelFactory, |
| 133 | + ChatModel: assembly.chatModel, |
| 134 | + Tools: tools, |
| 135 | + TaskStore: services.taskStore, |
| 136 | + ContextFiles: assembly.contextFiles, |
| 137 | + Skills: services.skills, |
| 138 | + SkillCatalog: services.skillCatalog, |
| 139 | + SkillUsage: services.skillUsage, |
| 140 | + HookRunner: assembly.hookRunner, |
| 141 | + DeferredToolsPreamble: assembly.deferredToolsPreamble, |
| 142 | + Reminders: assembly.reminders, |
| 143 | + PreambleInjected: len(input.sessionSnapshot.Messages) > 0, |
| 144 | + SkillAllowsSetter: services.approvalEngine.SetSkillAllows, |
| 145 | + }) |
| 146 | +} |
| 147 | + |
| 148 | +func wireSessionRuntime(input *resolvedInput, assembly *sessionAssembly, services *bootServices, session *agent.Session, baseTools, tools []agentcore.Tool, ag *agentcore.Agent, taskRT *agentcore.TaskRuntime, contextEngine *agentctx.ContextEngine, summaryCompact *agentctx.FullSummaryStrategy) { |
| 149 | + summaryCompact.SetPostSummaryHooks(session.PostSummaryRecoveryHook()) |
| 150 | + contextEngine.SetProjectHook(session.HandleProjectedRewrite) |
| 151 | + contextEngine.SetRecoverHook(session.HandleOverflowRewrite) |
| 152 | + |
| 153 | + for _, tool := range tools { |
| 154 | + if st, ok := tool.(*localtools.SkillTool); ok { |
| 155 | + st.SetInvocationApplier(session.ApplySkillInvocation) |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + assembly.subagentTool.SetTaskRuntime(taskRT) |
| 160 | + assembly.subagentTool.SetNotifyFn(ag.FollowUp) |
| 161 | + |
| 162 | + sessionID := input.sessionStore.Header().SessionID |
| 163 | + bgDir := filepath.Join(config.SessionsDir(input.cwd), sessionID, "bg") |
| 164 | + assembly.subagentTool.SetBgOutputFactory(func(taskID, agentName string) (io.WriteCloser, string, error) { |
| 165 | + dir := filepath.Join(bgDir, taskID) |
| 166 | + if err := os.MkdirAll(dir, 0o700); err != nil { |
| 167 | + return nil, "", err |
| 168 | + } |
| 169 | + path := filepath.Join(dir, "output.jsonl") |
| 170 | + f, err := os.Create(path) |
| 171 | + if err != nil { |
| 172 | + return nil, "", err |
| 173 | + } |
| 174 | + meta, _ := json.Marshal(map[string]string{"agent": agentName}) |
| 175 | + _ = os.WriteFile(filepath.Join(dir, "meta.json"), meta, 0o600) |
| 176 | + return f, path, nil |
| 177 | + }) |
| 178 | + |
| 179 | + if assembly.bashTool != nil { |
| 180 | + assembly.bashTool.SetTaskRuntime(taskRT) |
| 181 | + assembly.bashTool.SetNotifyFn(ag.FollowUp) |
| 182 | + assembly.bashTool.SetBgOutputFactory(func(shellID string) (io.WriteCloser, string, error) { |
| 183 | + dir := filepath.Join(bgDir, shellID) |
| 184 | + if err := os.MkdirAll(dir, 0o700); err != nil { |
| 185 | + return nil, "", err |
| 186 | + } |
| 187 | + path := filepath.Join(dir, "output.log") |
| 188 | + f, err := os.Create(path) |
| 189 | + return f, path, err |
| 190 | + }) |
| 191 | + } |
| 192 | + |
| 193 | + if services.mcpManager != nil { |
| 194 | + session.SetBeforePrompt(func() { |
| 195 | + mcpTools, ok := services.mcpManager.RefreshIfDirty(context.Background()) |
| 196 | + if !ok { |
| 197 | + return |
| 198 | + } |
| 199 | + all := make([]agentcore.Tool, len(baseTools), len(baseTools)+len(mcpTools)) |
| 200 | + copy(all, baseTools) |
| 201 | + all = append(all, mcpTools...) |
| 202 | + session.ReplaceAllTools(all) |
| 203 | + }) |
| 204 | + } |
| 205 | + |
| 206 | + if assembly.hookRunner != nil { |
| 207 | + assembly.hookRunner.RunSessionStart(context.Background()) |
| 208 | + } |
| 209 | +} |
| 210 | + |
| 211 | +func detectGitBranch(cwd string) string { |
| 212 | + cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") |
| 213 | + cmd.Dir = cwd |
| 214 | + out, err := cmd.Output() |
| 215 | + if err != nil { |
| 216 | + return "" |
| 217 | + } |
| 218 | + return strings.TrimSpace(string(out)) |
| 219 | +} |
0 commit comments