@vellumai/assistant 0.12.2 → 0.12.3-staging.1
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/.env.example +2 -0
- package/AGENTS.md +1 -1
- package/docs/architecture/integrations.md +7 -0
- package/docs/architecture/memory.md +17 -2
- package/docs/credential-execution-service.md +1 -1
- package/docs/desktop-browser-cli.md +7 -3
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/openapi.yaml +54 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +1 -0
- package/scripts/sync-llm-catalog.ts +3 -0
- package/scripts/voice-ttft-spike.ts +2 -2
- package/src/__tests__/agent-loop.test.ts +259 -0
- package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
- package/src/__tests__/anthropic-provider.test.ts +55 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/attachments-store.test.ts +22 -3
- package/src/__tests__/channel-approval.test.ts +9 -14
- package/src/__tests__/channel-reply-delivery.test.ts +56 -0
- package/src/__tests__/chat-credential-redaction.test.ts +23 -0
- package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
- package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +257 -6
- package/src/__tests__/conversation-attachments.test.ts +106 -0
- package/src/__tests__/conversation-fork-crud.test.ts +191 -0
- package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
- package/src/__tests__/credential-execution-client.test.ts +70 -53
- package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
- package/src/__tests__/events-tail-route.test.ts +33 -0
- package/src/__tests__/history-repair.test.ts +53 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
- package/src/__tests__/llm-catalog-parity.test.ts +27 -4
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-commands-routes.test.ts +114 -101
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/persist-media-references.test.ts +50 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
- package/src/__tests__/schedule-routes.test.ts +20 -0
- package/src/__tests__/scheduler-result-notification.test.ts +23 -4
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
- package/src/__tests__/secret-routes-scrub.test.ts +22 -27
- package/src/__tests__/secret-scanner.test.ts +20 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
- package/src/__tests__/secure-keys.test.ts +7 -3
- package/src/__tests__/server-tool-pairing.test.ts +107 -0
- package/src/__tests__/skills.test.ts +5 -4
- package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/tool-result-follow-up.test.ts +99 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
- package/src/__tests__/unicode.test.ts +36 -0
- package/src/agent/loop.ts +37 -12
- package/src/agent/tool-result-follow-up.ts +70 -0
- package/src/api/attachment-provenance.test.ts +68 -0
- package/src/api/computer-use-tool.test.ts +55 -0
- package/src/api/computer-use-tool.ts +27 -0
- package/src/api/events/assistant-outbound-attachment.ts +3 -0
- package/src/api/events/desktop-activity-changed.ts +10 -0
- package/src/api/events/question-request.ts +1 -0
- package/src/api/index.ts +11 -0
- package/src/api/responses/conversation-message.ts +3 -0
- package/src/approvals/approval-primitive.ts +5 -2
- package/src/approvals/scoped-approval-grants.ts +6 -2
- package/src/browser/virtual-desktop-target.ts +1 -2
- package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
- package/src/cli/commands/__tests__/plugins.test.ts +20 -1
- package/src/cli/commands/__tests__/schedules.test.ts +14 -0
- package/src/cli/commands/bash.help.ts +4 -3
- package/src/cli/commands/browser.help.ts +5 -1
- package/src/cli/commands/credentials.help.ts +3 -3
- package/src/cli/commands/oauth/index.help.ts +10 -0
- package/src/cli/commands/oauth/providers.ts +12 -0
- package/src/cli/commands/oauth/request.test.ts +179 -2
- package/src/cli/commands/oauth/request.ts +41 -10
- package/src/cli/commands/plugins.ts +12 -5
- package/src/cli/commands/schedules.ts +2 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
- package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
- package/src/cli/lib/bundled-marketplace.json +855 -0
- package/src/cli/lib/bundled-plugin-packages.json +783 -1
- package/src/cli/lib/inspect-plugin.ts +11 -4
- package/src/cli/lib/upgrade-plugin.ts +19 -11
- package/src/cli/output.ts +18 -4
- package/src/config/__tests__/default-provider.test.ts +4 -0
- package/src/config/bundled-skills/schedule/SKILL.md +6 -6
- package/src/config/loader.ts +9 -17
- package/src/config/platform-identity.ts +8 -8
- package/src/config/profile-text-generation.test.ts +51 -0
- package/src/config/profile-text-generation.ts +51 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
- package/src/config/schemas/llm.ts +7 -2
- package/src/config/schemas/mcp.ts +5 -1
- package/src/config/schemas/memory-v3.ts +12 -0
- package/src/credential-execution/ces-runtime.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +36 -134
- package/src/credential-execution/process-manager.test.ts +20 -24
- package/src/credential-execution/process-manager.ts +24 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
- package/src/daemon/assistant-attachments.ts +20 -12
- package/src/daemon/chat-credential-redaction.ts +16 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
- package/src/daemon/conversation-agent-loop.ts +12 -0
- package/src/daemon/conversation-attachments.ts +93 -12
- package/src/daemon/conversation-tool-setup.ts +71 -3
- package/src/daemon/conversation-turn-finalize.ts +26 -13
- package/src/daemon/conversation.ts +72 -17
- package/src/daemon/daemon-control.ts +3 -7
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/mcp-reload-service.ts +1 -1
- package/src/daemon/message-types/shared.ts +1 -0
- package/src/daemon/orphan-reaper.ts +4 -3
- package/src/daemon/persist-media-references.ts +20 -3
- package/src/daemon/tool-setup-types.ts +6 -0
- package/src/daemon/wake-conversation-ops.ts +50 -15
- package/src/desktop/desktop-automation-lease.test.ts +230 -0
- package/src/desktop/desktop-automation-lease.ts +90 -6
- package/src/desktop/desktop-help.ts +15 -0
- package/src/desktop/desktop-stream-bridge.test.ts +65 -10
- package/src/desktop/desktop-stream-bridge.ts +3 -3
- package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
- package/src/desktop/desktop-wallpaper-worker.ts +17 -0
- package/src/desktop/desktop-wallpaper.test.ts +48 -7
- package/src/desktop/desktop-wallpaper.ts +34 -102
- package/src/desktop/virtual-desktop-feature.ts +1 -1
- package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
- package/src/mcp/__tests__/startup.test.ts +30 -7
- package/src/mcp/__tests__/tool-caps.test.ts +107 -0
- package/src/mcp/manager.ts +168 -101
- package/src/mcp/startup.ts +33 -13
- package/src/mcp/tool-caps.ts +176 -0
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +70 -0
- package/src/notifications/copy-composer.ts +11 -3
- package/src/notifications/schedule-result-producer.ts +4 -6
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
- package/src/oauth/identity-verifier.ts +24 -0
- package/src/oauth/oauth-store.ts +11 -1
- package/src/oauth/provider-serializer.ts +1 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/permissions/confirmation-guardian-request.test.ts +16 -0
- package/src/permissions/confirmation-guardian-request.ts +2 -3
- package/src/permissions/question-prompter.test.ts +31 -0
- package/src/permissions/question-prompter.ts +2 -0
- package/src/persistence/attachments-store.ts +84 -44
- package/src/persistence/conversation-crud.ts +111 -20
- package/src/persistence/conversation-plugin-facade.ts +30 -0
- package/src/persistence/conversation-tool-surface.ts +135 -0
- package/src/persistence/conversation-types.test.ts +32 -0
- package/src/persistence/conversation-types.ts +26 -4
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
- package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/oauth.ts +1 -0
- package/src/persistence/steps.ts +24 -3
- package/src/plugin-api/conversation-turn.ts +31 -7
- package/src/plugin-api/index.ts +11 -1
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
- package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
- package/src/plugins/defaults/memory/AGENTS.md +39 -5
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
- package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
- package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
- package/src/plugins/defaults/memory/buffer-file.ts +354 -0
- package/src/plugins/defaults/memory/buffer-format.ts +40 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
- package/src/plugins/defaults/memory/context-search/format.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
- package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
- package/src/plugins/defaults/memory/host-utils.ts +0 -10
- package/src/plugins/defaults/memory/injectors.ts +4 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
- package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
- package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
- package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
- package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
- package/src/plugins/defaults/memory/v3/card.ts +2 -1
- package/src/plugins/defaults/memory/v3/injector.ts +212 -178
- package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
- package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
- package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
- package/src/plugins/defaults/memory/v3/sections.ts +2 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
- package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
- package/src/prompts/delegation-gate.ts +57 -0
- package/src/prompts/system-prompt.ts +14 -32
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/jev/client.test.ts +260 -0
- package/src/providers/jev/client.ts +518 -0
- package/src/providers/model-catalog.ts +58 -3
- package/src/providers/server-tool-pairing.ts +16 -7
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +94 -1
- package/src/runtime/agent-wake.ts +31 -4
- package/src/runtime/guardian-action-service.ts +2 -17
- package/src/runtime/guardian-reply-router.ts +1 -8
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
- package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -9
- package/src/runtime/routes/conversation-routes.ts +29 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +8 -5
- package/src/runtime/routes/guardian-approval-interception.ts +24 -0
- package/src/runtime/routes/inbound-message-handler.ts +2 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
- package/src/runtime/routes/inference-profiles-routes.ts +9 -0
- package/src/runtime/routes/oauth-commands-routes.ts +57 -54
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
- package/src/runtime/routes/oauth-request-hints.ts +149 -0
- package/src/runtime/routes/schedule-routes.ts +2 -0
- package/src/runtime/routes/secret-routes.ts +99 -119
- package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
- package/src/schedule/run-script.ts +2 -2
- package/src/schedule/scheduler.ts +47 -11
- package/src/schedule/tool-surface-readiness.ts +65 -0
- package/src/schedule/worker-mcp.ts +85 -0
- package/src/schedule/worker.ts +9 -31
- package/src/security/secure-keys.ts +60 -48
- package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
- package/src/tools/ask-question/ask-question-tool.ts +119 -23
- package/src/tools/browser/browser-execution.ts +7 -1
- package/src/tools/host-terminal/host-shell.ts +12 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
- package/src/tools/shared/shell-output.test.ts +10 -0
- package/src/tools/shared/shell-output.ts +14 -2
- package/src/tools/skills/sandbox-runner.ts +13 -2
- package/src/tools/skills/scaffold-managed.ts +2 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
- package/src/tools/terminal/safe-env.ts +35 -24
- package/src/tools/terminal/sanitized-bash.ts +15 -2
- package/src/tools/terminal/shell-launch.test.ts +162 -0
- package/src/tools/terminal/shell.test.ts +29 -0
- package/src/tools/terminal/shell.ts +13 -7
- package/src/util/browser-human-verification.ts +14 -0
- package/src/util/host-process.test.ts +17 -1
- package/src/util/host-process.ts +24 -0
- package/src/util/unicode.ts +29 -0
|
@@ -66,6 +66,40 @@ mock.module("../daemon/conversation-skill-tools.js", () => ({
|
|
|
66
66
|
})),
|
|
67
67
|
}));
|
|
68
68
|
|
|
69
|
+
// Records every wire tool surface the resolver persists (conversation id +
|
|
70
|
+
// tool names + the hash it already knew), and lets a test make the write fail.
|
|
71
|
+
type RecordedSurface = {
|
|
72
|
+
tools: ToolDefinition[];
|
|
73
|
+
delegateIndependentTasks: boolean | null;
|
|
74
|
+
};
|
|
75
|
+
let recordedSurfaces: Array<{
|
|
76
|
+
conversationId: string;
|
|
77
|
+
toolNames: string[];
|
|
78
|
+
delegateIndependentTasks: boolean | null;
|
|
79
|
+
knownHash: string | undefined;
|
|
80
|
+
}> = [];
|
|
81
|
+
let recordSurfaceThrows: Error | null = null;
|
|
82
|
+
mock.module("../persistence/conversation-tool-surface.js", () => ({
|
|
83
|
+
recordConversationToolSurface: (
|
|
84
|
+
conversationId: string,
|
|
85
|
+
surface: RecordedSurface,
|
|
86
|
+
knownHash?: string,
|
|
87
|
+
) => {
|
|
88
|
+
recordedSurfaces.push({
|
|
89
|
+
conversationId,
|
|
90
|
+
toolNames: surface.tools.map((t) => t.name),
|
|
91
|
+
delegateIndependentTasks: surface.delegateIndependentTasks,
|
|
92
|
+
knownHash,
|
|
93
|
+
});
|
|
94
|
+
if (recordSurfaceThrows) {
|
|
95
|
+
throw recordSurfaceThrows;
|
|
96
|
+
}
|
|
97
|
+
return `hash:${surface.tools.map((t) => t.name).join(",")}`;
|
|
98
|
+
},
|
|
99
|
+
hashConversationToolSurface: (surface: RecordedSurface) =>
|
|
100
|
+
`hash:${surface.tools.map((t) => t.name).join(",")}`,
|
|
101
|
+
}));
|
|
102
|
+
|
|
69
103
|
// ---------------------------------------------------------------------------
|
|
70
104
|
// Imports after mocks are in place
|
|
71
105
|
// ---------------------------------------------------------------------------
|
|
@@ -75,6 +109,7 @@ import { createSurfaceMutex } from "../daemon/conversation-surfaces.js";
|
|
|
75
109
|
import {
|
|
76
110
|
createResolveToolsCallback,
|
|
77
111
|
createToolExecutor,
|
|
112
|
+
createWireToolSurfaceRecorder,
|
|
78
113
|
isRefusedInReadOnlyPass,
|
|
79
114
|
type SubagentToolStats,
|
|
80
115
|
} from "../daemon/conversation-tool-setup.js";
|
|
@@ -1117,3 +1152,194 @@ describe("createResolveToolsCallback — read-only hides dynamic MCP tools", ()
|
|
|
1117
1152
|
expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toContain("srv_send");
|
|
1118
1153
|
});
|
|
1119
1154
|
});
|
|
1155
|
+
|
|
1156
|
+
// ---------------------------------------------------------------------------
|
|
1157
|
+
// Resolver: wire tool surface record + replay (fork cache parity)
|
|
1158
|
+
// ---------------------------------------------------------------------------
|
|
1159
|
+
|
|
1160
|
+
describe("createResolveToolsCallback: wire tool surface record and replay", () => {
|
|
1161
|
+
beforeEach(() => {
|
|
1162
|
+
recordedSurfaces = [];
|
|
1163
|
+
recordSurfaceThrows = null;
|
|
1164
|
+
projectedSkillToolNames = [];
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
test("the resolver itself never records: out-of-band callers read it too", () => {
|
|
1168
|
+
const toolDefs = [makeToolDef("remember"), makeToolDef("tool_b")];
|
|
1169
|
+
const ctx = makeProjectionCtx({ conversationId: "conv-live" });
|
|
1170
|
+
const resolve = createResolveToolsCallback(toolDefs, ctx)!;
|
|
1171
|
+
|
|
1172
|
+
// The `/compact` token count resolves outside any turn, where presence
|
|
1173
|
+
// reads clientless; recording here would overwrite the sent surface.
|
|
1174
|
+
resolve(EMPTY_HISTORY);
|
|
1175
|
+
|
|
1176
|
+
expect(recordedSurfaces).toEqual([]);
|
|
1177
|
+
expect(ctx.recordedToolSurfaceHash).toBeUndefined();
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
test("the send-boundary recorder records the sent array, keyed by conversation, and remembers its hash", () => {
|
|
1181
|
+
const ctx = makeProjectionCtx({
|
|
1182
|
+
conversationId: "conv-live",
|
|
1183
|
+
renderedDelegateIndependentTasks: true,
|
|
1184
|
+
});
|
|
1185
|
+
const record = createWireToolSurfaceRecorder(ctx);
|
|
1186
|
+
const sent = [makeToolDef("remember"), makeToolDef("web_search")];
|
|
1187
|
+
|
|
1188
|
+
record(sent);
|
|
1189
|
+
record(sent);
|
|
1190
|
+
|
|
1191
|
+
// Every provider call records; the persistence layer dedupes on the hash
|
|
1192
|
+
// handed back from the previous call.
|
|
1193
|
+
expect(recordedSurfaces).toEqual([
|
|
1194
|
+
{
|
|
1195
|
+
conversationId: "conv-live",
|
|
1196
|
+
toolNames: ["remember", "web_search"],
|
|
1197
|
+
delegateIndependentTasks: true,
|
|
1198
|
+
knownHash: undefined,
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
conversationId: "conv-live",
|
|
1202
|
+
toolNames: ["remember", "web_search"],
|
|
1203
|
+
delegateIndependentTasks: true,
|
|
1204
|
+
knownHash: "hash:remember,web_search",
|
|
1205
|
+
},
|
|
1206
|
+
]);
|
|
1207
|
+
expect(ctx.recordedToolSurfaceHash).toBe("hash:remember,web_search");
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
test("the recorder records the state the prompt build captured, never a re-derivation", () => {
|
|
1211
|
+
const sent = [makeToolDef("remember")];
|
|
1212
|
+
|
|
1213
|
+
// The loop sends the prompt built before the run, so the captured state
|
|
1214
|
+
// is recorded as is even where the live scope would now derive the
|
|
1215
|
+
// opposite: a spawn path excluded after the build (here, an allowlist
|
|
1216
|
+
// with none) still records the section on...
|
|
1217
|
+
createWireToolSurfaceRecorder(
|
|
1218
|
+
makeProjectionCtx({
|
|
1219
|
+
conversationId: "conv-captured-on",
|
|
1220
|
+
subagentAllowedTools: new Set(["remember"]),
|
|
1221
|
+
renderedDelegateIndependentTasks: true,
|
|
1222
|
+
}),
|
|
1223
|
+
)(sent);
|
|
1224
|
+
// ...and a path restored after the build still records it off.
|
|
1225
|
+
createWireToolSurfaceRecorder(
|
|
1226
|
+
makeProjectionCtx({
|
|
1227
|
+
conversationId: "conv-captured-off",
|
|
1228
|
+
renderedDelegateIndependentTasks: false,
|
|
1229
|
+
}),
|
|
1230
|
+
)(sent);
|
|
1231
|
+
// A verbatim system-prompt override captures unknown.
|
|
1232
|
+
createWireToolSurfaceRecorder(
|
|
1233
|
+
makeProjectionCtx({
|
|
1234
|
+
conversationId: "conv-override",
|
|
1235
|
+
renderedDelegateIndependentTasks: null,
|
|
1236
|
+
}),
|
|
1237
|
+
)(sent);
|
|
1238
|
+
// No prompt built yet: unknown as well.
|
|
1239
|
+
createWireToolSurfaceRecorder(
|
|
1240
|
+
makeProjectionCtx({ conversationId: "conv-unbuilt" }),
|
|
1241
|
+
)(sent);
|
|
1242
|
+
|
|
1243
|
+
expect(
|
|
1244
|
+
recordedSurfaces.map((r) => [
|
|
1245
|
+
r.conversationId,
|
|
1246
|
+
r.delegateIndependentTasks,
|
|
1247
|
+
]),
|
|
1248
|
+
).toEqual([
|
|
1249
|
+
["conv-captured-on", true],
|
|
1250
|
+
["conv-captured-off", false],
|
|
1251
|
+
["conv-override", null],
|
|
1252
|
+
["conv-unbuilt", null],
|
|
1253
|
+
]);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
test("a failed record is swallowed and still counts as recorded", () => {
|
|
1257
|
+
recordSurfaceThrows = new Error("no such table");
|
|
1258
|
+
const record = createWireToolSurfaceRecorder(
|
|
1259
|
+
makeProjectionCtx({ conversationId: "conv-live" }),
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1262
|
+
expect(() => record([makeToolDef("remember")])).not.toThrow();
|
|
1263
|
+
record([makeToolDef("remember")]);
|
|
1264
|
+
|
|
1265
|
+
// The second call carries the hash from the failed first one, so a
|
|
1266
|
+
// persistent failure is retried once per distinct surface, not per call.
|
|
1267
|
+
expect(recordedSurfaces.map((r) => r.knownHash)).toEqual([
|
|
1268
|
+
undefined,
|
|
1269
|
+
"hash:remember",
|
|
1270
|
+
]);
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test("the recorder skips arrays that are not the conversation's own surface", () => {
|
|
1274
|
+
const tools = [makeToolDef("remember")];
|
|
1275
|
+
|
|
1276
|
+
// A replaying wake sends its source's array.
|
|
1277
|
+
createWireToolSurfaceRecorder(
|
|
1278
|
+
makeProjectionCtx({ conversationId: "conv-fork", wireToolReplay: tools }),
|
|
1279
|
+
)(tools);
|
|
1280
|
+
// A tools-disabled call sends nothing; a fork replaying [] could never
|
|
1281
|
+
// call remember.
|
|
1282
|
+
createWireToolSurfaceRecorder(
|
|
1283
|
+
makeProjectionCtx({ conversationId: "conv-live" }),
|
|
1284
|
+
)([]);
|
|
1285
|
+
// Disk-pressure cleanup narrows the wire to cleanup tools.
|
|
1286
|
+
createWireToolSurfaceRecorder(
|
|
1287
|
+
makeProjectionCtx({
|
|
1288
|
+
conversationId: "conv-live",
|
|
1289
|
+
diskPressureCleanupModeActive: true,
|
|
1290
|
+
}),
|
|
1291
|
+
)(tools);
|
|
1292
|
+
|
|
1293
|
+
expect(recordedSurfaces).toEqual([]);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
test("replays wireToolReplay verbatim on the wire while execution still derives from the fork's own context", () => {
|
|
1297
|
+
projectedSkillToolNames = ["scaffold_managed_skill"];
|
|
1298
|
+
const replay: ToolDefinition[] = [
|
|
1299
|
+
makeToolDef("bash"),
|
|
1300
|
+
makeToolDef("remember"),
|
|
1301
|
+
makeToolDef("bell_jingle"),
|
|
1302
|
+
{
|
|
1303
|
+
type: "web_search_20250305",
|
|
1304
|
+
name: "web_search",
|
|
1305
|
+
max_uses: 5,
|
|
1306
|
+
} as unknown as ToolDefinition,
|
|
1307
|
+
];
|
|
1308
|
+
// The fork's own registry lacks bell_jingle (a user-plugin tool the
|
|
1309
|
+
// worker never loads) and web_search (a server tool the loop appends).
|
|
1310
|
+
const toolDefs = [makeToolDef("remember"), makeToolDef("bash")];
|
|
1311
|
+
const ctx = makeProjectionCtx({
|
|
1312
|
+
conversationId: "conv-fork",
|
|
1313
|
+
subagentAllowedTools: new Set(["remember"]),
|
|
1314
|
+
subagentToolGateMode: "execution",
|
|
1315
|
+
wireToolReplay: replay,
|
|
1316
|
+
});
|
|
1317
|
+
const resolve = createResolveToolsCallback(toolDefs, ctx)!;
|
|
1318
|
+
|
|
1319
|
+
const tools = resolve(EMPTY_HISTORY);
|
|
1320
|
+
|
|
1321
|
+
// Byte-identical to the source's recorded array, in the source's order,
|
|
1322
|
+
// including definitions this process could never resolve.
|
|
1323
|
+
expect(JSON.stringify(tools)).toBe(JSON.stringify(replay));
|
|
1324
|
+
// Execution-side inventory is still the fork's own derivation (plus the
|
|
1325
|
+
// preactivated skill tools), never widened by the replayed definitions.
|
|
1326
|
+
expect(ctx.allowedToolNames).toEqual(
|
|
1327
|
+
new Set(["remember", "bash", "scaffold_managed_skill"]),
|
|
1328
|
+
);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
test("replay returns a fresh array each call so the loop cannot mutate the recorded one", () => {
|
|
1332
|
+
const replay = [makeToolDef("remember")];
|
|
1333
|
+
const ctx = makeProjectionCtx({
|
|
1334
|
+
conversationId: "conv-fork",
|
|
1335
|
+
wireToolReplay: replay,
|
|
1336
|
+
});
|
|
1337
|
+
const resolve = createResolveToolsCallback([makeToolDef("remember")], ctx)!;
|
|
1338
|
+
|
|
1339
|
+
const first = resolve(EMPTY_HISTORY);
|
|
1340
|
+
first.push(makeToolDef("injected"));
|
|
1341
|
+
|
|
1342
|
+
expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual(["remember"]);
|
|
1343
|
+
expect(replay.map((t) => t.name)).toEqual(["remember"]);
|
|
1344
|
+
});
|
|
1345
|
+
});
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
formatShellOutput,
|
|
45
45
|
MAX_OUTPUT_LENGTH,
|
|
46
46
|
OUTPUT_TRUNCATED_TAG,
|
|
47
|
+
SHELL_DID_NOT_START_MESSAGE,
|
|
47
48
|
} from "../tools/shared/shell-output.js";
|
|
48
49
|
import {
|
|
49
50
|
ALWAYS_INJECTED_ENV_VARS,
|
|
@@ -334,6 +335,13 @@ describe("formatShellOutput", () => {
|
|
|
334
335
|
expect(result.isError).toBe(false);
|
|
335
336
|
});
|
|
336
337
|
|
|
338
|
+
test("a close with exit 0 and empty pipes is an error when the process never started", () => {
|
|
339
|
+
const result = formatShellOutput("", "", 0, false, 120, { started: false });
|
|
340
|
+
expect(result.content).toBe(SHELL_DID_NOT_START_MESSAGE);
|
|
341
|
+
expect(result.isError).toBe(true);
|
|
342
|
+
expect(result.content).not.toBe("<command_completed />");
|
|
343
|
+
});
|
|
344
|
+
|
|
337
345
|
test("failed command with no output shows exit code tag and descriptive message", () => {
|
|
338
346
|
const result = formatShellOutput("", "", 1, false, 120);
|
|
339
347
|
expect(result.content).toContain('<command_exit code="1" />');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { buildToolResultFollowUp } from "../agent/tool-result-follow-up.js";
|
|
4
|
+
import type { ContentBlock, Message, TextContent } from "../providers/types.js";
|
|
5
|
+
|
|
6
|
+
const opener: Message = {
|
|
7
|
+
role: "user",
|
|
8
|
+
content: [{ type: "text", text: "Check both" }],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Assistant turn calling two client tools and a native web search together. */
|
|
12
|
+
const mixedTurn: Message = {
|
|
13
|
+
role: "assistant",
|
|
14
|
+
content: [
|
|
15
|
+
{ type: "tool_use", id: "tu_a", name: "web_fetch", input: {} },
|
|
16
|
+
{ type: "tool_use", id: "tu_b", name: "file_read", input: {} },
|
|
17
|
+
{
|
|
18
|
+
type: "server_tool_use",
|
|
19
|
+
id: "srvtoolu_deferred",
|
|
20
|
+
name: "web_search",
|
|
21
|
+
input: { query: "news" },
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** The same turn without the search: nothing is deferred. */
|
|
27
|
+
const clientOnlyTurn: Message = {
|
|
28
|
+
role: "assistant",
|
|
29
|
+
content: mixedTurn.content.filter((b) => b.type === "tool_use"),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const erroredResult: ContentBlock = {
|
|
33
|
+
type: "tool_result",
|
|
34
|
+
tool_use_id: "tu_a",
|
|
35
|
+
content: "Error: HTTP 404",
|
|
36
|
+
is_error: true,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const okResult: ContentBlock = {
|
|
40
|
+
type: "tool_result",
|
|
41
|
+
tool_use_id: "tu_b",
|
|
42
|
+
content: "file contents",
|
|
43
|
+
is_error: false,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const guidance: TextContent = {
|
|
47
|
+
type: "text",
|
|
48
|
+
text: "<system_notice>retry</system_notice>",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe("buildToolResultFollowUp", () => {
|
|
52
|
+
test("returns the results untouched when there is no guidance", () => {
|
|
53
|
+
const results = [erroredResult, okResult];
|
|
54
|
+
|
|
55
|
+
expect(buildToolResultFollowUp([opener, mixedTurn], results, [])).toBe(
|
|
56
|
+
results,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("appends guidance as separate blocks when no server tool is deferred", () => {
|
|
61
|
+
const followUp = buildToolResultFollowUp(
|
|
62
|
+
[opener, clientOnlyTurn],
|
|
63
|
+
[erroredResult, okResult],
|
|
64
|
+
[guidance],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(followUp).toEqual([erroredResult, okResult, guidance]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("folds guidance into the errored tool_result when a server tool is deferred", () => {
|
|
71
|
+
const followUp = buildToolResultFollowUp(
|
|
72
|
+
[opener, mixedTurn],
|
|
73
|
+
[erroredResult, okResult],
|
|
74
|
+
[guidance],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(followUp.map((b) => b.type)).toEqual(["tool_result", "tool_result"]);
|
|
78
|
+
expect(followUp[0]).toEqual({
|
|
79
|
+
...erroredResult,
|
|
80
|
+
content: `Error: HTTP 404\n\n${guidance.text}`,
|
|
81
|
+
});
|
|
82
|
+
expect(followUp[1]).toBe(okResult);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("falls back to the last tool_result when none errored", () => {
|
|
86
|
+
const followUp = buildToolResultFollowUp(
|
|
87
|
+
[opener, mixedTurn],
|
|
88
|
+
[okResult, { ...okResult, tool_use_id: "tu_a" }],
|
|
89
|
+
[guidance],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(followUp.map((b) => b.type)).toEqual(["tool_result", "tool_result"]);
|
|
93
|
+
expect(followUp[0]).toBe(okResult);
|
|
94
|
+
expect(followUp[1]).toMatchObject({
|
|
95
|
+
tool_use_id: "tu_a",
|
|
96
|
+
content: `file contents\n\n${guidance.text}`,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -27,6 +27,7 @@ mock.module("../persistence/llm-request-log-store.js", () => ({
|
|
|
27
27
|
|
|
28
28
|
// ── Imports (after mocks) ─────────────────────────────────────────────────────
|
|
29
29
|
import type { AssistantEvent } from "../api/index.js";
|
|
30
|
+
import { toolImageFilename } from "../daemon/assistant-attachments.js";
|
|
30
31
|
import type {
|
|
31
32
|
EventHandlerDeps,
|
|
32
33
|
EventHandlerState,
|
|
@@ -34,6 +35,7 @@ import type {
|
|
|
34
35
|
import {
|
|
35
36
|
createEventHandlerState,
|
|
36
37
|
handleToolResult,
|
|
38
|
+
handleToolUse,
|
|
37
39
|
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
38
40
|
import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
|
|
39
41
|
|
|
@@ -139,3 +141,64 @@ describe("tool_result activityMetadata plumbing", () => {
|
|
|
139
141
|
expect(toolResultEvent?.activityMetadata).toBeUndefined();
|
|
140
142
|
});
|
|
141
143
|
});
|
|
144
|
+
|
|
145
|
+
describe("computer-use screenshot capture", () => {
|
|
146
|
+
test("tracks direct and wrapped invocations but excludes their images from ordinary promotion", () => {
|
|
147
|
+
const state = createEventHandlerState();
|
|
148
|
+
const { deps } = createCollectorDeps();
|
|
149
|
+
|
|
150
|
+
handleToolUse(state, deps, {
|
|
151
|
+
type: "tool_use",
|
|
152
|
+
id: "direct-call",
|
|
153
|
+
name: "computer_use_click",
|
|
154
|
+
input: { x: 10, y: 20 },
|
|
155
|
+
});
|
|
156
|
+
handleToolUse(state, deps, {
|
|
157
|
+
type: "tool_use",
|
|
158
|
+
id: "wrapped-call",
|
|
159
|
+
name: "skill_execute",
|
|
160
|
+
input: { tool: "computer_use_scroll" },
|
|
161
|
+
});
|
|
162
|
+
handleToolUse(state, deps, {
|
|
163
|
+
type: "tool_use",
|
|
164
|
+
id: "ordinary-call",
|
|
165
|
+
name: "browser_screenshot",
|
|
166
|
+
input: {},
|
|
167
|
+
});
|
|
168
|
+
handleToolResult(state, deps, {
|
|
169
|
+
type: "tool_result",
|
|
170
|
+
toolUseId: "wrapped-call",
|
|
171
|
+
content: "scrolled",
|
|
172
|
+
isError: false,
|
|
173
|
+
contentBlocks: [
|
|
174
|
+
{
|
|
175
|
+
type: "image",
|
|
176
|
+
source: {
|
|
177
|
+
type: "base64",
|
|
178
|
+
media_type: "image/png",
|
|
179
|
+
data: "c2NyZWVuc2hvdA==",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(state.computerUseToolUseIds).toEqual([
|
|
186
|
+
"direct-call",
|
|
187
|
+
"wrapped-call",
|
|
188
|
+
]);
|
|
189
|
+
expect(state.computerUseToolNames).toEqual(
|
|
190
|
+
new Map([
|
|
191
|
+
["direct-call", "computer_use_click"],
|
|
192
|
+
["wrapped-call", "computer_use_scroll"],
|
|
193
|
+
]),
|
|
194
|
+
);
|
|
195
|
+
expect(
|
|
196
|
+
toolImageFilename(
|
|
197
|
+
"image/png",
|
|
198
|
+
state.computerUseToolNames.get("wrapped-call"),
|
|
199
|
+
),
|
|
200
|
+
).toBe("computer-use-scroll.png");
|
|
201
|
+
expect(state.computerUseScreenshotBlocks.has("wrapped-call")).toBe(true);
|
|
202
|
+
expect(state.accumulatedToolContentBlocks).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
safeStringSlice,
|
|
5
5
|
stripOrphanedSurrogates,
|
|
6
6
|
stripOrphanedSurrogatesDeep,
|
|
7
|
+
surrogateSafeWindow,
|
|
7
8
|
} from "../util/unicode.js";
|
|
8
9
|
|
|
9
10
|
// U+1F389 PARTY POPPER = "\uD83C\uDF89" (a surrogate pair).
|
|
@@ -302,3 +303,38 @@ describe("stripOrphanedSurrogatesDeep", () => {
|
|
|
302
303
|
expect(() => JSON.parse(json)).not.toThrow();
|
|
303
304
|
});
|
|
304
305
|
});
|
|
306
|
+
|
|
307
|
+
describe("surrogateSafeWindow", () => {
|
|
308
|
+
const codeAt = (text: string) => (i: number) => text.charCodeAt(i);
|
|
309
|
+
|
|
310
|
+
test("a start that lands on a low surrogate backs up to include the pair", () => {
|
|
311
|
+
const text = `a${EMOJI}b`;
|
|
312
|
+
expect(surrogateSafeWindow(text.length, codeAt(text), 2, 10)).toEqual({
|
|
313
|
+
start: 1,
|
|
314
|
+
end: 4,
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("an end that would cut a pair backs off one unit", () => {
|
|
319
|
+
const text = `ab${EMOJI}cd`;
|
|
320
|
+
expect(surrogateSafeWindow(text.length, codeAt(text), 0, 3)).toEqual({
|
|
321
|
+
start: 0,
|
|
322
|
+
end: 2,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("a one-unit window on a pair takes the whole pair so paging advances", () => {
|
|
327
|
+
const text = `${EMOJI}x`;
|
|
328
|
+
expect(surrogateSafeWindow(text.length, codeAt(text), 0, 1)).toEqual({
|
|
329
|
+
start: 0,
|
|
330
|
+
end: 2,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("clamps to the text bounds", () => {
|
|
335
|
+
expect(surrogateSafeWindow(3, codeAt("abc"), 10, 5)).toEqual({
|
|
336
|
+
start: 3,
|
|
337
|
+
end: 3,
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -42,6 +42,7 @@ import type {
|
|
|
42
42
|
Provider,
|
|
43
43
|
ProviderResponse,
|
|
44
44
|
SendMessageOptions,
|
|
45
|
+
TextContent,
|
|
45
46
|
ToolDefinition,
|
|
46
47
|
ToolResultContent,
|
|
47
48
|
} from "../providers/types.js";
|
|
@@ -74,6 +75,7 @@ import {
|
|
|
74
75
|
isRepairableOrderingError,
|
|
75
76
|
isUserTerminalHistoryError,
|
|
76
77
|
} from "./history-repair/history-repair.js";
|
|
78
|
+
import { buildToolResultFollowUp } from "./tool-result-follow-up.js";
|
|
77
79
|
|
|
78
80
|
const log = getLogger("agent-loop");
|
|
79
81
|
|
|
@@ -1047,6 +1049,17 @@ export interface AgentLoopConstructorOptions {
|
|
|
1047
1049
|
tools?: ToolDefinition[];
|
|
1048
1050
|
toolExecutor?: LoopToolExecutor;
|
|
1049
1051
|
resolveTools?: (history: Message[]) => ToolDefinition[];
|
|
1052
|
+
/**
|
|
1053
|
+
* Observer for the final tool array of each provider call, invoked
|
|
1054
|
+
* immediately before the request leaves with exactly what goes on the wire
|
|
1055
|
+
* (after any provider-native tool is appended, past the inter-call throttle
|
|
1056
|
+
* and the pre-model hooks, and not at all once the run is aborted). This is
|
|
1057
|
+
* the only point that sees the sent array: the dynamic `resolveTools`
|
|
1058
|
+
* callback is also consulted out of band (token counting, compaction
|
|
1059
|
+
* estimates), so a consumer that needs "what the last request sent"
|
|
1060
|
+
* subscribes here rather than wrapping the resolver. Must not throw.
|
|
1061
|
+
*/
|
|
1062
|
+
onToolsSent?: (tools: ToolDefinition[]) => void;
|
|
1050
1063
|
/**
|
|
1051
1064
|
* Conversation this loop drives. Scopes the loop-held compaction circuit
|
|
1052
1065
|
* breaker and is the source of truth the loop's pipeline contexts and
|
|
@@ -1087,6 +1100,7 @@ export class AgentLoop {
|
|
|
1087
1100
|
private config: AgentLoopConfig;
|
|
1088
1101
|
private tools: ToolDefinition[];
|
|
1089
1102
|
private resolveTools: ((history: Message[]) => ToolDefinition[]) | null;
|
|
1103
|
+
private onToolsSent: ((tools: ToolDefinition[]) => void) | null;
|
|
1090
1104
|
private toolExecutor: LoopToolExecutor | null;
|
|
1091
1105
|
|
|
1092
1106
|
/**
|
|
@@ -1122,6 +1136,7 @@ export class AgentLoop {
|
|
|
1122
1136
|
tools,
|
|
1123
1137
|
toolExecutor,
|
|
1124
1138
|
resolveTools,
|
|
1139
|
+
onToolsSent,
|
|
1125
1140
|
conversationId,
|
|
1126
1141
|
resolveConversationDir,
|
|
1127
1142
|
transformCompactedHistory,
|
|
@@ -1131,6 +1146,7 @@ export class AgentLoop {
|
|
|
1131
1146
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1132
1147
|
this.tools = tools ?? [];
|
|
1133
1148
|
this.resolveTools = resolveTools ?? null;
|
|
1149
|
+
this.onToolsSent = onToolsSent ?? null;
|
|
1134
1150
|
this.toolExecutor = toolExecutor ?? null;
|
|
1135
1151
|
this.conversationId = conversationId;
|
|
1136
1152
|
this.resolveConversationDir = resolveConversationDir ?? null;
|
|
@@ -1544,7 +1560,7 @@ export class AgentLoop {
|
|
|
1544
1560
|
turn: number,
|
|
1545
1561
|
): Promise<{
|
|
1546
1562
|
resultBlocks: ContentBlock[];
|
|
1547
|
-
additionalContextBlocks:
|
|
1563
|
+
additionalContextBlocks: TextContent[];
|
|
1548
1564
|
}> => {
|
|
1549
1565
|
if (conversationDir) {
|
|
1550
1566
|
const toolCallByUseId = new Map(
|
|
@@ -1569,7 +1585,7 @@ export class AgentLoop {
|
|
|
1569
1585
|
180_000;
|
|
1570
1586
|
|
|
1571
1587
|
const resultBlocks: ContentBlock[] = [];
|
|
1572
|
-
const additionalContextBlocks:
|
|
1588
|
+
const additionalContextBlocks: TextContent[] = [];
|
|
1573
1589
|
for (const block of rawBlocks) {
|
|
1574
1590
|
if (block.type !== "tool_result") {
|
|
1575
1591
|
resultBlocks.push(block);
|
|
@@ -2241,6 +2257,11 @@ export class AgentLoop {
|
|
|
2241
2257
|
// Latency: the request is about to leave for the provider. The span
|
|
2242
2258
|
// from here to the first streamed token is time-to-first-token.
|
|
2243
2259
|
latencyTracker?.mark("request_sent");
|
|
2260
|
+
// Every await that could cancel the call is behind us; a run aborted
|
|
2261
|
+
// during the throttle or a hook never reports an array it did not send.
|
|
2262
|
+
if (!signal?.aborted) {
|
|
2263
|
+
this.onToolsSent?.(currentTools);
|
|
2264
|
+
}
|
|
2244
2265
|
let response: ProviderResponse;
|
|
2245
2266
|
try {
|
|
2246
2267
|
response = await traceAsyncSection("agent-loop:provider-send", () =>
|
|
@@ -2945,16 +2966,20 @@ export class AgentLoop {
|
|
|
2945
2966
|
|
|
2946
2967
|
toolUseTurns++;
|
|
2947
2968
|
|
|
2948
|
-
//
|
|
2949
|
-
// `additionalContext` (e.g. tool-error retry coaching)
|
|
2950
|
-
//
|
|
2951
|
-
//
|
|
2952
|
-
//
|
|
2953
|
-
// tool's actual result.
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2969
|
+
// Add the tool results, plus any guidance a post-tool-use hook
|
|
2970
|
+
// surfaced via `additionalContext` (e.g. tool-error retry coaching),
|
|
2971
|
+
// as a user message and continue the loop. The guidance joins the
|
|
2972
|
+
// provider-bound history only: it was not part of the tool_result
|
|
2973
|
+
// events emitted above, so the client-facing and persisted tool
|
|
2974
|
+
// output stay the tool's actual result.
|
|
2975
|
+
history.push({
|
|
2976
|
+
role: "user",
|
|
2977
|
+
content: buildToolResultFollowUp(
|
|
2978
|
+
history,
|
|
2979
|
+
resultBlocks,
|
|
2980
|
+
additionalContextBlocks,
|
|
2981
|
+
),
|
|
2982
|
+
});
|
|
2958
2983
|
|
|
2959
2984
|
// Invoke checkpoint callback after tool results are in history.
|
|
2960
2985
|
// Handoff takes precedence over the budget gate: a handoff decision
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Content of the user message that answers a tool-calling assistant turn:
|
|
2
|
+
// the tool_result blocks plus any guidance a post-tool-use hook surfaced via
|
|
3
|
+
// `additionalContext`.
|
|
4
|
+
|
|
5
|
+
import { analyzeServerToolPairing } from "../providers/server-tool-pairing.js";
|
|
6
|
+
import type {
|
|
7
|
+
ContentBlock,
|
|
8
|
+
Message,
|
|
9
|
+
TextContent,
|
|
10
|
+
ToolResultContent,
|
|
11
|
+
} from "../providers/types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook guidance rides as separate blocks after the results, so the model sees
|
|
15
|
+
* it while each tool_result stays the tool's actual output. The exception is
|
|
16
|
+
* an assistant turn that left a server tool (native web search) deferred: the
|
|
17
|
+
* provider runs a deferred server tool only when the answering message holds
|
|
18
|
+
* tool_result blocks alone, and any block after the results closes the
|
|
19
|
+
* assistant turn, after which the provider rejects the unanswered server tool
|
|
20
|
+
* as unpaired. The guidance is then folded into the content of the last
|
|
21
|
+
* errored tool_result (the last tool_result when none errored), where the
|
|
22
|
+
* model still sees it and the search still runs.
|
|
23
|
+
*/
|
|
24
|
+
export function buildToolResultFollowUp(
|
|
25
|
+
history: ReadonlyArray<Message>,
|
|
26
|
+
resultBlocks: ContentBlock[],
|
|
27
|
+
additionalContextBlocks: ReadonlyArray<TextContent>,
|
|
28
|
+
): ContentBlock[] {
|
|
29
|
+
if (additionalContextBlocks.length === 0) {
|
|
30
|
+
return resultBlocks;
|
|
31
|
+
}
|
|
32
|
+
const targetIndex = foldTargetIndex(resultBlocks);
|
|
33
|
+
const pairing = analyzeServerToolPairing([
|
|
34
|
+
...history,
|
|
35
|
+
{ role: "user", content: resultBlocks },
|
|
36
|
+
]);
|
|
37
|
+
if (pairing.deferredUseIds.size === 0 || targetIndex < 0) {
|
|
38
|
+
return [...resultBlocks, ...additionalContextBlocks];
|
|
39
|
+
}
|
|
40
|
+
const target = resultBlocks[targetIndex] as ToolResultContent;
|
|
41
|
+
const folded: ToolResultContent = {
|
|
42
|
+
...target,
|
|
43
|
+
content: [target.content, ...additionalContextBlocks.map((b) => b.text)]
|
|
44
|
+
.filter((text) => text.length > 0)
|
|
45
|
+
.join("\n\n"),
|
|
46
|
+
};
|
|
47
|
+
return resultBlocks.map((block, index) =>
|
|
48
|
+
index === targetIndex ? folded : block,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Index of the last errored tool_result, else the last tool_result, else -1. */
|
|
53
|
+
function foldTargetIndex(blocks: ReadonlyArray<ContentBlock>): number {
|
|
54
|
+
let lastResult = -1;
|
|
55
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
56
|
+
const block = blocks[index];
|
|
57
|
+
// guard:allow-tool-result-only: the answering user message carries client
|
|
58
|
+
// tool_result blocks only; server-side results live in assistant messages.
|
|
59
|
+
if (block.type !== "tool_result") {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (block.is_error) {
|
|
63
|
+
return index;
|
|
64
|
+
}
|
|
65
|
+
if (lastResult < 0) {
|
|
66
|
+
lastResult = index;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return lastResult;
|
|
70
|
+
}
|