@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,767 @@
|
|
|
1
|
+
import { AppError, ErrorCode, ModelSelection, createId, runWithCwd, } from "@chances-ai/runtime";
|
|
2
|
+
import { refreshActiveMarker } from "./worktree/index.js";
|
|
3
|
+
import { classifyProviderError, defaultRetryConfig, estimateCost, } from "../ai/index.js";
|
|
4
|
+
import { ASK_USER_QUESTION_TOOL_NAME, READONLY_CATEGORIES } from "../tools/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* (3.5 — codex Round-1 SHOULD-FIX #4) Anthropic-only overflow
|
|
7
|
+
* detection. Three patterns from pi's overflow catalogue
|
|
8
|
+
* (`pi/packages/ai/src/utils/overflow.ts:11`). The other 10
|
|
9
|
+
* providers we ship stay deferred until real-world telemetry shows
|
|
10
|
+
* a hit; their error shapes are less stable and a wrong regex would
|
|
11
|
+
* surface as silent overflow loops.
|
|
12
|
+
*/
|
|
13
|
+
function isAnthropicOverflowError(adapterId, message) {
|
|
14
|
+
if (adapterId !== "anthropic")
|
|
15
|
+
return false;
|
|
16
|
+
return (/prompt is too long/i.test(message) ||
|
|
17
|
+
/request_too_large/i.test(message) ||
|
|
18
|
+
/maximum.*context.*length/i.test(message));
|
|
19
|
+
}
|
|
20
|
+
/** Engine default when no caller-supplied or config-supplied value applies. */
|
|
21
|
+
export const DEFAULT_MAX_TURNS = 12;
|
|
22
|
+
/** Default base prompt the engine uses when no `systemBaseOverride` is set.
|
|
23
|
+
* Exported so tests can assert "is this the default or an agent override?" and
|
|
24
|
+
* so the doc + plugin authors can read the exact text. */
|
|
25
|
+
export const DEFAULT_BASE_PROMPT = [
|
|
26
|
+
"You are chances, a terminal coding agent. Use the provided tools to inspect and modify the workspace. Be concise.",
|
|
27
|
+
"",
|
|
28
|
+
"Persistent memory:",
|
|
29
|
+
"- Use memory_save to remember facts that will still matter in *future* conversations: user role/preferences (type: user), corrections you should not need twice (type: feedback), ongoing project context not derivable from code (type: project), or pointers to external systems (type: reference).",
|
|
30
|
+
"- Use scope 'global' for cross-project facts about the user; use 'project' for this-repo context.",
|
|
31
|
+
"- Before saving, check the indexes below (or call memory_list) — if a related entry already exists, update it rather than creating a duplicate. Duplicates waste context budget.",
|
|
32
|
+
"- Use memory_delete to remove outdated entries.",
|
|
33
|
+
"- Do NOT save: code patterns, file paths, git history, debugging recipes, or task-specific scratch — those are derivable from the repo.",
|
|
34
|
+
].join("\n");
|
|
35
|
+
/**
|
|
36
|
+
* Mediator over ai/session/tools/memory. Depends only on interfaces (ports), so
|
|
37
|
+
* any ProviderAdapter or Tool plugs in unchanged. Communicates outward purely by
|
|
38
|
+
* emitting events on the bus — it never imports the TUI.
|
|
39
|
+
*/
|
|
40
|
+
export class AgentEngine {
|
|
41
|
+
opts;
|
|
42
|
+
selection;
|
|
43
|
+
constructor(opts) {
|
|
44
|
+
this.opts = opts;
|
|
45
|
+
// Prefer the caller-supplied selection so they retain a handle for
|
|
46
|
+
// mid-session mutation; fall back to the deprecated scalars for back-compat.
|
|
47
|
+
this.selection =
|
|
48
|
+
opts.selection ??
|
|
49
|
+
new ModelSelection({ provider: opts.preferredProvider, model: opts.preferredModel });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Exposes the mutable selection so callers — typically slash commands like
|
|
53
|
+
* `/model` — can update it between turns. Returning the live instance (not a
|
|
54
|
+
* copy) is intentional: writes must propagate back.
|
|
55
|
+
*/
|
|
56
|
+
getSelection() {
|
|
57
|
+
return this.selection;
|
|
58
|
+
}
|
|
59
|
+
/** Bus-emit wrapper. Three responsibilities (3.4):
|
|
60
|
+
* 1. Suppress lifecycle frames (`turn:*`, `error`) when the engine is a
|
|
61
|
+
* child (`suppressLifecycleEvents=true`). Codex Round-1 MUST-FIX #2.
|
|
62
|
+
* 2. Stamp `agentId/agentName` on tagged event variants when an
|
|
63
|
+
* `agentContext` was supplied. Skipped silently when undefined.
|
|
64
|
+
* 3. Forward to `bus.emit` otherwise — fully transparent for parent engines.
|
|
65
|
+
*/
|
|
66
|
+
emit(event) {
|
|
67
|
+
if (this.opts.suppressLifecycleEvents &&
|
|
68
|
+
(event.type === "turn:start" || event.type === "turn:end" || event.type === "error")) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const ctx = this.opts.agentContext;
|
|
72
|
+
if (ctx) {
|
|
73
|
+
switch (event.type) {
|
|
74
|
+
case "assistant:delta":
|
|
75
|
+
case "assistant:message":
|
|
76
|
+
case "assistant:reset":
|
|
77
|
+
case "tool:call":
|
|
78
|
+
case "tool:permission":
|
|
79
|
+
case "tool:result":
|
|
80
|
+
case "usage":
|
|
81
|
+
// Conditional spread on agentId so sync subagents (which have
|
|
82
|
+
// no registry-issued id — see `agentContext` doc above) emit
|
|
83
|
+
// events without an `agentId: undefined` own-property. Cleaner
|
|
84
|
+
// for subscribers like the 3.6 OTel exporter that introspect
|
|
85
|
+
// keys to decide whether to stamp an attribute.
|
|
86
|
+
this.opts.bus.emit({
|
|
87
|
+
...event,
|
|
88
|
+
...(ctx.agentId !== undefined ? { agentId: ctx.agentId } : {}),
|
|
89
|
+
agentName: ctx.agentName,
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
default:
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.opts.bus.emit(event);
|
|
97
|
+
}
|
|
98
|
+
async runTurn(prompt, token, opts = {}) {
|
|
99
|
+
// (4.1) Isolated subagents: every descendant `await` inside this
|
|
100
|
+
// turn sees `worktreeCwd` as its ambient cwd. Tools resolve
|
|
101
|
+
// `safePath` against it, bash spawns inside it. The outer body
|
|
102
|
+
// delegates straight into the existing implementation; nothing
|
|
103
|
+
// about the rest of `runTurn` needs to know about isolation
|
|
104
|
+
// beyond the `worktreeCwd` peek in `runTool`.
|
|
105
|
+
if (this.opts.worktreeCwd !== undefined) {
|
|
106
|
+
const cwd = this.opts.worktreeCwd;
|
|
107
|
+
void refreshActiveMarker(cwd).catch(() => {
|
|
108
|
+
/* heartbeat is best-effort; failure means the marker was
|
|
109
|
+
* already removed (e.g. parent torn down the worktree) which
|
|
110
|
+
* we surface through the regular tool-error path on the next
|
|
111
|
+
* fs call. */
|
|
112
|
+
});
|
|
113
|
+
return runWithCwd(cwd, () => this.runTurnImpl(prompt, token, opts.expandMentions !== false));
|
|
114
|
+
}
|
|
115
|
+
return this.runTurnImpl(prompt, token, opts.expandMentions !== false);
|
|
116
|
+
}
|
|
117
|
+
async runTurnImpl(prompt, token, expandMentions) {
|
|
118
|
+
const { router, tools, gate, session, plugins, backgroundTasks } = this.opts;
|
|
119
|
+
const turnId = createId("turn");
|
|
120
|
+
// (3.6) Carry the active session id on `turn:start` so the OTel
|
|
121
|
+
// exporter can stamp `chances.gen_ai.session.id` correctly across
|
|
122
|
+
// `/resume` (which swaps SessionManager instances mid-process).
|
|
123
|
+
// Subscribers that don't care about session correlation ignore the
|
|
124
|
+
// field (it's optional in the event union).
|
|
125
|
+
this.emit({ type: "turn:start", turnId, prompt, sessionId: session.id });
|
|
126
|
+
await plugins?.runHook("beforePrompt", { prompt });
|
|
127
|
+
const toolDefs = tools.list().map((t) => ({
|
|
128
|
+
name: t.name,
|
|
129
|
+
description: t.description,
|
|
130
|
+
parameters: t.parameters,
|
|
131
|
+
}));
|
|
132
|
+
const system = this.composeSystem();
|
|
133
|
+
// 3.4: peek pending background task notifications BEFORE composing the
|
|
134
|
+
// turn messages. Each notification becomes part of a single synthetic
|
|
135
|
+
// user-role message prepended to the user's actual prompt so the model
|
|
136
|
+
// sees them at the start of this turn. FIFO ordering. Notifications
|
|
137
|
+
// queued during this turn's stream go to the NEXT turn (deliberate:
|
|
138
|
+
// never inject mid-stream).
|
|
139
|
+
//
|
|
140
|
+
// Codex Round-2 MUST-FIX #3: we PEEK rather than DRAIN here. If the
|
|
141
|
+
// turn cancels / throws before `session.appendTurn` below, the queue
|
|
142
|
+
// is preserved and the next turn re-delivers — the model never sees
|
|
143
|
+
// the notification twice because the prior cancelled turn was not
|
|
144
|
+
// persisted to `session.messages()`. Acknowledgement (queue removal)
|
|
145
|
+
// happens immediately after `session.appendTurn(turnMessages)`
|
|
146
|
+
// succeeds.
|
|
147
|
+
const notifications = backgroundTasks?.peekPendingNotifications() ?? [];
|
|
148
|
+
const notificationIds = notifications.map((n) => n.taskId);
|
|
149
|
+
const turnMessages = [];
|
|
150
|
+
if (notifications.length > 0) {
|
|
151
|
+
const xml = notifications.map(renderTaskNotificationXml).join("\n");
|
|
152
|
+
turnMessages.push({
|
|
153
|
+
role: "user",
|
|
154
|
+
content: [{ type: "text", text: xml }],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// (5.4) Expand `@<server>:<uri>` mentions in directly-typed user input into
|
|
158
|
+
// a synthetic resource message before the prompt. Skipped for submitted /
|
|
159
|
+
// prompt-command text (`expandMentions === false`, codex R1 M3 — that text
|
|
160
|
+
// can be untrusted MCP-server output and must not drive ungated reads). The
|
|
161
|
+
// resolver only resolves exact cached resources (R1 S2) and never throws on
|
|
162
|
+
// a bad ref. Cancellation still propagates (it rethrows AppError(Cancelled)).
|
|
163
|
+
if (expandMentions && this.opts.resolveMcpMentions) {
|
|
164
|
+
const resolved = await this.opts.resolveMcpMentions(prompt, token.signal);
|
|
165
|
+
const xml = renderMcpResourcesXml(resolved);
|
|
166
|
+
if (xml)
|
|
167
|
+
turnMessages.push({ role: "user", content: [{ type: "text", text: xml }] });
|
|
168
|
+
}
|
|
169
|
+
turnMessages.push({ role: "user", content: [{ type: "text", text: prompt }] });
|
|
170
|
+
const result = { text: "", inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
171
|
+
const maxTurns = this.opts.maxTurns ?? this.opts.maxIterations ?? DEFAULT_MAX_TURNS;
|
|
172
|
+
let resolved = false;
|
|
173
|
+
// (3.5 — codex Round-1 MUST-FIX #1) `result.inputTokens` aggregates
|
|
174
|
+
// every `usage` event across the multi-step tool loop. The compactor's
|
|
175
|
+
// threshold check needs the LAST stream's input only — that's what
|
|
176
|
+
// the provider will count for the NEXT request, plus the new user
|
|
177
|
+
// prompt. Tracked separately here; emitted via `usage:turn`.
|
|
178
|
+
let lastRequestInputTokens = 0;
|
|
179
|
+
// (3.5 — codex Round-1 SHOULD-FIX #4) Per-turn flag. Anthropic
|
|
180
|
+
// overflow recovery fires AT MOST ONCE per turn — a second 413 after
|
|
181
|
+
// we already compacted is an actual ceiling we can't paper over.
|
|
182
|
+
let recoveredFromOverflow = false;
|
|
183
|
+
// (3.5) Tracked at the outer scope so the post-turn compaction check
|
|
184
|
+
// can read `route.model`. The for-loop reuses the variable across
|
|
185
|
+
// iterations; we just need the most recent value to query the model
|
|
186
|
+
// descriptor for `contextWindow`.
|
|
187
|
+
let lastRoute;
|
|
188
|
+
for (let i = 0; i < maxTurns; i++) {
|
|
189
|
+
token.throwIfCancelled();
|
|
190
|
+
// Re-read selection per turn so a `/model` switch between turns lands on
|
|
191
|
+
// the next request without rebuilding the engine.
|
|
192
|
+
const choice = this.selection.get();
|
|
193
|
+
const route = router.pick({
|
|
194
|
+
preferredModel: choice.model,
|
|
195
|
+
preferredProvider: choice.provider,
|
|
196
|
+
needsTools: toolDefs.length > 0,
|
|
197
|
+
});
|
|
198
|
+
lastRoute = route;
|
|
199
|
+
const retry = this.opts.retry ?? defaultRetryConfig;
|
|
200
|
+
let textBuffer = "";
|
|
201
|
+
let calls = [];
|
|
202
|
+
let attempt = 0;
|
|
203
|
+
while (true) {
|
|
204
|
+
token.throwIfCancelled();
|
|
205
|
+
textBuffer = "";
|
|
206
|
+
calls = [];
|
|
207
|
+
// (3.5) Reset per attempt — only the LAST successful stream's
|
|
208
|
+
// last `usage.inputTokens` carries forward into the post-turn
|
|
209
|
+
// compactor check.
|
|
210
|
+
let attemptLastInputTokens = 0;
|
|
211
|
+
// (6.5b review) Stage usage in attempt-local accumulators instead of
|
|
212
|
+
// folding it straight into the turn-level `result`. A retryable
|
|
213
|
+
// mid-stream error (e.g. ECONNRESET after a partial stream) discards
|
|
214
|
+
// the attempt and restreams; folding here would double-count tokens
|
|
215
|
+
// and double-emit `usage`. We only merge into `result` + emit once
|
|
216
|
+
// the stream completes (`streamError === null`, below).
|
|
217
|
+
let attemptInputTokens = 0;
|
|
218
|
+
let attemptOutputTokens = 0;
|
|
219
|
+
let attemptCostUsd = 0;
|
|
220
|
+
let streamError = null;
|
|
221
|
+
const stream = route.adapter.stream({ model: route.model.id, system, messages: [...session.messages(), ...turnMessages], tools: toolDefs }, token.signal);
|
|
222
|
+
for await (const event of stream) {
|
|
223
|
+
// Enforce cancellation per-event so a provider that ignores or
|
|
224
|
+
// queues past the AbortSignal can't keep dripping text/tool-calls
|
|
225
|
+
// into a turn the user already abandoned. Particularly important
|
|
226
|
+
// for subagents: the parent's abort must stop the child instantly,
|
|
227
|
+
// not wait until the child stream naturally ends.
|
|
228
|
+
token.throwIfCancelled();
|
|
229
|
+
switch (event.type) {
|
|
230
|
+
case "text-delta":
|
|
231
|
+
textBuffer += event.text;
|
|
232
|
+
this.emit({ type: "assistant:delta", turnId, text: event.text });
|
|
233
|
+
break;
|
|
234
|
+
case "tool-call":
|
|
235
|
+
// Defer the `tool:call` bus emit until the execution loop
|
|
236
|
+
// below — pairs each emit atomically with its matching
|
|
237
|
+
// `tool:result`. Emitting here would leave orphan call frames
|
|
238
|
+
// on the bus whenever the turn aborts between stream-end and
|
|
239
|
+
// tool execution (Ctrl-C) or a retry attempt discards the
|
|
240
|
+
// collected calls and tries again on attempt N+1.
|
|
241
|
+
calls.push(event.call);
|
|
242
|
+
break;
|
|
243
|
+
case "usage": {
|
|
244
|
+
const costUsd = estimateCost(route.model, event.usage);
|
|
245
|
+
// (6.5b review) Accumulate into attempt-local totals; the merge
|
|
246
|
+
// into `result` + the `usage` emit happen once the stream
|
|
247
|
+
// succeeds, so a discarded retry attempt can't double-count.
|
|
248
|
+
attemptInputTokens += event.usage.inputTokens;
|
|
249
|
+
attemptOutputTokens += event.usage.outputTokens;
|
|
250
|
+
attemptCostUsd += costUsd;
|
|
251
|
+
// (3.5) Track most recent stream's last input count for the
|
|
252
|
+
// post-turn compaction threshold check. NOT the aggregate.
|
|
253
|
+
attemptLastInputTokens = event.usage.inputTokens;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case "error":
|
|
257
|
+
// Defer the bus emit until after cancellation check — if the
|
|
258
|
+
// user just hit Ctrl-C, the SDK's abort path surfaces as a
|
|
259
|
+
// stream error and we shouldn't shout "PROVIDER" at them.
|
|
260
|
+
streamError = event.message;
|
|
261
|
+
break;
|
|
262
|
+
case "done":
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
if (streamError !== null)
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
if (streamError === null) {
|
|
269
|
+
// Stream completed successfully — NOW fold this attempt's usage
|
|
270
|
+
// into the turn-level `result` and emit the (aggregated) `usage`
|
|
271
|
+
// frame. Deferred to here so a discarded retry attempt's partial
|
|
272
|
+
// usage never double-counts (6.5b review).
|
|
273
|
+
result.inputTokens += attemptInputTokens;
|
|
274
|
+
result.outputTokens += attemptOutputTokens;
|
|
275
|
+
result.costUsd += attemptCostUsd;
|
|
276
|
+
if (attemptInputTokens > 0 || attemptOutputTokens > 0 || attemptCostUsd > 0) {
|
|
277
|
+
this.emit({
|
|
278
|
+
type: "usage",
|
|
279
|
+
model: route.model.id,
|
|
280
|
+
inputTokens: attemptInputTokens,
|
|
281
|
+
outputTokens: attemptOutputTokens,
|
|
282
|
+
costUsd: attemptCostUsd,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// Persist this attempt's last input-token count for the post-turn
|
|
286
|
+
// compaction check.
|
|
287
|
+
lastRequestInputTokens = attemptLastInputTokens;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
// If the abort signal fired during the stream, the error we just
|
|
291
|
+
// captured is the SDK reacting to the cancellation — treat it as
|
|
292
|
+
// Cancelled rather than misclassifying as a provider error.
|
|
293
|
+
token.throwIfCancelled();
|
|
294
|
+
const decision = classifyProviderError(streamError);
|
|
295
|
+
const terminal = !decision.retryable || attempt >= retry.delaysMs.length;
|
|
296
|
+
if (terminal) {
|
|
297
|
+
// (3.5 — codex Round-1 SHOULD-FIX #4) Anthropic-only reactive
|
|
298
|
+
// overflow recovery. Catches the 413 BEFORE the terminal throw,
|
|
299
|
+
// runs compaction with `reason: "overflow"` (bypasses circuit
|
|
300
|
+
// breaker), and retries the stream once with the compacted
|
|
301
|
+
// history. Wider 10-provider catalogue stays deferred until
|
|
302
|
+
// telemetry shows a real-world miss.
|
|
303
|
+
if (this.opts.compactor &&
|
|
304
|
+
!recoveredFromOverflow &&
|
|
305
|
+
isAnthropicOverflowError(route.adapter.id, streamError)) {
|
|
306
|
+
recoveredFromOverflow = true;
|
|
307
|
+
try {
|
|
308
|
+
const recovery = await this.opts.compactor.compact("overflow", token.signal);
|
|
309
|
+
if (recovery.ok) {
|
|
310
|
+
// Reset the attempt counter so we get the full retry
|
|
311
|
+
// budget against the now-smaller request, AND clear the
|
|
312
|
+
// accumulated message buffer that the failed attempt
|
|
313
|
+
// wrote into `turnMessages`. The retry rebuilds from
|
|
314
|
+
// `session.messages()` (which now reflects compaction)
|
|
315
|
+
// plus this turn's prepended user/notification messages.
|
|
316
|
+
// (6.5b follow-up) Mirror the normal retry path's partial-undo:
|
|
317
|
+
// if this attempt streamed partial text before the overflow, drop
|
|
318
|
+
// it so the post-compaction restream doesn't append onto a stale
|
|
319
|
+
// partial. A 413 usually precedes any delta, so this is typically
|
|
320
|
+
// a no-op — emitted only when text was actually shown.
|
|
321
|
+
if (textBuffer.length > 0) {
|
|
322
|
+
this.emit({ type: "assistant:reset", turnId });
|
|
323
|
+
}
|
|
324
|
+
attempt = 0;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
// Compactor.compact never throws by contract, but be defensive:
|
|
330
|
+
// a malformed Compactor implementation shouldn't break the
|
|
331
|
+
// original error path.
|
|
332
|
+
this.emit({
|
|
333
|
+
type: "log",
|
|
334
|
+
level: "warn",
|
|
335
|
+
message: `overflow compaction unexpectedly threw: ${e.message ?? e}`,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Emit the bus `error` ONLY when we're about to throw. Emitting on
|
|
340
|
+
// every retry attempt would cause `runPrompt`'s `lastError`
|
|
341
|
+
// listener to record a transient failure as the turn's exit code
|
|
342
|
+
// even after a later attempt succeeded (codex re-review finding).
|
|
343
|
+
// Subagent engines suppress this emit — see `suppressTerminalErrors`.
|
|
344
|
+
if (!this.opts.suppressTerminalErrors) {
|
|
345
|
+
this.emit({ type: "error", code: "PROVIDER", message: streamError });
|
|
346
|
+
}
|
|
347
|
+
throw new AppError(ErrorCode.Provider, `Provider error (${decision.reason}): ${streamError}`);
|
|
348
|
+
}
|
|
349
|
+
const delayMs = retry.delaysMs[attempt] ?? 0;
|
|
350
|
+
this.emit({
|
|
351
|
+
type: "log",
|
|
352
|
+
level: "warn",
|
|
353
|
+
message: `provider stream errored (${decision.reason}); retry ${attempt + 1}/${retry.delaysMs.length} after ${delayMs}ms; original: ${streamError}`,
|
|
354
|
+
});
|
|
355
|
+
// (6.5b review) If this attempt already streamed partial assistant text
|
|
356
|
+
// to the bus, the upcoming restream would APPEND a fresh copy on top
|
|
357
|
+
// (consumers don't replace) — duplicating it on screen. Tell consumers
|
|
358
|
+
// to drop the in-flight partial first. `usage`/`tool-call` are deferred
|
|
359
|
+
// (not yet on the bus), so only `textBuffer` needs undoing.
|
|
360
|
+
if (textBuffer.length > 0) {
|
|
361
|
+
this.emit({ type: "assistant:reset", turnId });
|
|
362
|
+
}
|
|
363
|
+
attempt += 1;
|
|
364
|
+
await sleepCancellable(delayMs, token);
|
|
365
|
+
}
|
|
366
|
+
if (calls.length === 0) {
|
|
367
|
+
const content = [{ type: "text", text: textBuffer }];
|
|
368
|
+
turnMessages.push({ role: "assistant", content });
|
|
369
|
+
result.text = textBuffer;
|
|
370
|
+
this.emit({ type: "assistant:message", turnId, text: textBuffer });
|
|
371
|
+
await safeRunHook(plugins, "afterResponse", { text: textBuffer }, this.opts.bus);
|
|
372
|
+
resolved = true;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
// Record the assistant message that requested the tools.
|
|
376
|
+
const assistantContent = [];
|
|
377
|
+
if (textBuffer)
|
|
378
|
+
assistantContent.push({ type: "text", text: textBuffer });
|
|
379
|
+
for (const call of calls) {
|
|
380
|
+
assistantContent.push({ type: "tool-call", callId: call.callId, name: call.name, args: call.args });
|
|
381
|
+
}
|
|
382
|
+
turnMessages.push({ role: "assistant", content: assistantContent });
|
|
383
|
+
// Execute each tool through the permission gate, then feed results back.
|
|
384
|
+
for (const call of calls) {
|
|
385
|
+
// Check cancellation before each tool — a long batch of tool-calls
|
|
386
|
+
// from one model turn shouldn't keep running after the user aborts.
|
|
387
|
+
token.throwIfCancelled();
|
|
388
|
+
// Emit `tool:call` here (not in the stream loop) so each call is
|
|
389
|
+
// paired with its `tool:result` from runTool — keeps the bus
|
|
390
|
+
// observably balanced for subscribers (TUI, NDJSON, telemetry).
|
|
391
|
+
this.emit({ type: "tool:call", callId: call.callId, name: call.name, args: call.args });
|
|
392
|
+
const outcome = await this.runTool(call, token);
|
|
393
|
+
turnMessages.push({
|
|
394
|
+
role: "tool",
|
|
395
|
+
content: [{ type: "tool-result", callId: call.callId, name: call.name, output: outcome.output, ok: outcome.ok }],
|
|
396
|
+
});
|
|
397
|
+
// Round 3 codex SHOULD-FIX: check cancellation AFTER each tool
|
|
398
|
+
// result too. A tool that catches cancellation internally and
|
|
399
|
+
// returns `ok:false` (e.g. `bash` returning `(cancelled)`) does
|
|
400
|
+
// NOT re-throw, so without this check the loop would continue
|
|
401
|
+
// to the next turn and could exhaust `maxTurns`, surfacing as
|
|
402
|
+
// a misleading `PROVIDER: Reached maximum number of turns`
|
|
403
|
+
// instead of the user's actual `Cancelled` intent.
|
|
404
|
+
token.throwIfCancelled();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
session.appendTurn(turnMessages);
|
|
408
|
+
// Codex Round-2 MUST-FIX #3: ack notifications AFTER appendTurn so a
|
|
409
|
+
// cancellation between peek and persist leaves the queue intact.
|
|
410
|
+
if (notificationIds.length > 0) {
|
|
411
|
+
backgroundTasks?.acknowledgeNotifications(notificationIds);
|
|
412
|
+
}
|
|
413
|
+
// (3.5) Per-turn aggregate event. Lifecycle suppression honored —
|
|
414
|
+
// child engines (with `suppressLifecycleEvents`) skip this too.
|
|
415
|
+
// `lastRequestInputTokens` feeds the post-turn compaction check
|
|
416
|
+
// (codex Round-1 MUST-FIX #1) AND 3.6 OTel one-span-per-turn.
|
|
417
|
+
if (!this.opts.suppressLifecycleEvents) {
|
|
418
|
+
this.opts.bus.emit({
|
|
419
|
+
type: "usage:turn",
|
|
420
|
+
turnId,
|
|
421
|
+
inputTokens: result.inputTokens,
|
|
422
|
+
outputTokens: result.outputTokens,
|
|
423
|
+
costUsd: result.costUsd,
|
|
424
|
+
lastRequestInputTokens,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
// (3.5 — codex Round-1 MUST-FIX #4) Threshold-triggered compaction.
|
|
428
|
+
// Runs BEFORE `turn:end` so TUI subscribers don't see the turn as
|
|
429
|
+
// "over" while session is still mutating. Compaction emits its own
|
|
430
|
+
// `compaction:start` / `compaction:end` frames inside this await.
|
|
431
|
+
// The compactor itself swallows all failures into ok:false (never
|
|
432
|
+
// throws by contract); `Cancelled` propagates as `cancelled` reason.
|
|
433
|
+
if (this.opts.compactor && resolved && lastRoute) {
|
|
434
|
+
const should = this.opts.compactor.shouldCompact({
|
|
435
|
+
lastRequestInputTokens,
|
|
436
|
+
model: lastRoute.model,
|
|
437
|
+
});
|
|
438
|
+
if (should) {
|
|
439
|
+
await this.opts.compactor.compact("threshold", token.signal);
|
|
440
|
+
// (codex Round-2 MUST-FIX #2) The compactor swallows all
|
|
441
|
+
// failures into `ok:false`, including a `cancelled` reason
|
|
442
|
+
// when `signal.aborted` fired during summarization. Without
|
|
443
|
+
// a `throwIfCancelled` here the cancelled compaction would
|
|
444
|
+
// be invisible — the engine would emit `turn:end` and
|
|
445
|
+
// return normally, defeating the user's Ctrl-C. Re-checking
|
|
446
|
+
// the token after the await propagates Cancelled to the
|
|
447
|
+
// caller exactly like an in-stream cancellation would.
|
|
448
|
+
token.throwIfCancelled();
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
// Reset the compactor's "compacted-recently" latch so the
|
|
452
|
+
// NEXT turn's threshold check can re-evaluate.
|
|
453
|
+
this.opts.compactor.noteNonCompactionTurn();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
this.emit({ type: "turn:end", turnId });
|
|
457
|
+
if (!resolved) {
|
|
458
|
+
// Loop exhausted the turn budget without the model returning a final
|
|
459
|
+
// answer. Match claude-code's terminal-error pattern (`QueryEngine.ts:914`)
|
|
460
|
+
// — emit a bus error and throw so the caller sees a concrete signal
|
|
461
|
+
// instead of an empty result. The turn is still persisted above so
|
|
462
|
+
// `/resume` can pick up the partial work.
|
|
463
|
+
// Subagent engines suppress this emit — see `suppressTerminalErrors`.
|
|
464
|
+
const message = `Reached maximum number of turns (${maxTurns})`;
|
|
465
|
+
if (!this.opts.suppressTerminalErrors) {
|
|
466
|
+
this.emit({ type: "error", code: "PROVIDER", message });
|
|
467
|
+
}
|
|
468
|
+
throw new AppError(ErrorCode.Provider, message);
|
|
469
|
+
}
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
async runTool(call, token) {
|
|
473
|
+
const { bus, tools, gate, plugins, workspaceRoot } = this.opts;
|
|
474
|
+
const tool = tools.get(call.name);
|
|
475
|
+
if (!tool) {
|
|
476
|
+
const output = `Unknown tool: ${call.name}`;
|
|
477
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output });
|
|
478
|
+
return { ok: false, output };
|
|
479
|
+
}
|
|
480
|
+
// (4.1) Inside an isolated subagent's `runTurn`, `runWithCwd` has
|
|
481
|
+
// pushed `worktreeCwd` onto AsyncLocalStorage. Both `workspaceRoot`
|
|
482
|
+
// and `cwd` must flip together so `safePath` resolves against the
|
|
483
|
+
// worktree tree, not the parent's. When no override is active, the
|
|
484
|
+
// value is `undefined` and we fall back to the parent's
|
|
485
|
+
// `workspaceRoot` (unchanged 3.x behaviour).
|
|
486
|
+
//
|
|
487
|
+
// `parentWorkspaceRoot` (Round-2 SHOULD-FIX #4) carries the
|
|
488
|
+
// non-isolated workspace through so artifact persistence paths
|
|
489
|
+
// (`bash` oversized-output dump, etc.) land in the parent's
|
|
490
|
+
// `.chances/` rather than the worktree's — otherwise a
|
|
491
|
+
// clean-removed worktree would invalidate the path the tool just
|
|
492
|
+
// surfaced to the model.
|
|
493
|
+
const effectiveRoot = this.opts.worktreeCwd ?? workspaceRoot;
|
|
494
|
+
const ctx = {
|
|
495
|
+
workspaceRoot: effectiveRoot,
|
|
496
|
+
cwd: effectiveRoot,
|
|
497
|
+
signal: token.signal,
|
|
498
|
+
parentWorkspaceRoot: workspaceRoot,
|
|
499
|
+
lsp: this.opts.lsp, // (4.2) codex Round-1 MUST-FIX #1
|
|
500
|
+
callId: call.callId, // (5.7) artifact correlation — see ToolContext.callId
|
|
501
|
+
};
|
|
502
|
+
try {
|
|
503
|
+
await plugins?.runHook("beforeToolCall", { name: call.name, args: call.args });
|
|
504
|
+
// (5.3 codex Round-1 MUST-FIX #1) Session mode-block (plan's read-only
|
|
505
|
+
// floor) is enforced at the CATEGORY level here, BEFORE the per-op
|
|
506
|
+
// `permission()` hook. This is the only place that catches an op which
|
|
507
|
+
// returns `null` to bypass the gate (e.g. `pty.read`): without this,
|
|
508
|
+
// plan mode could be silently bypassed by such ops. Auto-approve /
|
|
509
|
+
// deny-floor / prompt all stay inside `gate.evaluate()` below.
|
|
510
|
+
const modeBlock = gate.blockByMode(tool.category);
|
|
511
|
+
if (modeBlock) {
|
|
512
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output: modeBlock.reason });
|
|
513
|
+
await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: false, output: modeBlock.reason }, bus);
|
|
514
|
+
return { ok: false, output: modeBlock.reason };
|
|
515
|
+
}
|
|
516
|
+
// (5.1 codex Round-2 MUST-FIX #1) — per-op permission hook. When
|
|
517
|
+
// a tool implements `permission()`, it can:
|
|
518
|
+
// - return `null` to bypass the gate entirely (e.g. `pty.read` /
|
|
519
|
+
// `pty.resize` / `pty.list` — already approved at session
|
|
520
|
+
// start),
|
|
521
|
+
// - return a `PermissionRequest` whose `cacheKey` scopes the
|
|
522
|
+
// positive-decision cache below `tool.name` (e.g.
|
|
523
|
+
// `pty.kill.SIGTERM/<sid>` so approving SIGTERM doesn't
|
|
524
|
+
// pre-approve SIGKILL).
|
|
525
|
+
// When unimplemented (the legacy path used by all 10 v5.0
|
|
526
|
+
// builtins), fall back to the original {name, category,
|
|
527
|
+
// summarize(args)} shape.
|
|
528
|
+
const decision = tool.permission ? tool.permission(call.args, ctx) : undefined;
|
|
529
|
+
if (decision === null) {
|
|
530
|
+
// Explicit bypass — skip the `tool:permission` event AND the
|
|
531
|
+
// gate. Subscribers (TUI, OTel) still see `tool:call` →
|
|
532
|
+
// `tool:result` for the op; only the permission frame is
|
|
533
|
+
// suppressed because there isn't one.
|
|
534
|
+
const outcome = await tool.execute(call.args, ctx);
|
|
535
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: outcome.ok, output: outcome.output });
|
|
536
|
+
await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: outcome.ok, output: outcome.output }, bus);
|
|
537
|
+
return outcome;
|
|
538
|
+
}
|
|
539
|
+
// (5.10b) QUESTION channel — fail-closed. A tool's `permission()` may emit
|
|
540
|
+
// a `question` request (the `ask_user_question` tool does), which skips
|
|
541
|
+
// `gate.evaluate()`'s allow/deny precedence. That bypass is safe ONLY for
|
|
542
|
+
// the dedicated read-only asker; any OTHER tool reaching here with a
|
|
543
|
+
// question request would be escaping authorization, so we refuse WITHOUT
|
|
544
|
+
// executing (codex R1 MUST-FIX #1). Double-guard: exact name + read-only
|
|
545
|
+
// category.
|
|
546
|
+
if (decision?.kind === "question") {
|
|
547
|
+
if (tool.name !== ASK_USER_QUESTION_TOOL_NAME || !READONLY_CATEGORIES.has(tool.category)) {
|
|
548
|
+
const output = "refused: the question channel is reserved for the read-only ask tool";
|
|
549
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output });
|
|
550
|
+
await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: false, output }, bus);
|
|
551
|
+
return { ok: false, output };
|
|
552
|
+
}
|
|
553
|
+
this.emit({
|
|
554
|
+
type: "tool:permission",
|
|
555
|
+
callId: call.callId,
|
|
556
|
+
name: call.name,
|
|
557
|
+
summary: `${decision.questions.length} question(s)`,
|
|
558
|
+
});
|
|
559
|
+
// The answers come back through the resolver and ride into `execute` on
|
|
560
|
+
// the ctx (never serialized by the engine — the tool owns the wording).
|
|
561
|
+
const questionDecision = await gate.ask({ ...decision, callId: call.callId });
|
|
562
|
+
const outcome = await tool.execute(call.args, { ...ctx, questionDecision });
|
|
563
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: outcome.ok, output: outcome.output });
|
|
564
|
+
await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: outcome.ok, output: outcome.output }, bus);
|
|
565
|
+
return outcome;
|
|
566
|
+
}
|
|
567
|
+
const summary = decision?.summary ?? tool.summarize(call.args, ctx);
|
|
568
|
+
this.emit({ type: "tool:permission", callId: call.callId, name: call.name, summary });
|
|
569
|
+
// (5.2 codex Round-1 MUST-FIX #3) Stamp the call id onto the request
|
|
570
|
+
// so an out-of-process resolver (RPC / ACP host) can ship a
|
|
571
|
+
// self-contained `request_permission` frame the client joins to the
|
|
572
|
+
// preceding `tool_call.callId`. In-process resolvers ignore it.
|
|
573
|
+
const req = {
|
|
574
|
+
...(decision ?? {
|
|
575
|
+
name: tool.name,
|
|
576
|
+
category: tool.category,
|
|
577
|
+
summary,
|
|
578
|
+
args: call.args,
|
|
579
|
+
}),
|
|
580
|
+
callId: call.callId,
|
|
581
|
+
};
|
|
582
|
+
// (5.3) `evaluate()` (not `check()`) so a denial can carry the user's
|
|
583
|
+
// deny-with-feedback text or a policy/mode reason into the tool result —
|
|
584
|
+
// the model then adapts instead of blindly retrying.
|
|
585
|
+
const evaluation = await gate.evaluate(req);
|
|
586
|
+
if (!evaluation.allow) {
|
|
587
|
+
const output = evaluation.feedback
|
|
588
|
+
? `Permission denied by user. User feedback: ${evaluation.feedback}`
|
|
589
|
+
: (evaluation.reason ?? "Permission denied by policy/user");
|
|
590
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output });
|
|
591
|
+
await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: false, output }, bus);
|
|
592
|
+
return { ok: false, output };
|
|
593
|
+
}
|
|
594
|
+
const outcome = await tool.execute(call.args, ctx);
|
|
595
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: outcome.ok, output: outcome.output });
|
|
596
|
+
await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: outcome.ok, output: outcome.output }, bus);
|
|
597
|
+
return outcome;
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
// Cancellation must terminate the whole turn — never swallow it as a tool
|
|
601
|
+
// error. But we DO emit a synthetic `tool:result` so the bus stays paired
|
|
602
|
+
// (every `tool:call` event has its matching result, even on Ctrl-C).
|
|
603
|
+
if (e instanceof AppError && e.code === ErrorCode.Cancelled) {
|
|
604
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output: "Cancelled" });
|
|
605
|
+
throw e;
|
|
606
|
+
}
|
|
607
|
+
if (token.isCancelled) {
|
|
608
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output: "Cancelled" });
|
|
609
|
+
throw new AppError(ErrorCode.Cancelled, "Operation cancelled");
|
|
610
|
+
}
|
|
611
|
+
const output = formatToolError(e);
|
|
612
|
+
this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output });
|
|
613
|
+
this.emit({ type: "log", level: "warn", message: `tool ${call.name} failed: ${output}` });
|
|
614
|
+
await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: false, output }, bus);
|
|
615
|
+
return { ok: false, output };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
composeSystem() {
|
|
619
|
+
const base = this.opts.systemBaseOverride ?? DEFAULT_BASE_PROMPT;
|
|
620
|
+
const memoryContext = this.opts.memory?.asSystemContext();
|
|
621
|
+
const isolation = this.opts.worktreeCwd ? ISOLATED_WORKTREE_NOTICE : undefined;
|
|
622
|
+
// (5.3) Plan mode is read-only — without this notice the model would keep
|
|
623
|
+
// attempting mutations and hitting denials. Read per-turn so toggling plan
|
|
624
|
+
// on/off (Shift+Tab) lands on the next turn, mirroring model selection.
|
|
625
|
+
const plan = this.opts.getApprovalMode?.() === "plan" ? PLAN_MODE_NOTICE : undefined;
|
|
626
|
+
const parts = [base];
|
|
627
|
+
if (memoryContext)
|
|
628
|
+
parts.push(memoryContext);
|
|
629
|
+
if (isolation)
|
|
630
|
+
parts.push(isolation);
|
|
631
|
+
if (plan)
|
|
632
|
+
parts.push(plan);
|
|
633
|
+
return parts.join("\n\n");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/** (5.3) Inserted into the system prompt while the session approval mode is
|
|
637
|
+
* `plan`. Tells the model it is read-only and should produce a plan instead of
|
|
638
|
+
* editing — the same posture claude-code's plan mode enforces. */
|
|
639
|
+
export const PLAN_MODE_NOTICE = [
|
|
640
|
+
"Approval mode: PLAN (read-only).",
|
|
641
|
+
"- You may read files, search, and inspect, but file writes, shell commands, MCP tools, and network fetches are BLOCKED and will be denied.",
|
|
642
|
+
"- Do not attempt mutating tools. Investigate, then present a concise, actionable plan for the user to review.",
|
|
643
|
+
"- The user exits plan mode with Shift+Tab (or /approval) when ready to execute.",
|
|
644
|
+
].join("\n");
|
|
645
|
+
/** (4.1) Inserted at the END of the child engine's system prompt when
|
|
646
|
+
* `worktreeCwd` is set. Tells the model two load-bearing facts: the
|
|
647
|
+
* worktree starts from HEAD (parent's uncommitted state is invisible),
|
|
648
|
+
* and writes are sandboxed (parent sees them as a diff after the task
|
|
649
|
+
* completes, not live). Round-1 SHOULD-FIX #2 wording. */
|
|
650
|
+
export const ISOLATED_WORKTREE_NOTICE = [
|
|
651
|
+
"Isolation: you are running in an isolated git worktree.",
|
|
652
|
+
"- This worktree starts from the parent agent's current HEAD; any uncommitted edits in the parent's working tree are NOT visible here.",
|
|
653
|
+
"- Files you write/edit/bash-modify here are sandboxed and not visible to the parent until your task completes; the parent will see your final diff once you return.",
|
|
654
|
+
].join("\n");
|
|
655
|
+
function formatToolError(e) {
|
|
656
|
+
if (e instanceof AppError)
|
|
657
|
+
return `${e.code}: ${e.message}`;
|
|
658
|
+
if (e instanceof Error)
|
|
659
|
+
return e.message || e.name || "Error";
|
|
660
|
+
return String(e);
|
|
661
|
+
}
|
|
662
|
+
/** Maximum bytes of `result` text included verbatim in an injected
|
|
663
|
+
* notification XML block. Beyond this, we truncate and tell the model the
|
|
664
|
+
* full result is still in the registry by id. Counted in UTF-16 code
|
|
665
|
+
* units rather than bytes — close enough for a 16 KiB-ish budget, and
|
|
666
|
+
* cheap to compute without a separate encoder pass. */
|
|
667
|
+
const NOTIFICATION_RESULT_BUDGET = 16 * 1024;
|
|
668
|
+
/** Control characters to strip from any text we inject. Keeps the XML
|
|
669
|
+
* payload terminal-safe and matches the C0+DEL strip used in
|
|
670
|
+
* `@chances-ai/agents`'s `renderCatalogForToolDescription`. */
|
|
671
|
+
const NOTIFICATION_CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
672
|
+
/** Renders one `<task-notification>` XML block, fully entity-escaped so
|
|
673
|
+
* `result`/`summary` text containing `<`, `>`, `&`, `"`, `'` can't break
|
|
674
|
+
* the surrounding tag structure or impersonate a different role.
|
|
675
|
+
*
|
|
676
|
+
* This is the format the model receives at the top of its next turn
|
|
677
|
+
* after a background subagent completes. The shape matches the one we
|
|
678
|
+
* (chances-cli's own host) observe receiving from background research
|
|
679
|
+
* subagents — claude-code's `<task_notification>` convention, with our
|
|
680
|
+
* hyphen-separated tag style.
|
|
681
|
+
*/
|
|
682
|
+
export function renderTaskNotificationXml(n) {
|
|
683
|
+
let result = n.result.replace(NOTIFICATION_CONTROL_RE, "");
|
|
684
|
+
let resultSuffix = "";
|
|
685
|
+
if (result.length > NOTIFICATION_RESULT_BUDGET) {
|
|
686
|
+
result = result.slice(0, NOTIFICATION_RESULT_BUDGET);
|
|
687
|
+
resultSuffix = `\n[…truncated, full result still in registry at task_id=${n.taskId}]`;
|
|
688
|
+
}
|
|
689
|
+
const summary = n.summary.replace(NOTIFICATION_CONTROL_RE, "");
|
|
690
|
+
return [
|
|
691
|
+
"<task-notification>",
|
|
692
|
+
` <task-id>${xmlEscape(n.taskId)}</task-id>`,
|
|
693
|
+
` <name>${xmlEscape(n.name)}</name>`,
|
|
694
|
+
` <status>${xmlEscape(n.status)}</status>`,
|
|
695
|
+
` <summary>${xmlEscape(summary)}</summary>`,
|
|
696
|
+
` <result>${xmlEscape(result)}${xmlEscape(resultSuffix)}</result>`,
|
|
697
|
+
` <duration-ms>${n.durationMs.toString()}</duration-ms>`,
|
|
698
|
+
"</task-notification>",
|
|
699
|
+
].join("\n");
|
|
700
|
+
}
|
|
701
|
+
function xmlEscape(s) {
|
|
702
|
+
return s
|
|
703
|
+
.replace(/&/g, "&")
|
|
704
|
+
.replace(/</g, "<")
|
|
705
|
+
.replace(/>/g, ">")
|
|
706
|
+
.replace(/"/g, """)
|
|
707
|
+
.replace(/'/g, "'");
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* (5.4) Build the synthetic resource message for `@<server>:<uri>` mentions.
|
|
711
|
+
* EVERY inserted field — server, uri, body, note — is XML-escaped (codex R1 M5:
|
|
712
|
+
* a malicious resource could otherwise close the tag and inject instructions),
|
|
713
|
+
* exactly like `renderTaskNotificationXml`. The resolver already stripped
|
|
714
|
+
* controls + redacted the body. Returns "" when nothing resolved (the caller
|
|
715
|
+
* skips the message entirely).
|
|
716
|
+
*/
|
|
717
|
+
export function renderMcpResourcesXml(resolved) {
|
|
718
|
+
if (resolved.length === 0)
|
|
719
|
+
return "";
|
|
720
|
+
const blocks = resolved.map((r) => {
|
|
721
|
+
const attrs = `server="${xmlEscape(r.server)}" uri="${xmlEscape(r.uri)}"`;
|
|
722
|
+
const body = r.text !== undefined ? xmlEscape(r.text) : `[${xmlEscape(r.note ?? "unavailable")}]`;
|
|
723
|
+
return ` <mcp-resource ${attrs}>\n${body}\n </mcp-resource>`;
|
|
724
|
+
});
|
|
725
|
+
return ["<mcp-resources>", ...blocks, "</mcp-resources>"].join("\n");
|
|
726
|
+
}
|
|
727
|
+
async function sleepCancellable(ms, token) {
|
|
728
|
+
if (ms <= 0) {
|
|
729
|
+
token.throwIfCancelled();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
await new Promise((resolve, reject) => {
|
|
733
|
+
let timer;
|
|
734
|
+
const abort = () => {
|
|
735
|
+
clearTimeout(timer);
|
|
736
|
+
token.signal.removeEventListener("abort", abort);
|
|
737
|
+
reject(new AppError(ErrorCode.Cancelled, "Operation cancelled"));
|
|
738
|
+
};
|
|
739
|
+
timer = setTimeout(() => {
|
|
740
|
+
token.signal.removeEventListener("abort", abort);
|
|
741
|
+
resolve();
|
|
742
|
+
}, ms);
|
|
743
|
+
if (token.signal.aborted)
|
|
744
|
+
abort();
|
|
745
|
+
else
|
|
746
|
+
token.signal.addEventListener("abort", abort);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Runs a plugin hook without letting plugin bugs derail the agent. Failures are
|
|
751
|
+
* logged on the bus and swallowed — plugins must use their hook return semantics
|
|
752
|
+
* (e.g. throw from beforeToolCall) to block; once a tool has run, an afterHook
|
|
753
|
+
* crash should never undo it.
|
|
754
|
+
*/
|
|
755
|
+
async function safeRunHook(plugins, name, input, bus) {
|
|
756
|
+
if (!plugins)
|
|
757
|
+
return;
|
|
758
|
+
try {
|
|
759
|
+
await plugins.runHook(name, input);
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
if (e instanceof AppError && e.code === ErrorCode.Cancelled)
|
|
763
|
+
throw e;
|
|
764
|
+
bus.emit({ type: "log", level: "warn", message: `plugin hook ${String(name)} threw: ${formatToolError(e)}` });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
//# sourceMappingURL=engine.js.map
|