@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
|
@@ -1977,6 +1977,130 @@ describe("AgentLoop", () => {
|
|
|
1977
1977
|
expect(calls[0].tools).not.toEqual(dummyTools);
|
|
1978
1978
|
});
|
|
1979
1979
|
|
|
1980
|
+
test("onToolsSent observes the exact tool array of every provider call", async () => {
|
|
1981
|
+
const perCall: ToolDefinition[][] = [
|
|
1982
|
+
[
|
|
1983
|
+
{
|
|
1984
|
+
name: "first",
|
|
1985
|
+
description: "First",
|
|
1986
|
+
input_schema: { type: "object" },
|
|
1987
|
+
},
|
|
1988
|
+
],
|
|
1989
|
+
[
|
|
1990
|
+
{
|
|
1991
|
+
name: "second",
|
|
1992
|
+
description: "Second",
|
|
1993
|
+
input_schema: { type: "object" },
|
|
1994
|
+
},
|
|
1995
|
+
],
|
|
1996
|
+
];
|
|
1997
|
+
let resolveCount = 0;
|
|
1998
|
+
const sent: ToolDefinition[][] = [];
|
|
1999
|
+
|
|
2000
|
+
const { provider, calls } = createMockProvider([
|
|
2001
|
+
toolUseResponse("t1", "first", {}),
|
|
2002
|
+
textResponse("Done"),
|
|
2003
|
+
]);
|
|
2004
|
+
const loop = new AgentLoop({
|
|
2005
|
+
provider: provider,
|
|
2006
|
+
systemPrompt: "system",
|
|
2007
|
+
conversationId: "test-conversation",
|
|
2008
|
+
toolExecutor: async () => ({ content: "result", isError: false }),
|
|
2009
|
+
resolveTools: () => perCall[resolveCount++]!,
|
|
2010
|
+
onToolsSent: (tools) => {
|
|
2011
|
+
sent.push(tools);
|
|
2012
|
+
},
|
|
2013
|
+
});
|
|
2014
|
+
await loop.run({
|
|
2015
|
+
requestId: "test-request",
|
|
2016
|
+
messages: [userMessage],
|
|
2017
|
+
onEvent: () => {},
|
|
2018
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
// One observation per provider call, each the array that call sent.
|
|
2022
|
+
expect(sent).toHaveLength(2);
|
|
2023
|
+
expect(calls[0].tools).toEqual(sent[0]);
|
|
2024
|
+
expect(calls[1].tools).toEqual(sent[1]);
|
|
2025
|
+
expect(sent.map((tools) => tools.map((t) => t.name))).toEqual([
|
|
2026
|
+
["first"],
|
|
2027
|
+
["second"],
|
|
2028
|
+
]);
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
test("onToolsSent sees the provider-native web_search tool the loop appends", async () => {
|
|
2032
|
+
const dynamicTools: ToolDefinition[] = [
|
|
2033
|
+
{
|
|
2034
|
+
name: "dynamic_tool",
|
|
2035
|
+
description: "Dynamic",
|
|
2036
|
+
input_schema: { type: "object" },
|
|
2037
|
+
},
|
|
2038
|
+
];
|
|
2039
|
+
const sent: ToolDefinition[][] = [];
|
|
2040
|
+
|
|
2041
|
+
const { provider, calls } = createMockProvider([textResponse("Hi")]);
|
|
2042
|
+
Object.assign(provider, { supportsNativeWebSearch: true });
|
|
2043
|
+
const loop = new AgentLoop({
|
|
2044
|
+
provider: provider,
|
|
2045
|
+
systemPrompt: "system",
|
|
2046
|
+
conversationId: "test-conversation",
|
|
2047
|
+
config: { enableNativeWebSearch: true },
|
|
2048
|
+
resolveTools: () => dynamicTools,
|
|
2049
|
+
onToolsSent: (tools) => {
|
|
2050
|
+
sent.push(tools);
|
|
2051
|
+
},
|
|
2052
|
+
});
|
|
2053
|
+
await loop.run({
|
|
2054
|
+
requestId: "test-request",
|
|
2055
|
+
messages: [userMessage],
|
|
2056
|
+
onEvent: () => {},
|
|
2057
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
// The resolver never saw web_search; the observer sees the wire array.
|
|
2061
|
+
expect(sent).toHaveLength(1);
|
|
2062
|
+
expect(sent[0]!.map((t) => t.name)).toEqual(["dynamic_tool", "web_search"]);
|
|
2063
|
+
expect(calls[0].tools).toEqual(sent[0]);
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
test("onToolsSent does not fire for a call cancelled before the request leaves", async () => {
|
|
2067
|
+
// A pre-model-call hook runs after tool resolution and before the send,
|
|
2068
|
+
// so aborting there is a call whose tools are resolved but never sent.
|
|
2069
|
+
const controller = new AbortController();
|
|
2070
|
+
registerPlugin({
|
|
2071
|
+
manifest: { name: "abort-before-send", version: "0.0.1" },
|
|
2072
|
+
hooks: {
|
|
2073
|
+
"pre-model-call": async (ctx) => {
|
|
2074
|
+
controller.abort();
|
|
2075
|
+
return ctx;
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
});
|
|
2079
|
+
const sent: ToolDefinition[][] = [];
|
|
2080
|
+
const { provider } = createMockProvider([textResponse("never")]);
|
|
2081
|
+
const loop = new AgentLoop({
|
|
2082
|
+
provider: provider,
|
|
2083
|
+
systemPrompt: "system",
|
|
2084
|
+
conversationId: "test-conversation",
|
|
2085
|
+
resolveTools: () => dummyTools,
|
|
2086
|
+
onToolsSent: (tools) => {
|
|
2087
|
+
sent.push(tools);
|
|
2088
|
+
},
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
await loop
|
|
2092
|
+
.run({
|
|
2093
|
+
requestId: "test-request",
|
|
2094
|
+
messages: [userMessage],
|
|
2095
|
+
onEvent: () => {},
|
|
2096
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
2097
|
+
signal: controller.signal,
|
|
2098
|
+
})
|
|
2099
|
+
.catch(() => {});
|
|
2100
|
+
|
|
2101
|
+
expect(sent).toEqual([]);
|
|
2102
|
+
});
|
|
2103
|
+
|
|
1980
2104
|
// 28. Tool list can change between turns
|
|
1981
2105
|
test("resolveTools can return different tools on each turn", async () => {
|
|
1982
2106
|
const toolsPerTurn: ToolDefinition[][] = [
|
|
@@ -2426,6 +2550,141 @@ describe("AgentLoop", () => {
|
|
|
2426
2550
|
expect(noRetryNudge).toBeUndefined();
|
|
2427
2551
|
});
|
|
2428
2552
|
|
|
2553
|
+
// A native web search left deferred by the same assistant turn (a
|
|
2554
|
+
// server_tool_use with no result, alongside the client tool call) runs on
|
|
2555
|
+
// the next request only if that request's tool-result message holds
|
|
2556
|
+
// tool_result blocks alone. The coaching then rides inside the errored
|
|
2557
|
+
// tool_result instead of as a trailing text block.
|
|
2558
|
+
test("folds retry coaching into the tool_result when the assistant turn left a server tool deferred", async () => {
|
|
2559
|
+
const mixedTurn: ProviderResponse = {
|
|
2560
|
+
content: [
|
|
2561
|
+
{
|
|
2562
|
+
type: "tool_use",
|
|
2563
|
+
id: "t1",
|
|
2564
|
+
name: "read_file",
|
|
2565
|
+
input: { path: "/missing.txt" },
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
type: "server_tool_use",
|
|
2569
|
+
id: "srvtoolu_1",
|
|
2570
|
+
name: "web_search",
|
|
2571
|
+
input: { query: "news" },
|
|
2572
|
+
},
|
|
2573
|
+
],
|
|
2574
|
+
model: "mock-model",
|
|
2575
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
2576
|
+
stopReason: "tool_use",
|
|
2577
|
+
};
|
|
2578
|
+
const { provider, calls } = createMockProvider([
|
|
2579
|
+
mixedTurn,
|
|
2580
|
+
textResponse("Reported the missing file."),
|
|
2581
|
+
]);
|
|
2582
|
+
const toolExecutor = async () => ({
|
|
2583
|
+
content: "Error: HTTP 404",
|
|
2584
|
+
isError: true,
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2587
|
+
const loop = new AgentLoop({
|
|
2588
|
+
provider,
|
|
2589
|
+
systemPrompt: "system",
|
|
2590
|
+
conversationId: "test-conversation",
|
|
2591
|
+
tools: dummyTools,
|
|
2592
|
+
toolExecutor,
|
|
2593
|
+
});
|
|
2594
|
+
await loop.run({
|
|
2595
|
+
requestId: "test-request",
|
|
2596
|
+
messages: [userMessage],
|
|
2597
|
+
onEvent: () => {},
|
|
2598
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
2599
|
+
});
|
|
2600
|
+
|
|
2601
|
+
expect(calls).toHaveLength(2);
|
|
2602
|
+
const followUpMessages = calls[1].messages;
|
|
2603
|
+
const followUp = followUpMessages[followUpMessages.length - 1];
|
|
2604
|
+
expect(followUp.role).toBe("user");
|
|
2605
|
+
expect(followUp.content.map((b) => b.type)).toEqual(["tool_result"]);
|
|
2606
|
+
const erroredToolResult = followUp.content[0] as Extract<
|
|
2607
|
+
ContentBlock,
|
|
2608
|
+
{ type: "tool_result" }
|
|
2609
|
+
>;
|
|
2610
|
+
expect(erroredToolResult.is_error).toBe(true);
|
|
2611
|
+
expect(erroredToolResult.content).toContain("Error: HTTP 404");
|
|
2612
|
+
expect(erroredToolResult.content).toContain("looks recoverable");
|
|
2613
|
+
});
|
|
2614
|
+
|
|
2615
|
+
// A deferred search answered by a message with text after the results
|
|
2616
|
+
// (history assembled elsewhere) is rejected by the provider as unpaired.
|
|
2617
|
+
// The ordering-repair retry stamps the synthetic result rather than
|
|
2618
|
+
// re-sending the same history.
|
|
2619
|
+
test("repairs a deferred search rejected as unpaired instead of re-sending the same history", async () => {
|
|
2620
|
+
const mixedTurn: Message = {
|
|
2621
|
+
role: "assistant",
|
|
2622
|
+
content: [
|
|
2623
|
+
{
|
|
2624
|
+
type: "tool_use",
|
|
2625
|
+
id: "t1",
|
|
2626
|
+
name: "read_file",
|
|
2627
|
+
input: { path: "/a" },
|
|
2628
|
+
},
|
|
2629
|
+
{
|
|
2630
|
+
type: "server_tool_use",
|
|
2631
|
+
id: "srvtoolu_1",
|
|
2632
|
+
name: "web_search",
|
|
2633
|
+
input: { query: "news" },
|
|
2634
|
+
},
|
|
2635
|
+
],
|
|
2636
|
+
};
|
|
2637
|
+
const answeredWithText: Message = {
|
|
2638
|
+
role: "user",
|
|
2639
|
+
content: [
|
|
2640
|
+
{
|
|
2641
|
+
type: "tool_result",
|
|
2642
|
+
tool_use_id: "t1",
|
|
2643
|
+
content: "Error: HTTP 404",
|
|
2644
|
+
is_error: true,
|
|
2645
|
+
},
|
|
2646
|
+
{ type: "text", text: "<system_notice>retry</system_notice>" },
|
|
2647
|
+
],
|
|
2648
|
+
};
|
|
2649
|
+
const { provider, calls } = createMockProvider([
|
|
2650
|
+
new Error(
|
|
2651
|
+
"Anthropic API error (400): messages.1: `web_search` tool use with id `srvtoolu_1` was found without a corresponding `web_search_tool_result` block",
|
|
2652
|
+
),
|
|
2653
|
+
textResponse("recovered"),
|
|
2654
|
+
]);
|
|
2655
|
+
const loop = new AgentLoop({
|
|
2656
|
+
provider,
|
|
2657
|
+
systemPrompt: "system",
|
|
2658
|
+
conversationId: "test-conversation",
|
|
2659
|
+
});
|
|
2660
|
+
const events: AgentEvent[] = [];
|
|
2661
|
+
|
|
2662
|
+
const { history } = await loop.run({
|
|
2663
|
+
requestId: "test-request",
|
|
2664
|
+
messages: [userMessage, mixedTurn, answeredWithText],
|
|
2665
|
+
onEvent: collectEvents(events),
|
|
2666
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
2667
|
+
});
|
|
2668
|
+
|
|
2669
|
+
expect(calls).toHaveLength(2);
|
|
2670
|
+
expect(events.filter((e) => e.type === "error")).toHaveLength(0);
|
|
2671
|
+
// Deep repair stamped the synthetic result on the orphaned search, and the
|
|
2672
|
+
// outbound sanitizer then summarized the answered pair as text, so the
|
|
2673
|
+
// retry carries no unanswered server_tool_use.
|
|
2674
|
+
expect(calls[1].messages[1].content.map((b) => b.type)).toEqual([
|
|
2675
|
+
"tool_use",
|
|
2676
|
+
"text",
|
|
2677
|
+
]);
|
|
2678
|
+
expect(calls[1].messages[1].content[1]).toMatchObject({
|
|
2679
|
+
type: "text",
|
|
2680
|
+
text: expect.stringContaining("results unavailable"),
|
|
2681
|
+
});
|
|
2682
|
+
expect(history[history.length - 1]).toEqual({
|
|
2683
|
+
role: "assistant",
|
|
2684
|
+
content: [{ type: "text", text: "recovered" }],
|
|
2685
|
+
});
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2429
2688
|
// Retry coaching stops after a tool fails 3 times in a row — past that the
|
|
2430
2689
|
// error is likely unrecoverable and further coaching only burns tokens.
|
|
2431
2690
|
test("stops appending retry coaching after 3 consecutive failures of a tool", async () => {
|
|
@@ -74,6 +74,7 @@ function makeTarget(onRun: (conv: Conversation) => void): {
|
|
|
74
74
|
subagentAllowedTools: undefined as ReadonlySet<string> | undefined,
|
|
75
75
|
subagentToolGateMode: undefined as string | undefined,
|
|
76
76
|
toolContextPin: undefined,
|
|
77
|
+
delegateIndependentTasksReplay: undefined as boolean | undefined,
|
|
77
78
|
preactivatedSkillIds: undefined as readonly string[] | undefined,
|
|
78
79
|
setSubagentAllowedTools: (tools?: ReadonlySet<string>) => {
|
|
79
80
|
target.subagentAllowedTools = tools;
|
|
@@ -82,9 +83,12 @@ function makeTarget(onRun: (conv: Conversation) => void): {
|
|
|
82
83
|
target.preactivatedSkillIds = ids;
|
|
83
84
|
},
|
|
84
85
|
// The section's gate, rendered as a marker so the assertions turn on the
|
|
85
|
-
// real predicate rather than on a stubbed answer.
|
|
86
|
+
// real predicate rather than on a stubbed answer. Mirrors
|
|
87
|
+
// `Conversation.buildCurrentSystemPrompt`: a replayed rendered state wins
|
|
88
|
+
// over the derivation.
|
|
86
89
|
buildCurrentSystemPrompt: () =>
|
|
87
|
-
|
|
90
|
+
(target.delegateIndependentTasksReplay ??
|
|
91
|
+
canSpawnSubagentsForTurn(target as unknown as Conversation))
|
|
88
92
|
? `base ${DELEGATION_SECTION}`
|
|
89
93
|
: "base",
|
|
90
94
|
// Mirrors `Conversation.syncLoopSystemPrompt`.
|
|
@@ -156,6 +160,64 @@ describe("the delegation section on a direct wake", () => {
|
|
|
156
160
|
expect(promptDuringRun).toContain(DELEGATION_SECTION);
|
|
157
161
|
});
|
|
158
162
|
|
|
163
|
+
test("a replaying wake renders the section its source rendered", async () => {
|
|
164
|
+
let promptDuringRun = "";
|
|
165
|
+
const { target, loopPrompt } = makeTarget(() => {
|
|
166
|
+
promptDuringRun = loopPrompt();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await wakeAgentForOpportunity(
|
|
170
|
+
{
|
|
171
|
+
conversationId: target.conversationId,
|
|
172
|
+
hint: "test hint",
|
|
173
|
+
source: "scheduler",
|
|
174
|
+
// The memory retrospective's shape: an allowlist with no spawn path,
|
|
175
|
+
// gated at execution...
|
|
176
|
+
allowedTools: ["remember", "skill_load"],
|
|
177
|
+
toolGateMode: "execution",
|
|
178
|
+
// ...replaying an interactive source whose live turn rendered the
|
|
179
|
+
// section.
|
|
180
|
+
wireToolDefinitions: [
|
|
181
|
+
{ name: "remember", description: "Save", input_schema: {} },
|
|
182
|
+
],
|
|
183
|
+
delegateIndependentTasks: true,
|
|
184
|
+
},
|
|
185
|
+
{ resolveTarget: async () => target },
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(promptDuringRun).toContain(DELEGATION_SECTION);
|
|
189
|
+
// The replay comes off with the rest of the wake's scope.
|
|
190
|
+
expect(target.delegateIndependentTasksReplay).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("a replaying wake renders the section off when its source did", async () => {
|
|
194
|
+
// A channel-delivered source records the section off. Replaying that
|
|
195
|
+
// keeps the fork's prompt in parity even though this wake's own scope
|
|
196
|
+
// could spawn.
|
|
197
|
+
let promptDuringRun = "";
|
|
198
|
+
const { target, loopPrompt } = makeTarget(() => {
|
|
199
|
+
promptDuringRun = loopPrompt();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await wakeAgentForOpportunity(
|
|
203
|
+
{
|
|
204
|
+
conversationId: target.conversationId,
|
|
205
|
+
hint: "test hint",
|
|
206
|
+
source: "scheduler",
|
|
207
|
+
allowedTools: ["skill_load", "skill_execute", "subagent_spawn"],
|
|
208
|
+
toolGateMode: "execution",
|
|
209
|
+
wireToolDefinitions: [
|
|
210
|
+
{ name: "skill_load", description: "Load", input_schema: {} },
|
|
211
|
+
],
|
|
212
|
+
delegateIndependentTasks: false,
|
|
213
|
+
},
|
|
214
|
+
{ resolveTarget: async () => target },
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(promptDuringRun).not.toContain(DELEGATION_SECTION);
|
|
218
|
+
expect(promptDuringRun.length).toBeGreaterThan(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
159
221
|
test("the restored prompt is built without the wake's persona", async () => {
|
|
160
222
|
// `wakePersonaOverride` feeds `buildCurrentSystemPrompt`, so a rebuild
|
|
161
223
|
// that ran before the clear would leave the loop holding the wake's
|
|
@@ -1650,6 +1650,61 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
1650
1650
|
).toHaveLength(0);
|
|
1651
1651
|
});
|
|
1652
1652
|
|
|
1653
|
+
test("mixed tail answered by a tool_result plus trailing text gets a synthetic web_search_tool_result", async () => {
|
|
1654
|
+
// Text after the client result closes the assistant turn on the provider
|
|
1655
|
+
// side, which then rejects the unanswered search as unpaired. The search
|
|
1656
|
+
// is an orphan here, so the synthetic error result keeps the request
|
|
1657
|
+
// valid at the cost of that one search.
|
|
1658
|
+
const messages: Message[] = [
|
|
1659
|
+
userMsg("Do things"),
|
|
1660
|
+
{
|
|
1661
|
+
role: "assistant",
|
|
1662
|
+
content: [
|
|
1663
|
+
{ type: "tool_use", id: "tu_a", name: "web_fetch", input: {} },
|
|
1664
|
+
{
|
|
1665
|
+
type: "server_tool_use",
|
|
1666
|
+
id: "srvtoolu_b",
|
|
1667
|
+
name: "web_search",
|
|
1668
|
+
input: { query: "test" },
|
|
1669
|
+
},
|
|
1670
|
+
],
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
role: "user",
|
|
1674
|
+
content: [
|
|
1675
|
+
{
|
|
1676
|
+
type: "tool_result",
|
|
1677
|
+
tool_use_id: "tu_a",
|
|
1678
|
+
content: "Error: HTTP 404",
|
|
1679
|
+
is_error: true,
|
|
1680
|
+
},
|
|
1681
|
+
{ type: "text", text: "<system_notice>retry</system_notice>" },
|
|
1682
|
+
],
|
|
1683
|
+
},
|
|
1684
|
+
];
|
|
1685
|
+
await provider.sendMessage(messages);
|
|
1686
|
+
|
|
1687
|
+
const sent = lastStreamParams!.messages as Array<{
|
|
1688
|
+
role: string;
|
|
1689
|
+
content: Array<{ type: string; tool_use_id?: string }>;
|
|
1690
|
+
}>;
|
|
1691
|
+
|
|
1692
|
+
// The repaired pair leaves the mixed message: ensureToolPairing keeps the
|
|
1693
|
+
// client tool_result adjacent to its tool_use and moves the now-answered
|
|
1694
|
+
// server pair into its own assistant turn, ahead of the trailing text.
|
|
1695
|
+
expect(sent.map((m) => m.content.map((b) => b.type))).toEqual([
|
|
1696
|
+
["text"],
|
|
1697
|
+
["tool_use"],
|
|
1698
|
+
["tool_result"],
|
|
1699
|
+
["server_tool_use", "web_search_tool_result"],
|
|
1700
|
+
["text"],
|
|
1701
|
+
]);
|
|
1702
|
+
expect(sent[3].content[1]).toMatchObject({
|
|
1703
|
+
type: "web_search_tool_result",
|
|
1704
|
+
tool_use_id: "srvtoolu_b",
|
|
1705
|
+
});
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1653
1708
|
test("deferred mixed heartbeat shape with text and multiple searches goes out verbatim", async () => {
|
|
1654
1709
|
const messages: Message[] = [
|
|
1655
1710
|
userMsg("Heartbeat: check the file and the news"),
|
|
@@ -149,6 +149,46 @@ describe("approval interception trust-class gates", () => {
|
|
|
149
149
|
expect(sessionMock).toHaveBeenCalled();
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
+
test("guardian apr: callback naming no approval action is consumed as a stale button, never routed as text", async () => {
|
|
153
|
+
_anchorPrincipalId = "guardian-principal-1";
|
|
154
|
+
const sessionMock = registerPendingInteraction(
|
|
155
|
+
"req-guardian-retired-1",
|
|
156
|
+
CONVERSATION_ID,
|
|
157
|
+
TOOL_NAME,
|
|
158
|
+
TOOL_INPUT,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// The channel normalizers copy callback data into the message content, so
|
|
162
|
+
// a button press arrives with both set to the same string.
|
|
163
|
+
const callbackData = "apr:req-guardian-retired-1:approve_always";
|
|
164
|
+
const result = await handleApprovalInterception({
|
|
165
|
+
conversationId: CONVERSATION_ID,
|
|
166
|
+
callbackData,
|
|
167
|
+
content: callbackData,
|
|
168
|
+
conversationExternalId: REQUESTER_CHAT,
|
|
169
|
+
sourceChannel: "telegram",
|
|
170
|
+
actorExternalId: "guardian-user-1",
|
|
171
|
+
replyCallbackUrl: "https://gateway.test/deliver",
|
|
172
|
+
trustCtx: {
|
|
173
|
+
sourceChannel: "telegram",
|
|
174
|
+
trustClass: "guardian",
|
|
175
|
+
requesterExternalUserId: "guardian-user-1",
|
|
176
|
+
guardianExternalUserId: "guardian-user-1",
|
|
177
|
+
guardianPrincipalId: "guardian-principal-1",
|
|
178
|
+
},
|
|
179
|
+
assistantId: ASSISTANT_ID,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(result.handled).toBe(true);
|
|
183
|
+
expect(result.type).toBe("stale_ignored");
|
|
184
|
+
// No decision, and no reply: the text parsers never saw the callback.
|
|
185
|
+
expect(sessionMock).not.toHaveBeenCalled();
|
|
186
|
+
expect(deliverSpy).not.toHaveBeenCalled();
|
|
187
|
+
expect(pendingInteractions.getByConversation(CONVERSATION_ID)).toHaveLength(
|
|
188
|
+
1,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
152
192
|
test("guardian apr: callback with a principal NOT matching the anchor is rejected before any decision", async () => {
|
|
153
193
|
_anchorPrincipalId = "the-real-guardian-principal";
|
|
154
194
|
const sessionMock = registerPendingInteraction(
|
|
@@ -478,9 +478,9 @@ describe("createInlineAttachment (workspace_ref persistence)", () => {
|
|
|
478
478
|
expect(filePath).toBeTruthy();
|
|
479
479
|
expect(result.filename).toBeDefined();
|
|
480
480
|
expect(filePath!.endsWith(result.filename!)).toBe(true);
|
|
481
|
-
expect(
|
|
482
|
-
|
|
483
|
-
);
|
|
481
|
+
expect(
|
|
482
|
+
result.filename!.startsWith(`${OVERSIZED_CONTENT_FILENAME_PREFIX}-`),
|
|
483
|
+
).toBe(true);
|
|
484
484
|
expect(readFileSync(filePath!).toString("utf8")).toBe(original);
|
|
485
485
|
});
|
|
486
486
|
});
|
|
@@ -598,6 +598,25 @@ describe("linkAttachmentToMessage + getAttachmentsForMessage", () => {
|
|
|
598
598
|
expect(linked[1].originalFilename).toBe("second.txt");
|
|
599
599
|
});
|
|
600
600
|
|
|
601
|
+
test("reuses a repeated message attachment link with its original position", async () => {
|
|
602
|
+
const conv = createConversation();
|
|
603
|
+
const msg = await addMessage(conv.id, "assistant", "One file");
|
|
604
|
+
const stored = await uploadAttachment("frame.png", "image/png", "AAAA");
|
|
605
|
+
|
|
606
|
+
expect(linkAttachmentToMessage(msg.id, stored.id, 4)).toBe(stored.id);
|
|
607
|
+
expect(linkAttachmentToMessage(msg.id, stored.id, 0)).toBe(stored.id);
|
|
608
|
+
|
|
609
|
+
const links = rawGet<{ count: number; position: number }>(
|
|
610
|
+
"test:repeatedAttachmentLink",
|
|
611
|
+
`SELECT COUNT(*) AS count, MIN(position) AS position
|
|
612
|
+
FROM message_attachments
|
|
613
|
+
WHERE message_id = ? AND attachment_id = ?`,
|
|
614
|
+
msg.id,
|
|
615
|
+
stored.id,
|
|
616
|
+
);
|
|
617
|
+
expect(links).toEqual({ count: 1, position: 4 });
|
|
618
|
+
});
|
|
619
|
+
|
|
601
620
|
test("returns empty for message with no attachments", async () => {
|
|
602
621
|
const conv = createConversation();
|
|
603
622
|
const msg = await addMessage(conv.id, "assistant", "No attachments");
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import type { ApprovalAction } from "../runtime/channel-approval-types.js";
|
|
4
3
|
import { parseCallbackData } from "../runtime/routes/channel-route-shared.js";
|
|
5
4
|
|
|
6
5
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -19,19 +18,15 @@ describe("parseCallbackData", () => {
|
|
|
19
18
|
expect(result!.source).toBe("button");
|
|
20
19
|
});
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
expect(result!.action).toBe(expectedAction as ApprovalAction);
|
|
32
|
-
expect(result!.requestId).toBe("req-123");
|
|
33
|
-
},
|
|
34
|
-
);
|
|
21
|
+
// The parser never maps one action id onto another: an id outside the
|
|
22
|
+
// approval vocabulary is no action, whatever it resembles.
|
|
23
|
+
test.each([
|
|
24
|
+
"apr:req-123:approve_10m",
|
|
25
|
+
"apr:req-123:approve_conversation",
|
|
26
|
+
"apr:req-123:approve_always",
|
|
27
|
+
])('returns null for the unrecognized action id in "%s"', (data) => {
|
|
28
|
+
expect(parseCallbackData(data)).toBeNull();
|
|
29
|
+
});
|
|
35
30
|
|
|
36
31
|
test("every channel's button press attributes as the button modality", () => {
|
|
37
32
|
for (const channel of ["slack", "telegram", "whatsapp", "discord"]) {
|
|
@@ -588,6 +588,62 @@ describe("channel-reply-delivery", () => {
|
|
|
588
588
|
expect(deliveryCalls[0].payload.text).toBe("Fallback text");
|
|
589
589
|
});
|
|
590
590
|
|
|
591
|
+
it("delivers a screenshot-only reply attachment", async () => {
|
|
592
|
+
const screenshot: RuntimeAttachmentMetadata = {
|
|
593
|
+
id: "final-screenshot",
|
|
594
|
+
filename: "computer-use-click.png",
|
|
595
|
+
mimeType: "image/png",
|
|
596
|
+
sizeBytes: 10,
|
|
597
|
+
kind: "image",
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
await deliverRenderedReplyViaCallback({
|
|
601
|
+
callbackUrl: "http://gateway/deliver/telegram",
|
|
602
|
+
chatId: "chat-screenshot",
|
|
603
|
+
textSegments: [],
|
|
604
|
+
fallbackText: "",
|
|
605
|
+
attachments: [screenshot],
|
|
606
|
+
interSegmentDelayMs: 0,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
expect(deliveryCalls).toHaveLength(1);
|
|
610
|
+
expect(deliveryCalls[0].payload).toMatchObject({
|
|
611
|
+
chatId: "chat-screenshot",
|
|
612
|
+
attachments: [screenshot],
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it("delivers the final screenshot with an unrelated PDF", async () => {
|
|
617
|
+
const attachments: RuntimeAttachmentMetadata[] = [
|
|
618
|
+
{
|
|
619
|
+
id: "final-screenshot",
|
|
620
|
+
filename: "computer-use-click.png",
|
|
621
|
+
mimeType: "image/png",
|
|
622
|
+
sizeBytes: 10,
|
|
623
|
+
kind: "image",
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
id: "explicit-report",
|
|
627
|
+
filename: "report.pdf",
|
|
628
|
+
mimeType: "application/pdf",
|
|
629
|
+
sizeBytes: 20,
|
|
630
|
+
kind: "document",
|
|
631
|
+
},
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
await deliverRenderedReplyViaCallback({
|
|
635
|
+
callbackUrl: "http://gateway/deliver/slack",
|
|
636
|
+
chatId: "chat-screenshot-pdf",
|
|
637
|
+
textSegments: ["Done."],
|
|
638
|
+
fallbackText: "Done.",
|
|
639
|
+
attachments,
|
|
640
|
+
interSegmentDelayMs: 0,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(deliveryCalls).toHaveLength(1);
|
|
644
|
+
expect(deliveryCalls[0].payload.attachments).toEqual(attachments);
|
|
645
|
+
});
|
|
646
|
+
|
|
591
647
|
it("uses rendered textSegments (tool boundaries) when delivering from conversation history", async () => {
|
|
592
648
|
conversationMessages.push(
|
|
593
649
|
{ id: "msg-user", role: "user", content: "hi" },
|
|
@@ -204,6 +204,22 @@ describe("collectRevealRefsFromCommand", () => {
|
|
|
204
204
|
);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
test("parses a legacy service/field path", () => {
|
|
208
|
+
expect(
|
|
209
|
+
collectRevealRefsFromCommand(
|
|
210
|
+
"assistant credentials reveal resend/api_key",
|
|
211
|
+
),
|
|
212
|
+
).toEqual([{ service: "resend", field: "api_key" }]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("legacy path survives malformed extra arguments", () => {
|
|
216
|
+
expect(
|
|
217
|
+
collectRevealRefsFromCommand(
|
|
218
|
+
"assistant credentials reveal resend/api_key --json extra",
|
|
219
|
+
),
|
|
220
|
+
).toEqual([{ service: "resend", field: "api_key" }]);
|
|
221
|
+
});
|
|
222
|
+
|
|
207
223
|
test("unparseable invocation yields no ref (fails safe)", () => {
|
|
208
224
|
expect(
|
|
209
225
|
collectRevealRefsFromCommand("assistant credentials reveal --service x"),
|
|
@@ -437,6 +453,13 @@ describe("redactSecretsForChat", () => {
|
|
|
437
453
|
expect(out).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
438
454
|
});
|
|
439
455
|
|
|
456
|
+
test("Resend key in persisted assistant text is redacted without a candidate", () => {
|
|
457
|
+
const key = `re_${"a".repeat(8)}_${"b".repeat(24)}`;
|
|
458
|
+
const out = redactSecretsForChat(`the key is ${key} and it failed`, []);
|
|
459
|
+
expect(out).not.toContain(key);
|
|
460
|
+
expect(out).toContain("\u3014redacted:Resend API Key\u3015");
|
|
461
|
+
});
|
|
462
|
+
|
|
440
463
|
test("no candidate match produces the plain sentinel — never a guess", () => {
|
|
441
464
|
const out = redactSecretsForChat(`key: ${SYNTHETIC_OPENAI_PROJECT_KEY}`, [
|
|
442
465
|
{ service: "openai", field: "api_key", value: "different-value" },
|