@chances-ai/engine 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/discover.d.ts +30 -0
- package/dist/agents/discover.d.ts.map +1 -0
- package/dist/agents/discover.js +183 -0
- package/dist/agents/discover.js.map +1 -0
- package/dist/agents/index.d.ts +20 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +52 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/parse.d.ts +61 -0
- package/dist/agents/parse.d.ts.map +1 -0
- package/dist/agents/parse.js +527 -0
- package/dist/agents/parse.js.map +1 -0
- package/dist/agents/types.d.ts +52 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +8 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/ai/adapters/ai-sdk-stream.d.ts +19 -0
- package/dist/ai/adapters/ai-sdk-stream.d.ts.map +1 -0
- package/dist/ai/adapters/ai-sdk-stream.js +125 -0
- package/dist/ai/adapters/ai-sdk-stream.js.map +1 -0
- package/dist/ai/adapters/ai-sdk.d.ts +56 -0
- package/dist/ai/adapters/ai-sdk.d.ts.map +1 -0
- package/dist/ai/adapters/ai-sdk.js +112 -0
- package/dist/ai/adapters/ai-sdk.js.map +1 -0
- package/dist/ai/adapters/mock.d.ts +13 -0
- package/dist/ai/adapters/mock.d.ts.map +1 -0
- package/dist/ai/adapters/mock.js +54 -0
- package/dist/ai/adapters/mock.js.map +1 -0
- package/dist/ai/adapters/openai-compatible.d.ts +23 -0
- package/dist/ai/adapters/openai-compatible.d.ts.map +1 -0
- package/dist/ai/adapters/openai-compatible.js +45 -0
- package/dist/ai/adapters/openai-compatible.js.map +1 -0
- package/dist/ai/cost.d.ts +3 -0
- package/dist/ai/cost.d.ts.map +1 -0
- package/dist/ai/cost.js +5 -0
- package/dist/ai/cost.js.map +1 -0
- package/dist/ai/index.d.ts +12 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +11 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/known-models.d.ts +20 -0
- package/dist/ai/known-models.d.ts.map +1 -0
- package/dist/ai/known-models.js +129 -0
- package/dist/ai/known-models.js.map +1 -0
- package/dist/ai/registry.d.ts +12 -0
- package/dist/ai/registry.d.ts.map +1 -0
- package/dist/ai/registry.js +24 -0
- package/dist/ai/registry.js.map +1 -0
- package/dist/ai/retry.d.ts +11 -0
- package/dist/ai/retry.d.ts.map +1 -0
- package/dist/ai/retry.js +14 -0
- package/dist/ai/retry.js.map +1 -0
- package/dist/ai/router.d.ts +25 -0
- package/dist/ai/router.d.ts.map +1 -0
- package/dist/ai/router.js +36 -0
- package/dist/ai/router.js.map +1 -0
- package/dist/ai/setup.d.ts +23 -0
- package/dist/ai/setup.d.ts.map +1 -0
- package/dist/ai/setup.js +47 -0
- package/dist/ai/setup.js.map +1 -0
- package/dist/ai/summarizer.d.ts +24 -0
- package/dist/ai/summarizer.d.ts.map +1 -0
- package/dist/ai/summarizer.js +56 -0
- package/dist/ai/summarizer.js.map +1 -0
- package/dist/ai/types.d.ts +83 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +2 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/core/compaction/circuit-breaker.d.ts +32 -0
- package/dist/core/compaction/circuit-breaker.d.ts.map +1 -0
- package/dist/core/compaction/circuit-breaker.js +42 -0
- package/dist/core/compaction/circuit-breaker.js.map +1 -0
- package/dist/core/compaction/compactor.d.ts +75 -0
- package/dist/core/compaction/compactor.d.ts.map +1 -0
- package/dist/core/compaction/compactor.js +261 -0
- package/dist/core/compaction/compactor.js.map +1 -0
- package/dist/core/compaction/estimate.d.ts +39 -0
- package/dist/core/compaction/estimate.d.ts.map +1 -0
- package/dist/core/compaction/estimate.js +74 -0
- package/dist/core/compaction/estimate.js.map +1 -0
- package/dist/core/compaction/index.d.ts +5 -0
- package/dist/core/compaction/index.d.ts.map +1 -0
- package/dist/core/compaction/index.js +5 -0
- package/dist/core/compaction/index.js.map +1 -0
- package/dist/core/compaction/prune.d.ts +43 -0
- package/dist/core/compaction/prune.d.ts.map +1 -0
- package/dist/core/compaction/prune.js +51 -0
- package/dist/core/compaction/prune.js.map +1 -0
- package/dist/core/engine.d.ts +268 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +767 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/task-tool.d.ts +175 -0
- package/dist/core/task-tool.d.ts.map +1 -0
- package/dist/core/task-tool.js +901 -0
- package/dist/core/task-tool.js.map +1 -0
- package/dist/core/workspace-query.d.ts +83 -0
- package/dist/core/workspace-query.d.ts.map +1 -0
- package/dist/core/workspace-query.js +217 -0
- package/dist/core/workspace-query.js.map +1 -0
- package/dist/core/worktree/active-marker.d.ts +31 -0
- package/dist/core/worktree/active-marker.d.ts.map +1 -0
- package/dist/core/worktree/active-marker.js +109 -0
- package/dist/core/worktree/active-marker.js.map +1 -0
- package/dist/core/worktree/create.d.ts +40 -0
- package/dist/core/worktree/create.d.ts.map +1 -0
- package/dist/core/worktree/create.js +121 -0
- package/dist/core/worktree/create.js.map +1 -0
- package/dist/core/worktree/errors.d.ts +7 -0
- package/dist/core/worktree/errors.d.ts.map +1 -0
- package/dist/core/worktree/errors.js +11 -0
- package/dist/core/worktree/errors.js.map +1 -0
- package/dist/core/worktree/gc.d.ts +39 -0
- package/dist/core/worktree/gc.d.ts.map +1 -0
- package/dist/core/worktree/gc.js +146 -0
- package/dist/core/worktree/gc.js.map +1 -0
- package/dist/core/worktree/git.d.ts +53 -0
- package/dist/core/worktree/git.d.ts.map +1 -0
- package/dist/core/worktree/git.js +166 -0
- package/dist/core/worktree/git.js.map +1 -0
- package/dist/core/worktree/index.d.ts +8 -0
- package/dist/core/worktree/index.d.ts.map +1 -0
- package/dist/core/worktree/index.js +8 -0
- package/dist/core/worktree/index.js.map +1 -0
- package/dist/core/worktree/paths.d.ts +26 -0
- package/dist/core/worktree/paths.d.ts.map +1 -0
- package/dist/core/worktree/paths.js +57 -0
- package/dist/core/worktree/paths.js.map +1 -0
- package/dist/core/worktree/slug.d.ts +6 -0
- package/dist/core/worktree/slug.d.ts.map +1 -0
- package/dist/core/worktree/slug.js +21 -0
- package/dist/core/worktree/slug.js.map +1 -0
- package/dist/local-vault/file-store.d.ts +64 -0
- package/dist/local-vault/file-store.d.ts.map +1 -0
- package/dist/local-vault/file-store.js +225 -0
- package/dist/local-vault/file-store.js.map +1 -0
- package/dist/local-vault/index.d.ts +57 -0
- package/dist/local-vault/index.d.ts.map +1 -0
- package/dist/local-vault/index.js +68 -0
- package/dist/local-vault/index.js.map +1 -0
- package/dist/local-vault/keychain.d.ts +58 -0
- package/dist/local-vault/keychain.d.ts.map +1 -0
- package/dist/local-vault/keychain.js +200 -0
- package/dist/local-vault/keychain.js.map +1 -0
- package/dist/local-vault/mutex.d.ts +20 -0
- package/dist/local-vault/mutex.d.ts.map +1 -0
- package/dist/local-vault/mutex.js +44 -0
- package/dist/local-vault/mutex.js.map +1 -0
- package/dist/local-vault/passphrase.d.ts +30 -0
- package/dist/local-vault/passphrase.d.ts.map +1 -0
- package/dist/local-vault/passphrase.js +72 -0
- package/dist/local-vault/passphrase.js.map +1 -0
- package/dist/lsp/config.d.ts +34 -0
- package/dist/lsp/config.d.ts.map +1 -0
- package/dist/lsp/config.js +68 -0
- package/dist/lsp/config.js.map +1 -0
- package/dist/lsp/detect.d.ts +7 -0
- package/dist/lsp/detect.d.ts.map +1 -0
- package/dist/lsp/detect.js +78 -0
- package/dist/lsp/detect.js.map +1 -0
- package/dist/lsp/errors.d.ts +11 -0
- package/dist/lsp/errors.d.ts.map +1 -0
- package/dist/lsp/errors.js +11 -0
- package/dist/lsp/errors.js.map +1 -0
- package/dist/lsp/formatters.d.ts +147 -0
- package/dist/lsp/formatters.d.ts.map +1 -0
- package/dist/lsp/formatters.js +259 -0
- package/dist/lsp/formatters.js.map +1 -0
- package/dist/lsp/index.d.ts +31 -0
- package/dist/lsp/index.d.ts.map +1 -0
- package/dist/lsp/index.js +31 -0
- package/dist/lsp/index.js.map +1 -0
- package/dist/lsp/instance.d.ts +72 -0
- package/dist/lsp/instance.d.ts.map +1 -0
- package/dist/lsp/instance.js +489 -0
- package/dist/lsp/instance.js.map +1 -0
- package/dist/lsp/lazy-load.d.ts +27 -0
- package/dist/lsp/lazy-load.d.ts.map +1 -0
- package/dist/lsp/lazy-load.js +57 -0
- package/dist/lsp/lazy-load.js.map +1 -0
- package/dist/lsp/manager.d.ts +59 -0
- package/dist/lsp/manager.d.ts.map +1 -0
- package/dist/lsp/manager.js +242 -0
- package/dist/lsp/manager.js.map +1 -0
- package/dist/lsp/ops.d.ts +13 -0
- package/dist/lsp/ops.d.ts.map +1 -0
- package/dist/lsp/ops.js +225 -0
- package/dist/lsp/ops.js.map +1 -0
- package/dist/lsp/rpc.d.ts +47 -0
- package/dist/lsp/rpc.d.ts.map +1 -0
- package/dist/lsp/rpc.js +134 -0
- package/dist/lsp/rpc.js.map +1 -0
- package/dist/lsp/safe-uri.d.ts +18 -0
- package/dist/lsp/safe-uri.d.ts.map +1 -0
- package/dist/lsp/safe-uri.js +96 -0
- package/dist/lsp/safe-uri.js.map +1 -0
- package/dist/lsp/types.d.ts +70 -0
- package/dist/lsp/types.d.ts.map +1 -0
- package/dist/lsp/types.js +16 -0
- package/dist/lsp/types.js.map +1 -0
- package/dist/mcp/bridge.d.ts +57 -0
- package/dist/mcp/bridge.d.ts.map +1 -0
- package/dist/mcp/bridge.js +98 -0
- package/dist/mcp/bridge.js.map +1 -0
- package/dist/mcp/category.d.ts +22 -0
- package/dist/mcp/category.d.ts.map +1 -0
- package/dist/mcp/category.js +11 -0
- package/dist/mcp/category.js.map +1 -0
- package/dist/mcp/client.d.ts +228 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +352 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/content.d.ts +86 -0
- package/dist/mcp/content.d.ts.map +1 -0
- package/dist/mcp/content.js +147 -0
- package/dist/mcp/content.js.map +1 -0
- package/dist/mcp/env.d.ts +19 -0
- package/dist/mcp/env.d.ts.map +1 -0
- package/dist/mcp/env.js +50 -0
- package/dist/mcp/env.js.map +1 -0
- package/dist/mcp/host.d.ts +199 -0
- package/dist/mcp/host.d.ts.map +1 -0
- package/dist/mcp/host.js +530 -0
- package/dist/mcp/host.js.map +1 -0
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +17 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/load-mcp-host.d.ts +32 -0
- package/dist/mcp/load-mcp-host.d.ts.map +1 -0
- package/dist/mcp/load-mcp-host.js +49 -0
- package/dist/mcp/load-mcp-host.js.map +1 -0
- package/dist/mcp/oauth/callback-server.d.ts +73 -0
- package/dist/mcp/oauth/callback-server.d.ts.map +1 -0
- package/dist/mcp/oauth/callback-server.js +280 -0
- package/dist/mcp/oauth/callback-server.js.map +1 -0
- package/dist/mcp/oauth/config-hash.d.ts +24 -0
- package/dist/mcp/oauth/config-hash.d.ts.map +1 -0
- package/dist/mcp/oauth/config-hash.js +55 -0
- package/dist/mcp/oauth/config-hash.js.map +1 -0
- package/dist/mcp/oauth/error-normalize.d.ts +39 -0
- package/dist/mcp/oauth/error-normalize.d.ts.map +1 -0
- package/dist/mcp/oauth/error-normalize.js +91 -0
- package/dist/mcp/oauth/error-normalize.js.map +1 -0
- package/dist/mcp/oauth/provider.d.ts +190 -0
- package/dist/mcp/oauth/provider.d.ts.map +1 -0
- package/dist/mcp/oauth/provider.js +305 -0
- package/dist/mcp/oauth/provider.js.map +1 -0
- package/dist/mcp/oauth/refresh-coalescer.d.ts +46 -0
- package/dist/mcp/oauth/refresh-coalescer.d.ts.map +1 -0
- package/dist/mcp/oauth/refresh-coalescer.js +77 -0
- package/dist/mcp/oauth/refresh-coalescer.js.map +1 -0
- package/dist/mcp/oauth/sdk-shapes.d.ts +77 -0
- package/dist/mcp/oauth/sdk-shapes.d.ts.map +1 -0
- package/dist/mcp/oauth/sdk-shapes.js +21 -0
- package/dist/mcp/oauth/sdk-shapes.js.map +1 -0
- package/dist/mcp/parse.d.ts +28 -0
- package/dist/mcp/parse.d.ts.map +1 -0
- package/dist/mcp/parse.js +209 -0
- package/dist/mcp/parse.js.map +1 -0
- package/dist/mcp/prompts.d.ts +31 -0
- package/dist/mcp/prompts.d.ts.map +1 -0
- package/dist/mcp/prompts.js +71 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/redact.d.ts +62 -0
- package/dist/mcp/redact.d.ts.map +1 -0
- package/dist/mcp/redact.js +87 -0
- package/dist/mcp/redact.js.map +1 -0
- package/dist/mcp/resources.d.ts +70 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +170 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/types.d.ts +123 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/memory/frontmatter.d.ts +18 -0
- package/dist/memory/frontmatter.d.ts.map +1 -0
- package/dist/memory/frontmatter.js +81 -0
- package/dist/memory/frontmatter.js.map +1 -0
- package/dist/memory/index.d.ts +5 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +5 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/store.d.ts +44 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +237 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/tools.d.ts +11 -0
- package/dist/memory/tools.d.ts.map +1 -0
- package/dist/memory/tools.js +159 -0
- package/dist/memory/tools.js.map +1 -0
- package/dist/memory/types.d.ts +32 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +20 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/plugin-api/index.d.ts +167 -0
- package/dist/plugin-api/index.d.ts.map +1 -0
- package/dist/plugin-api/index.js +151 -0
- package/dist/plugin-api/index.js.map +1 -0
- package/dist/plugin-logger/index.d.ts +21 -0
- package/dist/plugin-logger/index.d.ts.map +1 -0
- package/dist/plugin-logger/index.js +59 -0
- package/dist/plugin-logger/index.js.map +1 -0
- package/dist/session/index.d.ts +125 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +202 -0
- package/dist/session/index.js.map +1 -0
- package/dist/tools/approval.d.ts +33 -0
- package/dist/tools/approval.d.ts.map +1 -0
- package/dist/tools/approval.js +53 -0
- package/dist/tools/approval.js.map +1 -0
- package/dist/tools/builtins/_shared.d.ts +94 -0
- package/dist/tools/builtins/_shared.d.ts.map +1 -0
- package/dist/tools/builtins/_shared.js +246 -0
- package/dist/tools/builtins/_shared.js.map +1 -0
- package/dist/tools/builtins/ask-user-question.d.ts +27 -0
- package/dist/tools/builtins/ask-user-question.d.ts.map +1 -0
- package/dist/tools/builtins/ask-user-question.js +191 -0
- package/dist/tools/builtins/ask-user-question.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts +3 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -0
- package/dist/tools/builtins/bash.js +158 -0
- package/dist/tools/builtins/bash.js.map +1 -0
- package/dist/tools/builtins/diff.d.ts +3 -0
- package/dist/tools/builtins/diff.d.ts.map +1 -0
- package/dist/tools/builtins/diff.js +83 -0
- package/dist/tools/builtins/diff.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts +3 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -0
- package/dist/tools/builtins/edit.js +40 -0
- package/dist/tools/builtins/edit.js.map +1 -0
- package/dist/tools/builtins/glob.d.ts +3 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -0
- package/dist/tools/builtins/glob.js +37 -0
- package/dist/tools/builtins/glob.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts +3 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -0
- package/dist/tools/builtins/grep.js +81 -0
- package/dist/tools/builtins/grep.js.map +1 -0
- package/dist/tools/builtins/lsp.d.ts +3 -0
- package/dist/tools/builtins/lsp.d.ts.map +1 -0
- package/dist/tools/builtins/lsp.js +102 -0
- package/dist/tools/builtins/lsp.js.map +1 -0
- package/dist/tools/builtins/pty.d.ts +64 -0
- package/dist/tools/builtins/pty.d.ts.map +1 -0
- package/dist/tools/builtins/pty.js +536 -0
- package/dist/tools/builtins/pty.js.map +1 -0
- package/dist/tools/builtins/read.d.ts +3 -0
- package/dist/tools/builtins/read.d.ts.map +1 -0
- package/dist/tools/builtins/read.js +18 -0
- package/dist/tools/builtins/read.js.map +1 -0
- package/dist/tools/builtins/web-fetch.d.ts +4 -0
- package/dist/tools/builtins/web-fetch.d.ts.map +1 -0
- package/dist/tools/builtins/web-fetch.js +353 -0
- package/dist/tools/builtins/web-fetch.js.map +1 -0
- package/dist/tools/builtins/write.d.ts +3 -0
- package/dist/tools/builtins/write.d.ts.map +1 -0
- package/dist/tools/builtins/write.js +48 -0
- package/dist/tools/builtins/write.js.map +1 -0
- package/dist/tools/builtins.d.ts +9 -0
- package/dist/tools/builtins.d.ts.map +1 -0
- package/dist/tools/builtins.js +29 -0
- package/dist/tools/builtins.js.map +1 -0
- package/dist/tools/diff.d.ts +18 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +57 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/permission.d.ts +120 -0
- package/dist/tools/permission.d.ts.map +1 -0
- package/dist/tools/permission.js +208 -0
- package/dist/tools/permission.js.map +1 -0
- package/dist/tools/registry.d.ts +12 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +19 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +244 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +15 -0
- package/dist/tools/types.js.map +1 -0
- package/package.json +109 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { AppError, ErrorCode, ModelSelection, createId, tokenFromSignal, } from "@chances-ai/runtime";
|
|
3
|
+
import { SessionManager } from "../session/index.js";
|
|
4
|
+
import { ToolRegistry, createPtyTool } from "../tools/index.js";
|
|
5
|
+
import { renderCatalogForToolDescription } from "../agents/index.js";
|
|
6
|
+
import { AgentEngine, DEFAULT_MAX_TURNS } from "./engine.js";
|
|
7
|
+
import { estimateTokens } from "./compaction/estimate.js";
|
|
8
|
+
import { createAgentWorktree, WorktreeError, } from "./worktree/index.js";
|
|
9
|
+
import { gitDiffShortstat, gitStatusEmpty, gitUnmergedCommitCount, } from "./worktree/git.js";
|
|
10
|
+
/** Public tool name. Exported so callers building child registries (or codex
|
|
11
|
+
* review!) can refer to the constant rather than re-spelling "task". */
|
|
12
|
+
export const TASK_TOOL_NAME = "task";
|
|
13
|
+
/** (5.1) Sibling constant — both the parent's `pty` (registered by
|
|
14
|
+
* `buildEngine`) and any per-subagent re-registration (here) use the
|
|
15
|
+
* same well-known name. */
|
|
16
|
+
const PTY_TOOL_NAME = "pty";
|
|
17
|
+
/** Upper bound on the child's `maxTurns`. The parent's config-supplied value
|
|
18
|
+
* is honored; this clamp only ensures a misconfigured 1 000-turn budget
|
|
19
|
+
* doesn't let a single `task` invocation wedge the agent for hours. */
|
|
20
|
+
const CHILD_MAX_TURNS_CEILING = 50;
|
|
21
|
+
/** (5.5) Fraction of the effective child model's context window a `context:
|
|
22
|
+
* "fork"` subagent may fill with inherited history. The remainder is left for
|
|
23
|
+
* the child's own multi-turn work, system prompt, tool schemas, and responses.
|
|
24
|
+
* `config.agent.forkMaxContextTokens` overrides the derived budget entirely. */
|
|
25
|
+
const FORK_WINDOW_FRACTION = 0.5;
|
|
26
|
+
/** (5.5) Budget used only when the effective child model's window can't be
|
|
27
|
+
* resolved. Deliberately conservative (small windows exist — the estimator
|
|
28
|
+
* itself warns of 32K/65K providers) so an unknown model never gets an
|
|
29
|
+
* optimistic high cap. */
|
|
30
|
+
const CONSERVATIVE_FORK_DEFAULT = 24_000;
|
|
31
|
+
/** (5.5 / R2 MUST-FIX) Flat allowance added to the fork estimate for the parts
|
|
32
|
+
* of the child's first request we don't size exactly: the base system prompt
|
|
33
|
+
* and any plan-mode / worktree system reminders (the per-agent `pty` tool
|
|
34
|
+
* schema lands here too). Memory context + the other tool schemas + the
|
|
35
|
+
* directive + inherited history ARE counted explicitly; this covers the rest so
|
|
36
|
+
* the preflight errs toward refusing a borderline fork rather than overflowing. */
|
|
37
|
+
const SYSTEM_BASE_RESERVE_TOKENS = 1_500;
|
|
38
|
+
/** (5.5) Flatten a message's content parts to text for fork token estimation.
|
|
39
|
+
* Tool-call args and tool-result outputs count toward the inherited size, so
|
|
40
|
+
* we serialize them rather than only counting `text` parts. */
|
|
41
|
+
function messageToText(m) {
|
|
42
|
+
return m.content
|
|
43
|
+
.map((p) => {
|
|
44
|
+
if (p.type === "text")
|
|
45
|
+
return p.text;
|
|
46
|
+
if (p.type === "tool-call")
|
|
47
|
+
return `${p.name} ${JSON.stringify(p.args)}`;
|
|
48
|
+
if (p.type === "tool-result")
|
|
49
|
+
return p.output;
|
|
50
|
+
return "";
|
|
51
|
+
})
|
|
52
|
+
.join("\n");
|
|
53
|
+
}
|
|
54
|
+
/** (5.5 / R2 SHOULD-FIX) Rough token size of a session's full transcript — the
|
|
55
|
+
* consent-relevant "how much am I sharing" figure shown in the fork permission
|
|
56
|
+
* summary. The execute-time guard uses a fuller estimate (tools + system); this
|
|
57
|
+
* is just the inherited history. */
|
|
58
|
+
function inheritedForkTokens(session) {
|
|
59
|
+
return estimateTokens(session.messages().map(messageToText).join("\n"));
|
|
60
|
+
}
|
|
61
|
+
/** (5.5 / D6) Frame the inherited history so the child treats it as background
|
|
62
|
+
* context, not a conversation to continue, and surface the worktree caveat when
|
|
63
|
+
* the child is filesystem-isolated. Lands as the child's first user turn (after
|
|
64
|
+
* the forked history). */
|
|
65
|
+
function buildForkDirective(prompt, isWorktree) {
|
|
66
|
+
const worktreeNote = isWorktree
|
|
67
|
+
? " Note: you run in an isolated git worktree, so the parent's *uncommitted* working-tree edits referenced above are NOT visible here."
|
|
68
|
+
: "";
|
|
69
|
+
return ("[FORKED CONTEXT] The conversation above was inherited from the agent that " +
|
|
70
|
+
"delegated this task — it is background context only; do not continue it." +
|
|
71
|
+
worktreeNote +
|
|
72
|
+
"\n\nYour task:\n\n" +
|
|
73
|
+
prompt);
|
|
74
|
+
}
|
|
75
|
+
function strArg(args, key, required) {
|
|
76
|
+
const v = args[key];
|
|
77
|
+
if (v === undefined || v === null) {
|
|
78
|
+
if (required)
|
|
79
|
+
throw new AppError(ErrorCode.Tool, `Expected string arg "${key}"`);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (typeof v !== "string")
|
|
83
|
+
throw new AppError(ErrorCode.Tool, `Expected string arg "${key}"`);
|
|
84
|
+
return v;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Builds the `task` built-in: a model-callable tool that spins up a child
|
|
88
|
+
* `AgentEngine` with its own ephemeral session, runs to completion, and
|
|
89
|
+
* returns the child's final assistant text.
|
|
90
|
+
*
|
|
91
|
+
* **Register per-engine, not at app boot.** This factory must be called
|
|
92
|
+
* from inside `buildEngine` (per session), not once when the app starts.
|
|
93
|
+
* Two reasons:
|
|
94
|
+
* 1. The child engine consumes whatever tools are in the registry *at the
|
|
95
|
+
* moment the child runs* — MCP-bridged tools come and go between
|
|
96
|
+
* sessions, and the child should see the same surface the parent sees
|
|
97
|
+
* *now*, not the surface from boot.
|
|
98
|
+
* 2. The closure captures the parent's `bus` / `gate` / `memory`
|
|
99
|
+
* references; rebuilds replace those references when the engine is
|
|
100
|
+
* re-created (e.g. on `/resume`). Boot-time registration would freeze
|
|
101
|
+
* stale dependencies and quietly drift across respawns.
|
|
102
|
+
*
|
|
103
|
+
* **Anti-recursion.** The child's tool registry excludes `task` itself —
|
|
104
|
+
* a model that can call `task` recursively without depth limits will
|
|
105
|
+
* fan out subagents until the process exhausts memory. v1 keeps the
|
|
106
|
+
* blanket exclusion; v2+ may relax it with an explicit depth cap.
|
|
107
|
+
*
|
|
108
|
+
* **No `PluginHost` passthrough.** Plugins already observe the parent's
|
|
109
|
+
* activity via bus events; running their hooks on every child turn would
|
|
110
|
+
* either double-fire (afterToolCall for the same parent-level decision)
|
|
111
|
+
* or silently confuse plugins that assume a single conversation lifecycle.
|
|
112
|
+
* Revisit if a real need emerges.
|
|
113
|
+
*
|
|
114
|
+
* **Shared `EventBus`.** Child token usage, tool calls, assistant deltas
|
|
115
|
+
* fire on the parent's bus. `chances stats` and the TUI therefore already
|
|
116
|
+
* surface subagent cost without any new channels — the only signal that
|
|
117
|
+
* an event came from a child is the surrounding tool-call frame.
|
|
118
|
+
*/
|
|
119
|
+
export function createTaskTool(deps) {
|
|
120
|
+
const childMaxTurns = Math.min(Math.max(1, deps.maxTurns ?? DEFAULT_MAX_TURNS), CHILD_MAX_TURNS_CEILING);
|
|
121
|
+
const baseDescription = "Delegate a self-contained sub-task to a child agent. By default the child starts with a FRESH, empty conversation (it sees only your `prompt`) — use this when a task benefits from a clean context: focused file exploration, isolated refactor planning, scoped research. Set `context: \"fork\"` to instead give the child a copy of THIS conversation's completed history (everything you've already read/run), for \"continue this exact line of work\" tasks. The child has access to the same tools as you (except `task` itself — no recursion) and shares your permission gate, so session approvals carry through. The child runs to completion and returns its final assistant text as a single payload. Token usage / cost flow back to this session's telemetry.";
|
|
122
|
+
// Append the catalog block when agents are configured. Each entry's
|
|
123
|
+
// description is hard-capped + control-stripped + framed as metadata (not
|
|
124
|
+
// instructions) by `renderCatalogForToolDescription` — codex Round-1 #7/#8.
|
|
125
|
+
const catalog = deps.agents;
|
|
126
|
+
const rendered = catalog && catalog.size > 0 ? renderCatalogForToolDescription(catalog) : null;
|
|
127
|
+
const description = rendered && rendered.text
|
|
128
|
+
? `${baseDescription}\n\n${rendered.text}`
|
|
129
|
+
: baseDescription;
|
|
130
|
+
// Schema. `subagent_type` is open string (the catalog is dynamic; static
|
|
131
|
+
// enum would be stale the moment a user edits a file). All three ref
|
|
132
|
+
// repos converge on this shape. Built as a JSONValue object explicitly
|
|
133
|
+
// so the conditional `subagent_type` doesn't end up as a union type the
|
|
134
|
+
// surrounding `Tool.parameters: JSONValue` field can't accept.
|
|
135
|
+
const properties = {
|
|
136
|
+
description: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Short label (1–3 words) describing what this subtask does. Surfaces in the permission prompt so the user sees what they're approving.",
|
|
139
|
+
},
|
|
140
|
+
prompt: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Full task description for the child agent. Treat this like the very first user message — make it self-contained. Even with context:'fork', the child does NOT see your CURRENT turn (only completed prior turns), so always restate the immediate ask and paste any current-turn content (e.g. a resource you just @-mentioned) the child needs.",
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
if (catalog && catalog.size > 0) {
|
|
146
|
+
properties["subagent_type"] = {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "Optional persona from the catalog (see this tool's description). Omit to spawn the default subagent (full tool inheritance from the parent).",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (deps.backgroundTasks) {
|
|
152
|
+
properties["run_in_background"] = {
|
|
153
|
+
type: "boolean",
|
|
154
|
+
description: "Set to true to launch this subagent in the background and return immediately with a task id. You will receive the result as a <task-notification> block on a future turn. Use this when you want to fan out parallel work (issue multiple background launches in one turn) or when you want to keep working while a long-running investigation continues. Do not poll, do not relaunch.",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// (4.1) Worktree isolation, model-callable.
|
|
158
|
+
properties["isolation"] = {
|
|
159
|
+
type: "string",
|
|
160
|
+
enum: ["worktree", "none"],
|
|
161
|
+
description: "Optional FS isolation. 'worktree' runs this subagent inside a fresh git worktree under .chances/worktrees/; writes are not visible to the parent until the task completes. 'none' (the default) shares the parent's working tree. Overrides the agent catalog frontmatter when set.",
|
|
162
|
+
};
|
|
163
|
+
// (5.5) Conversation-context mode, model-callable (per-call only — no
|
|
164
|
+
// frontmatter default, since fork changes transcript disclosure + cost).
|
|
165
|
+
properties["context"] = {
|
|
166
|
+
type: "string",
|
|
167
|
+
enum: ["fork", "fresh"],
|
|
168
|
+
description: "Conversation context for the child. 'fresh' (the default) = empty context, child sees only `prompt`. 'fork' = the child inherits a copy of THIS conversation's completed turns as background context (for continuing the current line of work). Forking shares your full transcript with the child's model; it is refused if the inherited context is too large for that model, or if the child's model is a different provider than this session (unless config.agent.allowCrossProviderFork).",
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
name: TASK_TOOL_NAME,
|
|
172
|
+
description,
|
|
173
|
+
category: "integration",
|
|
174
|
+
parameters: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties,
|
|
177
|
+
required: ["prompt"],
|
|
178
|
+
},
|
|
179
|
+
summarize: (args) => {
|
|
180
|
+
const desc = strArg(args, "description", false);
|
|
181
|
+
const prompt = strArg(args, "prompt", true);
|
|
182
|
+
const subType = strArg(args, "subagent_type", false);
|
|
183
|
+
const head = prompt.length > 80 ? prompt.slice(0, 80) + "…" : prompt;
|
|
184
|
+
// When the model targets a persona, surface the effective tool list
|
|
185
|
+
// in the permission summary so the user sees the narrowed surface
|
|
186
|
+
// before approving the spawn (codex Round-1 #17).
|
|
187
|
+
const personaTail = subType && catalog?.has(subType)
|
|
188
|
+
? ` (tools: ${formatToolListForSummary(catalog.get(subType).tools)})`
|
|
189
|
+
: "";
|
|
190
|
+
const label = subType ? `task[${subType}]` : desc ? `task[${desc}]` : "task";
|
|
191
|
+
// (5.5 / D9) Surface fork transcript-disclosure + the effective
|
|
192
|
+
// model/provider in the permission summary so the user approving the
|
|
193
|
+
// spawn sees that the full conversation is handed to the child (and to
|
|
194
|
+
// which provider). Best-effort: never let summary resolution throw.
|
|
195
|
+
let forkTail = "";
|
|
196
|
+
if (strArg(args, "context", false) === "fork") {
|
|
197
|
+
try {
|
|
198
|
+
const personaModel = subType && catalog?.has(subType) ? catalog.get(subType).model : undefined;
|
|
199
|
+
const parentChoice = deps.parentSelection?.get() ?? {};
|
|
200
|
+
const parentProvider = deps.router.pick({
|
|
201
|
+
preferredModel: parentChoice.model,
|
|
202
|
+
preferredProvider: parentChoice.provider,
|
|
203
|
+
needsTools: true,
|
|
204
|
+
}).model.provider;
|
|
205
|
+
const childModel = (personaModel
|
|
206
|
+
? deps.router.pick({ preferredModel: personaModel, needsTools: true })
|
|
207
|
+
: deps.router.pick({
|
|
208
|
+
preferredModel: parentChoice.model,
|
|
209
|
+
preferredProvider: parentChoice.provider,
|
|
210
|
+
needsTools: true,
|
|
211
|
+
})).model;
|
|
212
|
+
const differs = childModel.provider !== parentProvider;
|
|
213
|
+
const tokTail = deps.parentSession
|
|
214
|
+
? ` ~${inheritedForkTokens(deps.parentSession)}tok`
|
|
215
|
+
: "";
|
|
216
|
+
forkTail = ` (fork: full transcript${tokTail} → ${childModel.id}/${childModel.provider}${differs ? " ⚠ differs from session" : ""})`;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
forkTail = " (fork: full transcript)";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return `${label}: ${head}${personaTail}${forkTail}`;
|
|
223
|
+
},
|
|
224
|
+
async execute(args, ctx) {
|
|
225
|
+
const prompt = strArg(args, "prompt", true);
|
|
226
|
+
const subagentType = strArg(args, "subagent_type", false);
|
|
227
|
+
let agent;
|
|
228
|
+
if (subagentType !== undefined && subagentType.length > 0) {
|
|
229
|
+
if (!catalog || catalog.size === 0) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
output: `subagent_type '${subagentType}' was provided but no agent catalog is configured for this session.`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
agent = catalog.get(subagentType);
|
|
236
|
+
if (!agent) {
|
|
237
|
+
const available = [...catalog.keys()].sort().join(", ");
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
output: `Unknown agent type '${subagentType}'. Available: ${available}`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Build the child registry from a single computed survivor list.
|
|
245
|
+
// 1. Snapshot parent → drop `task` (recursion still blocked in 3.3).
|
|
246
|
+
// 2. If persona has explicit `tools`, intersect (fail-closed on
|
|
247
|
+
// unknown names — codex Round-1 #5).
|
|
248
|
+
// 3. If persona has `disallowedTools`, subtract (same fail-closed
|
|
249
|
+
// semantics; unknown disallow name → ok:false rather than
|
|
250
|
+
// leaving the real tool reachable).
|
|
251
|
+
// 4. Construct one ToolRegistry from the final list. ToolRegistry's
|
|
252
|
+
// v1 surface only exposes `register` + `list`, so we never
|
|
253
|
+
// register-then-remove — we compute survivors first, then build.
|
|
254
|
+
const parentNames = new Set();
|
|
255
|
+
const parentByName = new Map();
|
|
256
|
+
for (const t of deps.parentTools.list()) {
|
|
257
|
+
if (t.name === TASK_TOOL_NAME)
|
|
258
|
+
continue;
|
|
259
|
+
parentNames.add(t.name);
|
|
260
|
+
parentByName.set(t.name, t);
|
|
261
|
+
}
|
|
262
|
+
if (agent) {
|
|
263
|
+
if (agent.tools !== "*") {
|
|
264
|
+
const unknown = agent.tools.filter((n) => !parentNames.has(n));
|
|
265
|
+
if (unknown.length > 0) {
|
|
266
|
+
const available = [...parentNames].sort().join(", ");
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
output: `Agent '${agent.name}' references unknown tool(s) in 'tools': ${unknown.join(", ")}. Available tools: ${available}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const unknownDisallowed = agent.disallowedTools.filter((n) => !parentNames.has(n));
|
|
274
|
+
if (unknownDisallowed.length > 0) {
|
|
275
|
+
const available = [...parentNames].sort().join(", ");
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
output: `Agent '${agent.name}' references unknown tool(s) in 'disallowedTools': ${unknownDisallowed.join(", ")}. Available tools: ${available}`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const allowSet = agent && agent.tools !== "*"
|
|
283
|
+
? new Set(agent.tools)
|
|
284
|
+
: parentNames; // `*` or no agent → full surface (minus `task`)
|
|
285
|
+
const denySet = new Set(agent?.disallowedTools ?? []);
|
|
286
|
+
// (4.1) Resolve effective isolation mode. Order:
|
|
287
|
+
// 1) `task` tool input `isolation` (model-callable override)
|
|
288
|
+
// 2) agent-catalog frontmatter `isolation`
|
|
289
|
+
// 3) default 'none'
|
|
290
|
+
// The schema enum makes this self-documenting; we still validate
|
|
291
|
+
// the value defensively because `JSONValue` is permissive.
|
|
292
|
+
const inputIsolation = strArg(args, "isolation", false);
|
|
293
|
+
let isolationMode = "none";
|
|
294
|
+
if (inputIsolation !== undefined) {
|
|
295
|
+
if (inputIsolation !== "worktree" && inputIsolation !== "none") {
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
output: `Invalid 'isolation' value '${inputIsolation}'. Expected 'worktree' or 'none'.`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
isolationMode = inputIsolation;
|
|
302
|
+
}
|
|
303
|
+
else if (agent?.isolation === "worktree") {
|
|
304
|
+
isolationMode = "worktree";
|
|
305
|
+
}
|
|
306
|
+
const childTools = new ToolRegistry();
|
|
307
|
+
const mcpFilterActive = isolationMode === "worktree" && !agent?.unsafeAllowMutatingMcp;
|
|
308
|
+
let mcpToolsFiltered = 0;
|
|
309
|
+
for (const t of parentByName.values()) {
|
|
310
|
+
if (!allowSet.has(t.name))
|
|
311
|
+
continue;
|
|
312
|
+
if (denySet.has(t.name))
|
|
313
|
+
continue;
|
|
314
|
+
// (5.1) Parent's `pty` instance is closed over the parent's
|
|
315
|
+
// agentId — registering it as-is for the child would create
|
|
316
|
+
// child sessions in the parent's ownership bucket, defeating
|
|
317
|
+
// D10 symmetric isolation and the `drainOwnedBy(childAgentId)`
|
|
318
|
+
// contract. Skip here and re-register with the child's id below.
|
|
319
|
+
if (t.name === PTY_TOOL_NAME)
|
|
320
|
+
continue;
|
|
321
|
+
// (4.1 — Round-1 MUST-FIX #2) MCP tools run in their own
|
|
322
|
+
// process; the worktree's cwd does NOT confine their writes.
|
|
323
|
+
// Inside an isolated subagent, filter MCP-source tools (named
|
|
324
|
+
// `mcp__<server>__<tool>`) to those whose declared category is
|
|
325
|
+
// in the read-only set. Unannotated tools default to
|
|
326
|
+
// `integration` which we treat as potentially mutating.
|
|
327
|
+
// Opt-out via frontmatter `unsafeAllowMutatingMcp: true`.
|
|
328
|
+
if (mcpFilterActive && isMcpToolName(t.name) && !isReadOnlyMcpCategory(t.category)) {
|
|
329
|
+
mcpToolsFiltered++;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
childTools.register(t);
|
|
333
|
+
}
|
|
334
|
+
// Resolve model override. `agent.model` MUST resolve through the
|
|
335
|
+
// registry; misses fail closed (codex Round-1 #4).
|
|
336
|
+
let childSelection;
|
|
337
|
+
if (agent?.model) {
|
|
338
|
+
if (!deps.registry) {
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
output: `Agent '${agent.name}' specifies model '${agent.model}' but the task tool was wired without a ModelRegistry; cannot validate.`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const resolved = deps.registry.get(agent.model);
|
|
345
|
+
if (!resolved) {
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
output: `Agent '${agent.name}' specifies model '${agent.model}' which is not in the registry. Run 'chances doctor' to see configured providers and known models.`,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
childSelection = new ModelSelection({ model: agent.model });
|
|
352
|
+
}
|
|
353
|
+
const childTurnsBudget = agent?.maxTurns
|
|
354
|
+
? Math.min(Math.max(1, agent.maxTurns), CHILD_MAX_TURNS_CEILING)
|
|
355
|
+
: childMaxTurns;
|
|
356
|
+
// (5.5) fork-from-parent resolution. Runs BEFORE any worktree is created
|
|
357
|
+
// so a gate refusal returns cleanly with nothing to tear down. All the
|
|
358
|
+
// fork-only state (the deep-cloned child session + the model selection
|
|
359
|
+
// the child runs under) is computed once here, at execute time, so the
|
|
360
|
+
// background path's deferred `run()` closure forks the parent at SPAWN
|
|
361
|
+
// time, not at run time (D7).
|
|
362
|
+
const inputContext = strArg(args, "context", false);
|
|
363
|
+
if (inputContext !== undefined && inputContext !== "fork" && inputContext !== "fresh") {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
output: `Invalid 'context' value '${inputContext}'. Expected 'fork' or 'fresh'.`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const forkRequested = inputContext === "fork";
|
|
370
|
+
// `effectiveSelection` is what the child engine runs under: a persona
|
|
371
|
+
// `model` override always wins; a fork with no override inherits the
|
|
372
|
+
// parent's CURRENT selection (pinned now); a fresh subagent keeps today's
|
|
373
|
+
// behaviour (undefined ⇒ engine default).
|
|
374
|
+
let effectiveSelection = childSelection;
|
|
375
|
+
let forkedChildSession;
|
|
376
|
+
// (D6) The prompt the child actually receives. For a fork it is wrapped
|
|
377
|
+
// with the directive (built here so the budget estimate counts it too —
|
|
378
|
+
// it depends only on `isolationMode`, not the not-yet-created worktree);
|
|
379
|
+
// a fresh subagent passes the prompt as-is.
|
|
380
|
+
let childPrompt = prompt;
|
|
381
|
+
if (forkRequested) {
|
|
382
|
+
if (!deps.parentSession) {
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
output: "context:'fork' was requested but the task tool was wired without a parent session; fork is unavailable. Use a fresh subagent with a self-contained prompt.",
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
// Resolve effective parent + child model descriptors through the router
|
|
389
|
+
// (same fallback the engine uses), so the budget + cross-provider gate
|
|
390
|
+
// see the model the child will actually run on.
|
|
391
|
+
const parentChoice = deps.parentSelection?.get() ?? {};
|
|
392
|
+
const parentModel = deps.router.pick({
|
|
393
|
+
preferredModel: parentChoice.model,
|
|
394
|
+
preferredProvider: parentChoice.provider,
|
|
395
|
+
needsTools: true,
|
|
396
|
+
}).model;
|
|
397
|
+
if (!effectiveSelection) {
|
|
398
|
+
// No persona override: inherit the parent's current model, pinned at
|
|
399
|
+
// execute time so a later `/model` switch (or background defer) can't
|
|
400
|
+
// move the child off the model the user forked on.
|
|
401
|
+
effectiveSelection = new ModelSelection({
|
|
402
|
+
provider: parentChoice.provider,
|
|
403
|
+
model: parentChoice.model,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const childChoice = effectiveSelection.get();
|
|
407
|
+
const childModel = deps.router.pick({
|
|
408
|
+
preferredModel: childChoice.model,
|
|
409
|
+
preferredProvider: childChoice.provider,
|
|
410
|
+
needsTools: true,
|
|
411
|
+
}).model;
|
|
412
|
+
// (D11) Cross-provider transcript-disclosure gate (fail-closed; not
|
|
413
|
+
// bypassable by auto-approve modes since it refuses before the gate).
|
|
414
|
+
if (childModel.provider !== parentModel.provider && !deps.allowCrossProviderFork) {
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
output: `context:'fork' would send the full conversation transcript to provider '${childModel.provider}' (model ${childModel.id}), which differs from this session's provider '${parentModel.provider}'. Set config.agent.allowCrossProviderFork=true to permit cross-provider forks, or drop the persona model override.`,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
// (D5 / R2 MUST-FIX) Model-aware budget. Fork the parent ONCE here (used
|
|
421
|
+
// for both the estimate and as the child's seeded session) and build the
|
|
422
|
+
// directive now so the estimate covers the SAME shape the child engine
|
|
423
|
+
// will send (`engine.ts` runTurnImpl: system + tool schemas + inherited
|
|
424
|
+
// messages + the directive-wrapped user turn) — not just the inherited
|
|
425
|
+
// history. Under-counting the tool schemas / directive would let a fork
|
|
426
|
+
// pass this guard and still overflow the model's window on its first
|
|
427
|
+
// request, which is exactly what this preflight exists to prevent.
|
|
428
|
+
forkedChildSession = SessionManager.forkFrom(deps.parentSession, "subagent");
|
|
429
|
+
childPrompt = buildForkDirective(prompt, isolationMode === "worktree");
|
|
430
|
+
const inheritedText = forkedChildSession.messages().map(messageToText).join("\n");
|
|
431
|
+
// `childTools` is fully populated here except the per-agent `pty`
|
|
432
|
+
// instance (registered in each branch below) — a one-tool undercount
|
|
433
|
+
// the window fraction + base reserve absorb. Memory context is the
|
|
434
|
+
// other model-visible system addition we can size cheaply; plan/worktree
|
|
435
|
+
// reminders + the base prompt are covered by SYSTEM_BASE_RESERVE_TOKENS.
|
|
436
|
+
const toolDefsText = childTools
|
|
437
|
+
.list()
|
|
438
|
+
.map((t) => `${t.name}\n${t.description}\n${JSON.stringify(t.parameters)}`)
|
|
439
|
+
.join("\n");
|
|
440
|
+
const systemText = deps.memory?.asSystemContext() ?? "";
|
|
441
|
+
const estimate = estimateTokens(`${systemText}\n${toolDefsText}\n${inheritedText}\n${childPrompt}`) +
|
|
442
|
+
SYSTEM_BASE_RESERVE_TOKENS;
|
|
443
|
+
const budget = deps.forkMaxContextTokens ??
|
|
444
|
+
(childModel.contextWindow
|
|
445
|
+
? Math.floor(childModel.contextWindow * FORK_WINDOW_FRACTION)
|
|
446
|
+
: CONSERVATIVE_FORK_DEFAULT);
|
|
447
|
+
if (estimate > budget) {
|
|
448
|
+
return {
|
|
449
|
+
ok: false,
|
|
450
|
+
output: `Fork context (~${estimate} tokens, incl. tools + system) exceeds the budget (${budget} tokens) for model ${childModel.id}. Spawn a fresh subagent with an explicit prompt, /compact the conversation first, or raise config.agent.forkMaxContextTokens.`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// (5.1) Drain helper: called on every subagent termination
|
|
455
|
+
// path (sync return / sync throw / background return). The
|
|
456
|
+
// registry only kills sessions still owned by `childAgentId`
|
|
457
|
+
// — sessions the parent has `pty.adopt()`-ed are re-bound to
|
|
458
|
+
// the parent's agentId and are no-ops here. Failure to drain
|
|
459
|
+
// is logged once on the bus but never blocks the caller; a
|
|
460
|
+
// 2-second deadline matches `AsyncTaskRegistry.killAll` from
|
|
461
|
+
// 3.4 so engine teardown shape is uniform.
|
|
462
|
+
const drainChildPty = async (childAgentId) => {
|
|
463
|
+
if (!deps.ptySessions)
|
|
464
|
+
return;
|
|
465
|
+
try {
|
|
466
|
+
// (5.1 codex Round-2 SHOULD-FIX #1) Capture survivors and
|
|
467
|
+
// surface them on the bus so operators see when a subagent
|
|
468
|
+
// PTY ignored TERM-then-KILL past the 2 s deadline. Matches
|
|
469
|
+
// the CLI shutdown's `pty dispose` survivor log so the two
|
|
470
|
+
// teardown paths report the same shape.
|
|
471
|
+
const { survivors } = await deps.ptySessions.drainOwnedBy(childAgentId, 2_000);
|
|
472
|
+
if (survivors.length > 0) {
|
|
473
|
+
deps.bus.emit({
|
|
474
|
+
type: "log",
|
|
475
|
+
level: "warn",
|
|
476
|
+
message: `subagent ${childAgentId}: ${survivors.length} pty session(s) past 2s drain deadline: ${survivors.join(", ")}`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
deps.bus.emit({
|
|
482
|
+
type: "log",
|
|
483
|
+
level: "warn",
|
|
484
|
+
message: `pty drainOwnedBy(${childAgentId}) failed: ${e.message ?? e}`,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
// 3.4 branch: `run_in_background:true` ONLY when the registry was
|
|
489
|
+
// wired. When undefined, the field is silently ignored (codex
|
|
490
|
+
// Round-1 SHOULD-FIX #1 — see design "Registry-absent fallback").
|
|
491
|
+
const wantBackground = deps.backgroundTasks !== undefined && boolArg(args, "run_in_background") === true;
|
|
492
|
+
const agentName = agent?.name ?? "default";
|
|
493
|
+
// (4.1) Create the worktree BEFORE spawning the child engine —
|
|
494
|
+
// synchronously for the sync branch, before launching for the
|
|
495
|
+
// background branch. The child engine receives the worktree
|
|
496
|
+
// path as `worktreeCwd`. On any failure path (including
|
|
497
|
+
// cancellation), `worktreeHandle.cleanup()` is invoked from a
|
|
498
|
+
// `finally` block (sync) or the registry's settle handler
|
|
499
|
+
// (background). Cleanup is idempotent so double-invocation is
|
|
500
|
+
// safe. Round-1 MUST-FIX #3 — `signal` flows into the spawn.
|
|
501
|
+
let worktreeHandle;
|
|
502
|
+
if (isolationMode === "worktree") {
|
|
503
|
+
try {
|
|
504
|
+
worktreeHandle = await createAgentWorktree(deps.workspaceRoot, {
|
|
505
|
+
signal: ctx.signal,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
if (e instanceof WorktreeError) {
|
|
510
|
+
return {
|
|
511
|
+
ok: false,
|
|
512
|
+
output: `Isolation worktree could not be created (${e.code}): ${e.message}. Hint: ensure the workspace is a git repository and 'git' is on PATH.`,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
throw e;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const worktreeCwd = worktreeHandle?.path;
|
|
519
|
+
// (5.5 / D6) `childPrompt` was finalised in the fork block above (directive
|
|
520
|
+
// for a fork, raw prompt for a fresh subagent). `isolationMode` — which
|
|
521
|
+
// the directive's worktree caveat depends on — is resolved before that
|
|
522
|
+
// block, so no recompute is needed here even though the worktree itself is
|
|
523
|
+
// created later.
|
|
524
|
+
if (wantBackground) {
|
|
525
|
+
// The registry runs `run(signal)` under its own unlinked
|
|
526
|
+
// AbortController — parent's `ctx.signal` does NOT bind. The
|
|
527
|
+
// child engine's loop honors `signal` via `tokenFromSignal`,
|
|
528
|
+
// same path 3.3 sync subagents use.
|
|
529
|
+
//
|
|
530
|
+
// Closure capture of the task id: `launch()` is synchronous and
|
|
531
|
+
// returns the handle before the registry kicks `run()` into
|
|
532
|
+
// microtask land. The child engine's `agentContext` is a SHARED
|
|
533
|
+
// mutable object — we set `.agentId` after `launch()` returns,
|
|
534
|
+
// and because `AgentEngine.emit` re-reads `ctx.agentId` per
|
|
535
|
+
// emit, the engine will pick up the registry-issued id by the
|
|
536
|
+
// time the child's first data frame fires.
|
|
537
|
+
const agentContextObj = {
|
|
538
|
+
agentId: "",
|
|
539
|
+
agentName,
|
|
540
|
+
};
|
|
541
|
+
// (5.1) PTY-ownership agentId for the child. Minted here, NOT
|
|
542
|
+
// re-using `handle.taskId` (that's pinned to AsyncTaskRegistry
|
|
543
|
+
// semantics; PTY ownership is a separate concern). The drain
|
|
544
|
+
// path at the end of `run()` calls `drainOwnedBy(ptyAgentId)`,
|
|
545
|
+
// matching sessions the model started through this subagent's
|
|
546
|
+
// `pty` tool. Adopted sessions have re-bound to the parent's
|
|
547
|
+
// agentId and are no-ops there.
|
|
548
|
+
const childPtyAgentId = createId("sub");
|
|
549
|
+
if (deps.ptySessions && deps.nativeCreatePtySession) {
|
|
550
|
+
childTools.register(createPtyTool({
|
|
551
|
+
registry: deps.ptySessions,
|
|
552
|
+
agentId: childPtyAgentId,
|
|
553
|
+
agentName,
|
|
554
|
+
workspaceRoot: deps.workspaceRoot,
|
|
555
|
+
// (5.1 codex Round-2 MUST-FIX #3) Isolated subagent's
|
|
556
|
+
// PTY commands default to the worktree path, not the
|
|
557
|
+
// parent's. Without this override, `pty.start({command:
|
|
558
|
+
// "pwd"})` from inside an isolated subagent would
|
|
559
|
+
// execute against the parent checkout.
|
|
560
|
+
defaultCwd: worktreeCwd ?? deps.workspaceRoot,
|
|
561
|
+
worktreeCwd,
|
|
562
|
+
createHandle: deps.nativeCreatePtySession,
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const handle = deps.backgroundTasks.launch({
|
|
567
|
+
name: agentName,
|
|
568
|
+
prompt,
|
|
569
|
+
// (5.7) Carry the worktree's RELATIVE path + branch (PII-free) so
|
|
570
|
+
// the OTel exporter can stamp `chances.gen_ai.worktree.*` on the
|
|
571
|
+
// synthesized background span. Absolute path is never carried.
|
|
572
|
+
...(worktreeHandle
|
|
573
|
+
? {
|
|
574
|
+
worktree: {
|
|
575
|
+
// RELATIVE to the worktree's OWN `canonicalRoot` (codex R2
|
|
576
|
+
// Q4) — not `deps.workspaceRoot`, which can differ when the
|
|
577
|
+
// caller's cwd sits inside a pre-existing user worktree, in
|
|
578
|
+
// which case `relative()` could emit `..` and leak an
|
|
579
|
+
// absolute-ish path. Against canonicalRoot this is always a
|
|
580
|
+
// clean `.chances/worktrees/agent-…`. The exporter guards
|
|
581
|
+
// again defensively before stamping.
|
|
582
|
+
relativePath: relative(worktreeHandle.canonicalRoot, worktreeHandle.path),
|
|
583
|
+
branch: worktreeHandle.branch,
|
|
584
|
+
},
|
|
585
|
+
}
|
|
586
|
+
: {}),
|
|
587
|
+
run: async (signal) => {
|
|
588
|
+
const startedAt = Date.now();
|
|
589
|
+
const childSession = forkedChildSession ?? SessionManager.create("subagent");
|
|
590
|
+
const childEngine = new AgentEngine({
|
|
591
|
+
bus: deps.bus,
|
|
592
|
+
router: deps.router,
|
|
593
|
+
tools: childTools,
|
|
594
|
+
gate: deps.gate,
|
|
595
|
+
getApprovalMode: deps.getApprovalMode,
|
|
596
|
+
// (5.4 D12) inherit the parent's MCP mention resolver.
|
|
597
|
+
resolveMcpMentions: deps.resolveMcpMentions,
|
|
598
|
+
session: childSession,
|
|
599
|
+
memory: deps.memory,
|
|
600
|
+
workspaceRoot: deps.workspaceRoot,
|
|
601
|
+
maxTurns: childTurnsBudget,
|
|
602
|
+
suppressTerminalErrors: true,
|
|
603
|
+
// 3.4: background child suppresses lifecycle events
|
|
604
|
+
// (turn:start/turn:end/error) so the TUI's `busy` flag
|
|
605
|
+
// doesn't flip when the background advances a turn.
|
|
606
|
+
suppressLifecycleEvents: true,
|
|
607
|
+
systemBaseOverride: agent?.systemPrompt,
|
|
608
|
+
selection: effectiveSelection,
|
|
609
|
+
// 3.4: stamp every data-frame event the child emits
|
|
610
|
+
// with the registry-issued task id + agent name so
|
|
611
|
+
// subscribers can demultiplex parent vs. child output.
|
|
612
|
+
agentContext: agentContextObj,
|
|
613
|
+
// (4.1) Worktree isolation — child engine flips its
|
|
614
|
+
// `ToolContext.workspaceRoot`/`cwd` to the worktree
|
|
615
|
+
// path through AsyncLocalStorage.
|
|
616
|
+
worktreeCwd,
|
|
617
|
+
});
|
|
618
|
+
const childToken = tokenFromSignal(signal);
|
|
619
|
+
try {
|
|
620
|
+
const result = await childEngine.runTurn(childPrompt, childToken, { expandMentions: false });
|
|
621
|
+
// (Round-2 SHOULD-FIX #2) Re-check cancellation BEFORE
|
|
622
|
+
// finalize. If a `kill` arrived after the model finished
|
|
623
|
+
// its last tool call but before this point, the registry
|
|
624
|
+
// will record the task as cancelled — the worktree
|
|
625
|
+
// should be cleaned up, not retained as success.
|
|
626
|
+
if (signal.aborted) {
|
|
627
|
+
if (worktreeHandle)
|
|
628
|
+
await worktreeHandle.cleanup();
|
|
629
|
+
return {
|
|
630
|
+
ok: false,
|
|
631
|
+
text: "Subagent cancelled",
|
|
632
|
+
durationMs: Date.now() - startedAt,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
const finalText = result.text.trim() || "(subagent finished without producing any text)";
|
|
636
|
+
const wrapped = await finalizeWorktree(worktreeHandle, deps.workspaceRoot, finalText, mcpToolsFiltered);
|
|
637
|
+
return {
|
|
638
|
+
ok: true,
|
|
639
|
+
text: wrapped,
|
|
640
|
+
durationMs: Date.now() - startedAt,
|
|
641
|
+
tokens: {
|
|
642
|
+
input: result.inputTokens,
|
|
643
|
+
output: result.outputTokens,
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
catch (e) {
|
|
648
|
+
// Failure / cancel: tear down the worktree (idempotent).
|
|
649
|
+
if (worktreeHandle)
|
|
650
|
+
await worktreeHandle.cleanup();
|
|
651
|
+
if (e instanceof AppError && e.code === ErrorCode.Cancelled) {
|
|
652
|
+
return {
|
|
653
|
+
ok: false,
|
|
654
|
+
text: "Subagent cancelled",
|
|
655
|
+
durationMs: Date.now() - startedAt,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const msg = e instanceof AppError
|
|
659
|
+
? `Subagent failed (${e.code}): ${e.message}`
|
|
660
|
+
: `Subagent failed: ${e.message || String(e)}`;
|
|
661
|
+
return { ok: false, text: msg, durationMs: Date.now() - startedAt };
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
// (5.1) Drain every still-running PTY session owned by
|
|
665
|
+
// the child. Adopted sessions re-bound to the parent
|
|
666
|
+
// are no-ops here. Fires before the registry surfaces
|
|
667
|
+
// the completion notification so the model never sees
|
|
668
|
+
// a `<task-notification>` for a subagent that still
|
|
669
|
+
// holds live shells in the background.
|
|
670
|
+
await drainChildPty(childPtyAgentId);
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
agentContextObj.agentId = handle.taskId;
|
|
675
|
+
const body = [
|
|
676
|
+
`(background-launched) task_id=${handle.taskId} name=${agentName}`,
|
|
677
|
+
...(isolationMode === "worktree" && worktreeHandle
|
|
678
|
+
? [`isolation=worktree path=${relative(deps.workspaceRoot, worktreeHandle.path) || worktreeHandle.path} branch=${worktreeHandle.branch}`]
|
|
679
|
+
: []),
|
|
680
|
+
"",
|
|
681
|
+
"You will receive a <task-notification> block on a future turn when this task completes. Do not poll; do not relaunch unless explicitly asked.",
|
|
682
|
+
].join("\n");
|
|
683
|
+
return { ok: true, output: body };
|
|
684
|
+
}
|
|
685
|
+
catch (e) {
|
|
686
|
+
// launch() failed — if we already created a worktree, tear it
|
|
687
|
+
// down so we don't leak it into the next stale-GC pass.
|
|
688
|
+
if (worktreeHandle)
|
|
689
|
+
await worktreeHandle.cleanup();
|
|
690
|
+
// Capacity refusal or registry-disposed surfaces as a labeled
|
|
691
|
+
// AppError(Tool); fold it into ok:false so the model sees the
|
|
692
|
+
// recovery message.
|
|
693
|
+
if (e instanceof AppError && e.code === ErrorCode.Tool) {
|
|
694
|
+
return { ok: false, output: e.message };
|
|
695
|
+
}
|
|
696
|
+
throw e;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Synchronous path. (3.6 codex Round-1 MUST-FIX #3) The 3.4
|
|
700
|
+
// background path correctly suppresses lifecycle frames and tags
|
|
701
|
+
// data frames with the agent identity; the sync path was missed
|
|
702
|
+
// — child `turn:start`/`turn:end`/`usage:turn` flowed onto the
|
|
703
|
+
// shared bus untagged, which (a) would flip TUI `busy` state
|
|
704
|
+
// for any subscriber that toggles on the bare event, and (b)
|
|
705
|
+
// would corrupt the 3.6 OTel exporter's `Map<turnId, Span>` by
|
|
706
|
+
// opening a second `invoke_agent` span inside the parent turn.
|
|
707
|
+
// Same shape as background: suppress lifecycle, tag data frames
|
|
708
|
+
// with `{agentName}` (no agentId — sync subagents complete before
|
|
709
|
+
// the parent's `tool:call`/`tool:result` boundary closes, so the
|
|
710
|
+
// parent already labels them at that level).
|
|
711
|
+
const syncChildPtyAgentId = createId("sub");
|
|
712
|
+
if (deps.ptySessions && deps.nativeCreatePtySession) {
|
|
713
|
+
childTools.register(createPtyTool({
|
|
714
|
+
registry: deps.ptySessions,
|
|
715
|
+
agentId: syncChildPtyAgentId,
|
|
716
|
+
agentName,
|
|
717
|
+
workspaceRoot: deps.workspaceRoot,
|
|
718
|
+
// (5.1 codex Round-2 MUST-FIX #3) Same worktree-cwd default
|
|
719
|
+
// override as the background branch above.
|
|
720
|
+
defaultCwd: worktreeCwd ?? deps.workspaceRoot,
|
|
721
|
+
worktreeCwd,
|
|
722
|
+
createHandle: deps.nativeCreatePtySession,
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
const childSession = forkedChildSession ?? SessionManager.create("subagent");
|
|
726
|
+
const child = new AgentEngine({
|
|
727
|
+
bus: deps.bus,
|
|
728
|
+
router: deps.router,
|
|
729
|
+
tools: childTools,
|
|
730
|
+
gate: deps.gate,
|
|
731
|
+
getApprovalMode: deps.getApprovalMode,
|
|
732
|
+
// (5.4 D12) inherit the parent's MCP mention resolver.
|
|
733
|
+
resolveMcpMentions: deps.resolveMcpMentions,
|
|
734
|
+
session: childSession,
|
|
735
|
+
memory: deps.memory,
|
|
736
|
+
workspaceRoot: deps.workspaceRoot,
|
|
737
|
+
maxTurns: childTurnsBudget,
|
|
738
|
+
suppressTerminalErrors: true,
|
|
739
|
+
suppressLifecycleEvents: true,
|
|
740
|
+
agentContext: { agentName },
|
|
741
|
+
systemBaseOverride: agent?.systemPrompt,
|
|
742
|
+
selection: effectiveSelection,
|
|
743
|
+
// (4.1) Worktree isolation — same wiring as the background
|
|
744
|
+
// branch above.
|
|
745
|
+
worktreeCwd,
|
|
746
|
+
});
|
|
747
|
+
const childToken = tokenFromSignal(ctx.signal);
|
|
748
|
+
try {
|
|
749
|
+
const result = await child.runTurn(childPrompt, childToken, { expandMentions: false });
|
|
750
|
+
const body = result.text.trim();
|
|
751
|
+
if (!body) {
|
|
752
|
+
if (worktreeHandle)
|
|
753
|
+
await worktreeHandle.cleanup();
|
|
754
|
+
return { ok: false, output: "Subagent finished without producing any text" };
|
|
755
|
+
}
|
|
756
|
+
const wrapped = await finalizeWorktree(worktreeHandle, deps.workspaceRoot, body, mcpToolsFiltered);
|
|
757
|
+
return { ok: true, output: wrapped };
|
|
758
|
+
}
|
|
759
|
+
catch (e) {
|
|
760
|
+
if (worktreeHandle)
|
|
761
|
+
await worktreeHandle.cleanup();
|
|
762
|
+
if (e instanceof AppError && e.code === ErrorCode.Cancelled)
|
|
763
|
+
throw e;
|
|
764
|
+
if (e instanceof AppError) {
|
|
765
|
+
return { ok: false, output: `Subagent failed (${e.code}): ${e.message}` };
|
|
766
|
+
}
|
|
767
|
+
return { ok: false, output: `Subagent failed: ${e.message || String(e)}` };
|
|
768
|
+
}
|
|
769
|
+
finally {
|
|
770
|
+
// (5.1) Drain every PTY session owned by the sync subagent.
|
|
771
|
+
// Same drain shape as the background branch — adopted
|
|
772
|
+
// sessions re-bound to the parent are no-ops here. Fires
|
|
773
|
+
// before this `execute` returns so the parent's next turn
|
|
774
|
+
// never sees `pty.list()` results for a defunct sync child.
|
|
775
|
+
await drainChildPty(syncChildPtyAgentId);
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/** (4.1) MCP-bridged tools follow the `mcp__<server>__<tool>` naming
|
|
781
|
+
* convention established in 3.1. Source-of-truth lives in `@chances-ai/mcp`
|
|
782
|
+
* but matching by prefix keeps task-tool free of an mcp import. */
|
|
783
|
+
function isMcpToolName(name) {
|
|
784
|
+
return name.startsWith("mcp__");
|
|
785
|
+
}
|
|
786
|
+
/** (4.1) Read-only chances tool categories. MCP tools whose declared
|
|
787
|
+
* category falls outside this set are filtered out of isolated
|
|
788
|
+
* subagents by default; `unsafeAllowMutatingMcp: true` opts back in. */
|
|
789
|
+
function isReadOnlyMcpCategory(category) {
|
|
790
|
+
return (category === "file-read" ||
|
|
791
|
+
category === "memory-read" ||
|
|
792
|
+
category === "network-read" ||
|
|
793
|
+
category === "search");
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* (4.1) Decides what to do with the worktree after the child finishes
|
|
797
|
+
* cleanly. Codex Round-2 caught three independent retention bugs in the
|
|
798
|
+
* v1 implementation; folded fixes:
|
|
799
|
+
*
|
|
800
|
+
* - MUST-FIX #1: `git diff --shortstat HEAD` is empty when the child
|
|
801
|
+
* COMMITTED its work (HEAD moved). Solution: also probe
|
|
802
|
+
* `gitUnmergedCommitCount(branch ^HEAD)` — any commit on the child's
|
|
803
|
+
* branch that isn't reachable from HEAD counts as "has work to
|
|
804
|
+
* keep".
|
|
805
|
+
* - MUST-FIX #2: `git diff` doesn't show untracked files. A subagent
|
|
806
|
+
* that creates new files only would look clean. Solution: also
|
|
807
|
+
* probe `gitStatusEmpty` — any untracked/modified/staged file
|
|
808
|
+
* counts as "has work to keep".
|
|
809
|
+
* - MUST-FIX #4: silently mapping probe failures to "empty diff"
|
|
810
|
+
* drives destructive cleanup on transient flakiness. Solution: any
|
|
811
|
+
* probe that throws marks `probeFailed = true` and we retain the
|
|
812
|
+
* worktree.
|
|
813
|
+
*
|
|
814
|
+
* Net rule: a worktree is silently removed ONLY when all three probes
|
|
815
|
+
* succeed AND all three report no effect. Otherwise we keep it, surface
|
|
816
|
+
* the path + branch + diff-stat banner to the parent, and let the user
|
|
817
|
+
* decide. Matches claude-code's belt-and-suspenders `AgentTool.tsx`
|
|
818
|
+
* policy.
|
|
819
|
+
*
|
|
820
|
+
* Also folds Round-2 SHOULD-FIX #3: when MCP tools were filtered, the
|
|
821
|
+
* notice is appended on BOTH the retained and the clean-removed
|
|
822
|
+
* branches (previously only retained).
|
|
823
|
+
*/
|
|
824
|
+
export async function __finalizeWorktreeForTest(handle, parentWorkspace, body, mcpToolsFiltered) {
|
|
825
|
+
return finalizeWorktree(handle, parentWorkspace, body, mcpToolsFiltered);
|
|
826
|
+
}
|
|
827
|
+
async function finalizeWorktree(handle, parentWorkspace, body, mcpToolsFiltered) {
|
|
828
|
+
if (!handle) {
|
|
829
|
+
return appendMcpFilterNotice(body, mcpToolsFiltered);
|
|
830
|
+
}
|
|
831
|
+
let shortstat = "";
|
|
832
|
+
let statusEmpty = true;
|
|
833
|
+
let unmergedCommits = 0;
|
|
834
|
+
let probeFailed = false;
|
|
835
|
+
try {
|
|
836
|
+
shortstat = await gitDiffShortstat(handle.path);
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
probeFailed = true;
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
statusEmpty = await gitStatusEmpty(handle.path);
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
probeFailed = true;
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
unmergedCommits = await gitUnmergedCommitCount(handle.canonicalRoot, handle.branch);
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
probeFailed = true;
|
|
852
|
+
}
|
|
853
|
+
const hasChanges = shortstat.length > 0 || !statusEmpty || unmergedCommits > 0;
|
|
854
|
+
if (!hasChanges && !probeFailed) {
|
|
855
|
+
await handle.cleanup();
|
|
856
|
+
return appendMcpFilterNotice(body, mcpToolsFiltered);
|
|
857
|
+
}
|
|
858
|
+
const relPath = relative(parentWorkspace, handle.path) || handle.path;
|
|
859
|
+
const reasons = [];
|
|
860
|
+
if (shortstat.length > 0)
|
|
861
|
+
reasons.push(shortstat);
|
|
862
|
+
if (!statusEmpty)
|
|
863
|
+
reasons.push("untracked or staged files present");
|
|
864
|
+
if (unmergedCommits > 0)
|
|
865
|
+
reasons.push(`${unmergedCommits} commit(s) on ${handle.branch} not on HEAD`);
|
|
866
|
+
if (probeFailed && reasons.length === 0)
|
|
867
|
+
reasons.push("probe error during retention check — worktree kept defensively");
|
|
868
|
+
const banner = [
|
|
869
|
+
`[worktree retained: ${relPath} on branch ${handle.branch}]`,
|
|
870
|
+
`[diff: ${reasons.join("; ")}]`,
|
|
871
|
+
mcpToolsFiltered > 0
|
|
872
|
+
? `[notice: ${mcpToolsFiltered} mutating MCP tool(s) were hidden from this subagent due to active isolation]`
|
|
873
|
+
: null,
|
|
874
|
+
]
|
|
875
|
+
.filter(Boolean)
|
|
876
|
+
.join("\n");
|
|
877
|
+
return `${banner}\n\n${body}`;
|
|
878
|
+
}
|
|
879
|
+
function appendMcpFilterNotice(body, n) {
|
|
880
|
+
if (n <= 0)
|
|
881
|
+
return body;
|
|
882
|
+
return `${body}\n\n[notice: ${n} mutating MCP tool(s) were hidden from this subagent due to active isolation]`;
|
|
883
|
+
}
|
|
884
|
+
/** Boolean arg reader. Returns true only when the value is literal `true`;
|
|
885
|
+
* everything else (false, missing, wrong type) returns false. We avoid
|
|
886
|
+
* coercion (truthy strings etc.) because schema-level safety matters here. */
|
|
887
|
+
function boolArg(args, key) {
|
|
888
|
+
const v = args[key];
|
|
889
|
+
return v === true;
|
|
890
|
+
}
|
|
891
|
+
/** Format an agent's tool spec into the one-line permission summary. */
|
|
892
|
+
function formatToolListForSummary(tools) {
|
|
893
|
+
if (tools === "*")
|
|
894
|
+
return "*";
|
|
895
|
+
if (tools.length === 0)
|
|
896
|
+
return "(none)";
|
|
897
|
+
if (tools.length <= 5)
|
|
898
|
+
return tools.join(", ");
|
|
899
|
+
return `${tools.slice(0, 5).join(", ")}, +${tools.length - 5} more`;
|
|
900
|
+
}
|
|
901
|
+
//# sourceMappingURL=task-tool.js.map
|