@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa
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/ARCHITECTURE.md +26 -3
- package/docs/automatic-done.md +32 -0
- package/docs/desktop-browser-cli.md +3 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
- package/node_modules/@vellumai/environments/src/shell.ts +22 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/openapi.yaml +270 -10
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-provider.test.ts +2 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
- package/src/__tests__/config-get-vision-flag.test.ts +1 -1
- package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
- package/src/__tests__/conversation-evictor.test.ts +54 -0
- package/src/__tests__/conversation-pairing.test.ts +112 -0
- package/src/__tests__/document-append-idempotency.test.ts +13 -0
- package/src/__tests__/document-create-dedupe.test.ts +13 -0
- package/src/__tests__/document-find-replace.test.ts +13 -0
- package/src/__tests__/document-revision-history.test.ts +390 -0
- package/src/__tests__/document-revision.test.ts +425 -0
- package/src/__tests__/document-tool-security.test.ts +13 -0
- package/src/__tests__/document-update-default-surface.test.ts +13 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
- package/src/__tests__/gateway-flag-listener.test.ts +59 -6
- package/src/__tests__/headless-browser-navigate.test.ts +20 -30
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -2
- package/src/__tests__/identity-routes.test.ts +4 -3
- package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -4
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/notification-platform-adapter.test.ts +83 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/openai-provider.test.ts +38 -11
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/public-ingress-urls.test.ts +26 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
- package/src/__tests__/skill-execute-input.test.ts +11 -0
- package/src/__tests__/tool-approval-handler.test.ts +4 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
- package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
- package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
- package/src/acp/agent-process.ts +6 -1
- package/src/acp/client-handler.ts +6 -1
- package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
- package/src/agent/loop.ts +161 -17
- package/src/api/events/document-editor-show.ts +5 -0
- package/src/api/events/document-editor-update.ts +10 -0
- package/src/api/events/tool-result.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +273 -1
- package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
- package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-controller.ts +359 -25
- package/src/calls/channel-admission-reader.ts +15 -9
- package/src/calls/media-stream-server.ts +18 -0
- package/src/calls/media-stream-stt-session.ts +30 -9
- package/src/calls/twilio-provider.ts +14 -31
- package/src/calls/voice-session-bridge.ts +18 -1
- package/src/cli/commands/browser.ts +1 -2
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
- package/src/cli/lib/bundled-marketplace.json +54 -2
- package/src/cli/lib/inspect-plugin.ts +15 -15
- package/src/cli/lib/list-installed-plugins.ts +29 -28
- package/src/cli/lib/plugin-artifact.ts +74 -63
- package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
- package/src/cli/lib/plugin-details.ts +27 -42
- package/src/cli/lib/plugin-marketplace.ts +6 -8
- package/src/cli/lib/plugin-surfaces.ts +8 -7
- package/src/cli.ts +1 -3
- package/src/config/__tests__/chat-settings.test.ts +121 -0
- package/src/config/assistant-feature-flags.ts +28 -23
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/chat-settings.ts +100 -0
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/feature-flag-cache.ts +7 -25
- package/src/config/feature-flag-registry.json +9 -9
- package/src/config/loader.ts +5 -1
- package/src/config/profile-text-generation.ts +25 -21
- package/src/config/schemas/conversations.ts +14 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/notifications.ts +8 -1
- package/src/contacts/gateway-channel-read.ts +1 -1
- package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
- package/src/conversations/__tests__/auto-archive.test.ts +304 -0
- package/src/conversations/auto-archive-activity.ts +61 -0
- package/src/conversations/auto-archive.ts +201 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
- package/src/daemon/conversation-agent-loop.ts +14 -6
- package/src/daemon/conversation-evictor.ts +50 -16
- package/src/daemon/conversation-messaging.ts +4 -0
- package/src/daemon/conversation-process.ts +15 -0
- package/src/daemon/conversation-queue-manager.ts +2 -0
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/doordash-steps.ts +1 -3
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
- package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
- package/src/daemon/handlers/config-a2a.ts +75 -2
- package/src/daemon/handlers/shared.ts +13 -2
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/lifecycle.ts +15 -2
- package/src/daemon/process-message.ts +5 -0
- package/src/daemon/shutdown-handlers.ts +3 -0
- package/src/daemon/turn-finalization.ts +4 -0
- package/src/desktop/desktop-browser-endpoint.ts +11 -2
- package/src/desktop/desktop-chrome-policy.test.ts +1 -0
- package/src/desktop/desktop-chrome-policy.ts +7 -1
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/desktop/desktop-session-manager.ts +5 -3
- package/src/documents/document-revisions-store.ts +153 -0
- package/src/documents/document-store.ts +471 -133
- package/src/followups/followup-store.ts +0 -4
- package/src/i18n/__tests__/i18n.test.ts +25 -0
- package/src/i18n/assistant-text.ts +18 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/messages.ts +11 -0
- package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
- package/src/inbound/oauth-callback-url.ts +30 -1
- package/src/inbound/public-ingress-urls.ts +12 -2
- package/src/ipc/gateway-client.test.ts +40 -0
- package/src/ipc/gateway-client.ts +12 -29
- package/src/ipc/gateway-flag-listener.ts +7 -10
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/protocol.test.ts +77 -0
- package/src/live-voice/__tests__/share-targets.test.ts +163 -0
- package/src/live-voice/live-voice-session.ts +23 -6
- package/src/live-voice/protocol.ts +22 -0
- package/src/live-voice/share-targets.ts +225 -0
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
- package/src/mcp/client.ts +1 -19
- package/src/mcp/mcp-oauth-provider.ts +66 -49
- package/src/media/openrouter-image-service.ts +39 -22
- package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
- package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
- package/src/monitoring/control.ts +2 -1
- package/src/monitoring/oom-inheritance-sweep.ts +96 -0
- package/src/monitoring/oom-kill-reporter.ts +469 -0
- package/src/monitoring/resource-sampler.ts +30 -0
- package/src/notifications/README.md +53 -4
- package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
- package/src/notifications/__tests__/broadcaster.test.ts +558 -6
- package/src/notifications/__tests__/completion-policy.test.ts +37 -0
- package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
- package/src/notifications/adapters/platform.ts +14 -0
- package/src/notifications/assistant-initiated-thread.ts +101 -0
- package/src/notifications/broadcaster.ts +94 -16
- package/src/notifications/chat-reply-policy.ts +11 -0
- package/src/notifications/completion-policy.ts +12 -2
- package/src/notifications/conversation-pairing.ts +15 -70
- package/src/notifications/emit-signal.ts +34 -3
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connect-orchestrator.ts +1 -2
- package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
- package/src/persistence/conversation-auto-archive.ts +170 -0
- package/src/persistence/conversation-crud.ts +2 -1
- package/src/persistence/conversation-queries.ts +15 -8
- package/src/persistence/embeddings/embed.test.ts +96 -0
- package/src/persistence/embeddings/embed.ts +20 -2
- package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
- package/src/persistence/embeddings/embedding-gemini.ts +39 -1
- package/src/persistence/embeddings/embedding-local.ts +6 -0
- package/src/persistence/embeddings/qdrant-manager.ts +5 -0
- package/src/persistence/job-utils.ts +10 -0
- package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
- package/src/persistence/migrations/384-add-document-revision.ts +21 -0
- package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
- package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
- package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
- package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
- package/src/persistence/schema/conversations.ts +1 -0
- package/src/persistence/schema/documents.ts +20 -0
- package/src/persistence/steps.ts +6 -0
- package/src/platform/managed-speech.ts +42 -46
- package/src/plugin-api/vision-support.test.ts +21 -21
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
- package/src/providers/anthropic/client.ts +1 -1
- package/src/providers/fireworks/client.test.ts +168 -0
- package/src/providers/fireworks/client.ts +24 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/ionet/client.test.ts +143 -0
- package/src/providers/ionet/client.ts +37 -0
- package/src/providers/model-catalog.ts +141 -84
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +33 -2
- package/src/providers/retry.ts +56 -4
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +10 -2
- package/src/providers/unparseable-tool-args.ts +29 -5
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/assistant-event-hub.ts +23 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
- package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +2 -0
- package/src/runtime/channel-retry-sweep.ts +1 -2
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
- package/src/runtime/migrations/vbundle-builder.ts +19 -3
- package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
- package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
- package/src/runtime/migrations/vbundle-validator.ts +54 -1
- package/src/runtime/pending-interactions.ts +20 -2
- package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
- package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
- package/src/runtime/routes/contact-routes.ts +2 -2
- package/src/runtime/routes/conversation-list-routes.ts +10 -2
- package/src/runtime/routes/conversation-query-routes.ts +48 -10
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/documents-routes.ts +185 -47
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -0
- package/src/runtime/routes/host-bash-routes.ts +1 -0
- package/src/runtime/routes/host-browser-routes.ts +15 -0
- package/src/runtime/routes/host-cu-routes.ts +4 -1
- package/src/runtime/routes/host-file-routes.ts +1 -0
- package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
- package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
- package/src/runtime/routes/host-transfer-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/integrations/a2a.ts +4 -1
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/notification-routes.ts +1 -2
- package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
- package/src/runtime/routes/user-route-dispatcher.ts +5 -1
- package/src/runtime/verification-templates.ts +1 -2
- package/src/sequence/analytics.ts +0 -4
- package/src/sequence/engine.ts +1 -11
- package/src/sequence/guardrails.ts +1 -6
- package/src/sequence/importer.ts +0 -8
- package/src/sequence/store.ts +0 -10
- package/src/signals/user-message.ts +0 -6
- package/src/skills/catalog-install.ts +52 -23
- package/src/tools/browser/browser-execution.ts +20 -23
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
- package/src/tools/document/document-tool.ts +13 -11
- package/src/tools/executor.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +4 -2
- package/src/tools/network/__tests__/web-search.test.ts +105 -0
- package/src/tools/network/web-fetch.ts +2 -4
- package/src/tools/network/web-search.ts +239 -12
- package/src/tools/shared/filesystem/format-diff.ts +1 -2
- package/src/tools/shared/shell-output.test.ts +42 -0
- package/src/tools/shared/shell-output.ts +27 -4
- package/src/tools/skills/execute.ts +10 -7
- package/src/tools/terminal/shell.ts +3 -3
- package/src/tools/tool-approval-handler.ts +9 -1
- package/src/tools/ui-surface/visual-validation.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/__tests__/oom-priority.test.ts +55 -0
- package/src/util/__tests__/worker-memory.test.ts +16 -0
- package/src/util/debounce.ts +0 -4
- package/src/util/oom-priority.ts +79 -0
- package/src/util/platform.ts +0 -2
- package/src/util/plugin-manifest.ts +5 -7
- package/src/util/pricing.ts +25 -0
- package/src/util/worker-memory.ts +9 -3
- package/src/util/worker-process.ts +8 -0
- package/src/workspace/byok-default-profile-ensure.ts +11 -0
- package/src/workspace/hatched-date.ts +11 -4
- package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
- package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { createMockProvider } from "../__tests__/helpers/mock-provider.js";
|
|
4
|
+
import { MESSAGE_CATALOGS, MESSAGE_KEYS } from "../i18n/index.js";
|
|
5
|
+
import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
|
|
6
|
+
import type {
|
|
7
|
+
ContentBlock,
|
|
8
|
+
Message,
|
|
9
|
+
ProviderResponse,
|
|
10
|
+
} from "../providers/types.js";
|
|
11
|
+
import { wrapUnparseableToolArgs } from "../providers/unparseable-tool-args.js";
|
|
12
|
+
import { type AgentEvent, AgentLoop } from "./loop.js";
|
|
13
|
+
|
|
14
|
+
const userMessage: Message = {
|
|
15
|
+
role: "user",
|
|
16
|
+
content: [{ type: "text", text: "Write the file" }],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function malformedToolResponse(
|
|
20
|
+
id: string,
|
|
21
|
+
raw: string,
|
|
22
|
+
options: { name?: string; text?: string } = {},
|
|
23
|
+
): ProviderResponse {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
...(options.text ? [{ type: "text" as const, text: options.text }] : []),
|
|
27
|
+
{
|
|
28
|
+
type: "tool_use",
|
|
29
|
+
id,
|
|
30
|
+
name: options.name ?? "file_write",
|
|
31
|
+
input: wrapUnparseableToolArgs(raw),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
model: "mock-model",
|
|
35
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
36
|
+
stopReason: "tool_use",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toolBlocks(
|
|
41
|
+
history: Message[],
|
|
42
|
+
type: "tool_use" | "tool_result",
|
|
43
|
+
): ContentBlock[] {
|
|
44
|
+
return history
|
|
45
|
+
.flatMap((message) => message.content)
|
|
46
|
+
.filter((block) => {
|
|
47
|
+
return block.type === type;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("AgentLoop: unparseable tool retries", () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
resetPluginRegistryAndRegisterDefaults();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("bounds persisted input and stops after three consecutive malformed turns", async () => {
|
|
57
|
+
const raw =
|
|
58
|
+
'{"path":"/workspace/file.md","content":"' + "x".repeat(2_000_000);
|
|
59
|
+
const { provider, calls } = createMockProvider([
|
|
60
|
+
malformedToolResponse("bad-1", raw),
|
|
61
|
+
malformedToolResponse("bad-2", raw),
|
|
62
|
+
malformedToolResponse("bad-3", raw, { text: "Still trying." }),
|
|
63
|
+
malformedToolResponse("bad-4", raw),
|
|
64
|
+
]);
|
|
65
|
+
const executionInputs: Record<string, unknown>[] = [];
|
|
66
|
+
const events: AgentEvent[] = [];
|
|
67
|
+
const loop = new AgentLoop({
|
|
68
|
+
provider,
|
|
69
|
+
systemPrompt: "system",
|
|
70
|
+
conversationId: "conversation-123",
|
|
71
|
+
tools: [
|
|
72
|
+
{
|
|
73
|
+
name: "file_write",
|
|
74
|
+
description: "",
|
|
75
|
+
input_schema: { type: "object" },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
toolExecutor: async (_name, input) => {
|
|
79
|
+
executionInputs.push(input);
|
|
80
|
+
return { content: "invalid JSON", isError: true };
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const result = await loop.run({
|
|
85
|
+
requestId: "request-123",
|
|
86
|
+
messages: [userMessage],
|
|
87
|
+
onEvent: (event) => {
|
|
88
|
+
events.push(event);
|
|
89
|
+
},
|
|
90
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(calls).toHaveLength(3);
|
|
94
|
+
expect(executionInputs).toHaveLength(2);
|
|
95
|
+
expect(executionInputs[0]).toEqual(wrapUnparseableToolArgs(raw));
|
|
96
|
+
expect(toolBlocks(result.history, "tool_use")).toHaveLength(2);
|
|
97
|
+
expect(toolBlocks(result.history, "tool_result")).toHaveLength(2);
|
|
98
|
+
|
|
99
|
+
const serializedHistory = JSON.stringify(result.history);
|
|
100
|
+
expect(serializedHistory.length).toBeLessThan(5_000);
|
|
101
|
+
expect(serializedHistory).not.toContain("x".repeat(1_000));
|
|
102
|
+
expect(result.history.at(-1)?.content).toEqual([
|
|
103
|
+
{ type: "text", text: "Still trying." },
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: `\n\n${MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP}`,
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
expect(events.filter((event) => event.type === "tool_use")).toHaveLength(2);
|
|
110
|
+
expect(
|
|
111
|
+
events
|
|
112
|
+
.filter((event) => event.type === "text_delta")
|
|
113
|
+
.map((event) => event.text)
|
|
114
|
+
.join(""),
|
|
115
|
+
).toEndWith(
|
|
116
|
+
`Still trying.\n\n${
|
|
117
|
+
MESSAGE_CATALOGS.en[MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP]
|
|
118
|
+
}`,
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("surfaces only the stop notice when assistant text is tool-gated", async () => {
|
|
123
|
+
const raw = "{" + "x".repeat(1_000);
|
|
124
|
+
const { provider } = createMockProvider([
|
|
125
|
+
malformedToolResponse("bad-1", raw, { text: "Private notes one." }),
|
|
126
|
+
malformedToolResponse("bad-2", raw, { text: "Private notes two." }),
|
|
127
|
+
malformedToolResponse("bad-3", raw, { text: "Private notes three." }),
|
|
128
|
+
]);
|
|
129
|
+
const events: AgentEvent[] = [];
|
|
130
|
+
const loop = new AgentLoop({
|
|
131
|
+
provider,
|
|
132
|
+
systemPrompt: "system",
|
|
133
|
+
conversationId: "conversation-gated",
|
|
134
|
+
tools: [
|
|
135
|
+
{
|
|
136
|
+
name: "file_write",
|
|
137
|
+
description: "",
|
|
138
|
+
input_schema: { type: "object" },
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
toolExecutor: async () => ({ content: "invalid JSON", isError: true }),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = await loop.run({
|
|
145
|
+
requestId: "request-gated",
|
|
146
|
+
messages: [userMessage],
|
|
147
|
+
onEvent: (event) => {
|
|
148
|
+
events.push(event);
|
|
149
|
+
},
|
|
150
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
151
|
+
suppressAssistantText: true,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(result.history.at(-1)?.content).toEqual([
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP,
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
expect(
|
|
161
|
+
events
|
|
162
|
+
.filter((event) => event.type === "text_delta")
|
|
163
|
+
.map((event) => event.text)
|
|
164
|
+
.join(""),
|
|
165
|
+
).toBe(
|
|
166
|
+
MESSAGE_CATALOGS.en[MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP],
|
|
167
|
+
);
|
|
168
|
+
const finalMessage = events
|
|
169
|
+
.filter((event) => event.type === "message_complete")
|
|
170
|
+
.at(-1);
|
|
171
|
+
expect(finalMessage?.assistantTextVisibility).toBe("visible");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("streams the localized stop notice while persisting its key", async () => {
|
|
175
|
+
const raw = "{" + "x".repeat(1_000);
|
|
176
|
+
const { provider } = createMockProvider([
|
|
177
|
+
malformedToolResponse("bad-1", raw),
|
|
178
|
+
malformedToolResponse("bad-2", raw),
|
|
179
|
+
malformedToolResponse("bad-3", raw),
|
|
180
|
+
]);
|
|
181
|
+
const events: AgentEvent[] = [];
|
|
182
|
+
const loop = new AgentLoop({
|
|
183
|
+
provider,
|
|
184
|
+
systemPrompt: "system",
|
|
185
|
+
conversationId: "conversation-spanish",
|
|
186
|
+
tools: [
|
|
187
|
+
{
|
|
188
|
+
name: "file_write",
|
|
189
|
+
description: "",
|
|
190
|
+
input_schema: { type: "object" },
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
toolExecutor: async () => ({ content: "invalid JSON", isError: true }),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const result = await loop.run({
|
|
197
|
+
requestId: "request-spanish",
|
|
198
|
+
messages: [userMessage],
|
|
199
|
+
onEvent: (event) => {
|
|
200
|
+
events.push(event);
|
|
201
|
+
},
|
|
202
|
+
locale: "es",
|
|
203
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(result.history.at(-1)?.content).toEqual([
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP,
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
expect(
|
|
213
|
+
events
|
|
214
|
+
.filter((event) => event.type === "text_delta")
|
|
215
|
+
.map((event) => event.text)
|
|
216
|
+
.join(""),
|
|
217
|
+
).toBe(
|
|
218
|
+
MESSAGE_CATALOGS.es[MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP],
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("does not count skill_execute envelopes recovered from valid outer JSON", async () => {
|
|
223
|
+
const recoverableRaw = JSON.stringify({
|
|
224
|
+
tool: "document_update",
|
|
225
|
+
input: "body",
|
|
226
|
+
activity: "Updating the document",
|
|
227
|
+
});
|
|
228
|
+
const malformedRaw = "{" + "x".repeat(1_000);
|
|
229
|
+
const done: ProviderResponse = {
|
|
230
|
+
content: [{ type: "text", text: "Done" }],
|
|
231
|
+
model: "mock-model",
|
|
232
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
233
|
+
stopReason: "end_turn",
|
|
234
|
+
};
|
|
235
|
+
const { provider, calls } = createMockProvider([
|
|
236
|
+
malformedToolResponse("recover-1", recoverableRaw, {
|
|
237
|
+
name: "skill_execute",
|
|
238
|
+
}),
|
|
239
|
+
malformedToolResponse("bad-1", malformedRaw),
|
|
240
|
+
malformedToolResponse("recover-2", recoverableRaw, {
|
|
241
|
+
name: "skill_execute",
|
|
242
|
+
}),
|
|
243
|
+
malformedToolResponse("bad-2", malformedRaw),
|
|
244
|
+
done,
|
|
245
|
+
]);
|
|
246
|
+
const loop = new AgentLoop({
|
|
247
|
+
provider,
|
|
248
|
+
systemPrompt: "system",
|
|
249
|
+
conversationId: "conversation-789",
|
|
250
|
+
tools: [
|
|
251
|
+
{
|
|
252
|
+
name: "skill_execute",
|
|
253
|
+
description: "",
|
|
254
|
+
input_schema: { type: "object" },
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "file_write",
|
|
258
|
+
description: "",
|
|
259
|
+
input_schema: { type: "object" },
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
toolExecutor: async () => ({ content: "result", isError: false }),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const result = await loop.run({
|
|
266
|
+
requestId: "request-789",
|
|
267
|
+
messages: [userMessage],
|
|
268
|
+
onEvent: () => {},
|
|
269
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(calls).toHaveLength(5);
|
|
273
|
+
expect(result.history.at(-1)?.content).toEqual([
|
|
274
|
+
{ type: "text", text: "Done" },
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("resets the streak after a well-formed tool turn", async () => {
|
|
279
|
+
const raw = "{" + "x".repeat(1_000);
|
|
280
|
+
const valid: ProviderResponse = {
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: "tool_use",
|
|
284
|
+
id: "valid",
|
|
285
|
+
name: "file_write",
|
|
286
|
+
input: { path: "/workspace/file.md", content: "ok" },
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
model: "mock-model",
|
|
290
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
291
|
+
stopReason: "tool_use",
|
|
292
|
+
};
|
|
293
|
+
const done: ProviderResponse = {
|
|
294
|
+
content: [{ type: "text", text: "Done" }],
|
|
295
|
+
model: "mock-model",
|
|
296
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
297
|
+
stopReason: "end_turn",
|
|
298
|
+
};
|
|
299
|
+
const { provider, calls } = createMockProvider([
|
|
300
|
+
malformedToolResponse("bad-1", raw),
|
|
301
|
+
malformedToolResponse("bad-2", raw),
|
|
302
|
+
valid,
|
|
303
|
+
malformedToolResponse("bad-3", raw),
|
|
304
|
+
malformedToolResponse("bad-4", raw),
|
|
305
|
+
done,
|
|
306
|
+
]);
|
|
307
|
+
const loop = new AgentLoop({
|
|
308
|
+
provider,
|
|
309
|
+
systemPrompt: "system",
|
|
310
|
+
conversationId: "conversation-456",
|
|
311
|
+
tools: [
|
|
312
|
+
{
|
|
313
|
+
name: "file_write",
|
|
314
|
+
description: "",
|
|
315
|
+
input_schema: { type: "object" },
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
toolExecutor: async () => ({ content: "result", isError: false }),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const result = await loop.run({
|
|
322
|
+
requestId: "request-456",
|
|
323
|
+
messages: [userMessage],
|
|
324
|
+
onEvent: () => {},
|
|
325
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(calls).toHaveLength(6);
|
|
329
|
+
expect(result.history.at(-1)?.content).toEqual([
|
|
330
|
+
{ type: "text", text: "Done" },
|
|
331
|
+
]);
|
|
332
|
+
});
|
|
333
|
+
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
getCalibrationProviderKey,
|
|
15
15
|
} from "../context/token-estimator.js";
|
|
16
16
|
import { spoolAndStubOversizedToolResults } from "../context/tool-result-spool.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
looksLikeContextOverflowError,
|
|
19
|
+
parseActualTokensFromError,
|
|
20
|
+
} from "../daemon/parse-actual-tokens-from-error.js";
|
|
18
21
|
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
19
22
|
import type {
|
|
20
23
|
AgentLoopExitReason,
|
|
@@ -25,6 +28,12 @@ import type {
|
|
|
25
28
|
PreModelCallInputContext,
|
|
26
29
|
StopInputContext,
|
|
27
30
|
} from "../hooks/types.js";
|
|
31
|
+
import {
|
|
32
|
+
MESSAGE_KEYS,
|
|
33
|
+
resolveStoredMessageText,
|
|
34
|
+
type SupportedLocale,
|
|
35
|
+
t,
|
|
36
|
+
} from "../i18n/index.js";
|
|
28
37
|
import {
|
|
29
38
|
timeSyncSection,
|
|
30
39
|
traceAsyncSection,
|
|
@@ -48,10 +57,11 @@ import type {
|
|
|
48
57
|
ToolDefinition,
|
|
49
58
|
ToolResultContent,
|
|
50
59
|
} from "../providers/types.js";
|
|
60
|
+
import { NATIVE_WEB_SEARCH_TOOL_NAME } from "../providers/types.js";
|
|
51
61
|
import {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} from "../providers/
|
|
62
|
+
boundUnparseableToolArgs,
|
|
63
|
+
isUnparseableToolArgs,
|
|
64
|
+
} from "../providers/unparseable-tool-args.js";
|
|
55
65
|
import { recordWatchdogEvent } from "../telemetry/watchdog-events-store.js";
|
|
56
66
|
import {
|
|
57
67
|
ABORT_SETTLE_GRACE_MS,
|
|
@@ -64,6 +74,7 @@ import {
|
|
|
64
74
|
applyStreamingSubstitution,
|
|
65
75
|
applySubstitutions,
|
|
66
76
|
} from "../tools/sensitive-output-placeholders.js";
|
|
77
|
+
import { recoverSkillExecuteEnvelope } from "../tools/skills/execute.js";
|
|
67
78
|
import {
|
|
68
79
|
abortedToolResultText,
|
|
69
80
|
isPreemptedByNewMessage,
|
|
@@ -83,6 +94,8 @@ const log = getLogger("agent-loop");
|
|
|
83
94
|
|
|
84
95
|
/** Watchdog check name for both tool-gated reply outcomes. */
|
|
85
96
|
const SEND_USER_MESSAGE_CHECK = "send_user_message_delivery";
|
|
97
|
+
const MAX_CONSECUTIVE_UNPARSEABLE_TOOL_TURNS = 3;
|
|
98
|
+
const SKILL_EXECUTE_TOOL_NAME = "skill_execute";
|
|
86
99
|
|
|
87
100
|
/**
|
|
88
101
|
* The user-facing message a `send_user_message` block carries, or null when
|
|
@@ -696,6 +709,8 @@ interface AgentLoopRunOptionsBase {
|
|
|
696
709
|
injectionLedgerResets?: Set<string>;
|
|
697
710
|
signal?: AbortSignal;
|
|
698
711
|
requestId: string;
|
|
712
|
+
/** Locale for daemon-authored text emitted during this run. */
|
|
713
|
+
locale?: SupportedLocale;
|
|
699
714
|
/**
|
|
700
715
|
* Explicit model override (provider/model string) for every LLM call in
|
|
701
716
|
* this run. When omitted, the model is resolved through the normal
|
|
@@ -747,6 +762,12 @@ interface AgentLoopRunOptionsBase {
|
|
|
747
762
|
* call-site named profile. Missing profile names silently fall through.
|
|
748
763
|
*/
|
|
749
764
|
overrideProfile?: string;
|
|
765
|
+
/**
|
|
766
|
+
* Who chose `overrideProfile`: `"auto"` when the Auto profile's router
|
|
767
|
+
* picked it for this turn. Threaded onto each send's config so usage
|
|
768
|
+
* attribution reports the routed profile under the `auto` source.
|
|
769
|
+
*/
|
|
770
|
+
overrideProfileOrigin?: "auto";
|
|
750
771
|
/**
|
|
751
772
|
* Float the override profile above the call-site layers (named site
|
|
752
773
|
* profile + call-site override) for non-main-agent call sites — the
|
|
@@ -1005,12 +1026,31 @@ function toolResultEventFields(
|
|
|
1005
1026
|
interface NormalizedToolUse {
|
|
1006
1027
|
/** Assistant content with at most one `tool_use` block per call id. */
|
|
1007
1028
|
content: ContentBlock[];
|
|
1008
|
-
/** The `tool_use` blocks in `content`, in order. */
|
|
1029
|
+
/** The bounded `tool_use` blocks in `content`, in order. */
|
|
1009
1030
|
toolUseBlocks: ToolUseBlock[];
|
|
1031
|
+
/** Original inputs retained only until this iteration's execution attempt. */
|
|
1032
|
+
executionInputsById: Map<string, Record<string, unknown>>;
|
|
1033
|
+
/** Number of executable calls whose provider arguments did not parse. */
|
|
1034
|
+
unparseableCount: number;
|
|
1010
1035
|
/** Coalesced copies: a call id and name for each block dropped. */
|
|
1011
1036
|
duplicates: Array<{ id: string; name: string }>;
|
|
1012
1037
|
}
|
|
1013
1038
|
|
|
1039
|
+
function isRetryableUnparseableToolCall(block: ToolUseBlock): boolean {
|
|
1040
|
+
if (!isUnparseableToolArgs(block.input)) {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
if (block.name !== SKILL_EXECUTE_TOOL_NAME) {
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
const recovered = recoverSkillExecuteEnvelope(block.input);
|
|
1047
|
+
return (
|
|
1048
|
+
recovered === block.input ||
|
|
1049
|
+
typeof recovered.tool !== "string" ||
|
|
1050
|
+
recovered.tool.length === 0
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1014
1054
|
/**
|
|
1015
1055
|
* Resolves an assistant reply's `tool_use` blocks into an executable set keyed
|
|
1016
1056
|
* by call id: a block with no id gets one, and a repeat of an id already in the
|
|
@@ -1024,8 +1064,10 @@ function normalizeToolUseBlocks(
|
|
|
1024
1064
|
): NormalizedToolUse {
|
|
1025
1065
|
const nextContent: ContentBlock[] = [];
|
|
1026
1066
|
const toolUseBlocks: ToolUseBlock[] = [];
|
|
1067
|
+
const executionInputsById = new Map<string, Record<string, unknown>>();
|
|
1027
1068
|
const duplicates: Array<{ id: string; name: string }> = [];
|
|
1028
1069
|
const seenIds = new Set<string>();
|
|
1070
|
+
let unparseableCount = 0;
|
|
1029
1071
|
|
|
1030
1072
|
for (const block of content) {
|
|
1031
1073
|
if (block.type !== "tool_use") {
|
|
@@ -1036,14 +1078,28 @@ function normalizeToolUseBlocks(
|
|
|
1036
1078
|
duplicates.push({ id: block.id, name: block.name });
|
|
1037
1079
|
continue;
|
|
1038
1080
|
}
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1081
|
+
const normalizedId = block.id.length === 0 ? crypto.randomUUID() : block.id;
|
|
1082
|
+
const bounded: ToolUseBlock = {
|
|
1083
|
+
...block,
|
|
1084
|
+
id: normalizedId,
|
|
1085
|
+
input: boundUnparseableToolArgs(block.input),
|
|
1086
|
+
};
|
|
1087
|
+
seenIds.add(normalizedId);
|
|
1088
|
+
executionInputsById.set(normalizedId, block.input);
|
|
1089
|
+
if (isRetryableUnparseableToolCall(block)) {
|
|
1090
|
+
unparseableCount++;
|
|
1091
|
+
}
|
|
1092
|
+
nextContent.push(bounded);
|
|
1093
|
+
toolUseBlocks.push(bounded);
|
|
1044
1094
|
}
|
|
1045
1095
|
|
|
1046
|
-
return {
|
|
1096
|
+
return {
|
|
1097
|
+
content: nextContent,
|
|
1098
|
+
toolUseBlocks,
|
|
1099
|
+
executionInputsById,
|
|
1100
|
+
unparseableCount,
|
|
1101
|
+
duplicates,
|
|
1102
|
+
};
|
|
1047
1103
|
}
|
|
1048
1104
|
|
|
1049
1105
|
/**
|
|
@@ -1454,6 +1510,7 @@ export class AgentLoop {
|
|
|
1454
1510
|
supportsDynamicUi = true,
|
|
1455
1511
|
trust,
|
|
1456
1512
|
overrideProfile,
|
|
1513
|
+
overrideProfileOrigin,
|
|
1457
1514
|
forceOverrideProfile = false,
|
|
1458
1515
|
resolveOverrideProfile,
|
|
1459
1516
|
onModelCallPrepared,
|
|
@@ -1479,6 +1536,7 @@ export class AgentLoop {
|
|
|
1479
1536
|
let newMessagesStart = history.length;
|
|
1480
1537
|
let toolUseTurns = 0;
|
|
1481
1538
|
let postModelCallContinues = 0;
|
|
1539
|
+
let consecutiveUnparseableToolTurns = 0;
|
|
1482
1540
|
// Whether the user has been told the OUTCOME of the work so far, not
|
|
1483
1541
|
// merely that work started. True only while the most recent tool-bearing
|
|
1484
1542
|
// response was `send_user_message` and nothing else: a response that sends
|
|
@@ -1734,6 +1792,7 @@ export class AgentLoop {
|
|
|
1734
1792
|
// request from a generation that died mid-flight. Declared here so that
|
|
1735
1793
|
// catch can reach it.
|
|
1736
1794
|
let streamedTokens = false;
|
|
1795
|
+
let unparseableToolRetryLimitReached = false;
|
|
1737
1796
|
|
|
1738
1797
|
try {
|
|
1739
1798
|
// ── Pre-call budget gate ─────────────────────────────────────
|
|
@@ -2036,6 +2095,16 @@ export class AgentLoop {
|
|
|
2036
2095
|
const effectiveOverrideProfile = resolveEffectiveOverrideProfile();
|
|
2037
2096
|
if (effectiveOverrideProfile) {
|
|
2038
2097
|
providerConfig.overrideProfile = effectiveOverrideProfile;
|
|
2098
|
+
// The origin describes the turn-start override. A profile switched
|
|
2099
|
+
// in mid-turn (a confirmed profile session) is the user's pick, so
|
|
2100
|
+
// the origin applies only while the effective override is still
|
|
2101
|
+
// the one the turn started with.
|
|
2102
|
+
if (
|
|
2103
|
+
overrideProfileOrigin &&
|
|
2104
|
+
effectiveOverrideProfile === overrideProfile
|
|
2105
|
+
) {
|
|
2106
|
+
providerConfig.overrideProfileOrigin = overrideProfileOrigin;
|
|
2107
|
+
}
|
|
2039
2108
|
if (forceOverrideProfile) {
|
|
2040
2109
|
providerConfig.forceOverrideProfile = true;
|
|
2041
2110
|
}
|
|
@@ -2236,7 +2305,13 @@ export class AgentLoop {
|
|
|
2236
2305
|
// resolver layers `llm.profiles[overrideProfile]` at the top of
|
|
2237
2306
|
// precedence for the user-facing call, so a model router can pick
|
|
2238
2307
|
// the profile per message; clearing it drops any seeded override.
|
|
2308
|
+
// The hook context is seeded with the effective override, so an
|
|
2309
|
+
// unchanged profile is still the router's pick and keeps its
|
|
2310
|
+
// origin; a profile the hook changed or cleared is the hook's.
|
|
2239
2311
|
const hookModelProfile = finalPreModelCtx.modelProfile?.trim();
|
|
2312
|
+
if (hookModelProfile !== effectiveOverrideProfile) {
|
|
2313
|
+
delete providerConfig.overrideProfileOrigin;
|
|
2314
|
+
}
|
|
2240
2315
|
if (hookModelProfile) {
|
|
2241
2316
|
providerConfig.overrideProfile = hookModelProfile;
|
|
2242
2317
|
if (forceOverrideProfile) {
|
|
@@ -2705,6 +2780,44 @@ export class AgentLoop {
|
|
|
2705
2780
|
content: normalizedToolUse.content,
|
|
2706
2781
|
};
|
|
2707
2782
|
toolUseBlocks = normalizedToolUse.toolUseBlocks;
|
|
2783
|
+
if (normalizedToolUse.unparseableCount > 0) {
|
|
2784
|
+
consecutiveUnparseableToolTurns++;
|
|
2785
|
+
} else {
|
|
2786
|
+
consecutiveUnparseableToolTurns = 0;
|
|
2787
|
+
}
|
|
2788
|
+
if (
|
|
2789
|
+
consecutiveUnparseableToolTurns >=
|
|
2790
|
+
MAX_CONSECUTIVE_UNPARSEABLE_TOOL_TURNS
|
|
2791
|
+
) {
|
|
2792
|
+
rlog.warn(
|
|
2793
|
+
{
|
|
2794
|
+
turn: toolUseTurns,
|
|
2795
|
+
consecutiveUnparseableToolTurns,
|
|
2796
|
+
unparseableToolCount: normalizedToolUse.unparseableCount,
|
|
2797
|
+
},
|
|
2798
|
+
"Repeated unparseable tool calls reached the per-run limit",
|
|
2799
|
+
);
|
|
2800
|
+
unparseableToolRetryLimitReached = true;
|
|
2801
|
+
const visiblePrefix = suppressAssistantText
|
|
2802
|
+
? []
|
|
2803
|
+
: assistantMessage.content.filter(
|
|
2804
|
+
(block) => block.type !== "tool_use",
|
|
2805
|
+
);
|
|
2806
|
+
assistantMessage = {
|
|
2807
|
+
role: "assistant",
|
|
2808
|
+
content: [
|
|
2809
|
+
...visiblePrefix,
|
|
2810
|
+
{
|
|
2811
|
+
type: "text",
|
|
2812
|
+
text: `${
|
|
2813
|
+
hasVisibleText(visiblePrefix) ? "\n\n" : ""
|
|
2814
|
+
}${MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP}`,
|
|
2815
|
+
},
|
|
2816
|
+
],
|
|
2817
|
+
};
|
|
2818
|
+
toolUseBlocks = [];
|
|
2819
|
+
userToldOutcome = false;
|
|
2820
|
+
}
|
|
2708
2821
|
|
|
2709
2822
|
// At the no-tool stop boundary the retry decision is actionable: a
|
|
2710
2823
|
// recovery hook may repair history and ask to re-query (a tool-bearing
|
|
@@ -2713,6 +2826,7 @@ export class AgentLoop {
|
|
|
2713
2826
|
// persisting it; the per-run backstop keeps a misbehaving hook from
|
|
2714
2827
|
// spinning forever.
|
|
2715
2828
|
if (
|
|
2829
|
+
!unparseableToolRetryLimitReached &&
|
|
2716
2830
|
toolUseBlocks.length === 0 &&
|
|
2717
2831
|
postModelCallDecision === "continue"
|
|
2718
2832
|
) {
|
|
@@ -2774,11 +2888,40 @@ export class AgentLoop {
|
|
|
2774
2888
|
// Surface the finalized text if the client saw nothing live (a
|
|
2775
2889
|
// deferred stream, a hook-rewritten empty turn, or the tool-gated
|
|
2776
2890
|
// fallback where no `send_user_message` call ever reached the user).
|
|
2777
|
-
|
|
2778
|
-
|
|
2891
|
+
let fallbackSurfaced = false;
|
|
2892
|
+
if (unparseableToolRetryLimitReached && streamedVisibleText) {
|
|
2893
|
+
onEvent({
|
|
2894
|
+
type: "text_delta",
|
|
2895
|
+
text: resolveStoredMessageText(
|
|
2896
|
+
assistantTextOf(assistantMessage.content.slice(-1)),
|
|
2897
|
+
options.locale,
|
|
2898
|
+
),
|
|
2899
|
+
});
|
|
2900
|
+
fallbackSurfaced = suppressAssistantText;
|
|
2901
|
+
} else {
|
|
2902
|
+
const displayContent = unparseableToolRetryLimitReached
|
|
2903
|
+
? assistantMessage.content.map((block) =>
|
|
2904
|
+
block.type === "text" &&
|
|
2905
|
+
block.text.trim() ===
|
|
2906
|
+
MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP
|
|
2907
|
+
? {
|
|
2908
|
+
...block,
|
|
2909
|
+
text: block.text.replace(
|
|
2910
|
+
MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP,
|
|
2911
|
+
t(
|
|
2912
|
+
MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP,
|
|
2913
|
+
options.locale,
|
|
2914
|
+
),
|
|
2915
|
+
),
|
|
2916
|
+
}
|
|
2917
|
+
: block,
|
|
2918
|
+
)
|
|
2919
|
+
: assistantMessage.content;
|
|
2920
|
+
fallbackSurfaced = emitFinalAssistantText(displayContent, {
|
|
2779
2921
|
turnEnding: toolUseBlocks.length === 0,
|
|
2780
|
-
})
|
|
2781
|
-
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
const textVisibility = textVisibilityOf(fallbackSurfaced);
|
|
2782
2925
|
|
|
2783
2926
|
history.push(assistantMessage);
|
|
2784
2927
|
|
|
@@ -2891,7 +3034,8 @@ export class AgentLoop {
|
|
|
2891
3034
|
}
|
|
2892
3035
|
const result = await this.toolExecutor!(
|
|
2893
3036
|
toolUse.name,
|
|
2894
|
-
toolUse.
|
|
3037
|
+
normalizedToolUse.executionInputsById.get(toolUse.id) ??
|
|
3038
|
+
toolUse.input,
|
|
2895
3039
|
(chunk) => {
|
|
2896
3040
|
onEvent({
|
|
2897
3041
|
type: "tool_output_chunk",
|
|
@@ -3170,7 +3314,7 @@ export class AgentLoop {
|
|
|
3170
3314
|
// it is disabled (e.g. agent wakes) there is no ladder to drive, so the
|
|
3171
3315
|
// overflow falls through to the generic error path below.
|
|
3172
3316
|
if (
|
|
3173
|
-
|
|
3317
|
+
looksLikeContextOverflowError(error) &&
|
|
3174
3318
|
(options.resolveContextWindow?.().overflowRecovery.enabled ?? false)
|
|
3175
3319
|
) {
|
|
3176
3320
|
if (overflowLadderExhausted) {
|
|
@@ -19,6 +19,11 @@ export const DocumentEditorShowEventSchema = z.object({
|
|
|
19
19
|
surfaceId: z.string(),
|
|
20
20
|
title: z.string(),
|
|
21
21
|
initialContent: z.string(),
|
|
22
|
+
/**
|
|
23
|
+
* The document's revision once this content is in place. A client that
|
|
24
|
+
* saves with `baseRevision` adopts it as its new base.
|
|
25
|
+
*/
|
|
26
|
+
revision: z.number().optional(),
|
|
22
27
|
});
|
|
23
28
|
|
|
24
29
|
export type DocumentEditorShowEvent = z.infer<
|
|
@@ -20,6 +20,16 @@ export const DocumentEditorUpdateEventSchema = z.object({
|
|
|
20
20
|
surfaceId: z.string(),
|
|
21
21
|
markdown: z.string(),
|
|
22
22
|
mode: z.string(),
|
|
23
|
+
/**
|
|
24
|
+
* The document's revision once this content is in place. A client that
|
|
25
|
+
* saves with `baseRevision` adopts it as its new base.
|
|
26
|
+
*/
|
|
27
|
+
revision: z.number().optional(),
|
|
28
|
+
/**
|
|
29
|
+
* The document's title when this update also sets it (a restore). A client
|
|
30
|
+
* renames the open document to it.
|
|
31
|
+
*/
|
|
32
|
+
title: z.string().optional(),
|
|
23
33
|
});
|
|
24
34
|
|
|
25
35
|
export type DocumentEditorUpdateEvent = z.infer<
|