@caupulican/pi-adaptative 0.81.26 → 0.81.29
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 +45 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +3 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +4 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +19 -2
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +30 -12
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +128 -36
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +6 -0
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +35 -0
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/autonomy/bounded-completion.d.ts.map +1 -1
- package/dist/core/autonomy/bounded-completion.js +59 -19
- package/dist/core/autonomy/bounded-completion.js.map +1 -1
- package/dist/core/autonomy/contracts.d.ts +2 -0
- package/dist/core/autonomy/contracts.d.ts.map +1 -1
- package/dist/core/autonomy/contracts.js.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.d.ts +31 -0
- package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -0
- package/dist/core/autonomy/lane-tool-surface.js +96 -0
- package/dist/core/autonomy/lane-tool-surface.js.map +1 -0
- package/dist/core/autonomy/lane-tracker.d.ts +5 -0
- package/dist/core/autonomy/lane-tracker.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tracker.js +14 -3
- package/dist/core/autonomy/lane-tracker.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts +28 -4
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +237 -28
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/billing-failover-controller.d.ts.map +1 -1
- package/dist/core/billing-failover-controller.js +1 -1
- package/dist/core/billing-failover-controller.js.map +1 -1
- package/dist/core/context/current-work-memory.d.ts +9 -0
- package/dist/core/context/current-work-memory.d.ts.map +1 -0
- package/dist/core/context/current-work-memory.js +61 -0
- package/dist/core/context/current-work-memory.js.map +1 -0
- package/dist/core/context/file-store-memory-provider.d.ts +9 -0
- package/dist/core/context/file-store-memory-provider.d.ts.map +1 -0
- package/dist/core/context/file-store-memory-provider.js +106 -0
- package/dist/core/context/file-store-memory-provider.js.map +1 -0
- package/dist/core/context/long-term-memory-trigger.d.ts +15 -0
- package/dist/core/context/long-term-memory-trigger.d.ts.map +1 -0
- package/dist/core/context/long-term-memory-trigger.js +39 -0
- package/dist/core/context/long-term-memory-trigger.js.map +1 -0
- package/dist/core/context/memory-prompt-block.d.ts +3 -1
- package/dist/core/context/memory-prompt-block.d.ts.map +1 -1
- package/dist/core/context/memory-prompt-block.js +20 -3
- package/dist/core/context/memory-prompt-block.js.map +1 -1
- package/dist/core/context/memory-prompt-budget.d.ts +17 -0
- package/dist/core/context/memory-prompt-budget.d.ts.map +1 -0
- package/dist/core/context/memory-prompt-budget.js +64 -0
- package/dist/core/context/memory-prompt-budget.js.map +1 -0
- package/dist/core/context/memory-provider-contract.d.ts.map +1 -1
- package/dist/core/context/memory-provider-contract.js +2 -5
- package/dist/core/context/memory-provider-contract.js.map +1 -1
- package/dist/core/context/memory-tier-composer.d.ts +27 -0
- package/dist/core/context/memory-tier-composer.d.ts.map +1 -0
- package/dist/core/context/memory-tier-composer.js +79 -0
- package/dist/core/context/memory-tier-composer.js.map +1 -0
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +2 -0
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/context-pipeline.d.ts +5 -3
- package/dist/core/context-pipeline.d.ts.map +1 -1
- package/dist/core/context-pipeline.js +15 -5
- package/dist/core/context-pipeline.js.map +1 -1
- package/dist/core/cost/cost-summary.d.ts +3 -1
- package/dist/core/cost/cost-summary.d.ts.map +1 -1
- package/dist/core/cost/cost-summary.js +3 -3
- package/dist/core/cost/cost-summary.js.map +1 -1
- package/dist/core/cost-guard.d.ts +17 -7
- package/dist/core/cost-guard.d.ts.map +1 -1
- package/dist/core/cost-guard.js +33 -10
- package/dist/core/cost-guard.js.map +1 -1
- package/dist/core/default-tool-surface.d.ts +9 -0
- package/dist/core/default-tool-surface.d.ts.map +1 -0
- package/dist/core/default-tool-surface.js +18 -0
- package/dist/core/default-tool-surface.js.map +1 -0
- package/dist/core/delegation/worker-result.d.ts.map +1 -1
- package/dist/core/delegation/worker-result.js +3 -0
- package/dist/core/delegation/worker-result.js.map +1 -1
- package/dist/core/delegation/worker-runner.d.ts +11 -5
- package/dist/core/delegation/worker-runner.d.ts.map +1 -1
- package/dist/core/delegation/worker-runner.js +77 -16
- package/dist/core/delegation/worker-runner.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +15 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +2 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +7 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +9 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/failure-corpus.d.ts.map +1 -1
- package/dist/core/failure-corpus.js +2 -4
- package/dist/core/failure-corpus.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +9 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/goals/goal-continuation-prompt.d.ts.map +1 -1
- package/dist/core/goals/goal-continuation-prompt.js +4 -3
- package/dist/core/goals/goal-continuation-prompt.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts +12 -0
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +42 -4
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/local-runtime-controller.d.ts +16 -0
- package/dist/core/local-runtime-controller.d.ts.map +1 -1
- package/dist/core/local-runtime-controller.js +107 -28
- package/dist/core/local-runtime-controller.js.map +1 -1
- package/dist/core/memory/memory-manager.d.ts +8 -3
- package/dist/core/memory/memory-manager.d.ts.map +1 -1
- package/dist/core/memory/memory-manager.js +27 -11
- package/dist/core/memory/memory-manager.js.map +1 -1
- package/dist/core/memory/memory-provider.d.ts +5 -1
- package/dist/core/memory/memory-provider.d.ts.map +1 -1
- package/dist/core/memory/memory-provider.js.map +1 -1
- package/dist/core/memory/providers/file-store.d.ts +3 -1
- package/dist/core/memory/providers/file-store.d.ts.map +1 -1
- package/dist/core/memory/providers/file-store.js +7 -2
- package/dist/core/memory/providers/file-store.js.map +1 -1
- package/dist/core/memory/providers/transcript-recall.d.ts +1 -0
- package/dist/core/memory/providers/transcript-recall.d.ts.map +1 -1
- package/dist/core/memory/providers/transcript-recall.js +4 -7
- package/dist/core/memory/providers/transcript-recall.js.map +1 -1
- package/dist/core/memory-controller.d.ts +36 -10
- package/dist/core/memory-controller.d.ts.map +1 -1
- package/dist/core/memory-controller.js +137 -21
- package/dist/core/memory-controller.js.map +1 -1
- package/dist/core/model-registry.d.ts +25 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +91 -8
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +30 -11
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-router-controller.d.ts.map +1 -1
- package/dist/core/model-router-controller.js +33 -29
- package/dist/core/model-router-controller.js.map +1 -1
- package/dist/core/model-selection-controller.d.ts +3 -0
- package/dist/core/model-selection-controller.d.ts.map +1 -1
- package/dist/core/model-selection-controller.js +8 -1
- package/dist/core/model-selection-controller.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts +2 -0
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +10 -4
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/models/perf-profile.d.ts.map +1 -1
- package/dist/core/models/perf-profile.js +95 -33
- package/dist/core/models/perf-profile.js.map +1 -1
- package/dist/core/models/runtime-arbiter.d.ts +11 -0
- package/dist/core/models/runtime-arbiter.d.ts.map +1 -1
- package/dist/core/models/runtime-arbiter.js +35 -13
- package/dist/core/models/runtime-arbiter.js.map +1 -1
- package/dist/core/profile-filter-controller.d.ts +11 -0
- package/dist/core/profile-filter-controller.d.ts.map +1 -1
- package/dist/core/profile-filter-controller.js +56 -11
- package/dist/core/profile-filter-controller.js.map +1 -1
- package/dist/core/profile-registry.d.ts +2 -2
- package/dist/core/profile-registry.d.ts.map +1 -1
- package/dist/core/profile-registry.js +58 -7
- package/dist/core/profile-registry.js.map +1 -1
- package/dist/core/reflection-controller.d.ts +9 -7
- package/dist/core/reflection-controller.d.ts.map +1 -1
- package/dist/core/reflection-controller.js +81 -12
- package/dist/core/reflection-controller.js.map +1 -1
- package/dist/core/research/research-runner.d.ts +2 -2
- package/dist/core/research/research-runner.d.ts.map +1 -1
- package/dist/core/research/research-runner.js +8 -6
- package/dist/core/research/research-runner.js.map +1 -1
- package/dist/core/resource-loader.d.ts +4 -2
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +11 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/runtime-builder.d.ts +30 -3
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +140 -40
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/sdk.d.ts +8 -7
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +38 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/security/secret-text.d.ts +3 -0
- package/dist/core/security/secret-text.d.ts.map +1 -0
- package/dist/core/security/secret-text.js +26 -0
- package/dist/core/security/secret-text.js.map +1 -0
- package/dist/core/settings-manager.d.ts +59 -17
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +300 -51
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +1 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +7 -0
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.js +23 -1
- package/dist/core/system-prompt-builder.js.map +1 -1
- package/dist/core/tool-gate-controller.d.ts +3 -0
- package/dist/core/tool-gate-controller.d.ts.map +1 -1
- package/dist/core/tool-gate-controller.js +20 -15
- package/dist/core/tool-gate-controller.js.map +1 -1
- package/dist/core/tool-selection/expected-utility.d.ts +51 -0
- package/dist/core/tool-selection/expected-utility.d.ts.map +1 -0
- package/dist/core/tool-selection/expected-utility.js +85 -0
- package/dist/core/tool-selection/expected-utility.js.map +1 -0
- package/dist/core/tool-selection/tool-performance-store.d.ts +82 -0
- package/dist/core/tool-selection/tool-performance-store.d.ts.map +1 -0
- package/dist/core/tool-selection/tool-performance-store.js +249 -0
- package/dist/core/tool-selection/tool-performance-store.js.map +1 -0
- package/dist/core/tool-selection/tool-selection-controller.d.ts +41 -0
- package/dist/core/tool-selection/tool-selection-controller.d.ts.map +1 -0
- package/dist/core/tool-selection/tool-selection-controller.js +199 -0
- package/dist/core/tool-selection/tool-selection-controller.js.map +1 -0
- package/dist/core/tools/delegate-status.d.ts +9 -0
- package/dist/core/tools/delegate-status.d.ts.map +1 -0
- package/dist/core/tools/delegate-status.js +52 -0
- package/dist/core/tools/delegate-status.js.map +1 -0
- package/dist/core/tools/delegate.d.ts +10 -0
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +36 -13
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/run-toolkit-script.d.ts +3 -0
- package/dist/core/tools/run-toolkit-script.d.ts.map +1 -1
- package/dist/core/tools/run-toolkit-script.js +17 -7
- package/dist/core/tools/run-toolkit-script.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +8 -1
- 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 +69 -29
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +2 -0
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/config-backup.d.ts +7 -1
- package/dist/modes/interactive/config-backup.d.ts.map +1 -1
- package/dist/modes/interactive/config-backup.js +196 -32
- package/dist/modes/interactive/config-backup.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +22 -28
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
- package/dist/modes/interactive/local-model-commands.js +2 -0
- package/dist/modes/interactive/local-model-commands.js.map +1 -1
- package/dist/modes/interactive/profile-menu-controller.d.ts +6 -4
- package/dist/modes/interactive/profile-menu-controller.d.ts.map +1 -1
- package/dist/modes/interactive/profile-menu-controller.js +248 -65
- package/dist/modes/interactive/profile-menu-controller.js.map +1 -1
- package/dist/modes/interactive/report-commands.d.ts.map +1 -1
- package/dist/modes/interactive/report-commands.js +8 -2
- package/dist/modes/interactive/report-commands.js.map +1 -1
- package/dist/modes/interactive/settings-selector-flow.d.ts.map +1 -1
- package/dist/modes/interactive/settings-selector-flow.js +8 -0
- package/dist/modes/interactive/settings-selector-flow.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +3 -1
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/docs/extensions.md +32 -0
- package/docs/integration-sweep-builder-blueprint-2026-07-09.md +365 -0
- package/docs/integration-sweep-resume-2026-07-09.md +407 -0
- package/docs/resources.md +15 -7
- package/docs/settings.md +56 -5
- 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/preset.ts +2 -2
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +44 -35
- package/package.json +4 -4
|
@@ -38,23 +38,27 @@ export class ToolGateController {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
const runner = this.deps.getExtensionRunner();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
let extensionResult;
|
|
42
|
+
if (runner.hasHandlers("tool_call")) {
|
|
43
|
+
try {
|
|
44
|
+
extensionResult = await runner.emitToolCall({
|
|
45
|
+
type: "tool_call",
|
|
46
|
+
toolName: toolCall.name,
|
|
47
|
+
toolCallId: toolCall.id,
|
|
48
|
+
input: args,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err instanceof Error) {
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Extension failed, blocking execution: ${String(err)}`);
|
|
55
56
|
}
|
|
56
|
-
throw new Error(`Extension failed, blocking execution: ${String(err)}`);
|
|
57
57
|
}
|
|
58
|
+
if (extensionResult)
|
|
59
|
+
return extensionResult;
|
|
60
|
+
this.deps.getToolSelectionController?.()?.begin(toolCall.id, toolCall.name, args);
|
|
61
|
+
return undefined;
|
|
58
62
|
};
|
|
59
63
|
afterToolCall = async ({ toolCall, args, result, isError }) => {
|
|
60
64
|
const runner = this.deps.getExtensionRunner();
|
|
@@ -85,6 +89,7 @@ export class ToolGateController {
|
|
|
85
89
|
const wrapped = content.map((block) => block.type === "text" ? { ...block, text: wrapUntrustedText(block.text, source) } : block);
|
|
86
90
|
content = wrapped;
|
|
87
91
|
}
|
|
92
|
+
this.deps.getToolSelectionController?.()?.complete(toolCall.id, !resolvedIsError, content);
|
|
88
93
|
if (content === result.content && details === result.details && resolvedIsError === isError) {
|
|
89
94
|
return undefined;
|
|
90
95
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-gate-controller.js","sourceRoot":"","sources":["../../src/core/tool-gate-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-gate-controller.js","sourceRoot":"","sources":["../../src/core/tool-gate-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Agent tool-call gate: model-router escalation, autonomy gating, extension tool hooks, and the\n * untrusted-content output boundary.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Provides the two agent-core\n * hooks (`beforeToolCall`/`afterToolCall`) as bound arrow-field members the session installs onto its\n * agent. `beforeToolCall` runs the router escalation, then the autonomy tool gate (recording the\n * outcome when a capability envelope is active), then extension `tool_call` handlers. `afterToolCall`\n * runs extension `tool_result` handlers and structurally fences output from untrusted-content sources.\n */\n\nimport type { Agent, BeforeToolCallResult } from \"@caupulican/pi-agent-core\";\nimport type { CapabilityEnvelope, GateOutcome } from \"./autonomy/contracts.ts\";\nimport { evaluateToolGate } from \"./autonomy/gates.ts\";\nimport type { ExtensionRunner } from \"./extensions/index.ts\";\nimport { classifyToolTrust, wrapUntrustedText } from \"./security/untrusted-boundary.ts\";\nimport type { ToolSelectionController } from \"./tool-selection/tool-selection-controller.ts\";\n\ntype BeforeToolCall = NonNullable<Agent[\"beforeToolCall\"]>;\ntype AfterToolCall = NonNullable<Agent[\"afterToolCall\"]>;\n\nexport interface ToolGateControllerDeps {\n\t/** Router escalation: block a tool the active cheap route is not allowed to run. */\n\tmaybeEscalateToolCall(toolName: string, args: unknown): { block: true; reason: string } | undefined;\n\tgetCwd(): string;\n\t/** Active foreground capability envelope, if any — presence enables gate-outcome recording. */\n\tgetCapabilityEnvelope(): CapabilityEnvelope | undefined;\n\t/** Record an autonomy gate outcome (only when a capability envelope is active). */\n\trecordGateOutcome(outcome: GateOutcome): void;\n\tgetExtensionRunner(): ExtensionRunner;\n\t/** Observe an execution only after all pre-execution gates and extension hooks allow it. */\n\tgetToolSelectionController?(): ToolSelectionController | undefined;\n}\n\nexport class ToolGateController {\n\tprivate readonly deps: ToolGateControllerDeps;\n\n\tconstructor(deps: ToolGateControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\treadonly beforeToolCall: BeforeToolCall = async ({ toolCall, args }) => {\n\t\tconst escalation = this.deps.maybeEscalateToolCall(toolCall.name, args);\n\t\tif (escalation) {\n\t\t\treturn escalation;\n\t\t}\n\n\t\t// Autonomy tool gating\n\t\tconst envelope = this.deps.getCapabilityEnvelope();\n\t\tconst gateResult = evaluateToolGate({\n\t\t\ttoolName: toolCall.name,\n\t\t\targs,\n\t\t\tcwd: this.deps.getCwd(),\n\t\t\tenvelope,\n\t\t});\n\n\t\tif (envelope) {\n\t\t\tthis.deps.recordGateOutcome(gateResult);\n\t\t}\n\n\t\tif (gateResult.outcome === \"block\" || gateResult.outcome === \"ask-user\") {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason: `Tool execution blocked by autonomy gate [${gateResult.gate}]: ${gateResult.message} (${gateResult.reasonCode})`,\n\t\t\t};\n\t\t}\n\n\t\tconst runner = this.deps.getExtensionRunner();\n\t\tlet extensionResult: BeforeToolCallResult | undefined;\n\t\tif (runner.hasHandlers(\"tool_call\")) {\n\t\t\ttry {\n\t\t\t\textensionResult = await runner.emitToolCall({\n\t\t\t\t\ttype: \"tool_call\",\n\t\t\t\t\ttoolName: toolCall.name,\n\t\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof Error) {\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`Extension failed, blocking execution: ${String(err)}`);\n\t\t\t}\n\t\t}\n\t\tif (extensionResult) return extensionResult;\n\t\tthis.deps.getToolSelectionController?.()?.begin(toolCall.id, toolCall.name, args);\n\t\treturn undefined;\n\t};\n\n\treadonly afterToolCall: AfterToolCall = async ({ toolCall, args, result, isError }) => {\n\t\tconst runner = this.deps.getExtensionRunner();\n\t\tlet content = result.content;\n\t\tlet details = result.details;\n\t\tlet resolvedIsError = isError;\n\n\t\tif (runner.hasHandlers(\"tool_result\")) {\n\t\t\tconst hookResult = await runner.emitToolResult({\n\t\t\t\ttype: \"tool_result\",\n\t\t\t\ttoolName: toolCall.name,\n\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\tcontent,\n\t\t\t\tdetails,\n\t\t\t\tisError,\n\t\t\t});\n\t\t\tif (hookResult) {\n\t\t\t\tcontent = hookResult.content ?? content;\n\t\t\t\tdetails = hookResult.details;\n\t\t\t\tresolvedIsError = hookResult.isError ?? isError;\n\t\t\t}\n\t\t}\n\n\t\t// Untrusted-content boundary: structurally fence output from attacker-controllable sources\n\t\t// (web/search, subagents, recall, third-party tools) so injection payloads are framed as data.\n\t\t// First-party tools (read/grep/find/ls/edit/write/bash) are trusted and pass through unchanged.\n\t\tif (classifyToolTrust(toolCall.name) === \"untrusted\") {\n\t\t\tconst source = `tool:${toolCall.name}`;\n\t\t\tconst wrapped = content.map((block) =>\n\t\t\t\tblock.type === \"text\" ? { ...block, text: wrapUntrustedText(block.text, source) } : block,\n\t\t\t);\n\t\t\tcontent = wrapped;\n\t\t}\n\n\t\tthis.deps.getToolSelectionController?.()?.complete(toolCall.id, !resolvedIsError, content);\n\t\tif (content === result.content && details === result.details && resolvedIsError === isError) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn { content, details, isError: resolvedIsError };\n\t};\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ExpectedUtilityWeights {
|
|
2
|
+
latency: number;
|
|
3
|
+
tokens: number;
|
|
4
|
+
risk: number;
|
|
5
|
+
context: number;
|
|
6
|
+
latencyScaleMs: number;
|
|
7
|
+
tokenScale: number;
|
|
8
|
+
minimumEvidence: number;
|
|
9
|
+
minimumMargin: number;
|
|
10
|
+
highEntropy: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const DEFAULT_EXPECTED_UTILITY_WEIGHTS: ExpectedUtilityWeights;
|
|
13
|
+
export interface ExpectedUtilityCandidate {
|
|
14
|
+
tool: string;
|
|
15
|
+
value: number;
|
|
16
|
+
alpha: number;
|
|
17
|
+
beta: number;
|
|
18
|
+
sampleCount: number;
|
|
19
|
+
latencyMs?: number;
|
|
20
|
+
tokenEstimate?: number;
|
|
21
|
+
riskCost?: number;
|
|
22
|
+
contextCost?: number;
|
|
23
|
+
deterministicMatch?: boolean;
|
|
24
|
+
/** Candidates with unresolved paths may be observed/shortlisted but never recommended. */
|
|
25
|
+
automaticEligible?: boolean;
|
|
26
|
+
/** `no_tool` uses a known probability rather than a learned posterior. */
|
|
27
|
+
successProbability?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface RankedToolCandidate extends ExpectedUtilityCandidate {
|
|
30
|
+
successProbability: number;
|
|
31
|
+
latencyCost: number;
|
|
32
|
+
tokenCost: number;
|
|
33
|
+
riskCost: number;
|
|
34
|
+
contextCost: number;
|
|
35
|
+
utility: number;
|
|
36
|
+
probability: number;
|
|
37
|
+
}
|
|
38
|
+
export type ToolSelectionDisposition = "recommend" | "shortlist" | "abstain";
|
|
39
|
+
export interface ToolSelectionDecision {
|
|
40
|
+
disposition: ToolSelectionDisposition;
|
|
41
|
+
recommendation?: string;
|
|
42
|
+
shortlist: string[];
|
|
43
|
+
entropy: number;
|
|
44
|
+
margin: number;
|
|
45
|
+
ranked: RankedToolCandidate[];
|
|
46
|
+
}
|
|
47
|
+
export declare function betaSuccessProbability(alpha: number, beta: number): number;
|
|
48
|
+
export declare function normalizedEntropy(probabilities: readonly number[]): number;
|
|
49
|
+
export declare function rankExpectedUtilityCandidates(candidates: readonly ExpectedUtilityCandidate[], weights?: ExpectedUtilityWeights): RankedToolCandidate[];
|
|
50
|
+
export declare function decideExpectedUtility(candidates: readonly ExpectedUtilityCandidate[], weights?: ExpectedUtilityWeights): ToolSelectionDecision;
|
|
51
|
+
//# sourceMappingURL=expected-utility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expected-utility.d.ts","sourceRoot":"","sources":["../../../src/core/tool-selection/expected-utility.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,gCAAgC,EAAE,sBAU9C,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0FAA0F;IAC1F,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,0EAA0E;IAC1E,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAoB,SAAQ,wBAAwB;IACpE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,wBAAwB,GAAG,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,qBAAqB;IACrC,WAAW,EAAE,wBAAwB,CAAC;IACtC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,mBAAmB,EAAE,CAAC;CAC9B;AAUD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAK1E;AAED,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAM1E;AAUD,wBAAgB,6BAA6B,CAC5C,UAAU,EAAE,SAAS,wBAAwB,EAAE,EAC/C,OAAO,GAAE,sBAAyD,GAChE,mBAAmB,EAAE,CAqBvB;AAED,wBAAgB,qBAAqB,CACpC,UAAU,EAAE,SAAS,wBAAwB,EAAE,EAC/C,OAAO,GAAE,sBAAyD,GAChE,qBAAqB,CAwBvB","sourcesContent":["export interface ExpectedUtilityWeights {\n\tlatency: number;\n\ttokens: number;\n\trisk: number;\n\tcontext: number;\n\tlatencyScaleMs: number;\n\ttokenScale: number;\n\tminimumEvidence: number;\n\tminimumMargin: number;\n\thighEntropy: number;\n}\n\nexport const DEFAULT_EXPECTED_UTILITY_WEIGHTS: ExpectedUtilityWeights = {\n\tlatency: 0.15,\n\ttokens: 0.1,\n\trisk: 0.2,\n\tcontext: 0.1,\n\tlatencyScaleMs: 5_000,\n\ttokenScale: 1_000,\n\tminimumEvidence: 3,\n\tminimumMargin: 0.1,\n\thighEntropy: 0.85,\n};\n\nexport interface ExpectedUtilityCandidate {\n\ttool: string;\n\tvalue: number;\n\talpha: number;\n\tbeta: number;\n\tsampleCount: number;\n\tlatencyMs?: number;\n\ttokenEstimate?: number;\n\triskCost?: number;\n\tcontextCost?: number;\n\tdeterministicMatch?: boolean;\n\t/** Candidates with unresolved paths may be observed/shortlisted but never recommended. */\n\tautomaticEligible?: boolean;\n\t/** `no_tool` uses a known probability rather than a learned posterior. */\n\tsuccessProbability?: number;\n}\n\nexport interface RankedToolCandidate extends ExpectedUtilityCandidate {\n\tsuccessProbability: number;\n\tlatencyCost: number;\n\ttokenCost: number;\n\triskCost: number;\n\tcontextCost: number;\n\tutility: number;\n\tprobability: number;\n}\n\nexport type ToolSelectionDisposition = \"recommend\" | \"shortlist\" | \"abstain\";\n\nexport interface ToolSelectionDecision {\n\tdisposition: ToolSelectionDisposition;\n\trecommendation?: string;\n\tshortlist: string[];\n\tentropy: number;\n\tmargin: number;\n\tranked: RankedToolCandidate[];\n}\n\nfunction clampUnit(value: number): number {\n\treturn Math.max(0, Math.min(1, value));\n}\n\nfunction finiteOr(value: number | undefined, fallback: number): number {\n\treturn typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nexport function betaSuccessProbability(alpha: number, beta: number): number {\n\tconst positiveAlpha = Math.max(0, finiteOr(alpha, 0));\n\tconst positiveBeta = Math.max(0, finiteOr(beta, 0));\n\tconst total = positiveAlpha + positiveBeta;\n\treturn total > 0 ? positiveAlpha / total : 0.5;\n}\n\nexport function normalizedEntropy(probabilities: readonly number[]): number {\n\tif (probabilities.length <= 1) return 0;\n\tconst positive = probabilities.filter((probability) => probability > 0 && Number.isFinite(probability));\n\tif (positive.length === 0) return 0;\n\tconst entropy = -positive.reduce((total, probability) => total + probability * Math.log(probability), 0);\n\treturn entropy <= 0 ? 0 : entropy / Math.log(probabilities.length);\n}\n\nfunction softmax(utilities: readonly number[]): number[] {\n\tif (utilities.length === 0) return [];\n\tconst maximum = Math.max(...utilities);\n\tconst exponentials = utilities.map((utility) => Math.exp(Math.max(-60, utility - maximum)));\n\tconst total = exponentials.reduce((sum, value) => sum + value, 0);\n\treturn total > 0 ? exponentials.map((value) => value / total) : utilities.map(() => 1 / utilities.length);\n}\n\nexport function rankExpectedUtilityCandidates(\n\tcandidates: readonly ExpectedUtilityCandidate[],\n\tweights: ExpectedUtilityWeights = DEFAULT_EXPECTED_UTILITY_WEIGHTS,\n): RankedToolCandidate[] {\n\tconst unranked = candidates.map((candidate) => {\n\t\tconst successProbability = clampUnit(\n\t\t\tcandidate.successProbability ?? betaSuccessProbability(candidate.alpha, candidate.beta),\n\t\t);\n\t\tconst latencyCost = clampUnit(finiteOr(candidate.latencyMs, 0) / Math.max(1, weights.latencyScaleMs));\n\t\tconst tokenCost = clampUnit(finiteOr(candidate.tokenEstimate, 0) / Math.max(1, weights.tokenScale));\n\t\tconst riskCost = clampUnit(finiteOr(candidate.riskCost, 0));\n\t\tconst contextCost = clampUnit(finiteOr(candidate.contextCost, 0));\n\t\tconst utility =\n\t\t\tsuccessProbability * clampUnit(candidate.value) -\n\t\t\tweights.latency * latencyCost -\n\t\t\tweights.tokens * tokenCost -\n\t\t\tweights.risk * riskCost -\n\t\t\tweights.context * contextCost;\n\t\treturn { ...candidate, successProbability, latencyCost, tokenCost, riskCost, contextCost, utility };\n\t});\n\tconst probabilities = softmax(unranked.map((candidate) => candidate.utility));\n\treturn unranked\n\t\t.map((candidate, index) => ({ ...candidate, probability: probabilities[index] ?? 0 }))\n\t\t.sort((left, right) => right.utility - left.utility || left.tool.localeCompare(right.tool));\n}\n\nexport function decideExpectedUtility(\n\tcandidates: readonly ExpectedUtilityCandidate[],\n\tweights: ExpectedUtilityWeights = DEFAULT_EXPECTED_UTILITY_WEIGHTS,\n): ToolSelectionDecision {\n\tconst ranked = rankExpectedUtilityCandidates(candidates, weights);\n\tconst best = ranked[0];\n\tconst runnerUp = ranked[1];\n\tconst entropy = normalizedEntropy(ranked.map((candidate) => candidate.probability));\n\tconst margin = best && runnerUp ? best.utility - runnerUp.utility : (best?.utility ?? 0);\n\tconst shortlist = ranked.slice(0, 3).map((candidate) => candidate.tool);\n\tif (!best || best.utility <= 0) {\n\t\treturn { disposition: \"abstain\", shortlist: [], entropy, margin, ranked };\n\t}\n\tif (entropy >= weights.highEntropy || margin < weights.minimumMargin) {\n\t\treturn { disposition: \"shortlist\", shortlist, entropy, margin, ranked };\n\t}\n\tif (best.automaticEligible !== false && (best.deterministicMatch || best.sampleCount >= weights.minimumEvidence)) {\n\t\treturn {\n\t\t\tdisposition: \"recommend\",\n\t\t\trecommendation: best.tool,\n\t\t\tshortlist: [],\n\t\t\tentropy,\n\t\t\tmargin,\n\t\t\tranked,\n\t\t};\n\t}\n\treturn { disposition: \"abstain\", shortlist: [], entropy, margin, ranked };\n}\n"]}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const DEFAULT_EXPECTED_UTILITY_WEIGHTS = {
|
|
2
|
+
latency: 0.15,
|
|
3
|
+
tokens: 0.1,
|
|
4
|
+
risk: 0.2,
|
|
5
|
+
context: 0.1,
|
|
6
|
+
latencyScaleMs: 5_000,
|
|
7
|
+
tokenScale: 1_000,
|
|
8
|
+
minimumEvidence: 3,
|
|
9
|
+
minimumMargin: 0.1,
|
|
10
|
+
highEntropy: 0.85,
|
|
11
|
+
};
|
|
12
|
+
function clampUnit(value) {
|
|
13
|
+
return Math.max(0, Math.min(1, value));
|
|
14
|
+
}
|
|
15
|
+
function finiteOr(value, fallback) {
|
|
16
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
17
|
+
}
|
|
18
|
+
export function betaSuccessProbability(alpha, beta) {
|
|
19
|
+
const positiveAlpha = Math.max(0, finiteOr(alpha, 0));
|
|
20
|
+
const positiveBeta = Math.max(0, finiteOr(beta, 0));
|
|
21
|
+
const total = positiveAlpha + positiveBeta;
|
|
22
|
+
return total > 0 ? positiveAlpha / total : 0.5;
|
|
23
|
+
}
|
|
24
|
+
export function normalizedEntropy(probabilities) {
|
|
25
|
+
if (probabilities.length <= 1)
|
|
26
|
+
return 0;
|
|
27
|
+
const positive = probabilities.filter((probability) => probability > 0 && Number.isFinite(probability));
|
|
28
|
+
if (positive.length === 0)
|
|
29
|
+
return 0;
|
|
30
|
+
const entropy = -positive.reduce((total, probability) => total + probability * Math.log(probability), 0);
|
|
31
|
+
return entropy <= 0 ? 0 : entropy / Math.log(probabilities.length);
|
|
32
|
+
}
|
|
33
|
+
function softmax(utilities) {
|
|
34
|
+
if (utilities.length === 0)
|
|
35
|
+
return [];
|
|
36
|
+
const maximum = Math.max(...utilities);
|
|
37
|
+
const exponentials = utilities.map((utility) => Math.exp(Math.max(-60, utility - maximum)));
|
|
38
|
+
const total = exponentials.reduce((sum, value) => sum + value, 0);
|
|
39
|
+
return total > 0 ? exponentials.map((value) => value / total) : utilities.map(() => 1 / utilities.length);
|
|
40
|
+
}
|
|
41
|
+
export function rankExpectedUtilityCandidates(candidates, weights = DEFAULT_EXPECTED_UTILITY_WEIGHTS) {
|
|
42
|
+
const unranked = candidates.map((candidate) => {
|
|
43
|
+
const successProbability = clampUnit(candidate.successProbability ?? betaSuccessProbability(candidate.alpha, candidate.beta));
|
|
44
|
+
const latencyCost = clampUnit(finiteOr(candidate.latencyMs, 0) / Math.max(1, weights.latencyScaleMs));
|
|
45
|
+
const tokenCost = clampUnit(finiteOr(candidate.tokenEstimate, 0) / Math.max(1, weights.tokenScale));
|
|
46
|
+
const riskCost = clampUnit(finiteOr(candidate.riskCost, 0));
|
|
47
|
+
const contextCost = clampUnit(finiteOr(candidate.contextCost, 0));
|
|
48
|
+
const utility = successProbability * clampUnit(candidate.value) -
|
|
49
|
+
weights.latency * latencyCost -
|
|
50
|
+
weights.tokens * tokenCost -
|
|
51
|
+
weights.risk * riskCost -
|
|
52
|
+
weights.context * contextCost;
|
|
53
|
+
return { ...candidate, successProbability, latencyCost, tokenCost, riskCost, contextCost, utility };
|
|
54
|
+
});
|
|
55
|
+
const probabilities = softmax(unranked.map((candidate) => candidate.utility));
|
|
56
|
+
return unranked
|
|
57
|
+
.map((candidate, index) => ({ ...candidate, probability: probabilities[index] ?? 0 }))
|
|
58
|
+
.sort((left, right) => right.utility - left.utility || left.tool.localeCompare(right.tool));
|
|
59
|
+
}
|
|
60
|
+
export function decideExpectedUtility(candidates, weights = DEFAULT_EXPECTED_UTILITY_WEIGHTS) {
|
|
61
|
+
const ranked = rankExpectedUtilityCandidates(candidates, weights);
|
|
62
|
+
const best = ranked[0];
|
|
63
|
+
const runnerUp = ranked[1];
|
|
64
|
+
const entropy = normalizedEntropy(ranked.map((candidate) => candidate.probability));
|
|
65
|
+
const margin = best && runnerUp ? best.utility - runnerUp.utility : (best?.utility ?? 0);
|
|
66
|
+
const shortlist = ranked.slice(0, 3).map((candidate) => candidate.tool);
|
|
67
|
+
if (!best || best.utility <= 0) {
|
|
68
|
+
return { disposition: "abstain", shortlist: [], entropy, margin, ranked };
|
|
69
|
+
}
|
|
70
|
+
if (entropy >= weights.highEntropy || margin < weights.minimumMargin) {
|
|
71
|
+
return { disposition: "shortlist", shortlist, entropy, margin, ranked };
|
|
72
|
+
}
|
|
73
|
+
if (best.automaticEligible !== false && (best.deterministicMatch || best.sampleCount >= weights.minimumEvidence)) {
|
|
74
|
+
return {
|
|
75
|
+
disposition: "recommend",
|
|
76
|
+
recommendation: best.tool,
|
|
77
|
+
shortlist: [],
|
|
78
|
+
entropy,
|
|
79
|
+
margin,
|
|
80
|
+
ranked,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { disposition: "abstain", shortlist: [], entropy, margin, ranked };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=expected-utility.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expected-utility.js","sourceRoot":"","sources":["../../../src/core/tool-selection/expected-utility.ts"],"names":[],"mappings":"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","sourcesContent":["export interface ExpectedUtilityWeights {\n\tlatency: number;\n\ttokens: number;\n\trisk: number;\n\tcontext: number;\n\tlatencyScaleMs: number;\n\ttokenScale: number;\n\tminimumEvidence: number;\n\tminimumMargin: number;\n\thighEntropy: number;\n}\n\nexport const DEFAULT_EXPECTED_UTILITY_WEIGHTS: ExpectedUtilityWeights = {\n\tlatency: 0.15,\n\ttokens: 0.1,\n\trisk: 0.2,\n\tcontext: 0.1,\n\tlatencyScaleMs: 5_000,\n\ttokenScale: 1_000,\n\tminimumEvidence: 3,\n\tminimumMargin: 0.1,\n\thighEntropy: 0.85,\n};\n\nexport interface ExpectedUtilityCandidate {\n\ttool: string;\n\tvalue: number;\n\talpha: number;\n\tbeta: number;\n\tsampleCount: number;\n\tlatencyMs?: number;\n\ttokenEstimate?: number;\n\triskCost?: number;\n\tcontextCost?: number;\n\tdeterministicMatch?: boolean;\n\t/** Candidates with unresolved paths may be observed/shortlisted but never recommended. */\n\tautomaticEligible?: boolean;\n\t/** `no_tool` uses a known probability rather than a learned posterior. */\n\tsuccessProbability?: number;\n}\n\nexport interface RankedToolCandidate extends ExpectedUtilityCandidate {\n\tsuccessProbability: number;\n\tlatencyCost: number;\n\ttokenCost: number;\n\triskCost: number;\n\tcontextCost: number;\n\tutility: number;\n\tprobability: number;\n}\n\nexport type ToolSelectionDisposition = \"recommend\" | \"shortlist\" | \"abstain\";\n\nexport interface ToolSelectionDecision {\n\tdisposition: ToolSelectionDisposition;\n\trecommendation?: string;\n\tshortlist: string[];\n\tentropy: number;\n\tmargin: number;\n\tranked: RankedToolCandidate[];\n}\n\nfunction clampUnit(value: number): number {\n\treturn Math.max(0, Math.min(1, value));\n}\n\nfunction finiteOr(value: number | undefined, fallback: number): number {\n\treturn typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nexport function betaSuccessProbability(alpha: number, beta: number): number {\n\tconst positiveAlpha = Math.max(0, finiteOr(alpha, 0));\n\tconst positiveBeta = Math.max(0, finiteOr(beta, 0));\n\tconst total = positiveAlpha + positiveBeta;\n\treturn total > 0 ? positiveAlpha / total : 0.5;\n}\n\nexport function normalizedEntropy(probabilities: readonly number[]): number {\n\tif (probabilities.length <= 1) return 0;\n\tconst positive = probabilities.filter((probability) => probability > 0 && Number.isFinite(probability));\n\tif (positive.length === 0) return 0;\n\tconst entropy = -positive.reduce((total, probability) => total + probability * Math.log(probability), 0);\n\treturn entropy <= 0 ? 0 : entropy / Math.log(probabilities.length);\n}\n\nfunction softmax(utilities: readonly number[]): number[] {\n\tif (utilities.length === 0) return [];\n\tconst maximum = Math.max(...utilities);\n\tconst exponentials = utilities.map((utility) => Math.exp(Math.max(-60, utility - maximum)));\n\tconst total = exponentials.reduce((sum, value) => sum + value, 0);\n\treturn total > 0 ? exponentials.map((value) => value / total) : utilities.map(() => 1 / utilities.length);\n}\n\nexport function rankExpectedUtilityCandidates(\n\tcandidates: readonly ExpectedUtilityCandidate[],\n\tweights: ExpectedUtilityWeights = DEFAULT_EXPECTED_UTILITY_WEIGHTS,\n): RankedToolCandidate[] {\n\tconst unranked = candidates.map((candidate) => {\n\t\tconst successProbability = clampUnit(\n\t\t\tcandidate.successProbability ?? betaSuccessProbability(candidate.alpha, candidate.beta),\n\t\t);\n\t\tconst latencyCost = clampUnit(finiteOr(candidate.latencyMs, 0) / Math.max(1, weights.latencyScaleMs));\n\t\tconst tokenCost = clampUnit(finiteOr(candidate.tokenEstimate, 0) / Math.max(1, weights.tokenScale));\n\t\tconst riskCost = clampUnit(finiteOr(candidate.riskCost, 0));\n\t\tconst contextCost = clampUnit(finiteOr(candidate.contextCost, 0));\n\t\tconst utility =\n\t\t\tsuccessProbability * clampUnit(candidate.value) -\n\t\t\tweights.latency * latencyCost -\n\t\t\tweights.tokens * tokenCost -\n\t\t\tweights.risk * riskCost -\n\t\t\tweights.context * contextCost;\n\t\treturn { ...candidate, successProbability, latencyCost, tokenCost, riskCost, contextCost, utility };\n\t});\n\tconst probabilities = softmax(unranked.map((candidate) => candidate.utility));\n\treturn unranked\n\t\t.map((candidate, index) => ({ ...candidate, probability: probabilities[index] ?? 0 }))\n\t\t.sort((left, right) => right.utility - left.utility || left.tool.localeCompare(right.tool));\n}\n\nexport function decideExpectedUtility(\n\tcandidates: readonly ExpectedUtilityCandidate[],\n\tweights: ExpectedUtilityWeights = DEFAULT_EXPECTED_UTILITY_WEIGHTS,\n): ToolSelectionDecision {\n\tconst ranked = rankExpectedUtilityCandidates(candidates, weights);\n\tconst best = ranked[0];\n\tconst runnerUp = ranked[1];\n\tconst entropy = normalizedEntropy(ranked.map((candidate) => candidate.probability));\n\tconst margin = best && runnerUp ? best.utility - runnerUp.utility : (best?.utility ?? 0);\n\tconst shortlist = ranked.slice(0, 3).map((candidate) => candidate.tool);\n\tif (!best || best.utility <= 0) {\n\t\treturn { disposition: \"abstain\", shortlist: [], entropy, margin, ranked };\n\t}\n\tif (entropy >= weights.highEntropy || margin < weights.minimumMargin) {\n\t\treturn { disposition: \"shortlist\", shortlist, entropy, margin, ranked };\n\t}\n\tif (best.automaticEligible !== false && (best.deterministicMatch || best.sampleCount >= weights.minimumEvidence)) {\n\t\treturn {\n\t\t\tdisposition: \"recommend\",\n\t\t\trecommendation: best.tool,\n\t\t\tshortlist: [],\n\t\t\tentropy,\n\t\t\tmargin,\n\t\t\tranked,\n\t\t};\n\t}\n\treturn { disposition: \"abstain\", shortlist: [], entropy, margin, ranked };\n}\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type HostFingerprint } from "../models/fitness-store.ts";
|
|
2
|
+
export type ToolSelectionIntentClass = "read" | "search" | "execute" | "write" | "retrieve" | "explain" | "other";
|
|
3
|
+
export interface ToolPerformanceKey {
|
|
4
|
+
modelRef: string;
|
|
5
|
+
intentClass: ToolSelectionIntentClass;
|
|
6
|
+
tool: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ToolPerformanceStats extends ToolPerformanceKey {
|
|
9
|
+
alpha: number;
|
|
10
|
+
beta: number;
|
|
11
|
+
sampleCount: number;
|
|
12
|
+
latencyEwmaMs?: number;
|
|
13
|
+
latencyDeviationEwmaMs?: number;
|
|
14
|
+
inputTokenEstimateEwma?: number;
|
|
15
|
+
outputTokenEstimateEwma?: number;
|
|
16
|
+
repairCount: number;
|
|
17
|
+
bounceCount: number;
|
|
18
|
+
failureCount: number;
|
|
19
|
+
lastUsedAt: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ToolSelectionObservation {
|
|
22
|
+
at: string;
|
|
23
|
+
modelRef: string;
|
|
24
|
+
intentClass: ToolSelectionIntentClass;
|
|
25
|
+
actualTool: string;
|
|
26
|
+
firstTool: boolean;
|
|
27
|
+
succeeded: boolean;
|
|
28
|
+
disposition: "recommend" | "shortlist" | "abstain";
|
|
29
|
+
recommendation?: string;
|
|
30
|
+
shortlist: string[];
|
|
31
|
+
entropy: number;
|
|
32
|
+
margin: number;
|
|
33
|
+
/** Redacted ranking: names and numeric scores only, never prompts, arguments, paths, or output. */
|
|
34
|
+
ranked: Array<{
|
|
35
|
+
tool: string;
|
|
36
|
+
utility: number;
|
|
37
|
+
probability: number;
|
|
38
|
+
}>;
|
|
39
|
+
latencyMs?: number;
|
|
40
|
+
inputTokenEstimate?: number;
|
|
41
|
+
outputTokenEstimate?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ToolSelectionMetrics {
|
|
44
|
+
firstToolAttempts: number;
|
|
45
|
+
firstToolSuccesses: number;
|
|
46
|
+
wrongToolOrFailureCount: number;
|
|
47
|
+
recommendationCount: number;
|
|
48
|
+
recommendationMatchedCount: number;
|
|
49
|
+
shortlistCount: number;
|
|
50
|
+
abstentionCount: number;
|
|
51
|
+
averageLatencyMs?: number;
|
|
52
|
+
averageInputTokenEstimate?: number;
|
|
53
|
+
averageOutputTokenEstimate?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface ToolExecutionObservation {
|
|
56
|
+
key: ToolPerformanceKey;
|
|
57
|
+
success: boolean;
|
|
58
|
+
latencyMs: number;
|
|
59
|
+
inputTokenEstimate?: number;
|
|
60
|
+
outputTokenEstimate?: number;
|
|
61
|
+
selection: Omit<ToolSelectionObservation, "at" | "modelRef" | "intentClass" | "actualTool" | "succeeded">;
|
|
62
|
+
at?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare class ToolPerformanceStore {
|
|
65
|
+
private readonly filePath;
|
|
66
|
+
private readonly fingerprint;
|
|
67
|
+
constructor(filePath: string, options?: {
|
|
68
|
+
fingerprint?: () => HostFingerprint;
|
|
69
|
+
});
|
|
70
|
+
static forAgentDir(agentDir: string, options?: {
|
|
71
|
+
fingerprint?: () => HostFingerprint;
|
|
72
|
+
}): ToolPerformanceStore;
|
|
73
|
+
private load;
|
|
74
|
+
private save;
|
|
75
|
+
private hostData;
|
|
76
|
+
get(key: ToolPerformanceKey): ToolPerformanceStats;
|
|
77
|
+
recordValidation(key: ToolPerformanceKey, outcome: "repaired" | "bounced", at?: string): ToolPerformanceStats;
|
|
78
|
+
recordExecution(observation: ToolExecutionObservation): ToolPerformanceStats;
|
|
79
|
+
getMetrics(modelRef?: string): ToolSelectionMetrics;
|
|
80
|
+
getObservations(modelRef?: string): ToolSelectionObservation[];
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=tool-performance-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-performance-store.d.ts","sourceRoot":"","sources":["../../../src/core/tool-selection/tool-performance-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { currentHostFingerprint, type HostFingerprint } from \"../models/fitness-store.ts\";\n\nconst STORE_VERSION = 1;\nconst MAX_STATS_PER_HOST = 500;\nconst MAX_OBSERVATIONS_PER_HOST = 1_000;\nconst EWMA_ALPHA = 0.25;\n\nexport type ToolSelectionIntentClass = \"read\" | \"search\" | \"execute\" | \"write\" | \"retrieve\" | \"explain\" | \"other\";\n\nexport interface ToolPerformanceKey {\n\tmodelRef: string;\n\tintentClass: ToolSelectionIntentClass;\n\ttool: string;\n}\n\nexport interface ToolPerformanceStats extends ToolPerformanceKey {\n\talpha: number;\n\tbeta: number;\n\tsampleCount: number;\n\tlatencyEwmaMs?: number;\n\tlatencyDeviationEwmaMs?: number;\n\tinputTokenEstimateEwma?: number;\n\toutputTokenEstimateEwma?: number;\n\trepairCount: number;\n\tbounceCount: number;\n\tfailureCount: number;\n\tlastUsedAt: string;\n}\n\nexport interface ToolSelectionObservation {\n\tat: string;\n\tmodelRef: string;\n\tintentClass: ToolSelectionIntentClass;\n\tactualTool: string;\n\tfirstTool: boolean;\n\tsucceeded: boolean;\n\tdisposition: \"recommend\" | \"shortlist\" | \"abstain\";\n\trecommendation?: string;\n\tshortlist: string[];\n\tentropy: number;\n\tmargin: number;\n\t/** Redacted ranking: names and numeric scores only, never prompts, arguments, paths, or output. */\n\tranked: Array<{ tool: string; utility: number; probability: number }>;\n\tlatencyMs?: number;\n\tinputTokenEstimate?: number;\n\toutputTokenEstimate?: number;\n}\n\nexport interface ToolSelectionMetrics {\n\tfirstToolAttempts: number;\n\tfirstToolSuccesses: number;\n\twrongToolOrFailureCount: number;\n\trecommendationCount: number;\n\trecommendationMatchedCount: number;\n\tshortlistCount: number;\n\tabstentionCount: number;\n\taverageLatencyMs?: number;\n\taverageInputTokenEstimate?: number;\n\taverageOutputTokenEstimate?: number;\n}\n\ninterface HostToolPerformanceData {\n\thost: HostFingerprint;\n\tstats: Record<string, ToolPerformanceStats>;\n\tobservations: ToolSelectionObservation[];\n}\n\ninterface ToolPerformanceStoreFile {\n\tversion: 1;\n\thosts: Record<string, HostToolPerformanceData>;\n}\n\nexport interface ToolExecutionObservation {\n\tkey: ToolPerformanceKey;\n\tsuccess: boolean;\n\tlatencyMs: number;\n\tinputTokenEstimate?: number;\n\toutputTokenEstimate?: number;\n\tselection: Omit<ToolSelectionObservation, \"at\" | \"modelRef\" | \"intentClass\" | \"actualTool\" | \"succeeded\">;\n\tat?: string;\n}\n\nfunction statKey(key: ToolPerformanceKey): string {\n\treturn `${key.modelRef}\\0${key.intentClass}\\0${key.tool}`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isIntentClass(value: unknown): value is ToolSelectionIntentClass {\n\treturn (\n\t\tvalue === \"read\" ||\n\t\tvalue === \"search\" ||\n\t\tvalue === \"execute\" ||\n\t\tvalue === \"write\" ||\n\t\tvalue === \"retrieve\" ||\n\t\tvalue === \"explain\" ||\n\t\tvalue === \"other\"\n\t);\n}\n\nfunction isStats(value: unknown): value is ToolPerformanceStats {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.modelRef === \"string\" &&\n\t\tisIntentClass(value.intentClass) &&\n\t\ttypeof value.tool === \"string\" &&\n\t\ttypeof value.alpha === \"number\" &&\n\t\ttypeof value.beta === \"number\" &&\n\t\ttypeof value.sampleCount === \"number\" &&\n\t\ttypeof value.repairCount === \"number\" &&\n\t\ttypeof value.bounceCount === \"number\" &&\n\t\ttypeof value.failureCount === \"number\" &&\n\t\ttypeof value.lastUsedAt === \"string\"\n\t);\n}\n\nfunction isObservation(value: unknown): value is ToolSelectionObservation {\n\treturn (\n\t\tisRecord(value) &&\n\t\ttypeof value.at === \"string\" &&\n\t\ttypeof value.modelRef === \"string\" &&\n\t\tisIntentClass(value.intentClass) &&\n\t\ttypeof value.actualTool === \"string\" &&\n\t\ttypeof value.firstTool === \"boolean\" &&\n\t\ttypeof value.succeeded === \"boolean\" &&\n\t\t(value.disposition === \"recommend\" || value.disposition === \"shortlist\" || value.disposition === \"abstain\") &&\n\t\tArray.isArray(value.shortlist) &&\n\t\tvalue.shortlist.every((tool) => typeof tool === \"string\") &&\n\t\ttypeof value.entropy === \"number\" &&\n\t\ttypeof value.margin === \"number\" &&\n\t\tArray.isArray(value.ranked) &&\n\t\tvalue.ranked.every(\n\t\t\t(entry) =>\n\t\t\t\tisRecord(entry) &&\n\t\t\t\ttypeof entry.tool === \"string\" &&\n\t\t\t\ttypeof entry.utility === \"number\" &&\n\t\t\t\ttypeof entry.probability === \"number\",\n\t\t) &&\n\t\t(value.latencyMs === undefined || typeof value.latencyMs === \"number\") &&\n\t\t(value.inputTokenEstimate === undefined || typeof value.inputTokenEstimate === \"number\") &&\n\t\t(value.outputTokenEstimate === undefined || typeof value.outputTokenEstimate === \"number\")\n\t);\n}\n\nfunction emptyStats(key: ToolPerformanceKey, at: string): ToolPerformanceStats {\n\treturn {\n\t\t...key,\n\t\talpha: 1,\n\t\tbeta: 1,\n\t\tsampleCount: 0,\n\t\trepairCount: 0,\n\t\tbounceCount: 0,\n\t\tfailureCount: 0,\n\t\tlastUsedAt: at,\n\t};\n}\n\nfunction finiteNonNegative(value: number | undefined): number | undefined {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\nfunction updateEwma(previous: number | undefined, next: number | undefined): number | undefined {\n\tif (next === undefined) return previous;\n\treturn previous === undefined ? next : previous + EWMA_ALPHA * (next - previous);\n}\n\nfunction updateDeviation(\n\tpreviousValue: number | undefined,\n\tpreviousDeviation: number | undefined,\n\tnext: number | undefined,\n): number | undefined {\n\tif (next === undefined) return previousDeviation;\n\tconst deviation = previousValue === undefined ? 0 : Math.abs(next - previousValue);\n\treturn updateEwma(previousDeviation, deviation);\n}\n\nfunction parseHost(value: unknown): HostToolPerformanceData | undefined {\n\tif (!isRecord(value) || !isRecord(value.host) || !isRecord(value.stats) || !Array.isArray(value.observations))\n\t\treturn undefined;\n\tconst host = value.host;\n\tif (\n\t\ttypeof host.id !== \"string\" ||\n\t\ttypeof host.cpu !== \"string\" ||\n\t\ttypeof host.cores !== \"number\" ||\n\t\ttypeof host.totalMemGb !== \"number\"\n\t)\n\t\treturn undefined;\n\treturn {\n\t\thost: { id: host.id, cpu: host.cpu, cores: host.cores, totalMemGb: host.totalMemGb },\n\t\tstats: Object.fromEntries(\n\t\t\tObject.entries(value.stats).filter((entry): entry is [string, ToolPerformanceStats] => isStats(entry[1])),\n\t\t),\n\t\tobservations: value.observations.filter(isObservation),\n\t};\n}\n\nfunction parseFile(value: unknown): ToolPerformanceStoreFile {\n\tif (!isRecord(value) || value.version !== STORE_VERSION || !isRecord(value.hosts)) {\n\t\treturn { version: STORE_VERSION, hosts: {} };\n\t}\n\treturn {\n\t\tversion: STORE_VERSION,\n\t\thosts: Object.fromEntries(\n\t\t\tObject.entries(value.hosts)\n\t\t\t\t.map(([hostId, host]) => [hostId, parseHost(host)] as const)\n\t\t\t\t.filter((entry): entry is readonly [string, HostToolPerformanceData] => entry[1] !== undefined),\n\t\t),\n\t};\n}\n\nfunction trimStats(stats: Record<string, ToolPerformanceStats>): Record<string, ToolPerformanceStats> {\n\tconst entries = Object.entries(stats);\n\tif (entries.length <= MAX_STATS_PER_HOST) return stats;\n\treturn Object.fromEntries(\n\t\tentries\n\t\t\t.sort(([, left], [, right]) => Date.parse(right.lastUsedAt) - Date.parse(left.lastUsedAt))\n\t\t\t.slice(0, MAX_STATS_PER_HOST),\n\t);\n}\n\nexport class ToolPerformanceStore {\n\tprivate readonly filePath: string;\n\tprivate readonly fingerprint: () => HostFingerprint;\n\n\tconstructor(filePath: string, options: { fingerprint?: () => HostFingerprint } = {}) {\n\t\tthis.filePath = filePath;\n\t\tthis.fingerprint = options.fingerprint ?? currentHostFingerprint;\n\t}\n\n\tstatic forAgentDir(agentDir: string, options: { fingerprint?: () => HostFingerprint } = {}): ToolPerformanceStore {\n\t\treturn new ToolPerformanceStore(join(agentDir, \"state\", \"tool-performance.json\"), options);\n\t}\n\n\tprivate load(): ToolPerformanceStoreFile {\n\t\ttry {\n\t\t\tif (!existsSync(this.filePath)) return { version: STORE_VERSION, hosts: {} };\n\t\t\treturn parseFile(JSON.parse(readFileSync(this.filePath, \"utf8\")));\n\t\t} catch {\n\t\t\treturn { version: STORE_VERSION, hosts: {} };\n\t\t}\n\t}\n\n\tprivate save(file: ToolPerformanceStoreFile): void {\n\t\tmkdirSync(dirname(this.filePath), { recursive: true });\n\t\twriteFileSync(this.filePath, `${JSON.stringify(file, null, \"\\t\")}\\n`, \"utf8\");\n\t}\n\n\tprivate hostData(file: ToolPerformanceStoreFile): HostToolPerformanceData {\n\t\tconst fingerprint = this.fingerprint();\n\t\tconst existing = file.hosts[fingerprint.id];\n\t\tif (existing) return existing;\n\t\tconst created: HostToolPerformanceData = { host: fingerprint, stats: {}, observations: [] };\n\t\tfile.hosts[fingerprint.id] = created;\n\t\treturn created;\n\t}\n\n\tget(key: ToolPerformanceKey): ToolPerformanceStats {\n\t\tconst file = this.load();\n\t\tconst host = file.hosts[this.fingerprint().id];\n\t\tconst stats = host?.stats[statKey(key)];\n\t\treturn stats ? { ...stats } : emptyStats(key, new Date(0).toISOString());\n\t}\n\n\trecordValidation(\n\t\tkey: ToolPerformanceKey,\n\t\toutcome: \"repaired\" | \"bounced\",\n\t\tat = new Date().toISOString(),\n\t): ToolPerformanceStats {\n\t\tconst file = this.load();\n\t\tconst host = this.hostData(file);\n\t\tconst storageKey = statKey(key);\n\t\tconst current = host.stats[storageKey] ?? emptyStats(key, at);\n\t\tconst next: ToolPerformanceStats = {\n\t\t\t...current,\n\t\t\trepairCount: current.repairCount + (outcome === \"repaired\" ? 1 : 0),\n\t\t\tbounceCount: current.bounceCount + (outcome === \"bounced\" ? 1 : 0),\n\t\t\tlastUsedAt: at,\n\t\t};\n\t\thost.stats[storageKey] = next;\n\t\thost.stats = trimStats(host.stats);\n\t\tthis.save(file);\n\t\treturn { ...next };\n\t}\n\n\trecordExecution(observation: ToolExecutionObservation): ToolPerformanceStats {\n\t\tconst at = observation.at ?? new Date().toISOString();\n\t\tconst file = this.load();\n\t\tconst host = this.hostData(file);\n\t\tconst storageKey = statKey(observation.key);\n\t\tconst current = host.stats[storageKey] ?? emptyStats(observation.key, at);\n\t\tconst latencyMs = finiteNonNegative(observation.latencyMs);\n\t\tconst inputTokenEstimate = finiteNonNegative(observation.inputTokenEstimate);\n\t\tconst outputTokenEstimate = finiteNonNegative(observation.outputTokenEstimate);\n\t\tconst next: ToolPerformanceStats = {\n\t\t\t...current,\n\t\t\talpha: current.alpha + (observation.success ? 1 : 0),\n\t\t\tbeta: current.beta + (observation.success ? 0 : 1),\n\t\t\tsampleCount: current.sampleCount + 1,\n\t\t\tlatencyEwmaMs: updateEwma(current.latencyEwmaMs, latencyMs),\n\t\t\tlatencyDeviationEwmaMs: updateDeviation(current.latencyEwmaMs, current.latencyDeviationEwmaMs, latencyMs),\n\t\t\tinputTokenEstimateEwma: updateEwma(current.inputTokenEstimateEwma, inputTokenEstimate),\n\t\t\toutputTokenEstimateEwma: updateEwma(current.outputTokenEstimateEwma, outputTokenEstimate),\n\t\t\tfailureCount: current.failureCount + (observation.success ? 0 : 1),\n\t\t\tlastUsedAt: at,\n\t\t};\n\t\thost.stats[storageKey] = next;\n\t\thost.stats = trimStats(host.stats);\n\t\thost.observations.push({\n\t\t\t...observation.selection,\n\t\t\tat,\n\t\t\tmodelRef: observation.key.modelRef,\n\t\t\tintentClass: observation.key.intentClass,\n\t\t\tactualTool: observation.key.tool,\n\t\t\tsucceeded: observation.success,\n\t\t\tranked: observation.selection.ranked.slice(0, 6),\n\t\t\tshortlist: observation.selection.shortlist.slice(0, 3),\n\t\t\tlatencyMs,\n\t\t\tinputTokenEstimate,\n\t\t\toutputTokenEstimate,\n\t\t});\n\t\tif (host.observations.length > MAX_OBSERVATIONS_PER_HOST) {\n\t\t\thost.observations = host.observations.slice(-MAX_OBSERVATIONS_PER_HOST);\n\t\t}\n\t\tthis.save(file);\n\t\treturn { ...next };\n\t}\n\n\tgetMetrics(modelRef?: string): ToolSelectionMetrics {\n\t\tconst host = this.load().hosts[this.fingerprint().id];\n\t\tconst observations = (host?.observations ?? []).filter(\n\t\t\t(observation) => modelRef === undefined || observation.modelRef === modelRef,\n\t\t);\n\t\tconst firstTools = observations.filter((observation) => observation.firstTool);\n\t\tconst recommended = observations.filter((observation) => observation.disposition === \"recommend\");\n\t\tconst average = (values: Array<number | undefined>): number | undefined => {\n\t\t\tconst present = values.filter((value): value is number => value !== undefined);\n\t\t\treturn present.length > 0 ? present.reduce((sum, value) => sum + value, 0) / present.length : undefined;\n\t\t};\n\t\treturn {\n\t\t\tfirstToolAttempts: firstTools.length,\n\t\t\tfirstToolSuccesses: firstTools.filter((observation) => observation.succeeded).length,\n\t\t\twrongToolOrFailureCount: observations.filter(\n\t\t\t\t(observation) =>\n\t\t\t\t\t!observation.succeeded ||\n\t\t\t\t\t(observation.disposition === \"recommend\" && observation.recommendation !== observation.actualTool),\n\t\t\t).length,\n\t\t\trecommendationCount: recommended.length,\n\t\t\trecommendationMatchedCount: recommended.filter(\n\t\t\t\t(observation) => observation.recommendation === observation.actualTool,\n\t\t\t).length,\n\t\t\tshortlistCount: observations.filter((observation) => observation.disposition === \"shortlist\").length,\n\t\t\tabstentionCount: observations.filter((observation) => observation.disposition === \"abstain\").length,\n\t\t\taverageLatencyMs: average(observations.map((observation) => observation.latencyMs)),\n\t\t\taverageInputTokenEstimate: average(observations.map((observation) => observation.inputTokenEstimate)),\n\t\t\taverageOutputTokenEstimate: average(observations.map((observation) => observation.outputTokenEstimate)),\n\t\t};\n\t}\n\n\tgetObservations(modelRef?: string): ToolSelectionObservation[] {\n\t\tconst host = this.load().hosts[this.fingerprint().id];\n\t\treturn (host?.observations ?? [])\n\t\t\t.filter((observation) => modelRef === undefined || observation.modelRef === modelRef)\n\t\t\t.map((observation) => ({\n\t\t\t\t...observation,\n\t\t\t\tshortlist: [...observation.shortlist],\n\t\t\t\tranked: observation.ranked.map((candidate) => ({ ...candidate })),\n\t\t\t}));\n\t}\n}\n"]}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { currentHostFingerprint } from "../models/fitness-store.js";
|
|
4
|
+
const STORE_VERSION = 1;
|
|
5
|
+
const MAX_STATS_PER_HOST = 500;
|
|
6
|
+
const MAX_OBSERVATIONS_PER_HOST = 1_000;
|
|
7
|
+
const EWMA_ALPHA = 0.25;
|
|
8
|
+
function statKey(key) {
|
|
9
|
+
return `${key.modelRef}\0${key.intentClass}\0${key.tool}`;
|
|
10
|
+
}
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function isIntentClass(value) {
|
|
15
|
+
return (value === "read" ||
|
|
16
|
+
value === "search" ||
|
|
17
|
+
value === "execute" ||
|
|
18
|
+
value === "write" ||
|
|
19
|
+
value === "retrieve" ||
|
|
20
|
+
value === "explain" ||
|
|
21
|
+
value === "other");
|
|
22
|
+
}
|
|
23
|
+
function isStats(value) {
|
|
24
|
+
return (isRecord(value) &&
|
|
25
|
+
typeof value.modelRef === "string" &&
|
|
26
|
+
isIntentClass(value.intentClass) &&
|
|
27
|
+
typeof value.tool === "string" &&
|
|
28
|
+
typeof value.alpha === "number" &&
|
|
29
|
+
typeof value.beta === "number" &&
|
|
30
|
+
typeof value.sampleCount === "number" &&
|
|
31
|
+
typeof value.repairCount === "number" &&
|
|
32
|
+
typeof value.bounceCount === "number" &&
|
|
33
|
+
typeof value.failureCount === "number" &&
|
|
34
|
+
typeof value.lastUsedAt === "string");
|
|
35
|
+
}
|
|
36
|
+
function isObservation(value) {
|
|
37
|
+
return (isRecord(value) &&
|
|
38
|
+
typeof value.at === "string" &&
|
|
39
|
+
typeof value.modelRef === "string" &&
|
|
40
|
+
isIntentClass(value.intentClass) &&
|
|
41
|
+
typeof value.actualTool === "string" &&
|
|
42
|
+
typeof value.firstTool === "boolean" &&
|
|
43
|
+
typeof value.succeeded === "boolean" &&
|
|
44
|
+
(value.disposition === "recommend" || value.disposition === "shortlist" || value.disposition === "abstain") &&
|
|
45
|
+
Array.isArray(value.shortlist) &&
|
|
46
|
+
value.shortlist.every((tool) => typeof tool === "string") &&
|
|
47
|
+
typeof value.entropy === "number" &&
|
|
48
|
+
typeof value.margin === "number" &&
|
|
49
|
+
Array.isArray(value.ranked) &&
|
|
50
|
+
value.ranked.every((entry) => isRecord(entry) &&
|
|
51
|
+
typeof entry.tool === "string" &&
|
|
52
|
+
typeof entry.utility === "number" &&
|
|
53
|
+
typeof entry.probability === "number") &&
|
|
54
|
+
(value.latencyMs === undefined || typeof value.latencyMs === "number") &&
|
|
55
|
+
(value.inputTokenEstimate === undefined || typeof value.inputTokenEstimate === "number") &&
|
|
56
|
+
(value.outputTokenEstimate === undefined || typeof value.outputTokenEstimate === "number"));
|
|
57
|
+
}
|
|
58
|
+
function emptyStats(key, at) {
|
|
59
|
+
return {
|
|
60
|
+
...key,
|
|
61
|
+
alpha: 1,
|
|
62
|
+
beta: 1,
|
|
63
|
+
sampleCount: 0,
|
|
64
|
+
repairCount: 0,
|
|
65
|
+
bounceCount: 0,
|
|
66
|
+
failureCount: 0,
|
|
67
|
+
lastUsedAt: at,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function finiteNonNegative(value) {
|
|
71
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
72
|
+
}
|
|
73
|
+
function updateEwma(previous, next) {
|
|
74
|
+
if (next === undefined)
|
|
75
|
+
return previous;
|
|
76
|
+
return previous === undefined ? next : previous + EWMA_ALPHA * (next - previous);
|
|
77
|
+
}
|
|
78
|
+
function updateDeviation(previousValue, previousDeviation, next) {
|
|
79
|
+
if (next === undefined)
|
|
80
|
+
return previousDeviation;
|
|
81
|
+
const deviation = previousValue === undefined ? 0 : Math.abs(next - previousValue);
|
|
82
|
+
return updateEwma(previousDeviation, deviation);
|
|
83
|
+
}
|
|
84
|
+
function parseHost(value) {
|
|
85
|
+
if (!isRecord(value) || !isRecord(value.host) || !isRecord(value.stats) || !Array.isArray(value.observations))
|
|
86
|
+
return undefined;
|
|
87
|
+
const host = value.host;
|
|
88
|
+
if (typeof host.id !== "string" ||
|
|
89
|
+
typeof host.cpu !== "string" ||
|
|
90
|
+
typeof host.cores !== "number" ||
|
|
91
|
+
typeof host.totalMemGb !== "number")
|
|
92
|
+
return undefined;
|
|
93
|
+
return {
|
|
94
|
+
host: { id: host.id, cpu: host.cpu, cores: host.cores, totalMemGb: host.totalMemGb },
|
|
95
|
+
stats: Object.fromEntries(Object.entries(value.stats).filter((entry) => isStats(entry[1]))),
|
|
96
|
+
observations: value.observations.filter(isObservation),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseFile(value) {
|
|
100
|
+
if (!isRecord(value) || value.version !== STORE_VERSION || !isRecord(value.hosts)) {
|
|
101
|
+
return { version: STORE_VERSION, hosts: {} };
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
version: STORE_VERSION,
|
|
105
|
+
hosts: Object.fromEntries(Object.entries(value.hosts)
|
|
106
|
+
.map(([hostId, host]) => [hostId, parseHost(host)])
|
|
107
|
+
.filter((entry) => entry[1] !== undefined)),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function trimStats(stats) {
|
|
111
|
+
const entries = Object.entries(stats);
|
|
112
|
+
if (entries.length <= MAX_STATS_PER_HOST)
|
|
113
|
+
return stats;
|
|
114
|
+
return Object.fromEntries(entries
|
|
115
|
+
.sort(([, left], [, right]) => Date.parse(right.lastUsedAt) - Date.parse(left.lastUsedAt))
|
|
116
|
+
.slice(0, MAX_STATS_PER_HOST));
|
|
117
|
+
}
|
|
118
|
+
export class ToolPerformanceStore {
|
|
119
|
+
filePath;
|
|
120
|
+
fingerprint;
|
|
121
|
+
constructor(filePath, options = {}) {
|
|
122
|
+
this.filePath = filePath;
|
|
123
|
+
this.fingerprint = options.fingerprint ?? currentHostFingerprint;
|
|
124
|
+
}
|
|
125
|
+
static forAgentDir(agentDir, options = {}) {
|
|
126
|
+
return new ToolPerformanceStore(join(agentDir, "state", "tool-performance.json"), options);
|
|
127
|
+
}
|
|
128
|
+
load() {
|
|
129
|
+
try {
|
|
130
|
+
if (!existsSync(this.filePath))
|
|
131
|
+
return { version: STORE_VERSION, hosts: {} };
|
|
132
|
+
return parseFile(JSON.parse(readFileSync(this.filePath, "utf8")));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return { version: STORE_VERSION, hosts: {} };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
save(file) {
|
|
139
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
140
|
+
writeFileSync(this.filePath, `${JSON.stringify(file, null, "\t")}\n`, "utf8");
|
|
141
|
+
}
|
|
142
|
+
hostData(file) {
|
|
143
|
+
const fingerprint = this.fingerprint();
|
|
144
|
+
const existing = file.hosts[fingerprint.id];
|
|
145
|
+
if (existing)
|
|
146
|
+
return existing;
|
|
147
|
+
const created = { host: fingerprint, stats: {}, observations: [] };
|
|
148
|
+
file.hosts[fingerprint.id] = created;
|
|
149
|
+
return created;
|
|
150
|
+
}
|
|
151
|
+
get(key) {
|
|
152
|
+
const file = this.load();
|
|
153
|
+
const host = file.hosts[this.fingerprint().id];
|
|
154
|
+
const stats = host?.stats[statKey(key)];
|
|
155
|
+
return stats ? { ...stats } : emptyStats(key, new Date(0).toISOString());
|
|
156
|
+
}
|
|
157
|
+
recordValidation(key, outcome, at = new Date().toISOString()) {
|
|
158
|
+
const file = this.load();
|
|
159
|
+
const host = this.hostData(file);
|
|
160
|
+
const storageKey = statKey(key);
|
|
161
|
+
const current = host.stats[storageKey] ?? emptyStats(key, at);
|
|
162
|
+
const next = {
|
|
163
|
+
...current,
|
|
164
|
+
repairCount: current.repairCount + (outcome === "repaired" ? 1 : 0),
|
|
165
|
+
bounceCount: current.bounceCount + (outcome === "bounced" ? 1 : 0),
|
|
166
|
+
lastUsedAt: at,
|
|
167
|
+
};
|
|
168
|
+
host.stats[storageKey] = next;
|
|
169
|
+
host.stats = trimStats(host.stats);
|
|
170
|
+
this.save(file);
|
|
171
|
+
return { ...next };
|
|
172
|
+
}
|
|
173
|
+
recordExecution(observation) {
|
|
174
|
+
const at = observation.at ?? new Date().toISOString();
|
|
175
|
+
const file = this.load();
|
|
176
|
+
const host = this.hostData(file);
|
|
177
|
+
const storageKey = statKey(observation.key);
|
|
178
|
+
const current = host.stats[storageKey] ?? emptyStats(observation.key, at);
|
|
179
|
+
const latencyMs = finiteNonNegative(observation.latencyMs);
|
|
180
|
+
const inputTokenEstimate = finiteNonNegative(observation.inputTokenEstimate);
|
|
181
|
+
const outputTokenEstimate = finiteNonNegative(observation.outputTokenEstimate);
|
|
182
|
+
const next = {
|
|
183
|
+
...current,
|
|
184
|
+
alpha: current.alpha + (observation.success ? 1 : 0),
|
|
185
|
+
beta: current.beta + (observation.success ? 0 : 1),
|
|
186
|
+
sampleCount: current.sampleCount + 1,
|
|
187
|
+
latencyEwmaMs: updateEwma(current.latencyEwmaMs, latencyMs),
|
|
188
|
+
latencyDeviationEwmaMs: updateDeviation(current.latencyEwmaMs, current.latencyDeviationEwmaMs, latencyMs),
|
|
189
|
+
inputTokenEstimateEwma: updateEwma(current.inputTokenEstimateEwma, inputTokenEstimate),
|
|
190
|
+
outputTokenEstimateEwma: updateEwma(current.outputTokenEstimateEwma, outputTokenEstimate),
|
|
191
|
+
failureCount: current.failureCount + (observation.success ? 0 : 1),
|
|
192
|
+
lastUsedAt: at,
|
|
193
|
+
};
|
|
194
|
+
host.stats[storageKey] = next;
|
|
195
|
+
host.stats = trimStats(host.stats);
|
|
196
|
+
host.observations.push({
|
|
197
|
+
...observation.selection,
|
|
198
|
+
at,
|
|
199
|
+
modelRef: observation.key.modelRef,
|
|
200
|
+
intentClass: observation.key.intentClass,
|
|
201
|
+
actualTool: observation.key.tool,
|
|
202
|
+
succeeded: observation.success,
|
|
203
|
+
ranked: observation.selection.ranked.slice(0, 6),
|
|
204
|
+
shortlist: observation.selection.shortlist.slice(0, 3),
|
|
205
|
+
latencyMs,
|
|
206
|
+
inputTokenEstimate,
|
|
207
|
+
outputTokenEstimate,
|
|
208
|
+
});
|
|
209
|
+
if (host.observations.length > MAX_OBSERVATIONS_PER_HOST) {
|
|
210
|
+
host.observations = host.observations.slice(-MAX_OBSERVATIONS_PER_HOST);
|
|
211
|
+
}
|
|
212
|
+
this.save(file);
|
|
213
|
+
return { ...next };
|
|
214
|
+
}
|
|
215
|
+
getMetrics(modelRef) {
|
|
216
|
+
const host = this.load().hosts[this.fingerprint().id];
|
|
217
|
+
const observations = (host?.observations ?? []).filter((observation) => modelRef === undefined || observation.modelRef === modelRef);
|
|
218
|
+
const firstTools = observations.filter((observation) => observation.firstTool);
|
|
219
|
+
const recommended = observations.filter((observation) => observation.disposition === "recommend");
|
|
220
|
+
const average = (values) => {
|
|
221
|
+
const present = values.filter((value) => value !== undefined);
|
|
222
|
+
return present.length > 0 ? present.reduce((sum, value) => sum + value, 0) / present.length : undefined;
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
firstToolAttempts: firstTools.length,
|
|
226
|
+
firstToolSuccesses: firstTools.filter((observation) => observation.succeeded).length,
|
|
227
|
+
wrongToolOrFailureCount: observations.filter((observation) => !observation.succeeded ||
|
|
228
|
+
(observation.disposition === "recommend" && observation.recommendation !== observation.actualTool)).length,
|
|
229
|
+
recommendationCount: recommended.length,
|
|
230
|
+
recommendationMatchedCount: recommended.filter((observation) => observation.recommendation === observation.actualTool).length,
|
|
231
|
+
shortlistCount: observations.filter((observation) => observation.disposition === "shortlist").length,
|
|
232
|
+
abstentionCount: observations.filter((observation) => observation.disposition === "abstain").length,
|
|
233
|
+
averageLatencyMs: average(observations.map((observation) => observation.latencyMs)),
|
|
234
|
+
averageInputTokenEstimate: average(observations.map((observation) => observation.inputTokenEstimate)),
|
|
235
|
+
averageOutputTokenEstimate: average(observations.map((observation) => observation.outputTokenEstimate)),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
getObservations(modelRef) {
|
|
239
|
+
const host = this.load().hosts[this.fingerprint().id];
|
|
240
|
+
return (host?.observations ?? [])
|
|
241
|
+
.filter((observation) => modelRef === undefined || observation.modelRef === modelRef)
|
|
242
|
+
.map((observation) => ({
|
|
243
|
+
...observation,
|
|
244
|
+
shortlist: [...observation.shortlist],
|
|
245
|
+
ranked: observation.ranked.map((candidate) => ({ ...candidate })),
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=tool-performance-store.js.map
|