open-multi-agent-kit 0.95.1 → 0.96.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/CHANGELOG.md +37 -0
- package/README.md +16 -1
- package/dist/approvals/approval-receipt-store.d.ts +18 -0
- package/dist/approvals/approval-receipt-store.d.ts.map +1 -0
- package/dist/approvals/approval-receipt-store.js +194 -0
- package/dist/approvals/approval-receipt-store.js.map +1 -0
- package/dist/approvals/approval-receipt.d.ts +43 -0
- package/dist/approvals/approval-receipt.d.ts.map +1 -0
- package/dist/approvals/approval-receipt.js +208 -0
- package/dist/approvals/approval-receipt.js.map +1 -0
- package/dist/approvals/approval-verifier.d.ts +17 -0
- package/dist/approvals/approval-verifier.d.ts.map +1 -0
- package/dist/approvals/approval-verifier.js +23 -0
- package/dist/approvals/approval-verifier.js.map +1 -0
- package/dist/approvals/plannotator-approval-bridge.d.ts +58 -0
- package/dist/approvals/plannotator-approval-bridge.d.ts.map +1 -0
- package/dist/approvals/plannotator-approval-bridge.js +169 -0
- package/dist/approvals/plannotator-approval-bridge.js.map +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/package-doctor-cli.d.ts +13 -0
- package/dist/commands/package-doctor-cli.d.ts.map +1 -0
- package/dist/commands/package-doctor-cli.js +80 -0
- package/dist/commands/package-doctor-cli.js.map +1 -0
- package/dist/commands/router-feedback-cli.d.ts +11 -0
- package/dist/commands/router-feedback-cli.d.ts.map +1 -0
- package/dist/commands/router-feedback-cli.js +99 -0
- package/dist/commands/router-feedback-cli.js.map +1 -0
- package/dist/commands/stats-cli.d.ts +20 -0
- package/dist/commands/stats-cli.d.ts.map +1 -0
- package/dist/commands/stats-cli.js +99 -0
- package/dist/commands/stats-cli.js.map +1 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +4 -1
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +99 -31
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +624 -216
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +8 -2
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +9 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -1
- package/dist/core/compaction/transaction.js +17 -2
- package/dist/core/compaction/transaction.js.map +1 -1
- package/dist/core/context-budget-governor-v2.d.ts +1 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +1 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt.js +18 -2
- package/dist/core/context-budget-system-prompt.js.map +1 -1
- package/dist/core/context-budget-v2-cache-disk.d.ts +109 -0
- package/dist/core/context-budget-v2-cache-disk.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-disk.js +343 -0
- package/dist/core/context-budget-v2-cache-disk.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +22 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +4 -4
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache.d.ts +1 -0
- package/dist/core/context-budget-v2-cache.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache.js +1 -0
- package/dist/core/context-budget-v2-cache.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +17 -14
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +1 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +45 -22
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/mcp/client.d.ts +105 -0
- package/dist/core/mcp/client.d.ts.map +1 -0
- package/dist/core/mcp/client.js +198 -0
- package/dist/core/mcp/client.js.map +1 -0
- package/dist/core/mcp/config.d.ts +23 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +89 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/index.d.ts +15 -0
- package/dist/core/mcp/index.d.ts.map +1 -0
- package/dist/core/mcp/index.js +15 -0
- package/dist/core/mcp/index.js.map +1 -0
- package/dist/core/mcp/manager.d.ts +84 -0
- package/dist/core/mcp/manager.d.ts.map +1 -0
- package/dist/core/mcp/manager.js +173 -0
- package/dist/core/mcp/manager.js.map +1 -0
- package/dist/core/mcp/protocol.d.ts +59 -0
- package/dist/core/mcp/protocol.d.ts.map +1 -0
- package/dist/core/mcp/protocol.js +103 -0
- package/dist/core/mcp/protocol.js.map +1 -0
- package/dist/core/mcp/stdio-transport.d.ts +63 -0
- package/dist/core/mcp/stdio-transport.d.ts.map +1 -0
- package/dist/core/mcp/stdio-transport.js +132 -0
- package/dist/core/mcp/stdio-transport.js.map +1 -0
- package/dist/core/mcp/tools.d.ts +47 -0
- package/dist/core/mcp/tools.d.ts.map +1 -0
- package/dist/core/mcp/tools.js +102 -0
- package/dist/core/mcp/tools.js.map +1 -0
- package/dist/core/package-archive.d.ts +3 -0
- package/dist/core/package-archive.d.ts.map +1 -0
- package/dist/core/package-archive.js +161 -0
- package/dist/core/package-archive.js.map +1 -0
- package/dist/core/package-doctor-source-scan.d.ts +13 -0
- package/dist/core/package-doctor-source-scan.d.ts.map +1 -0
- package/dist/core/package-doctor-source-scan.js +124 -0
- package/dist/core/package-doctor-source-scan.js.map +1 -0
- package/dist/core/package-doctor.d.ts +41 -0
- package/dist/core/package-doctor.d.ts.map +1 -0
- package/dist/core/package-doctor.js +145 -0
- package/dist/core/package-doctor.js.map +1 -0
- package/dist/core/package-manager.d.ts +10 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +175 -42
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/package-manifest.d.ts +26 -0
- package/dist/core/package-manifest.d.ts.map +1 -0
- package/dist/core/package-manifest.js +85 -0
- package/dist/core/package-manifest.js.map +1 -0
- package/dist/core/pi-compat.d.ts +5 -0
- package/dist/core/pi-compat.d.ts.map +1 -0
- package/dist/core/pi-compat.js +18 -0
- package/dist/core/pi-compat.js.map +1 -0
- package/dist/core/provider-resilience.d.ts +7 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +11 -0
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/reasoning-router-bias.d.ts +10 -14
- package/dist/core/reasoning-router-bias.d.ts.map +1 -1
- package/dist/core/reasoning-router-bias.js +22 -15
- package/dist/core/reasoning-router-bias.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts +9 -1
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +20 -5
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/redaction.d.ts +2 -5
- package/dist/core/redaction.d.ts.map +1 -1
- package/dist/core/redaction.js +14 -1
- package/dist/core/redaction.js.map +1 -1
- package/dist/core/repository-learning-scope.d.ts +10 -0
- package/dist/core/repository-learning-scope.d.ts.map +1 -0
- package/dist/core/repository-learning-scope.js +42 -0
- package/dist/core/repository-learning-scope.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +16 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/router-feedback-collector.d.ts +2 -1
- package/dist/core/router-feedback-collector.d.ts.map +1 -1
- package/dist/core/router-feedback-collector.js +2 -2
- package/dist/core/router-feedback-collector.js.map +1 -1
- package/dist/core/runtime-provenance.d.ts +54 -0
- package/dist/core/runtime-provenance.d.ts.map +1 -0
- package/dist/core/runtime-provenance.js +115 -0
- package/dist/core/runtime-provenance.js.map +1 -0
- package/dist/core/sandbox/backend.d.ts.map +1 -1
- package/dist/core/sandbox/backend.js +39 -21
- package/dist/core/sandbox/backend.js.map +1 -1
- package/dist/core/sandbox/default-policy.d.ts +4 -5
- package/dist/core/sandbox/default-policy.d.ts.map +1 -1
- package/dist/core/sandbox/default-policy.js +9 -11
- package/dist/core/sandbox/default-policy.js.map +1 -1
- package/dist/core/session-bash-runtime.d.ts +4 -4
- package/dist/core/session-bash-runtime.d.ts.map +1 -1
- package/dist/core/session-bash-runtime.js +6 -5
- package/dist/core/session-bash-runtime.js.map +1 -1
- package/dist/core/session-compaction-service.d.ts.map +1 -1
- package/dist/core/session-compaction-service.js +7 -7
- package/dist/core/session-compaction-service.js.map +1 -1
- package/dist/core/session-manager.d.ts +4 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +47 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -4
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +11 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/subagent-orchestration.d.ts.map +1 -1
- package/dist/core/subagent-orchestration.js +4 -2
- package/dist/core/subagent-orchestration.js.map +1 -1
- package/dist/core/system-prompt.d.ts +9 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +10 -4
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +2 -2
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +2 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/diagnostics.d.ts.map +1 -1
- package/dist/core/tools/diagnostics.js +8 -2
- package/dist/core/tools/diagnostics.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +15 -0
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/core/turn-metrics.d.ts +135 -0
- package/dist/core/turn-metrics.d.ts.map +1 -0
- package/dist/core/turn-metrics.js +261 -0
- package/dist/core/turn-metrics.js.map +1 -0
- package/dist/core/verified-bash-runtime.d.ts.map +1 -1
- package/dist/core/verified-bash-runtime.js +10 -4
- package/dist/core/verified-bash-runtime.js.map +1 -1
- package/dist/guardrails/evidence-protocol.d.ts +10 -0
- package/dist/guardrails/evidence-protocol.d.ts.map +1 -0
- package/dist/guardrails/evidence-protocol.js +34 -0
- package/dist/guardrails/evidence-protocol.js.map +1 -0
- package/dist/guardrails/evidence-system.d.ts +3 -1
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +22 -14
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +7 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
- package/dist/guardrails/replay-ledger-lock-owner.js +42 -1
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
- package/dist/guardrails/replay-payload-hash.d.ts +6 -0
- package/dist/guardrails/replay-payload-hash.d.ts.map +1 -0
- package/dist/guardrails/replay-payload-hash.js +24 -0
- package/dist/guardrails/replay-payload-hash.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +18 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.js +2 -2
- package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +3 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +40 -12
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.d.ts +14 -2
- package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.js +106 -19
- package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +10 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +104 -17
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +1 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/types/evidence.d.ts +11 -3
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/docs/book-to-skill.md +79 -0
- package/docs/compaction.md +35 -3
- package/docs/containerization.md +14 -2
- package/docs/docs.json +16 -0
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +75 -16
- package/docs/index.md +3 -0
- package/docs/mcp.md +97 -0
- package/docs/metrics.md +87 -0
- package/docs/models.md +12 -0
- package/docs/packages.md +25 -4
- package/docs/provider-resilience.md +37 -56
- package/docs/providers.md +5 -1
- package/docs/run-protocol.md +72 -0
- package/docs/sdk.md +40 -6
- package/docs/sessions.md +6 -0
- package/docs/settings.md +10 -7
- package/docs/skills.md +9 -1
- package/docs/usage.md +14 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/plannotator-approval-bridge.ts +83 -0
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +21 -11
- package/examples/extensions/subagent/adaptive-agent-runtime.ts +9 -3
- package/examples/extensions/subagent/agents/planner.md +0 -2
- package/examples/extensions/subagent/agents/reviewer.md +0 -2
- package/examples/extensions/subagent/agents/scout.md +0 -2
- package/examples/extensions/subagent/agents/worker.md +0 -1
- package/examples/extensions/subagent/agents.test.ts +9 -0
- package/examples/extensions/subagent/agents.ts +11 -4
- package/examples/extensions/subagent/deadline-budget.test.ts +32 -0
- package/examples/extensions/subagent/deadline-budget.ts +25 -1
- package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +10 -0
- package/examples/extensions/subagent/index.ts +234 -55
- package/examples/extensions/subagent/subagent-execution-policy.test.ts +27 -0
- package/examples/extensions/subagent/subagent-extension-smoke.test.ts +167 -2
- package/examples/extensions/subagent/workflow-graph.test.ts +80 -0
- package/examples/extensions/subagent/workflow-graph.ts +89 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +275 -439
- package/package.json +11 -8
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { createHash, randomUUID } from "node:crypto";
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
-
import { basename, dirname } from "node:path";
|
|
17
|
+
import { basename, dirname, join as joinPath } from "node:path";
|
|
18
18
|
import { isDeepStrictEqual } from "node:util";
|
|
19
|
-
import {
|
|
19
|
+
import { getVisionRouteModel, isVisionRouteModel } from "omk-agent-core";
|
|
20
|
+
import { clampThinkingLevel, cleanupSessionResources, deriveContextPromptCacheKey, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "omk-ai";
|
|
21
|
+
import { APP_NAME, VERSION } from "../config.js";
|
|
20
22
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
21
23
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
22
24
|
import { resolvePath } from "../utils/paths.js";
|
|
@@ -31,6 +33,7 @@ import { createCompactionHysteresisConfig, createCompactionHysteresisState, step
|
|
|
31
33
|
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, } from "./compaction/index.js";
|
|
32
34
|
import { compactionEmitWillRetry } from "./compaction/resume-policy.js";
|
|
33
35
|
import { estimateToolResultReserve, } from "./context-budget-reserved-tokens.js";
|
|
36
|
+
import { createDiskContextBudgetCacheProviderV2, DiskContextBudgetCacheProviderV2, } from "./context-budget-v2-cache-disk.js";
|
|
34
37
|
import { applyContextCacheInvalidation, createContextCacheInvalidationSnapshot, } from "./context-budget-v2-cache-invalidation.js";
|
|
35
38
|
import { createMemoryContextBudgetCacheProviderV2 } from "./context-budget-v2-cache-provider.js";
|
|
36
39
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
@@ -41,21 +44,49 @@ import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
|
41
44
|
import { assertTextChatModelForCompletion } from "./grok-harness.js";
|
|
42
45
|
import { grokPlaybookAppendForProvider } from "./grok-playbook.js";
|
|
43
46
|
import { decideLoadoutAccess } from "./loadout-access-policy.js";
|
|
47
|
+
import { loadMcpServerConfigs } from "./mcp/config.js";
|
|
48
|
+
import { McpManager } from "./mcp/manager.js";
|
|
49
|
+
import { buildRuntimeProvenance } from "./runtime-provenance.js";
|
|
50
|
+
import { TurnMetricsSink } from "./turn-metrics.js";
|
|
51
|
+
/** Values that disable a session-level feature through its environment variable. */
|
|
52
|
+
const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "disable", "disabled"]);
|
|
53
|
+
const MAX_OVERFLOW_RECOVERY_ATTEMPTS = 2;
|
|
54
|
+
const OVERFLOW_RECOVERY_EMERGENCY_TOKENS = 4_096;
|
|
55
|
+
function isDisabledEnvValue(value) {
|
|
56
|
+
return value !== undefined && DISABLED_ENV_VALUES.has(value.trim().toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
/** Extract the first text block of a tool result, for failure classification only. */
|
|
59
|
+
function firstTextContent(result) {
|
|
60
|
+
const content = result?.content;
|
|
61
|
+
if (!Array.isArray(content))
|
|
62
|
+
return undefined;
|
|
63
|
+
for (const block of content) {
|
|
64
|
+
if (block && typeof block === "object" && block.type === "text") {
|
|
65
|
+
const text = block.text;
|
|
66
|
+
if (typeof text === "string")
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
import { redactCredentialShapedContent } from "./compaction/transaction.js";
|
|
44
73
|
import { findExactModelReferenceMatch } from "./model-resolver.js";
|
|
45
74
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
46
|
-
import { isContentSafetyStopMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
|
|
47
|
-
import { getBiasStepsForCell,
|
|
48
|
-
import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoning-router-v4.js";
|
|
49
|
-
import { redactSensitiveText } from "./redaction.js";
|
|
50
|
-
import {
|
|
75
|
+
import { isContentSafetyStopMessage, isQuotaExhaustionMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
|
|
76
|
+
import { getBiasStepsForCell, parseRouterBiasSnapshot } from "./reasoning-router-bias.js";
|
|
77
|
+
import { classifyTaskV4, deriveRouterFeedbackFeaturesV4, resolveThinkingLevelV4WithUncertainty, TASK_CLASS_THINKING_LEVELS_V4, } from "./reasoning-router-v4.js";
|
|
78
|
+
import { redactSensitiveText, redactSensitiveTextForced } from "./redaction.js";
|
|
79
|
+
import { getRepositoryRouterLearningPaths } from "./repository-learning-scope.js";
|
|
80
|
+
import { appendRouterFeedbackRecord, ROUTER_FEEDBACK_LEVELS, } from "./router-feedback-collector.js";
|
|
51
81
|
import { RunJournalStore } from "./run-journal-store.js";
|
|
52
82
|
import { SessionBashRuntime } from "./session-bash-runtime.js";
|
|
53
83
|
import { SessionBashService } from "./session-bash-service.js";
|
|
54
84
|
import { SessionCompactionService } from "./session-compaction-service.js";
|
|
55
85
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
86
|
+
import { acquireSessionOwnerLeaseSync } from "./session-owner-lease.js";
|
|
56
87
|
import { classifySessionTermination, } from "./session-termination.js";
|
|
57
88
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
58
|
-
import {
|
|
89
|
+
import { buildSystemPromptPlan } from "./system-prompt.js";
|
|
59
90
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
60
91
|
import { createAllToolDefinitions } from "./tools/index.js";
|
|
61
92
|
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
|
@@ -110,7 +141,13 @@ function pendingToolResultClass(name, args) {
|
|
|
110
141
|
/** Standard thinking levels */
|
|
111
142
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
112
143
|
function normalizeToolNames(toolNames) {
|
|
113
|
-
return [...new Set(toolNames.map((name) => name.trim()).filter((name) => name !== ""))].sort()
|
|
144
|
+
return [...new Set(toolNames.map((name) => name.trim()).filter((name) => name !== ""))].sort((left, right) => {
|
|
145
|
+
if (left < right)
|
|
146
|
+
return -1;
|
|
147
|
+
if (left > right)
|
|
148
|
+
return 1;
|
|
149
|
+
return 0;
|
|
150
|
+
});
|
|
114
151
|
}
|
|
115
152
|
function toolNameSetsEqual(left, right) {
|
|
116
153
|
const normalizedLeft = normalizeToolNames(left);
|
|
@@ -159,7 +196,8 @@ function clonePlainSnapshot(value, ancestors = new WeakSet(), copies = new WeakM
|
|
|
159
196
|
ancestors.add(value);
|
|
160
197
|
try {
|
|
161
198
|
if (Array.isArray(value)) {
|
|
162
|
-
const copy =
|
|
199
|
+
const copy = [];
|
|
200
|
+
copy.length = value.length;
|
|
163
201
|
copies.set(value, copy);
|
|
164
202
|
for (const key of Reflect.ownKeys(value)) {
|
|
165
203
|
if (key === "length")
|
|
@@ -243,6 +281,7 @@ export class AgentSession {
|
|
|
243
281
|
*/
|
|
244
282
|
_reasoningRouterBiasSnapshot = null;
|
|
245
283
|
_reasoningRouterBiasSnapshotLoaded = false;
|
|
284
|
+
_lastAutoRouterDecision;
|
|
246
285
|
/**
|
|
247
286
|
* AdaptOrch advisory bridge (default-off, global-only opt-in via
|
|
248
287
|
* `adaptorchBridge.enabled` in ~/.omk/agent/settings.json). Lazily
|
|
@@ -265,7 +304,7 @@ export class AgentSession {
|
|
|
265
304
|
// Compaction state
|
|
266
305
|
_compactionAbortController = undefined;
|
|
267
306
|
_autoCompactionAbortController = undefined;
|
|
268
|
-
|
|
307
|
+
_overflowRecoveryAttempts = 0;
|
|
269
308
|
_thresholdCompactionEmergency = false;
|
|
270
309
|
_compactionHysteresisState = createCompactionHysteresisState();
|
|
271
310
|
// Branch summarization state
|
|
@@ -285,7 +324,12 @@ export class AgentSession {
|
|
|
285
324
|
_resourceLoader;
|
|
286
325
|
_customTools;
|
|
287
326
|
_baseToolDefinitions = new Map();
|
|
327
|
+
_mcpManager;
|
|
328
|
+
_mcpToolNames = new Set();
|
|
329
|
+
_turnMetricsSink;
|
|
330
|
+
_turnMetricsState;
|
|
288
331
|
_cwd;
|
|
332
|
+
_repositoryRouterLearningPaths;
|
|
289
333
|
_extensionRunnerRef;
|
|
290
334
|
_initialActiveToolNames;
|
|
291
335
|
_allowedToolNames;
|
|
@@ -302,6 +346,7 @@ export class AgentSession {
|
|
|
302
346
|
_extensionErrorUnsubscriber;
|
|
303
347
|
// Required durable run/audit chain, persisted next to the session file.
|
|
304
348
|
_runJournalStore;
|
|
349
|
+
_ownedSessionOwnerLease;
|
|
305
350
|
_activeRunId = null;
|
|
306
351
|
_pendingRuntimeTerminationCause;
|
|
307
352
|
_activeRunToolTermination;
|
|
@@ -326,7 +371,13 @@ export class AgentSession {
|
|
|
326
371
|
_toolPromptGuidelines = new Map();
|
|
327
372
|
// Base system prompt (without extension appends) - used to apply fresh appends each turn
|
|
328
373
|
_baseSystemPrompt = "";
|
|
374
|
+
_baseSystemPromptCacheBoundary;
|
|
329
375
|
_baseSystemPromptOptions;
|
|
376
|
+
_promptCacheKey;
|
|
377
|
+
_promptCacheKeyChanges = 0;
|
|
378
|
+
_promptCacheBoundaryBypasses = 0;
|
|
379
|
+
_promptCacheStablePrefixCharacters = 0;
|
|
380
|
+
_promptCacheLastBreakReason;
|
|
330
381
|
constructor(config) {
|
|
331
382
|
this.agent = config.agent;
|
|
332
383
|
this.sessionManager = config.sessionManager;
|
|
@@ -389,31 +440,55 @@ export class AgentSession {
|
|
|
389
440
|
recordCommit: () => this._recordCompactionCommitForHysteresis(),
|
|
390
441
|
});
|
|
391
442
|
const sessionFile = this.sessionManager.getSessionFile();
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
this.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
443
|
+
let ownerLease = this.sessionManager.getOwnerLease();
|
|
444
|
+
try {
|
|
445
|
+
if (sessionFile && !ownerLease) {
|
|
446
|
+
ownerLease = acquireSessionOwnerLeaseSync(sessionFile);
|
|
447
|
+
this._ownedSessionOwnerLease = ownerLease;
|
|
448
|
+
this.sessionManager.setOwnerLease(ownerLease);
|
|
449
|
+
}
|
|
450
|
+
this._runJournalStore = RunJournalStore.open({
|
|
451
|
+
...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
|
|
452
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
453
|
+
...(ownerLease ? { ownerLease } : {}),
|
|
454
|
+
});
|
|
455
|
+
const startupRecords = this._runJournalStore.records;
|
|
456
|
+
const startupTerminal = startupRecords[startupRecords.length - 1];
|
|
457
|
+
if (startupTerminal?.event === "run_recovered") {
|
|
458
|
+
this._lastTermination = startupTerminal.termination;
|
|
459
|
+
}
|
|
460
|
+
// Always subscribe to agent events for internal handling
|
|
461
|
+
// (session persistence, extensions, auto-compaction, retry logic)
|
|
462
|
+
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
463
|
+
this._installAgentToolHooks();
|
|
464
|
+
// Durable transcript-repair audit for a repair applied on open/resume.
|
|
465
|
+
if (this._transcriptRepair) {
|
|
466
|
+
this._invalidateContextBudgetCache({ type: "transcriptRepair" });
|
|
467
|
+
this._appendRunJournalAudit("transcript_repaired", {
|
|
468
|
+
insertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],
|
|
469
|
+
reason: this._transcriptRepair.reason,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
this._buildRuntime({
|
|
473
|
+
activeToolNames: this._initialActiveToolNames,
|
|
474
|
+
includeAllExtensionTools: true,
|
|
411
475
|
});
|
|
412
476
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
477
|
+
catch (error) {
|
|
478
|
+
const ownedLease = this._ownedSessionOwnerLease;
|
|
479
|
+
if (ownedLease) {
|
|
480
|
+
try {
|
|
481
|
+
ownedLease.release();
|
|
482
|
+
if (this.sessionManager.getOwnerLease() === ownedLease)
|
|
483
|
+
this.sessionManager.setOwnerLease(undefined);
|
|
484
|
+
this._ownedSessionOwnerLease = undefined;
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
throw new Error("AgentSession initialization cleanup failed");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
417
492
|
}
|
|
418
493
|
/** Model registry for API key resolution and model discovery */
|
|
419
494
|
get modelRegistry() {
|
|
@@ -512,12 +587,15 @@ export class AgentSession {
|
|
|
512
587
|
if (/stop_reason\s*=\s*(refusal|sensitive)|content\/safety stop|safety stop|provider\.refusal|kind=provider_refusal/i.test(text)) {
|
|
513
588
|
return { area: "provider", code: "refusal" };
|
|
514
589
|
}
|
|
590
|
+
// Quota/billing exhaustion is checked BEFORE the generic 401/403 auth
|
|
591
|
+
// patterns: "403 ... usage limit for this billing cycle" is transient per
|
|
592
|
+
// cycle and must fail over, not terminate the turn as an auth error.
|
|
593
|
+
if (isQuotaExhaustionMessage(text) || /rate.?limit|too many requests|429/i.test(text)) {
|
|
594
|
+
return { area: "provider", code: "rate_limit" };
|
|
595
|
+
}
|
|
515
596
|
if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
|
|
516
597
|
return { area: "provider", code: "auth" };
|
|
517
598
|
}
|
|
518
|
-
if (/rate.?limit|too many requests|429|quota|available balance|billing/i.test(text)) {
|
|
519
|
-
return { area: "provider", code: "rate_limit" };
|
|
520
|
-
}
|
|
521
599
|
// Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.
|
|
522
600
|
// Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.
|
|
523
601
|
if (/tool_call_id\s+is\s+not\s+found|tool_call_id\s+not\s+found|unknown\s+tool_call_id/i.test(text)) {
|
|
@@ -536,6 +614,9 @@ export class AgentSession {
|
|
|
536
614
|
if (!this.model || /no model|model selected|model is required/i.test(message)) {
|
|
537
615
|
return { area: "configuration", code: "invalid" };
|
|
538
616
|
}
|
|
617
|
+
if (isQuotaExhaustionMessage(message)) {
|
|
618
|
+
return { area: "provider", code: "rate_limit" };
|
|
619
|
+
}
|
|
539
620
|
if (/auth|api key|unauthori[sz]ed|forbidden|401|403|\/login/i.test(message)) {
|
|
540
621
|
return { area: "provider", code: "auth" };
|
|
541
622
|
}
|
|
@@ -564,9 +645,14 @@ export class AgentSession {
|
|
|
564
645
|
}
|
|
565
646
|
_classifyRunTermination(runId, event) {
|
|
566
647
|
const timestamp = new Date().toISOString();
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
648
|
+
let assistant;
|
|
649
|
+
for (let index = event.messages.length - 1; index >= 0; index -= 1) {
|
|
650
|
+
const message = event.messages[index];
|
|
651
|
+
if (message?.role === "assistant") {
|
|
652
|
+
assistant = message;
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
570
656
|
let cause;
|
|
571
657
|
let message;
|
|
572
658
|
let sideEffects = this._sessionRiskLevel === "elevated" ? "possible" : "none";
|
|
@@ -595,6 +681,12 @@ export class AgentSession {
|
|
|
595
681
|
cause = { area: "completed" };
|
|
596
682
|
message = "Run completed.";
|
|
597
683
|
}
|
|
684
|
+
let provider = this.model?.provider;
|
|
685
|
+
if (assistant?.provider)
|
|
686
|
+
provider = assistant.provider;
|
|
687
|
+
let model = this.model?.id;
|
|
688
|
+
if (assistant?.model)
|
|
689
|
+
model = assistant.model;
|
|
598
690
|
return classifySessionTermination({
|
|
599
691
|
sessionId: this.sessionId,
|
|
600
692
|
runId,
|
|
@@ -603,12 +695,8 @@ export class AgentSession {
|
|
|
603
695
|
message,
|
|
604
696
|
cause,
|
|
605
697
|
sideEffects,
|
|
606
|
-
...(
|
|
607
|
-
|
|
608
|
-
: this.model
|
|
609
|
-
? { provider: this.model.provider }
|
|
610
|
-
: {}),
|
|
611
|
-
...(assistant?.model ? { model: assistant.model } : this.model ? { model: this.model.id } : {}),
|
|
698
|
+
...(provider ? { provider } : {}),
|
|
699
|
+
...(model ? { model } : {}),
|
|
612
700
|
...(toolCallId ? { toolCallId } : {}),
|
|
613
701
|
...(toolName ? { toolName } : {}),
|
|
614
702
|
});
|
|
@@ -637,11 +725,13 @@ export class AgentSession {
|
|
|
637
725
|
const cause = this._runtimeFailureCause(error);
|
|
638
726
|
const runId = this._activeRunId ?? `runtime-${randomUUID()}`;
|
|
639
727
|
const timestamp = new Date().toISOString();
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
728
|
+
let message = "The AgentSession runtime failed before completing the run.";
|
|
729
|
+
if (cause.area === "persistence") {
|
|
730
|
+
message = "A required runtime persistence operation failed.";
|
|
731
|
+
}
|
|
732
|
+
else if (cause.area === "compaction") {
|
|
733
|
+
message = "Runtime compaction failed.";
|
|
734
|
+
}
|
|
645
735
|
let termination = classifySessionTermination({
|
|
646
736
|
sessionId: this.sessionId,
|
|
647
737
|
runId,
|
|
@@ -884,7 +974,7 @@ export class AgentSession {
|
|
|
884
974
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
885
975
|
// This ensures the UI sees the updated queue state
|
|
886
976
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
887
|
-
this.
|
|
977
|
+
this._overflowRecoveryAttempts = 0;
|
|
888
978
|
const messageText = this._getUserMessageText(event.message);
|
|
889
979
|
if (messageText) {
|
|
890
980
|
// Check steering queue first
|
|
@@ -935,7 +1025,7 @@ export class AgentSession {
|
|
|
935
1025
|
this._lastAssistantMessage = finalizedEvent.message;
|
|
936
1026
|
const assistantMsg = finalizedEvent.message;
|
|
937
1027
|
if (assistantMsg.stopReason !== "error") {
|
|
938
|
-
this.
|
|
1028
|
+
this._overflowRecoveryAttempts = 0;
|
|
939
1029
|
}
|
|
940
1030
|
// Reset retry counter immediately on successful assistant response
|
|
941
1031
|
// This prevents accumulation across multiple LLM calls within a turn
|
|
@@ -993,9 +1083,13 @@ export class AgentSession {
|
|
|
993
1083
|
? "Finalized message was not found in agent state"
|
|
994
1084
|
: "Finalized message is ambiguous in agent state");
|
|
995
1085
|
}
|
|
1086
|
+
const matchingIndex = matchingIndexes[0];
|
|
1087
|
+
if (matchingIndex === undefined) {
|
|
1088
|
+
throw new Error("Finalized message was not found in agent state");
|
|
1089
|
+
}
|
|
996
1090
|
const finalizedReplacement = createImmutableMessageSnapshot(replacement);
|
|
997
|
-
const nextMessages = messages
|
|
998
|
-
nextMessages[
|
|
1091
|
+
const nextMessages = [...messages];
|
|
1092
|
+
nextMessages[matchingIndex] = finalizedReplacement;
|
|
999
1093
|
this.agent.state.messages = nextMessages;
|
|
1000
1094
|
return finalizedReplacement;
|
|
1001
1095
|
}
|
|
@@ -1009,6 +1103,13 @@ export class AgentSession {
|
|
|
1009
1103
|
await this._extensionRunner.emit({ type: "agent_end", messages: event.messages });
|
|
1010
1104
|
}
|
|
1011
1105
|
else if (event.type === "turn_start") {
|
|
1106
|
+
this._turnMetricsState = {
|
|
1107
|
+
startedAtEpochMs: Date.now(),
|
|
1108
|
+
toolCalls: [],
|
|
1109
|
+
toolStarts: new Map(),
|
|
1110
|
+
requestedModel: this.model ? `${this.model.provider}/${this.model.id}` : null,
|
|
1111
|
+
requestedThinking: this.thinkingMode === "auto" ? "auto" : this.thinkingLevel,
|
|
1112
|
+
};
|
|
1012
1113
|
const extensionEvent = {
|
|
1013
1114
|
type: "turn_start",
|
|
1014
1115
|
turnIndex: this._turnIndex,
|
|
@@ -1017,6 +1118,7 @@ export class AgentSession {
|
|
|
1017
1118
|
await this._extensionRunner.emit(extensionEvent);
|
|
1018
1119
|
}
|
|
1019
1120
|
else if (event.type === "turn_end") {
|
|
1121
|
+
this._recordTurnMetrics(event.message);
|
|
1020
1122
|
const extensionEvent = {
|
|
1021
1123
|
type: "turn_end",
|
|
1022
1124
|
turnIndex: this._turnIndex,
|
|
@@ -1053,6 +1155,7 @@ export class AgentSession {
|
|
|
1053
1155
|
}
|
|
1054
1156
|
}
|
|
1055
1157
|
else if (event.type === "tool_execution_start") {
|
|
1158
|
+
this._turnMetricsState?.toolStarts.set(event.toolCallId, Date.now());
|
|
1056
1159
|
const extensionEvent = {
|
|
1057
1160
|
type: "tool_execution_start",
|
|
1058
1161
|
toolCallId: event.toolCallId,
|
|
@@ -1072,6 +1175,7 @@ export class AgentSession {
|
|
|
1072
1175
|
await this._extensionRunner.emit(extensionEvent);
|
|
1073
1176
|
}
|
|
1074
1177
|
else if (event.type === "tool_execution_end") {
|
|
1178
|
+
this._recordToolMetric(event.toolCallId, event.toolName, event.isError === true, event.result);
|
|
1075
1179
|
const extensionEvent = {
|
|
1076
1180
|
type: "tool_execution_end",
|
|
1077
1181
|
toolCallId: event.toolCallId,
|
|
@@ -1082,6 +1186,81 @@ export class AgentSession {
|
|
|
1082
1186
|
await this._extensionRunner.emit(extensionEvent);
|
|
1083
1187
|
}
|
|
1084
1188
|
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Turn metrics sink for this session, created on first use.
|
|
1191
|
+
*
|
|
1192
|
+
* Writes to `<cwd>/.omk/metrics/`. Set `OMK_TURN_METRICS=0` to disable, or
|
|
1193
|
+
* `OMK_TURN_METRICS_DIR` to relocate. Returns `undefined` when disabled, so
|
|
1194
|
+
* every call site is a no-op rather than a branch.
|
|
1195
|
+
*/
|
|
1196
|
+
_metricsSink() {
|
|
1197
|
+
if (isDisabledEnvValue(process.env.OMK_TURN_METRICS))
|
|
1198
|
+
return undefined;
|
|
1199
|
+
if (!this._turnMetricsSink) {
|
|
1200
|
+
const dir = process.env.OMK_TURN_METRICS_DIR ?? joinPath(this._cwd, ".omk", "metrics");
|
|
1201
|
+
this._turnMetricsSink = new TurnMetricsSink({ dir });
|
|
1202
|
+
}
|
|
1203
|
+
return this._turnMetricsSink;
|
|
1204
|
+
}
|
|
1205
|
+
_recordToolMetric(toolCallId, toolName, isError, result) {
|
|
1206
|
+
const state = this._turnMetricsState;
|
|
1207
|
+
if (!state)
|
|
1208
|
+
return;
|
|
1209
|
+
const startedAt = state.toolStarts.get(toolCallId);
|
|
1210
|
+
state.toolStarts.delete(toolCallId);
|
|
1211
|
+
state.toolCalls.push({
|
|
1212
|
+
name: toolName,
|
|
1213
|
+
durationMs: startedAt === undefined ? 0 : Date.now() - startedAt,
|
|
1214
|
+
ok: !isError,
|
|
1215
|
+
...(isError ? { error: firstTextContent(result) } : {}),
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
/** Persist one turn. Metrics are advisory: a failure here never affects the turn. */
|
|
1219
|
+
_recordTurnMetrics(message) {
|
|
1220
|
+
const state = this._turnMetricsState;
|
|
1221
|
+
this._turnMetricsState = undefined;
|
|
1222
|
+
if (!state)
|
|
1223
|
+
return;
|
|
1224
|
+
const sink = this._metricsSink();
|
|
1225
|
+
if (!sink)
|
|
1226
|
+
return;
|
|
1227
|
+
const assistant = message.role === "assistant" ? message : undefined;
|
|
1228
|
+
const usage = assistant?.usage;
|
|
1229
|
+
const selectedModel = this.model ? `${this.model.provider}/${this.model.id}` : null;
|
|
1230
|
+
const runtimeProvenance = buildRuntimeProvenance({
|
|
1231
|
+
requestedModel: state.requestedModel,
|
|
1232
|
+
selectedModel,
|
|
1233
|
+
responseModel: assistant?.responseModel ?? (assistant ? `${assistant.provider}/${assistant.model}` : null),
|
|
1234
|
+
requestedThinking: state.requestedThinking,
|
|
1235
|
+
effectiveThinking: this.thinkingLevel,
|
|
1236
|
+
source: "session",
|
|
1237
|
+
});
|
|
1238
|
+
try {
|
|
1239
|
+
sink.record({
|
|
1240
|
+
sessionId: this.sessionId,
|
|
1241
|
+
turnIndex: this._turnIndex,
|
|
1242
|
+
provider: this.model?.provider,
|
|
1243
|
+
model: this.model?.id,
|
|
1244
|
+
startedAtEpochMs: state.startedAtEpochMs,
|
|
1245
|
+
endedAtEpochMs: Date.now(),
|
|
1246
|
+
stopReason: assistant?.stopReason,
|
|
1247
|
+
runtimeProvenance,
|
|
1248
|
+
usage: usage
|
|
1249
|
+
? {
|
|
1250
|
+
input: usage.input,
|
|
1251
|
+
output: usage.output,
|
|
1252
|
+
cacheRead: usage.cacheRead,
|
|
1253
|
+
cacheWrite: usage.cacheWrite,
|
|
1254
|
+
costUsd: usage.cost.total,
|
|
1255
|
+
}
|
|
1256
|
+
: undefined,
|
|
1257
|
+
toolCalls: state.toolCalls,
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
// Never let observability break a turn.
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1085
1264
|
/**
|
|
1086
1265
|
* Subscribe to agent events.
|
|
1087
1266
|
* Session persistence is handled internally (saves messages on message_end).
|
|
@@ -1123,19 +1302,59 @@ export class AgentSession {
|
|
|
1123
1302
|
*/
|
|
1124
1303
|
dispose() {
|
|
1125
1304
|
try {
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1305
|
+
try {
|
|
1306
|
+
this.abortRetry();
|
|
1307
|
+
this.abortCompaction();
|
|
1308
|
+
this.abortBranchSummary();
|
|
1309
|
+
this.abortBash();
|
|
1310
|
+
this.agent.abort();
|
|
1311
|
+
}
|
|
1312
|
+
catch {
|
|
1313
|
+
// Dispose must succeed even if an abort hook throws.
|
|
1314
|
+
}
|
|
1315
|
+
this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured extension API or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
|
|
1316
|
+
this._disconnectFromAgent();
|
|
1317
|
+
this._eventListeners = [];
|
|
1318
|
+
if (this._contextBudgetCacheProvider instanceof DiskContextBudgetCacheProviderV2) {
|
|
1319
|
+
// Persist whatever the session rendered; a failed flush is never fatal.
|
|
1320
|
+
this._contextBudgetCacheProvider.close();
|
|
1321
|
+
}
|
|
1322
|
+
this._mcpManager?.close();
|
|
1323
|
+
this._mcpManager = undefined;
|
|
1324
|
+
cleanupSessionResources(this.sessionId);
|
|
1325
|
+
}
|
|
1326
|
+
finally {
|
|
1327
|
+
const ownerLease = this._ownedSessionOwnerLease;
|
|
1328
|
+
if (ownerLease) {
|
|
1329
|
+
ownerLease.release();
|
|
1330
|
+
if (this.sessionManager.getOwnerLease() === ownerLease)
|
|
1331
|
+
this.sessionManager.setOwnerLease(undefined);
|
|
1332
|
+
this._ownedSessionOwnerLease = undefined;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Build the context-budget cache provider for this session.
|
|
1338
|
+
*
|
|
1339
|
+
* Defaults to a workspace-layer provider persisted under `<cwd>/.omk/cache`,
|
|
1340
|
+
* so a second session reuses representations the first one already rendered
|
|
1341
|
+
* (representation keys are content-addressed; see
|
|
1342
|
+
* `context-budget-v2-cache-keys.ts`). Set
|
|
1343
|
+
* `OMK_CONTEXT_GOVERNOR_CACHE=memory` to opt out, or
|
|
1344
|
+
* `OMK_CONTEXT_GOVERNOR_CACHE_DIR` to relocate the snapshot.
|
|
1345
|
+
*/
|
|
1346
|
+
_createContextBudgetCacheProvider() {
|
|
1347
|
+
if (process.env.OMK_CONTEXT_GOVERNOR_CACHE === "memory") {
|
|
1348
|
+
return createMemoryContextBudgetCacheProviderV2("session");
|
|
1349
|
+
}
|
|
1350
|
+
const dir = process.env.OMK_CONTEXT_GOVERNOR_CACHE_DIR ?? joinPath(this._cwd, ".omk", "cache", "context-budget-v2");
|
|
1351
|
+
try {
|
|
1352
|
+
return createDiskContextBudgetCacheProviderV2({ dir, layer: "workspace" });
|
|
1131
1353
|
}
|
|
1132
1354
|
catch {
|
|
1133
|
-
//
|
|
1355
|
+
// An unusable cache directory must not take the session down.
|
|
1356
|
+
return createMemoryContextBudgetCacheProviderV2("session");
|
|
1134
1357
|
}
|
|
1135
|
-
this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured extension API or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
|
|
1136
|
-
this._disconnectFromAgent();
|
|
1137
|
-
this._eventListeners = [];
|
|
1138
|
-
cleanupSessionResources(this.sessionId);
|
|
1139
1358
|
}
|
|
1140
1359
|
// =========================================================================
|
|
1141
1360
|
// Read-only State Access
|
|
@@ -1220,6 +1439,9 @@ export class AgentSession {
|
|
|
1220
1439
|
// Rebuild base system prompt with new tool set
|
|
1221
1440
|
this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
|
|
1222
1441
|
this.agent.state.systemPrompt = this._baseSystemPrompt;
|
|
1442
|
+
this.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;
|
|
1443
|
+
this.agent.state.systemPromptCacheBoundaryBypass = false;
|
|
1444
|
+
this._recordPromptCachePlan("tool-set");
|
|
1223
1445
|
}
|
|
1224
1446
|
/**
|
|
1225
1447
|
* Recompute the Agent's per-name timeout map for the active tool set:
|
|
@@ -1316,7 +1538,7 @@ export class AgentSession {
|
|
|
1316
1538
|
const recent = messages.slice(-3);
|
|
1317
1539
|
for (let i = recent.length - 1; i >= 0; i--) {
|
|
1318
1540
|
const msg = recent[i];
|
|
1319
|
-
if (!("role" in msg) || msg.role !== "user") {
|
|
1541
|
+
if (!msg || !("role" in msg) || msg.role !== "user") {
|
|
1320
1542
|
continue;
|
|
1321
1543
|
}
|
|
1322
1544
|
const content = msg.content;
|
|
@@ -1325,9 +1547,7 @@ export class AgentSession {
|
|
|
1325
1547
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
1326
1548
|
}
|
|
1327
1549
|
if (Array.isArray(content)) {
|
|
1328
|
-
const textParts = content
|
|
1329
|
-
.filter((c) => "type" in c && c.type === "text")
|
|
1330
|
-
.map((c) => c.text);
|
|
1550
|
+
const textParts = content.flatMap((item) => (item.type === "text" ? [item.text] : []));
|
|
1331
1551
|
const joined = textParts.join("\n").trim();
|
|
1332
1552
|
return joined.length > 0 ? joined : undefined;
|
|
1333
1553
|
}
|
|
@@ -1377,7 +1597,7 @@ export class AgentSession {
|
|
|
1377
1597
|
responseReserveTokens =
|
|
1378
1598
|
parsePositiveIntegerEnv("OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS") ?? LEGACY_RESPONSE_RESERVE_TOKENS;
|
|
1379
1599
|
}
|
|
1380
|
-
const cacheProvider = this._contextBudgetCacheProvider ??
|
|
1600
|
+
const cacheProvider = this._contextBudgetCacheProvider ?? this._createContextBudgetCacheProvider();
|
|
1381
1601
|
cacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);
|
|
1382
1602
|
this._contextBudgetCacheProvider = cacheProvider;
|
|
1383
1603
|
// Enforce floor
|
|
@@ -1411,6 +1631,32 @@ export class AgentSession {
|
|
|
1411
1631
|
const ratio = overrideRatio ?? RESPONSE_RESERVE_RATIO;
|
|
1412
1632
|
return Math.max(Math.floor(contextWindow * ratio), LEGACY_RESPONSE_RESERVE_TOKENS);
|
|
1413
1633
|
}
|
|
1634
|
+
_recordPromptCachePlan(reason) {
|
|
1635
|
+
const boundary = this.agent.state.systemPromptCacheBoundary;
|
|
1636
|
+
const scope = this.model ? `${this.model.provider}/${this.model.id}` : "unknown";
|
|
1637
|
+
const nextKey = deriveContextPromptCacheKey({
|
|
1638
|
+
systemPrompt: this.agent.state.systemPrompt,
|
|
1639
|
+
systemPromptCacheBoundary: boundary,
|
|
1640
|
+
systemPromptCacheBoundaryBypass: this.agent.state.systemPromptCacheBoundaryBypass,
|
|
1641
|
+
messages: [],
|
|
1642
|
+
tools: this.agent.state.tools,
|
|
1643
|
+
}, scope);
|
|
1644
|
+
if (!nextKey) {
|
|
1645
|
+
this._promptCacheBoundaryBypasses += 1;
|
|
1646
|
+
this._promptCacheStablePrefixCharacters = 0;
|
|
1647
|
+
if (this._promptCacheKey !== undefined) {
|
|
1648
|
+
this._promptCacheLastBreakReason = reason;
|
|
1649
|
+
}
|
|
1650
|
+
this._promptCacheKey = undefined;
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
if (this._promptCacheKey !== undefined && this._promptCacheKey !== nextKey) {
|
|
1654
|
+
this._promptCacheKeyChanges += 1;
|
|
1655
|
+
this._promptCacheLastBreakReason = reason;
|
|
1656
|
+
}
|
|
1657
|
+
this._promptCacheKey = nextKey;
|
|
1658
|
+
this._promptCacheStablePrefixCharacters = boundary ?? 0;
|
|
1659
|
+
}
|
|
1414
1660
|
_rebuildSystemPrompt(toolNames) {
|
|
1415
1661
|
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
|
1416
1662
|
const toolSnippets = {};
|
|
@@ -1446,7 +1692,9 @@ export class AgentSession {
|
|
|
1446
1692
|
promptGuidelines,
|
|
1447
1693
|
contextBudget: this._getContextBudgetOptions(),
|
|
1448
1694
|
};
|
|
1449
|
-
|
|
1695
|
+
const built = buildSystemPromptPlan(this._baseSystemPromptOptions);
|
|
1696
|
+
this._baseSystemPromptCacheBoundary = built.cacheBoundary;
|
|
1697
|
+
return built.prompt;
|
|
1450
1698
|
}
|
|
1451
1699
|
// =========================================================================
|
|
1452
1700
|
// Prompting
|
|
@@ -1625,9 +1873,9 @@ export class AgentSession {
|
|
|
1625
1873
|
...(promptActiveSkillNames.length > 0 ? { activeSkillNames: promptActiveSkillNames } : {}),
|
|
1626
1874
|
...(promptActiveSkillSource ? { activeSkillSource: promptActiveSkillSource } : {}),
|
|
1627
1875
|
};
|
|
1628
|
-
const turnSystemPrompt =
|
|
1876
|
+
const turnSystemPrompt = buildSystemPromptPlan(turnSystemPromptOptions);
|
|
1629
1877
|
// Emit before_agent_start extension event
|
|
1630
|
-
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, turnSystemPrompt, turnSystemPromptOptions);
|
|
1878
|
+
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, turnSystemPrompt.prompt, turnSystemPromptOptions);
|
|
1631
1879
|
// Add all custom messages from extensions
|
|
1632
1880
|
if (result?.messages) {
|
|
1633
1881
|
for (const msg of result.messages) {
|
|
@@ -1641,14 +1889,22 @@ export class AgentSession {
|
|
|
1641
1889
|
});
|
|
1642
1890
|
}
|
|
1643
1891
|
}
|
|
1644
|
-
// Apply extension-modified system prompt, or reset to
|
|
1892
|
+
// Apply extension-modified system prompt, or reset to the turn plan.
|
|
1645
1893
|
if (result?.systemPrompt) {
|
|
1894
|
+
const preservesCacheBoundary = result.systemPrompt === turnSystemPrompt.prompt;
|
|
1646
1895
|
this.agent.state.systemPrompt = result.systemPrompt;
|
|
1896
|
+
this.agent.state.systemPromptCacheBoundary = preservesCacheBoundary
|
|
1897
|
+
? turnSystemPrompt.cacheBoundary
|
|
1898
|
+
: undefined;
|
|
1899
|
+
this.agent.state.systemPromptCacheBoundaryBypass = !preservesCacheBoundary;
|
|
1647
1900
|
}
|
|
1648
1901
|
else {
|
|
1649
|
-
// Ensure we're using the
|
|
1650
|
-
this.agent.state.systemPrompt = turnSystemPrompt;
|
|
1902
|
+
// Ensure we're using the turn prompt (in case the previous turn had modifications).
|
|
1903
|
+
this.agent.state.systemPrompt = turnSystemPrompt.prompt;
|
|
1904
|
+
this.agent.state.systemPromptCacheBoundary = turnSystemPrompt.cacheBoundary;
|
|
1905
|
+
this.agent.state.systemPromptCacheBoundaryBypass = false;
|
|
1651
1906
|
}
|
|
1907
|
+
this._recordPromptCachePlan(result?.systemPrompt ? "extension-override" : "turn-plan");
|
|
1652
1908
|
await this._checkProjectedCompaction(messages);
|
|
1653
1909
|
}
|
|
1654
1910
|
catch (error) {
|
|
@@ -2080,6 +2336,18 @@ export class AgentSession {
|
|
|
2080
2336
|
});
|
|
2081
2337
|
}
|
|
2082
2338
|
}
|
|
2339
|
+
/** Apply an explicit user choice and retain it as bounded router feedback. */
|
|
2340
|
+
setUserThinkingLevel(level) {
|
|
2341
|
+
const overridesAuto = this._thinkingMode === "auto";
|
|
2342
|
+
this.setThinkingMode("manual");
|
|
2343
|
+
this.setThinkingLevel(level);
|
|
2344
|
+
if (overridesAuto) {
|
|
2345
|
+
this._recordUserThinkingOverride(this.thinkingLevel);
|
|
2346
|
+
}
|
|
2347
|
+
else {
|
|
2348
|
+
this._lastAutoRouterDecision = undefined;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2083
2351
|
/**
|
|
2084
2352
|
* Set the thinking mode. "auto" resolves a level per turn via the reasoning
|
|
2085
2353
|
* router; "manual" keeps the explicitly selected level. The mode persists for
|
|
@@ -2107,21 +2375,20 @@ export class AgentSession {
|
|
|
2107
2375
|
* context-pressure bucket, routed through the confidence-bearing v4 classifier
|
|
2108
2376
|
* and its uncertainty-aware resolver. No
|
|
2109
2377
|
* `laneType` applies to the main session (always "none"/`undefined`);
|
|
2110
|
-
*
|
|
2111
|
-
*
|
|
2112
|
-
* adaptorch-bridge.ts). `bias` stays `0` unless BOTH hold: (a) the global,
|
|
2378
|
+
* the default-off Adaptorch bridge is wired but its current transport returns
|
|
2379
|
+
* no advisory hint. `bias` stays `0` unless BOTH hold: (a) the global,
|
|
2113
2380
|
* owner-only `reasoningRouterLearning.enabled` setting is `true` (default
|
|
2114
2381
|
* off; a project-scope `.omk/settings.json` value for this key is never
|
|
2115
2382
|
* consulted -- see settings-manager.ts), and (b) a compiled
|
|
2116
2383
|
* `RouterBiasSnapshot` was found and passed strict validation at the
|
|
2117
|
-
* configured
|
|
2118
|
-
*
|
|
2119
|
-
* enabled,
|
|
2120
|
-
* path/diff/session/model/provider/tool/hook
|
|
2121
|
-
* router-feedback-collector.ts's exact ten-key schema) is
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2124
|
-
*
|
|
2384
|
+
* configured path or opaque repository/worktree-scoped default, loaded and
|
|
2385
|
+
* cached ("pinned") at most once per session (see
|
|
2386
|
+
* `_getReasoningRouterBiasSnapshot`). When learning is enabled, one bounded
|
|
2387
|
+
* "accepted" record (no raw prompt/path/diff/session/model/provider/tool/hook
|
|
2388
|
+
* content; see router-feedback-collector.ts's exact ten-key schema) is
|
|
2389
|
+
* appended after every v4 auto-turn. A later explicit user level records one
|
|
2390
|
+
* bounded `s1-override` direction for the same cell. Both remain inert until
|
|
2391
|
+
* a future, separate offline compile step. The
|
|
2125
2392
|
* resolver's own confidence-band/fallback-reason escalation (see
|
|
2126
2393
|
* reasoning-router-v4.ts) still applies on top of the base+lane+bias
|
|
2127
2394
|
* target, so a low-confidence or fallback-decided verdict can still only
|
|
@@ -2129,17 +2396,17 @@ export class AgentSession {
|
|
|
2129
2396
|
*/
|
|
2130
2397
|
_applyAutoThinkingLevelV4(promptText) {
|
|
2131
2398
|
const availableLevels = this.getAvailableThinkingLevels();
|
|
2399
|
+
const features = deriveRouterFeedbackFeaturesV4(promptText);
|
|
2132
2400
|
const verdict = classifyTaskV4({
|
|
2133
2401
|
prompt: promptText,
|
|
2134
2402
|
history: this._taskClassHistory,
|
|
2135
2403
|
pressureBucket: this._computePressureBucket(),
|
|
2136
|
-
});
|
|
2404
|
+
}, undefined, features);
|
|
2137
2405
|
this._taskClassHistory.unshift(verdict.taskClass);
|
|
2138
2406
|
if (this._taskClassHistory.length > 8)
|
|
2139
2407
|
this._taskClassHistory.length = 8;
|
|
2140
2408
|
const learningEnabled = this.settingsManager.getReasoningRouterLearningEnabled();
|
|
2141
2409
|
const snapshot = learningEnabled ? this._getReasoningRouterBiasSnapshot() : null;
|
|
2142
|
-
const features = this._deriveRouterFeedbackFeatures(promptText);
|
|
2143
2410
|
const bias = snapshot === null
|
|
2144
2411
|
? 0
|
|
2145
2412
|
: getBiasStepsForCell(snapshot, {
|
|
@@ -2150,22 +2417,23 @@ export class AgentSession {
|
|
|
2150
2417
|
hadDiff: features.hadDiff,
|
|
2151
2418
|
});
|
|
2152
2419
|
const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias, this._getAdaptorchHint(verdict, features));
|
|
2420
|
+
this._lastAutoRouterDecision = undefined;
|
|
2153
2421
|
if (learningEnabled && resolved !== "off") {
|
|
2154
|
-
const
|
|
2422
|
+
const decision = {
|
|
2155
2423
|
routerVersion: "v4",
|
|
2156
2424
|
laneType: "none",
|
|
2157
2425
|
predictedClass: verdict.taskClass,
|
|
2158
2426
|
resolvedLevel: resolved,
|
|
2159
|
-
acceptedLevel: resolved,
|
|
2160
|
-
signal: "s2-accept",
|
|
2161
|
-
outcome: "accepted",
|
|
2162
2427
|
lenBucket: features.lenBucket,
|
|
2163
2428
|
hadFence: features.hadFence,
|
|
2164
2429
|
hadDiff: features.hadDiff,
|
|
2165
2430
|
};
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2431
|
+
this._lastAutoRouterDecision = decision;
|
|
2432
|
+
this._appendRouterFeedback({
|
|
2433
|
+
...decision,
|
|
2434
|
+
acceptedLevel: resolved,
|
|
2435
|
+
signal: "s2-accept",
|
|
2436
|
+
outcome: "accepted",
|
|
2169
2437
|
});
|
|
2170
2438
|
}
|
|
2171
2439
|
const previousLevel = this.agent.state.thinkingLevel;
|
|
@@ -2175,21 +2443,44 @@ export class AgentSession {
|
|
|
2175
2443
|
this.sessionManager.appendThinkingLevelChange(resolved);
|
|
2176
2444
|
this._emit({ type: "thinking_level_changed", level: resolved });
|
|
2177
2445
|
}
|
|
2446
|
+
_getRepositoryRouterLearningPaths() {
|
|
2447
|
+
if (this._repositoryRouterLearningPaths === undefined) {
|
|
2448
|
+
this._repositoryRouterLearningPaths = getRepositoryRouterLearningPaths(this._cwd);
|
|
2449
|
+
}
|
|
2450
|
+
return this._repositoryRouterLearningPaths;
|
|
2451
|
+
}
|
|
2452
|
+
_appendRouterFeedback(record) {
|
|
2453
|
+
appendRouterFeedbackRecord(record, {
|
|
2454
|
+
enabled: this.settingsManager.getReasoningRouterLearningEnabled(),
|
|
2455
|
+
ledgerPath: this.settingsManager.getReasoningRouterLearningFeedbackLedgerPath() ??
|
|
2456
|
+
this._getRepositoryRouterLearningPaths().ledgerPath,
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
_recordUserThinkingOverride(acceptedLevel) {
|
|
2460
|
+
const decision = this._lastAutoRouterDecision;
|
|
2461
|
+
this._lastAutoRouterDecision = undefined;
|
|
2462
|
+
if (!this.settingsManager.getReasoningRouterLearningEnabled() || !decision || acceptedLevel === "off")
|
|
2463
|
+
return;
|
|
2464
|
+
const resolvedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(decision.resolvedLevel);
|
|
2465
|
+
const acceptedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(acceptedLevel);
|
|
2466
|
+
const outcome = acceptedIndex > resolvedIndex ? "up" : acceptedIndex < resolvedIndex ? "down" : "same";
|
|
2467
|
+
this._appendRouterFeedback({
|
|
2468
|
+
...decision,
|
|
2469
|
+
acceptedLevel,
|
|
2470
|
+
signal: "s1-override",
|
|
2471
|
+
outcome,
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2178
2474
|
/**
|
|
2179
|
-
*
|
|
2180
|
-
*
|
|
2181
|
-
* session ("pinned"): the first call attempts the read and caches whatever
|
|
2182
|
-
* it finds (including a `null` miss/failure); every later call in the same
|
|
2183
|
-
* session reuses that cached result without touching disk again. Returns
|
|
2184
|
-
* `null` when no file exists at the configured/default path, the file
|
|
2185
|
-
* cannot be read, or its contents fail `parseRouterBiasSnapshot`'s schema
|
|
2186
|
-
* validation -- never throws.
|
|
2475
|
+
* Strictly load the compiled bias snapshot once per session. The first call
|
|
2476
|
+
* pins either a valid snapshot or a null miss; later calls never touch disk.
|
|
2187
2477
|
*/
|
|
2188
2478
|
_getReasoningRouterBiasSnapshot() {
|
|
2189
2479
|
if (this._reasoningRouterBiasSnapshotLoaded)
|
|
2190
2480
|
return this._reasoningRouterBiasSnapshot;
|
|
2191
2481
|
this._reasoningRouterBiasSnapshotLoaded = true;
|
|
2192
|
-
const path = this.settingsManager.getReasoningRouterLearningBiasSnapshotPath() ??
|
|
2482
|
+
const path = this.settingsManager.getReasoningRouterLearningBiasSnapshotPath() ??
|
|
2483
|
+
this._getRepositoryRouterLearningPaths().biasSnapshotPath;
|
|
2193
2484
|
try {
|
|
2194
2485
|
if (existsSync(path)) {
|
|
2195
2486
|
this._reasoningRouterBiasSnapshot = parseRouterBiasSnapshot(readFileSync(path, "utf-8"));
|
|
@@ -2200,29 +2491,6 @@ export class AgentSession {
|
|
|
2200
2491
|
}
|
|
2201
2492
|
return this._reasoningRouterBiasSnapshot;
|
|
2202
2493
|
}
|
|
2203
|
-
/**
|
|
2204
|
-
* Locally derives the same three bounded, privacy-safe feedback-ledger
|
|
2205
|
-
* features (`lenBucket`, `hadFence`, `hadDiff`) the v4 classifier computes
|
|
2206
|
-
* internally (reasoning-router-v4.ts keeps those helpers file-private, so
|
|
2207
|
-
* they cannot be imported here). Mirrors reasoning-router-v4.ts's
|
|
2208
|
-
* length-bucket, `hasCodeFence`, and `hasDiffMarkers` helpers. Operates on the same trimmed prompt text
|
|
2209
|
-
* the classifier scores; never returns raw prompt content or its exact
|
|
2210
|
-
* length, only the clamped [0,7] bucket and two booleans.
|
|
2211
|
-
*/
|
|
2212
|
-
_deriveRouterFeedbackFeatures(promptText) {
|
|
2213
|
-
const trimmed = promptText.trim();
|
|
2214
|
-
let lenBucket = 0;
|
|
2215
|
-
let remaining = trimmed.length + 1;
|
|
2216
|
-
while (remaining > 1 && lenBucket < 7) {
|
|
2217
|
-
remaining >>= 1;
|
|
2218
|
-
lenBucket++;
|
|
2219
|
-
}
|
|
2220
|
-
const hadFence = trimmed.includes("```");
|
|
2221
|
-
const hadDiff = /^@@[^\n]*@@/m.test(trimmed) ||
|
|
2222
|
-
/^diff --git /m.test(trimmed) ||
|
|
2223
|
-
(/^\+(?!\+)/m.test(trimmed) && /^-(?!-)/m.test(trimmed));
|
|
2224
|
-
return { lenBucket: lenBucket, hadFence, hadDiff };
|
|
2225
|
-
}
|
|
2226
2494
|
/**
|
|
2227
2495
|
* Context-pressure band 0..3 from the projected token estimate over the
|
|
2228
2496
|
* model context window (reuses estimateProjectedContextTokens). 0..<0.5,
|
|
@@ -2298,20 +2566,7 @@ export class AgentSession {
|
|
|
2298
2566
|
// Map confidenceBand -> numeric confidence for the resolver's threshold gate
|
|
2299
2567
|
const confidenceMap = { low: 0.5, medium: 0.75, high: 0.95 };
|
|
2300
2568
|
const confidence = confidenceMap[hint.confidenceBand] ?? 0.5;
|
|
2301
|
-
|
|
2302
|
-
const levelMap = {
|
|
2303
|
-
trivial: "minimal",
|
|
2304
|
-
"simple-edit": "low",
|
|
2305
|
-
"code-gen": "medium",
|
|
2306
|
-
debug: "high",
|
|
2307
|
-
refactor: "high",
|
|
2308
|
-
review: "high",
|
|
2309
|
-
plan: "xhigh",
|
|
2310
|
-
};
|
|
2311
|
-
const level = levelMap[hint.taskClass];
|
|
2312
|
-
if (level === undefined)
|
|
2313
|
-
return null;
|
|
2314
|
-
return { level, confidence };
|
|
2569
|
+
return { level: TASK_CLASS_THINKING_LEVELS_V4[hint.taskClass], confidence };
|
|
2315
2570
|
}
|
|
2316
2571
|
/**
|
|
2317
2572
|
* Cycle to next thinking level.
|
|
@@ -2324,7 +2579,12 @@ export class AgentSession {
|
|
|
2324
2579
|
const currentIndex = levels.indexOf(this.thinkingLevel);
|
|
2325
2580
|
const nextIndex = (currentIndex + 1) % levels.length;
|
|
2326
2581
|
const nextLevel = levels[nextIndex];
|
|
2582
|
+
const overridesAuto = this._thinkingMode === "auto";
|
|
2327
2583
|
this.setThinkingLevel(nextLevel);
|
|
2584
|
+
if (overridesAuto) {
|
|
2585
|
+
this.setThinkingMode("manual");
|
|
2586
|
+
this._recordUserThinkingOverride(nextLevel);
|
|
2587
|
+
}
|
|
2328
2588
|
return nextLevel;
|
|
2329
2589
|
}
|
|
2330
2590
|
/**
|
|
@@ -2340,7 +2600,7 @@ export class AgentSession {
|
|
|
2340
2600
|
* Check if current model supports thinking/reasoning.
|
|
2341
2601
|
*/
|
|
2342
2602
|
supportsThinking() {
|
|
2343
|
-
return
|
|
2603
|
+
return Boolean(this.model?.reasoning);
|
|
2344
2604
|
}
|
|
2345
2605
|
_getThinkingLevelForModelSwitch(explicitLevel) {
|
|
2346
2606
|
if (explicitLevel !== undefined) {
|
|
@@ -2490,8 +2750,8 @@ export class AgentSession {
|
|
|
2490
2750
|
* @param customInstructions Optional instructions for the compaction summary
|
|
2491
2751
|
*/
|
|
2492
2752
|
async compact(customInstructions) {
|
|
2493
|
-
this._disconnectFromAgent();
|
|
2494
2753
|
await this.abort();
|
|
2754
|
+
this._disconnectFromAgent();
|
|
2495
2755
|
this._compactionAbortController = new AbortController();
|
|
2496
2756
|
this._emit({ type: "compaction_start", reason: "manual" });
|
|
2497
2757
|
let committedCompaction = false;
|
|
@@ -2554,7 +2814,7 @@ export class AgentSession {
|
|
|
2554
2814
|
throw new Error("Compaction cancelled");
|
|
2555
2815
|
}
|
|
2556
2816
|
const compactionResult = {
|
|
2557
|
-
summary:
|
|
2817
|
+
summary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),
|
|
2558
2818
|
firstKeptEntryId,
|
|
2559
2819
|
tokensBefore,
|
|
2560
2820
|
details,
|
|
@@ -2582,6 +2842,11 @@ export class AgentSession {
|
|
|
2582
2842
|
const message = error instanceof Error ? error.message : String(error);
|
|
2583
2843
|
const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
|
|
2584
2844
|
const stale = /stale|session changed during compaction|already compacted/i.test(message);
|
|
2845
|
+
let compactionCode = "failed";
|
|
2846
|
+
if (aborted)
|
|
2847
|
+
compactionCode = "aborted";
|
|
2848
|
+
else if (stale)
|
|
2849
|
+
compactionCode = "stale";
|
|
2585
2850
|
const timestamp = new Date().toISOString();
|
|
2586
2851
|
this._publishTermination(classifySessionTermination({
|
|
2587
2852
|
sessionId: this.sessionId,
|
|
@@ -2589,7 +2854,7 @@ export class AgentSession {
|
|
|
2589
2854
|
timestamp,
|
|
2590
2855
|
source: "observed",
|
|
2591
2856
|
message: this._terminationMessage(message, "Manual compaction failed."),
|
|
2592
|
-
cause: { area: "compaction", code:
|
|
2857
|
+
cause: { area: "compaction", code: compactionCode },
|
|
2593
2858
|
sideEffects: committedCompaction ? "confirmed" : "none",
|
|
2594
2859
|
...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),
|
|
2595
2860
|
}));
|
|
@@ -2621,11 +2886,46 @@ export class AgentSession {
|
|
|
2621
2886
|
abortBranchSummary() {
|
|
2622
2887
|
this._branchSummaryAbortController?.abort();
|
|
2623
2888
|
}
|
|
2889
|
+
/** True when the transcript carries image content that forces a vision-route turn. */
|
|
2890
|
+
_transcriptHasImages(messages) {
|
|
2891
|
+
return messages.some((m) => {
|
|
2892
|
+
const content = m.content;
|
|
2893
|
+
if (!Array.isArray(content))
|
|
2894
|
+
return false;
|
|
2895
|
+
return content.some((part) => {
|
|
2896
|
+
return typeof part === "object" && part !== null && part.type === "image";
|
|
2897
|
+
});
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Effective context window for the upcoming turn.
|
|
2902
|
+
* Image-bearing turns with a text-only session model are auto-routed to the
|
|
2903
|
+
* vision model (gpt-5.6-luna, 400K) — that window is the real limit. Text-only
|
|
2904
|
+
* turns keep the session model's (typically much larger) window.
|
|
2905
|
+
*/
|
|
2906
|
+
_effectiveTurnContextWindow(pendingMessages, sessionWindow) {
|
|
2907
|
+
// Vision routing keys off the full transcript, not just the pending turn:
|
|
2908
|
+
// images retained in history keep every subsequent request on the vision model.
|
|
2909
|
+
const hasImages = this._transcriptHasImages(this.agent.state.messages) || this._transcriptHasImages(pendingMessages);
|
|
2910
|
+
const model = this.model;
|
|
2911
|
+
if (!hasImages || !model || (model.input ?? []).includes("image")) {
|
|
2912
|
+
return sessionWindow;
|
|
2913
|
+
}
|
|
2914
|
+
return Math.min(sessionWindow, getVisionRouteModel(model).contextWindow);
|
|
2915
|
+
}
|
|
2624
2916
|
async _checkProjectedCompaction(pendingMessages) {
|
|
2625
2917
|
const settings = this.settingsManager.getCompactionSettings();
|
|
2626
2918
|
if (!settings.enabled || pendingMessages.length === 0)
|
|
2627
2919
|
return false;
|
|
2628
|
-
const
|
|
2920
|
+
const sessionWindow = this.model?.contextWindow ?? 0;
|
|
2921
|
+
if (sessionWindow <= 0)
|
|
2922
|
+
return false;
|
|
2923
|
+
// Image-bearing turns are served by the auto-routed vision model, not the
|
|
2924
|
+
// session model. Its window (not the session model's) is the real limit the
|
|
2925
|
+
// provider enforces, so threshold compaction must use it or the vision
|
|
2926
|
+
// request overflows (context_length_exceeded) before deepseek-based
|
|
2927
|
+
// compaction would fire.
|
|
2928
|
+
const contextWindow = this._effectiveTurnContextWindow(pendingMessages, sessionWindow);
|
|
2629
2929
|
if (contextWindow <= 0)
|
|
2630
2930
|
return false;
|
|
2631
2931
|
const messages = [...this.agent.state.messages, ...pendingMessages];
|
|
@@ -2640,7 +2940,7 @@ export class AgentSession {
|
|
|
2640
2940
|
}
|
|
2641
2941
|
const decision = this._runtimeCompactionDecision(estimate.tokens, contextWindow, settings);
|
|
2642
2942
|
if (decision.compact) {
|
|
2643
|
-
return
|
|
2943
|
+
return this._runThresholdCompaction(decision.emergency);
|
|
2644
2944
|
}
|
|
2645
2945
|
return false;
|
|
2646
2946
|
}
|
|
@@ -2668,6 +2968,13 @@ export class AgentSession {
|
|
|
2668
2968
|
// to a larger-context model (e.g. codex) - the overflow error from the old model
|
|
2669
2969
|
// shouldn't trigger compaction for the new model.
|
|
2670
2970
|
const sameModel = this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
|
|
2971
|
+
// Image-bearing turns are auto-routed to a vision-capable model (gpt-5.6-luna)
|
|
2972
|
+
// while the session model stays text-only (deepseek). An overflow from that
|
|
2973
|
+
// routed model must still count as the session's own overflow: compaction is
|
|
2974
|
+
// the fix, and the raw context_length_exceeded error must never reach the user.
|
|
2975
|
+
const visionRouteOverflow = this.model != null &&
|
|
2976
|
+
!(this.model.input ?? []).includes("image") &&
|
|
2977
|
+
isVisionRouteModel({ provider: assistantMessage.provider, id: assistantMessage.model });
|
|
2671
2978
|
// Skip compaction checks if this assistant message is older than the latest
|
|
2672
2979
|
// compaction boundary. This prevents a stale pre-compaction usage/error
|
|
2673
2980
|
// from retriggering compaction on the first prompt after compaction.
|
|
@@ -2677,26 +2984,26 @@ export class AgentSession {
|
|
|
2677
2984
|
return false;
|
|
2678
2985
|
}
|
|
2679
2986
|
// Case 1: Overflow - LLM returned context overflow error
|
|
2680
|
-
if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
|
|
2681
|
-
if (this.
|
|
2987
|
+
if ((sameModel || visionRouteOverflow) && isContextOverflow(assistantMessage, contextWindow)) {
|
|
2988
|
+
if (this._overflowRecoveryAttempts >= MAX_OVERFLOW_RECOVERY_ATTEMPTS) {
|
|
2682
2989
|
this._emit({
|
|
2683
2990
|
type: "compaction_end",
|
|
2684
2991
|
reason: "overflow",
|
|
2685
2992
|
result: undefined,
|
|
2686
2993
|
aborted: false,
|
|
2687
2994
|
willRetry: false,
|
|
2688
|
-
errorMessage: "Context overflow recovery failed after
|
|
2995
|
+
errorMessage: "Context overflow recovery failed after two staged compact-and-retry attempts. Reduce the latest input or switch to a model with a larger effective context window.",
|
|
2689
2996
|
});
|
|
2690
2997
|
return false;
|
|
2691
2998
|
}
|
|
2692
|
-
this.
|
|
2999
|
+
this._overflowRecoveryAttempts++;
|
|
2693
3000
|
// Remove the error message from agent state (it IS saved to session for history,
|
|
2694
3001
|
// but we don't want it in context for the retry)
|
|
2695
3002
|
const messages = this.agent.state.messages;
|
|
2696
3003
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2697
3004
|
this.agent.state.messages = messages.slice(0, -1);
|
|
2698
3005
|
}
|
|
2699
|
-
return
|
|
3006
|
+
return this._runAutoCompaction("overflow", true);
|
|
2700
3007
|
}
|
|
2701
3008
|
// Case 2: Threshold - context is getting large
|
|
2702
3009
|
// For error messages (no usage data), estimate from last successful response.
|
|
@@ -2723,15 +3030,28 @@ export class AgentSession {
|
|
|
2723
3030
|
}
|
|
2724
3031
|
const decision = this._runtimeCompactionDecision(contextTokens, contextWindow, settings);
|
|
2725
3032
|
if (decision.compact) {
|
|
2726
|
-
return
|
|
3033
|
+
return this._runThresholdCompaction(decision.emergency);
|
|
2727
3034
|
}
|
|
2728
3035
|
return false;
|
|
2729
3036
|
}
|
|
3037
|
+
_overflowCompactionSettings(settings, attempt) {
|
|
3038
|
+
if (attempt < MAX_OVERFLOW_RECOVERY_ATTEMPTS)
|
|
3039
|
+
return settings;
|
|
3040
|
+
return {
|
|
3041
|
+
...settings,
|
|
3042
|
+
reserveTokens: Math.min(settings.reserveTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),
|
|
3043
|
+
reservedOutputTokens: Math.min(settings.reservedOutputTokens ?? settings.reserveTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),
|
|
3044
|
+
keepRecentTokens: Math.min(settings.keepRecentTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
2730
3047
|
/**
|
|
2731
3048
|
* Internal: Run auto-compaction with events.
|
|
2732
3049
|
*/
|
|
2733
3050
|
async _runAutoCompaction(reason, willRetry, emergency = reason === "overflow" || this._thresholdCompactionEmergency) {
|
|
2734
|
-
const
|
|
3051
|
+
const configuredSettings = this.settingsManager.getCompactionSettings();
|
|
3052
|
+
const settings = reason === "overflow"
|
|
3053
|
+
? this._overflowCompactionSettings(configuredSettings, this._overflowRecoveryAttempts)
|
|
3054
|
+
: configuredSettings;
|
|
2735
3055
|
this._emit({ type: "compaction_start", reason });
|
|
2736
3056
|
this._autoCompactionAbortController = new AbortController();
|
|
2737
3057
|
try {
|
|
@@ -2837,7 +3157,7 @@ export class AgentSession {
|
|
|
2837
3157
|
return false;
|
|
2838
3158
|
}
|
|
2839
3159
|
const result = {
|
|
2840
|
-
summary:
|
|
3160
|
+
summary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),
|
|
2841
3161
|
firstKeptEntryId,
|
|
2842
3162
|
tokensBefore,
|
|
2843
3163
|
details,
|
|
@@ -2953,6 +3273,9 @@ export class AgentSession {
|
|
|
2953
3273
|
this._resourceLoader.extendResources(extensionPaths);
|
|
2954
3274
|
this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
|
|
2955
3275
|
this.agent.state.systemPrompt = this._baseSystemPrompt;
|
|
3276
|
+
this.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;
|
|
3277
|
+
this.agent.state.systemPromptCacheBoundaryBypass = false;
|
|
3278
|
+
this._recordPromptCachePlan("resource-reload");
|
|
2956
3279
|
}
|
|
2957
3280
|
buildExtensionResourcePaths(entries) {
|
|
2958
3281
|
return entries.map((entry) => {
|
|
@@ -3108,6 +3431,54 @@ export class AgentSession {
|
|
|
3108
3431
|
},
|
|
3109
3432
|
});
|
|
3110
3433
|
}
|
|
3434
|
+
/**
|
|
3435
|
+
* Connect configured MCP servers and register their tools for this session.
|
|
3436
|
+
*
|
|
3437
|
+
* Connection is lazy by construction: nothing is spawned until this is called,
|
|
3438
|
+
* and a server that fails to start is reported in the returned status instead
|
|
3439
|
+
* of taking the session down. Calling it twice replaces the previously
|
|
3440
|
+
* attached MCP tools rather than duplicating them.
|
|
3441
|
+
*
|
|
3442
|
+
* Returns per-server status so a caller can surface failures; an empty
|
|
3443
|
+
* configuration returns an empty array without spawning anything.
|
|
3444
|
+
*/
|
|
3445
|
+
async attachMcpServers(options) {
|
|
3446
|
+
const servers = options?.servers ?? loadMcpServerConfigs(this._cwd);
|
|
3447
|
+
this._mcpManager?.close();
|
|
3448
|
+
this._mcpManager = undefined;
|
|
3449
|
+
this._customTools = this._customTools.filter((definition) => !this._mcpToolNames.has(definition.name));
|
|
3450
|
+
this._mcpToolNames = new Set();
|
|
3451
|
+
if (servers.length === 0) {
|
|
3452
|
+
this._refreshToolRegistry();
|
|
3453
|
+
return [];
|
|
3454
|
+
}
|
|
3455
|
+
const manager = new McpManager({
|
|
3456
|
+
servers,
|
|
3457
|
+
cwd: this._cwd,
|
|
3458
|
+
clientInfo: { name: APP_NAME, version: VERSION },
|
|
3459
|
+
callTimeoutMs: options?.callTimeoutMs,
|
|
3460
|
+
});
|
|
3461
|
+
this._mcpManager = manager;
|
|
3462
|
+
const definitions = await manager.listToolDefinitions();
|
|
3463
|
+
// A builtin always wins a name collision; MCP must never shadow `bash`.
|
|
3464
|
+
const usable = definitions.filter((definition) => !this._baseToolDefinitions.has(definition.name));
|
|
3465
|
+
this._mcpToolNames = new Set(usable.map((definition) => definition.name));
|
|
3466
|
+
this._customTools = [...this._customTools, ...usable];
|
|
3467
|
+
this._refreshToolRegistry();
|
|
3468
|
+
return manager.status();
|
|
3469
|
+
}
|
|
3470
|
+
/** Status of MCP servers attached to this session. Empty when none were attached. */
|
|
3471
|
+
mcpServerStatus() {
|
|
3472
|
+
return this._mcpManager?.status() ?? [];
|
|
3473
|
+
}
|
|
3474
|
+
/**
|
|
3475
|
+
* Ping attached MCP servers and return fresh status, so callers (e.g. the
|
|
3476
|
+
* status rail) can keep displaying real connectivity instead of the last
|
|
3477
|
+
* handshake result. Resolves to [] when no manager is attached.
|
|
3478
|
+
*/
|
|
3479
|
+
mcpCheckHealth(options) {
|
|
3480
|
+
return this._mcpManager?.checkHealth(options) ?? Promise.resolve([]);
|
|
3481
|
+
}
|
|
3111
3482
|
_refreshToolRegistry(options) {
|
|
3112
3483
|
const previousRegistryNames = new Set(this._toolRegistry.keys());
|
|
3113
3484
|
const previousActiveToolNames = this.getActiveToolNames();
|
|
@@ -3128,15 +3499,17 @@ export class AgentSession {
|
|
|
3128
3499
|
throw new Error(`loadout extension tool shadows builtin: ${shadowedBuiltins.join(", ")}`);
|
|
3129
3500
|
}
|
|
3130
3501
|
}
|
|
3131
|
-
const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries())
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3502
|
+
const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries()).flatMap(([name, definition]) => isAllowedTool(name)
|
|
3503
|
+
? [
|
|
3504
|
+
[
|
|
3505
|
+
name,
|
|
3506
|
+
{
|
|
3507
|
+
definition,
|
|
3508
|
+
sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
|
|
3509
|
+
},
|
|
3510
|
+
],
|
|
3511
|
+
]
|
|
3512
|
+
: []));
|
|
3140
3513
|
for (const tool of allCustomTools) {
|
|
3141
3514
|
definitionRegistry.set(tool.definition.name, {
|
|
3142
3515
|
definition: tool.definition,
|
|
@@ -3144,26 +3517,29 @@ export class AgentSession {
|
|
|
3144
3517
|
});
|
|
3145
3518
|
}
|
|
3146
3519
|
this._toolDefinitions = definitionRegistry;
|
|
3147
|
-
this._toolPromptSnippets = new Map(Array.from(definitionRegistry.values())
|
|
3148
|
-
.map(({ definition }) => {
|
|
3520
|
+
this._toolPromptSnippets = new Map(Array.from(definitionRegistry.values()).flatMap(({ definition }) => {
|
|
3149
3521
|
const snippet = this._normalizePromptSnippet(definition.promptSnippet);
|
|
3150
|
-
return snippet ? [definition.name, snippet] :
|
|
3151
|
-
})
|
|
3152
|
-
|
|
3153
|
-
this._toolPromptGuidelines = new Map(Array.from(definitionRegistry.values())
|
|
3154
|
-
.map(({ definition }) => {
|
|
3522
|
+
return snippet ? [[definition.name, snippet]] : [];
|
|
3523
|
+
}));
|
|
3524
|
+
this._toolPromptGuidelines = new Map(Array.from(definitionRegistry.values()).flatMap(({ definition }) => {
|
|
3155
3525
|
const guidelines = this._normalizePromptGuidelines(definition.promptGuidelines);
|
|
3156
|
-
return guidelines.length > 0 ? [definition.name, guidelines] :
|
|
3157
|
-
})
|
|
3158
|
-
|
|
3526
|
+
return guidelines.length > 0 ? [[definition.name, guidelines]] : [];
|
|
3527
|
+
}));
|
|
3528
|
+
// Tools contributed by extensions the harness itself loads (`<builtin:*>`),
|
|
3529
|
+
// as opposed to user/project/SDK extensions.
|
|
3530
|
+
const builtinExtensionToolNames = new Set(allCustomTools.flatMap((tool) => (tool.sourceInfo.path.startsWith("<builtin:") ? [tool.definition.name] : [])));
|
|
3159
3531
|
const runner = this._extensionRunner;
|
|
3160
3532
|
const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner);
|
|
3161
|
-
const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values())
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3533
|
+
const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values()).flatMap((definition) => isAllowedTool(definition.name)
|
|
3534
|
+
? [
|
|
3535
|
+
{
|
|
3536
|
+
definition,
|
|
3537
|
+
sourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, {
|
|
3538
|
+
source: "builtin",
|
|
3539
|
+
}),
|
|
3540
|
+
},
|
|
3541
|
+
]
|
|
3542
|
+
: []), runner);
|
|
3167
3543
|
const toolRegistry = new Map(wrappedBuiltInTools.map((tool) => [tool.name, tool]));
|
|
3168
3544
|
for (const tool of wrappedExtensionTools) {
|
|
3169
3545
|
toolRegistry.set(tool.name, tool);
|
|
@@ -3186,7 +3562,14 @@ export class AgentSession {
|
|
|
3186
3562
|
}
|
|
3187
3563
|
}
|
|
3188
3564
|
else if (options?.includeAllExtensionTools) {
|
|
3565
|
+
// "Keep extension tools on" is about the *user's* extensions. Tools shipped
|
|
3566
|
+
// by a built-in extension (e.g. update_todo) are part of the harness's own
|
|
3567
|
+
// tool surface, so they follow the built-in policy: when the caller asked
|
|
3568
|
+
// for no built-in tools, they stay off too.
|
|
3569
|
+
const builtinToolsDisabled = options.activeToolNames?.length === 0;
|
|
3189
3570
|
for (const tool of wrappedExtensionTools) {
|
|
3571
|
+
if (builtinToolsDisabled && builtinExtensionToolNames.has(tool.name))
|
|
3572
|
+
continue;
|
|
3190
3573
|
nextActiveToolNames.push(tool.name);
|
|
3191
3574
|
}
|
|
3192
3575
|
}
|
|
@@ -3217,12 +3600,14 @@ export class AgentSession {
|
|
|
3217
3600
|
canWritePath: (path) => loadoutAccessGuard({ operation: "write", toolName: "write", path }).allowed,
|
|
3218
3601
|
}
|
|
3219
3602
|
: {};
|
|
3220
|
-
const baseToolDefinitions =
|
|
3221
|
-
|
|
3222
|
-
name,
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3603
|
+
const baseToolDefinitions = (() => {
|
|
3604
|
+
if (this._baseToolsOverride) {
|
|
3605
|
+
return Object.fromEntries(Object.entries(this._baseToolsOverride).map(([name, tool]) => [
|
|
3606
|
+
name,
|
|
3607
|
+
createToolDefinitionFromAgentTool(tool),
|
|
3608
|
+
]));
|
|
3609
|
+
}
|
|
3610
|
+
return createAllToolDefinitions(this._cwd, {
|
|
3226
3611
|
read: { autoResizeImages, ...loadoutReadOptions },
|
|
3227
3612
|
bash: {
|
|
3228
3613
|
commandPrefix: shellCommandPrefix,
|
|
@@ -3255,6 +3640,7 @@ export class AgentSession {
|
|
|
3255
3640
|
edit: loadoutWriteOptions,
|
|
3256
3641
|
write: loadoutWriteOptions,
|
|
3257
3642
|
});
|
|
3643
|
+
})();
|
|
3258
3644
|
this._baseToolDefinitions = new Map(Object.entries(baseToolDefinitions).map(([name, tool]) => [name, tool]));
|
|
3259
3645
|
const extensionsResult = this._resourceLoader.getExtensions();
|
|
3260
3646
|
if (options.flagValues) {
|
|
@@ -3301,9 +3687,6 @@ export class AgentSession {
|
|
|
3301
3687
|
// =========================================================================
|
|
3302
3688
|
// Auto-Retry
|
|
3303
3689
|
// =========================================================================
|
|
3304
|
-
_isNonRetryableProviderLimitError(errorMessage) {
|
|
3305
|
-
return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(errorMessage);
|
|
3306
|
-
}
|
|
3307
3690
|
/**
|
|
3308
3691
|
* Check if an error is retryable (overloaded, rate limit, server errors).
|
|
3309
3692
|
* Context overflow errors are NOT retryable (handled by compaction instead).
|
|
@@ -3316,8 +3699,11 @@ export class AgentSession {
|
|
|
3316
3699
|
if (isContextOverflow(message, contextWindow))
|
|
3317
3700
|
return false;
|
|
3318
3701
|
const err = message.errorMessage;
|
|
3319
|
-
|
|
3320
|
-
|
|
3702
|
+
// Billing/quota exhaustion: useless to retry the same model until the
|
|
3703
|
+
// cycle resets, but the turn may still be saved by failing over to the
|
|
3704
|
+
// next candidate, so let _prepareRetry run its failover path.
|
|
3705
|
+
if (isQuotaExhaustionMessage(err))
|
|
3706
|
+
return true;
|
|
3321
3707
|
// Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
|
|
3322
3708
|
return isTransientProviderErrorMessage(err);
|
|
3323
3709
|
}
|
|
@@ -3363,7 +3749,9 @@ export class AgentSession {
|
|
|
3363
3749
|
return;
|
|
3364
3750
|
const pick = pickFailoverCandidate(resilience.failoverCandidates, { provider: current.provider, id: current.id }, (c) => {
|
|
3365
3751
|
const next = this._modelRegistry.find(c.provider, c.id);
|
|
3366
|
-
|
|
3752
|
+
if (!next)
|
|
3753
|
+
return false;
|
|
3754
|
+
return this._modelRegistry.hasConfiguredAuth(next);
|
|
3367
3755
|
});
|
|
3368
3756
|
if (!pick) {
|
|
3369
3757
|
throw new Error(stickySafetyBlockMessage(current.id, current.provider));
|
|
@@ -3385,8 +3773,11 @@ export class AgentSession {
|
|
|
3385
3773
|
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
3386
3774
|
if (!resilience.autoFailoverOnSafetyStop)
|
|
3387
3775
|
return undefined;
|
|
3388
|
-
|
|
3776
|
+
// Fire on safety-stop FPs AND on billing/quota exhaustion: both mean the
|
|
3777
|
+
// current model cannot finish this turn and same-model retry is useless.
|
|
3778
|
+
if (!isContentSafetyStopMessage(message.errorMessage) && !isQuotaExhaustionMessage(message.errorMessage)) {
|
|
3389
3779
|
return undefined;
|
|
3780
|
+
}
|
|
3390
3781
|
const current = this.model;
|
|
3391
3782
|
// v10.0-Ω: do NOT gate on isStickySafetyModel(source).
|
|
3392
3783
|
// claude-opus-5 emits the same stop_reason=refusal FP; same-model retry is useless.
|
|
@@ -3398,7 +3789,9 @@ export class AgentSession {
|
|
|
3398
3789
|
if (this._refusedModels.has(`${c.provider}/${c.id}`))
|
|
3399
3790
|
return false;
|
|
3400
3791
|
const next = this._modelRegistry.find(c.provider, c.id);
|
|
3401
|
-
|
|
3792
|
+
if (!next)
|
|
3793
|
+
return false;
|
|
3794
|
+
return this._modelRegistry.hasConfiguredAuth(next);
|
|
3402
3795
|
});
|
|
3403
3796
|
if (!pick)
|
|
3404
3797
|
return undefined;
|
|
@@ -3623,6 +4016,8 @@ export class AgentSession {
|
|
|
3623
4016
|
let summaryDetails;
|
|
3624
4017
|
if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
|
|
3625
4018
|
const model = this.model;
|
|
4019
|
+
if (!model)
|
|
4020
|
+
throw new Error("No model available for summarization");
|
|
3626
4021
|
const { apiKey, headers } = await this._getRequiredRequestAuth(model);
|
|
3627
4022
|
const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
|
|
3628
4023
|
const result = await generateBranchSummary(entriesToSummarize, {
|
|
@@ -3664,13 +4059,18 @@ export class AgentSession {
|
|
|
3664
4059
|
else if (targetEntry.type === "custom_message") {
|
|
3665
4060
|
// Custom message: leaf = parent (null if root), text goes to editor
|
|
3666
4061
|
newLeafId = targetEntry.parentId;
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
4062
|
+
if (typeof targetEntry.content === "string") {
|
|
4063
|
+
editorText = targetEntry.content;
|
|
4064
|
+
}
|
|
4065
|
+
else {
|
|
4066
|
+
editorText = targetEntry.content
|
|
4067
|
+
.flatMap((item) => {
|
|
4068
|
+
if (item.type === "text")
|
|
4069
|
+
return [item.text];
|
|
4070
|
+
return [];
|
|
4071
|
+
})
|
|
4072
|
+
.join("");
|
|
4073
|
+
}
|
|
3674
4074
|
}
|
|
3675
4075
|
else {
|
|
3676
4076
|
// Non-user message: leaf = selected node
|
|
@@ -3741,8 +4141,7 @@ export class AgentSession {
|
|
|
3741
4141
|
return content;
|
|
3742
4142
|
if (Array.isArray(content)) {
|
|
3743
4143
|
return content
|
|
3744
|
-
.
|
|
3745
|
-
.map((c) => c.text)
|
|
4144
|
+
.flatMap((item) => (item.type === "text" && typeof item.text === "string" ? [item.text] : []))
|
|
3746
4145
|
.join("");
|
|
3747
4146
|
}
|
|
3748
4147
|
return "";
|
|
@@ -3772,6 +4171,7 @@ export class AgentSession {
|
|
|
3772
4171
|
totalCost += assistantMsg.usage.cost.total;
|
|
3773
4172
|
}
|
|
3774
4173
|
}
|
|
4174
|
+
const providerEligibleInputTokens = totalInput + totalCacheRead + totalCacheWrite;
|
|
3775
4175
|
return {
|
|
3776
4176
|
sessionFile: this.sessionFile,
|
|
3777
4177
|
sessionId: this.sessionId,
|
|
@@ -3787,6 +4187,14 @@ export class AgentSession {
|
|
|
3787
4187
|
cacheWrite: totalCacheWrite,
|
|
3788
4188
|
total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
|
|
3789
4189
|
},
|
|
4190
|
+
promptCache: {
|
|
4191
|
+
providerEligibleInputTokens,
|
|
4192
|
+
providerHitRate: providerEligibleInputTokens > 0 ? totalCacheRead / providerEligibleInputTokens : 0,
|
|
4193
|
+
keyChanges: this._promptCacheKeyChanges,
|
|
4194
|
+
boundaryBypasses: this._promptCacheBoundaryBypasses,
|
|
4195
|
+
stablePrefixCharacters: this._promptCacheStablePrefixCharacters,
|
|
4196
|
+
...(this._promptCacheLastBreakReason ? { lastBreakReason: this._promptCacheLastBreakReason } : {}),
|
|
4197
|
+
},
|
|
3790
4198
|
cost: totalCost,
|
|
3791
4199
|
contextUsage: this.getContextUsage(),
|
|
3792
4200
|
};
|
|
@@ -3845,7 +4253,7 @@ export class AgentSession {
|
|
|
3845
4253
|
theme,
|
|
3846
4254
|
cwd: this.sessionManager.getCwd(),
|
|
3847
4255
|
});
|
|
3848
|
-
return
|
|
4256
|
+
return exportSessionToHtml(this.sessionManager, this.state, {
|
|
3849
4257
|
outputPath,
|
|
3850
4258
|
themeName,
|
|
3851
4259
|
toolRenderer,
|
|
@@ -3891,18 +4299,18 @@ export class AgentSession {
|
|
|
3891
4299
|
* @returns Text content, or undefined if no assistant message exists
|
|
3892
4300
|
*/
|
|
3893
4301
|
getLastAssistantText() {
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
.
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
const msg = m;
|
|
4302
|
+
let lastAssistant;
|
|
4303
|
+
for (let index = this.messages.length - 1; index >= 0; index -= 1) {
|
|
4304
|
+
const message = this.messages[index];
|
|
4305
|
+
if (message?.role !== "assistant")
|
|
4306
|
+
continue;
|
|
4307
|
+
const assistant = message;
|
|
3901
4308
|
// Skip aborted messages with no content
|
|
3902
|
-
if (
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
4309
|
+
if (assistant.stopReason === "aborted" && assistant.content.length === 0)
|
|
4310
|
+
continue;
|
|
4311
|
+
lastAssistant = assistant;
|
|
4312
|
+
break;
|
|
4313
|
+
}
|
|
3906
4314
|
if (!lastAssistant)
|
|
3907
4315
|
return undefined;
|
|
3908
4316
|
let text = "";
|