@vellumai/assistant 0.12.6-dev.202609292116.41f7caa → 0.12.6-staging.2
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 +0 -2
- package/ARCHITECTURE.md +3 -3
- package/openapi.yaml +4 -8
- package/package.json +1 -1
- package/src/__tests__/list-messages-client-message-id.test.ts +0 -43
- package/src/__tests__/openai-provider.test.ts +0 -27
- package/src/__tests__/skill-execute-input.test.ts +0 -11
- package/src/__tests__/tool-approval-handler.test.ts +0 -4
- package/src/__tests__/tool-preview-lifecycle.test.ts +0 -26
- package/src/agent/loop.ts +12 -133
- package/src/calls/__tests__/call-controller.test.ts +1 -273
- package/src/calls/__tests__/media-stream-server-integration.test.ts +0 -40
- package/src/calls/__tests__/media-stream-stt-session.test.ts +0 -33
- package/src/calls/call-constants.ts +0 -10
- package/src/calls/call-controller.ts +25 -359
- package/src/calls/media-stream-server.ts +0 -18
- package/src/calls/media-stream-stt-session.ts +9 -30
- package/src/cli/lib/bundled-marketplace.json +0 -39
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schemas/llm.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -18
- package/src/daemon/conversation-agent-loop.ts +0 -3
- package/src/daemon/conversation-messaging.ts +0 -4
- package/src/daemon/conversation-process.ts +0 -15
- package/src/daemon/conversation-queue-manager.ts +0 -2
- package/src/daemon/conversation.ts +0 -2
- package/src/daemon/handlers/shared.ts +2 -13
- package/src/daemon/process-message.ts +0 -5
- package/src/i18n/__tests__/i18n.test.ts +0 -25
- package/src/i18n/index.ts +0 -1
- package/src/i18n/messages.ts +0 -11
- package/src/providers/__tests__/unparseable-tool-args.test.ts +0 -25
- package/src/providers/inference/adapter-factory.ts +0 -6
- package/src/providers/model-catalog.ts +0 -40
- package/src/providers/openai/chat-completions-provider.ts +2 -33
- package/src/providers/retry.ts +4 -10
- package/src/providers/unparseable-tool-args.ts +5 -29
- package/src/runtime/channel-reply-delivery.ts +0 -2
- package/src/runtime/http-types.ts +0 -3
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +10 -35
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -48
- package/src/runtime/migrations/vbundle-builder.ts +3 -19
- package/src/runtime/migrations/vbundle-streaming-importer.ts +19 -14
- package/src/runtime/migrations/vbundle-streaming-validator.ts +7 -14
- package/src/runtime/migrations/vbundle-validator.ts +1 -54
- package/src/runtime/routes/conversation-routes.ts +0 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +0 -4
- package/src/runtime/routes/migration-routes.ts +1 -3
- package/src/tools/host-terminal/host-shell.ts +2 -4
- package/src/tools/shared/shell-output.test.ts +0 -42
- package/src/tools/shared/shell-output.ts +4 -27
- package/src/tools/skills/execute.ts +7 -10
- package/src/tools/terminal/shell.ts +0 -2
- package/src/tools/tool-approval-handler.ts +1 -9
- package/src/agent/loop-unparseable-tool-retry.test.ts +0 -333
- package/src/i18n/assistant-text.ts +0 -18
- package/src/providers/ionet/client.test.ts +0 -143
- package/src/providers/ionet/client.ts +0 -37
package/.env.example
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
ANTHROPIC_API_KEY=sk-ant-...
|
|
3
3
|
OPENAI_API_KEY=sk-...
|
|
4
4
|
GEMINI_API_KEY=...
|
|
5
|
-
# Optional. IO Intelligence (io.net) open-models endpoint.
|
|
6
|
-
IONET_API_KEY=...
|
|
7
5
|
# Optional. TypeSafe / Jev System One decision model.
|
|
8
6
|
TYPESAFE_API_KEY=...
|
|
9
7
|
# Optional for authenticated/proxied Ollama deployments
|
package/ARCHITECTURE.md
CHANGED
|
@@ -618,12 +618,12 @@ Every phone call connects over Twilio Media Streams: the voice webhook emits `<C
|
|
|
618
618
|
|
|
619
619
|
Transcription mode is selected once per session in `media-stream-stt-session.ts`:
|
|
620
620
|
|
|
621
|
-
- **Streaming** (default): when `calls.voice.telephonyStreaming` is enabled and the `telephony` role resolves a streaming transcriber (`resolveStreamingTranscriber({ role: "telephony" })`), inbound audio is decoded (mu-law → PCM16, resampled 8 kHz → 16 kHz) and fed to the provider's realtime adapter. Replies trigger only on utterance-boundary finals (for Deepgram, `speech_final`/`UtteranceEnd`, never mid-sentence `is_final` segments), and barge-in fires from local energy VAD, never from transcriber partials.
|
|
621
|
+
- **Streaming** (default): when `calls.voice.telephonyStreaming` is enabled and the `telephony` role resolves a streaming transcriber (`resolveStreamingTranscriber({ role: "telephony" })`), inbound audio is decoded (mu-law → PCM16, resampled 8 kHz → 16 kHz) and fed to the provider's realtime adapter. Replies trigger only on utterance-boundary finals (for Deepgram, `speech_final`/`UtteranceEnd`, never mid-sentence `is_final` segments), and barge-in fires from local energy VAD, never from transcriber partials. While something is interruptible (an assistant turn in flight, thinking or speaking, or a completed turn's tail still playing from Twilio's buffer), caller speech arms the shared sustained-speech barge-in guard (`src/calls/barge-in-guard.ts`, the same accounting live voice uses: speech accumulates toward 250 ms, short gaps are tolerated, a run that is mostly silence resets) and every inbound frame feeds it; outside that window the guard is dropped, so the caller's own utterance never carries into a turn that starts before the local VAD ends it. Only a fired guard reaches `CallController.handleBargeIn`, which interrupts a turn in either phase and ignores an idle controller (the playing tail is cleared instead).
|
|
622
622
|
- **Batch fallback**: otherwise the session segments turns with the energy-based `MediaTurnDetector` and transcribes each completed turn via the same role's batch API. Both halves of a call read the `telephony` role, which is why a role names its consumer rather than a boundary.
|
|
623
623
|
|
|
624
|
-
Every phone turn runs the same two-leg triage as live voice through `startVoiceTurn` (`src/calls/voice-session-bridge.ts`): `call-controller.ts` opens on a toolless front-door leg (`routingLeg: "front-door"`, the `voiceFrontDoor` call site) and drives it through the shared `createFrontDoorLegCoordinator` (`src/calls/voice-leg-coordinator.ts`), which reads the stream through the verdict machine and sequences the hand-off (pause narration, abort the leg, resolve and speak the bridge, mark it as the floor holder, start the escalated leg pinned to the conversation's own model, re-arm narration); each driver supplies only a host for how text and the bridge are spoken, how a leg is started or aborted, and (live voice only) the speculative hold and commit.
|
|
624
|
+
Every phone turn runs the same two-leg triage as live voice through `startVoiceTurn` (`src/calls/voice-session-bridge.ts`): `call-controller.ts` opens on a toolless front-door leg (`routingLeg: "front-door"`, the `voiceFrontDoor` call site) and drives it through the shared `createFrontDoorLegCoordinator` (`src/calls/voice-leg-coordinator.ts`), which reads the stream through the verdict machine and sequences the hand-off (pause narration, abort the leg, resolve and speak the bridge, mark it as the floor holder, start the escalated leg pinned to the conversation's own model, re-arm narration); each driver supplies only a host for how text and the bridge are spoken, how a leg is started or aborted, and (live voice only) the speculative hold and commit. Phone has no partial transcripts, so the hold verdict is never taught and routing is escalate-only. The controller also passes the bridge's turn callbacks (tool activity is recorded as `tool_use_started` / `tool_use_completed` call events, persisted row ids ride the `assistant_spoke` event), `launchedAtMs` for dispatch timing, and a `voiceTelemetry` bag keyed by the call session with a `phone_inbound` / `phone_outbound` entry. Both drivers share the spoken progress narration cadence (`src/calls/voice-progress-cadence.ts`, tuned by `voice.frontModel.progress`): the cadence owns the tool-activity log, the triggers (an ops burst, a long operation completing, a full interval of audible silence with news, the `maxSilenceMs` heartbeat) and the generated or static phrase, while each driver supplies its own view of audible silence (live voice from its TTS queue and playback-tail estimate; the media-stream transport from `isPlaybackIdle()` and a running sum of sent frame durations) and how to speak a phrase.
|
|
625
625
|
|
|
626
|
-
Both drivers also share the per-turn latency marks (`src/calls/voice-metrics.ts`). The controller opens a turn's marks when it dispatches, seeded with when the caller's transcript arrived
|
|
626
|
+
Both drivers also share the per-turn latency marks (`src/calls/voice-metrics.ts`). The controller opens a turn's marks when it dispatches, seeded with when the caller's transcript arrived, and stamps the leg dispatch, the first assistant delta, the first audible audio, a barge-in, and how the turn settled. Live voice streams a snapshot to its client on every mark; a call has no client on the line, so the aggregate is logged once per turn, cancelled turns included. Each call also records a `phone_call_started` / `phone_call_ended` pair on the onboarding telemetry substrate (`src/telemetry/phone-call-funnel.ts`), written by the call store at session creation and at the single terminal status transition: duration is the gap between the two rows, and a call that took no caller turn carries why on the end row (`silent_no_connect` when it never connected, `silent_no_turn` when it did and nobody spoke).
|
|
627
627
|
|
|
628
628
|
A credential preflight (`resolveTelephonyCredentialReadiness()` in `src/calls/telephony-credential-preflight.ts`) gates every call: it requires a credentialed, telephony-capable STT provider **and** a media-stream-playable TTS provider (the configured one or a credentialed playable fallback). Inbound calls that fail the preflight receive `<Say>` setup-required copy plus `<Hangup/>` instead of a doomed stream; outbound placement fails before dialing via `preflightVoiceIngress()` with the same user-facing message.
|
|
629
629
|
|
package/openapi.yaml
CHANGED
|
@@ -6025,7 +6025,6 @@ paths:
|
|
|
6025
6025
|
- minimax
|
|
6026
6026
|
- atlascloud
|
|
6027
6027
|
- baseten
|
|
6028
|
-
- ionet
|
|
6029
6028
|
- poolside
|
|
6030
6029
|
connectionName:
|
|
6031
6030
|
type: string
|
|
@@ -16974,8 +16973,8 @@ paths:
|
|
|
16974
16973
|
type: string
|
|
16975
16974
|
description:
|
|
16976
16975
|
"Filter by provider id. One of: anthropic, openai, gemini, ollama, fireworks, together, openrouter,
|
|
16977
|
-
vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten,
|
|
16978
|
-
|
|
16976
|
+
vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, poolside, typesafe,
|
|
16977
|
+
vellum"
|
|
16979
16978
|
responses:
|
|
16980
16979
|
"200":
|
|
16981
16980
|
description: Successful response
|
|
@@ -17247,8 +17246,8 @@ paths:
|
|
|
17247
17246
|
type: string
|
|
17248
17247
|
description:
|
|
17249
17248
|
"Filter by provider. One of: anthropic, openai, gemini, ollama, fireworks, together, openrouter,
|
|
17250
|
-
vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten,
|
|
17251
|
-
|
|
17249
|
+
vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, poolside, typesafe,
|
|
17250
|
+
vellum, chatgpt"
|
|
17252
17251
|
responses:
|
|
17253
17252
|
"200":
|
|
17254
17253
|
description: Successful response
|
|
@@ -39426,7 +39425,6 @@ components:
|
|
|
39426
39425
|
- minimax
|
|
39427
39426
|
- atlascloud
|
|
39428
39427
|
- baseten
|
|
39429
|
-
- ionet
|
|
39430
39428
|
- poolside
|
|
39431
39429
|
- typesafe
|
|
39432
39430
|
- vellum
|
|
@@ -39691,7 +39689,6 @@ components:
|
|
|
39691
39689
|
- minimax
|
|
39692
39690
|
- atlascloud
|
|
39693
39691
|
- baseten
|
|
39694
|
-
- ionet
|
|
39695
39692
|
- poolside
|
|
39696
39693
|
connectionName:
|
|
39697
39694
|
type: string
|
|
@@ -40080,7 +40077,6 @@ components:
|
|
|
40080
40077
|
- minimax
|
|
40081
40078
|
- atlascloud
|
|
40082
40079
|
- baseten
|
|
40083
|
-
- ionet
|
|
40084
40080
|
- poolside
|
|
40085
40081
|
- type: "null"
|
|
40086
40082
|
connectionName:
|
package/package.json
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
10
10
|
|
|
11
|
-
import { MESSAGE_KEYS } from "../i18n/index.js";
|
|
12
11
|
import { setConfig } from "./helpers/set-config.js";
|
|
13
12
|
|
|
14
13
|
// Keep the memory system off so addMessage skips indexing side effects.
|
|
@@ -32,7 +31,6 @@ function resetTables() {
|
|
|
32
31
|
|
|
33
32
|
interface MessagePayload {
|
|
34
33
|
role: string;
|
|
35
|
-
textSegments?: string[];
|
|
36
34
|
clientMessageId?: string;
|
|
37
35
|
}
|
|
38
36
|
|
|
@@ -61,47 +59,6 @@ describe("handleListMessages clientMessageId", () => {
|
|
|
61
59
|
expect(body.messages[0].clientMessageId).toBe("nonce-abc");
|
|
62
60
|
});
|
|
63
61
|
|
|
64
|
-
test("localizes persisted assistant keys without rewriting legacy text", async () => {
|
|
65
|
-
const localizedConv = createConversation();
|
|
66
|
-
await addMessage(
|
|
67
|
-
localizedConv.id,
|
|
68
|
-
"assistant",
|
|
69
|
-
JSON.stringify([
|
|
70
|
-
{
|
|
71
|
-
type: "text",
|
|
72
|
-
text: MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP,
|
|
73
|
-
},
|
|
74
|
-
]),
|
|
75
|
-
{ skipIndexing: true },
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const localizedResponse = await handleListMessages({
|
|
79
|
-
queryParams: { conversationId: localizedConv.id },
|
|
80
|
-
headers: { "accept-language": "es" },
|
|
81
|
-
});
|
|
82
|
-
const localizedBody = localizedResponse as { messages: MessagePayload[] };
|
|
83
|
-
expect(localizedBody.messages[0]?.textSegments).toEqual([
|
|
84
|
-
"Me detuve después de varias llamadas de herramienta con formato incorrecto para evitar que esta conversación creciera indefinidamente. Inténtalo de nuevo o cambia de modelo si el problema continúa.",
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
const legacyConv = createConversation();
|
|
88
|
-
const legacy =
|
|
89
|
-
"I stopped after repeated malformed tool calls to keep this conversation from growing indefinitely. Please try again, or switch models if the problem continues.";
|
|
90
|
-
await addMessage(
|
|
91
|
-
legacyConv.id,
|
|
92
|
-
"assistant",
|
|
93
|
-
JSON.stringify([{ type: "text", text: legacy }]),
|
|
94
|
-
{ skipIndexing: true },
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const legacyResponse = await handleListMessages({
|
|
98
|
-
queryParams: { conversationId: legacyConv.id },
|
|
99
|
-
headers: { "accept-language": "es" },
|
|
100
|
-
});
|
|
101
|
-
const legacyBody = legacyResponse as { messages: MessagePayload[] };
|
|
102
|
-
expect(legacyBody.messages[0]?.textSegments).toEqual([legacy]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
62
|
test("omits clientMessageId when the row was persisted without one", async () => {
|
|
106
63
|
// GIVEN a user message persisted without a client nonce (e.g. a channel send)
|
|
107
64
|
const conv = createConversation();
|
|
@@ -1178,33 +1178,6 @@ describe("OpenAIProvider", () => {
|
|
|
1178
1178
|
});
|
|
1179
1179
|
});
|
|
1180
1180
|
|
|
1181
|
-
test("bounds malformed arguments in the diagnostic raw response only", async () => {
|
|
1182
|
-
const raw = '{"content":"' + "x".repeat(1_000);
|
|
1183
|
-
fakeChunks = [
|
|
1184
|
-
...toolCallChunks([{ id: "call_large_bad", name: "test", args: raw }]),
|
|
1185
|
-
usageChunk(10, 5),
|
|
1186
|
-
];
|
|
1187
|
-
|
|
1188
|
-
const result = await provider.sendMessage([userMsg("test")]);
|
|
1189
|
-
|
|
1190
|
-
expect(result.content[0]).toEqual({
|
|
1191
|
-
type: "tool_use",
|
|
1192
|
-
id: "call_large_bad",
|
|
1193
|
-
name: "test",
|
|
1194
|
-
input: { _raw: raw },
|
|
1195
|
-
});
|
|
1196
|
-
const rawResponse = result.rawResponse as {
|
|
1197
|
-
choices: Array<{
|
|
1198
|
-
message: {
|
|
1199
|
-
tool_calls: Array<{ function: { arguments: string } }>;
|
|
1200
|
-
};
|
|
1201
|
-
}>;
|
|
1202
|
-
};
|
|
1203
|
-
expect(
|
|
1204
|
-
rawResponse.choices[0]!.message.tool_calls[0]!.function.arguments,
|
|
1205
|
-
).toBe(raw.slice(0, 200) + "…");
|
|
1206
|
-
});
|
|
1207
|
-
|
|
1208
1181
|
// -----------------------------------------------------------------------
|
|
1209
1182
|
// stream_options and model
|
|
1210
1183
|
// -----------------------------------------------------------------------
|
|
@@ -188,17 +188,6 @@ describe("recoverSkillExecuteEnvelope", () => {
|
|
|
188
188
|
expect(recoverSkillExecuteEnvelope(wrapped)).toBe(wrapped);
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
-
test("keeps recovered JSON wrapped when it lacks a string tool name", () => {
|
|
192
|
-
for (const raw of [
|
|
193
|
-
JSON.stringify({ input: { content: "hi" }, activity: "x" }),
|
|
194
|
-
JSON.stringify({ tool: 42, input: {}, activity: "x" }),
|
|
195
|
-
JSON.stringify(["document_update"]),
|
|
196
|
-
]) {
|
|
197
|
-
const wrapped = { _raw: raw };
|
|
198
|
-
expect(recoverSkillExecuteEnvelope(wrapped)).toBe(wrapped);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
191
|
test("passes a normal envelope through untouched", () => {
|
|
203
192
|
const envelope = {
|
|
204
193
|
tool: "document_update",
|
|
@@ -1357,10 +1357,6 @@ describe("ToolApprovalHandler / unparseable tool args gate", () => {
|
|
|
1357
1357
|
}
|
|
1358
1358
|
expect(result.result.content).not.toContain(raw);
|
|
1359
1359
|
expect(result.result.content).toContain("…");
|
|
1360
|
-
expect(auditCalls.error).toHaveLength(1);
|
|
1361
|
-
expect(auditCalls.error[0].input).toEqual({
|
|
1362
|
-
_raw: raw.slice(0, 200) + "…",
|
|
1363
|
-
});
|
|
1364
1360
|
});
|
|
1365
1361
|
|
|
1366
1362
|
test("legitimate input containing a _raw field among others is not rejected", async () => {
|
|
@@ -341,32 +341,6 @@ describe("tool preview lifecycle", () => {
|
|
|
341
341
|
expect((emitted as any).conversationId).toBe("test-session-id");
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
-
test("caps cumulative app preview payloads and stops forwarding growth", () => {
|
|
345
|
-
const collector = createEventCollector();
|
|
346
|
-
const deps = createMockDeps({ onEvent: collector.onEvent });
|
|
347
|
-
const accumulatedJson = '{"code":"' + "x".repeat(300_000);
|
|
348
|
-
|
|
349
|
-
handleInputJsonDelta(state, deps, {
|
|
350
|
-
type: "input_json_delta",
|
|
351
|
-
toolName: "app_create",
|
|
352
|
-
toolUseId: "toolu_large_app",
|
|
353
|
-
accumulatedJson,
|
|
354
|
-
});
|
|
355
|
-
handleInputJsonDelta(state, deps, {
|
|
356
|
-
type: "input_json_delta",
|
|
357
|
-
toolName: "app_create",
|
|
358
|
-
toolUseId: "toolu_large_app",
|
|
359
|
-
accumulatedJson: accumulatedJson + "more",
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
expect(collector.events).toHaveLength(1);
|
|
363
|
-
const emitted = collector.events[0] as Extract<
|
|
364
|
-
AssistantEvent,
|
|
365
|
-
{ type: "tool_input_delta" }
|
|
366
|
-
>;
|
|
367
|
-
expect(emitted.content).toBe(accumulatedJson.slice(0, 256_000));
|
|
368
|
-
});
|
|
369
|
-
|
|
370
344
|
test("a streaming ui_show visual announces its surface exactly once", () => {
|
|
371
345
|
const collector = createEventCollector();
|
|
372
346
|
const deps = createMockDeps({ onEvent: collector.onEvent });
|
package/src/agent/loop.ts
CHANGED
|
@@ -28,12 +28,6 @@ import type {
|
|
|
28
28
|
PreModelCallInputContext,
|
|
29
29
|
StopInputContext,
|
|
30
30
|
} from "../hooks/types.js";
|
|
31
|
-
import {
|
|
32
|
-
MESSAGE_KEYS,
|
|
33
|
-
resolveStoredMessageText,
|
|
34
|
-
type SupportedLocale,
|
|
35
|
-
t,
|
|
36
|
-
} from "../i18n/index.js";
|
|
37
31
|
import {
|
|
38
32
|
timeSyncSection,
|
|
39
33
|
traceAsyncSection,
|
|
@@ -58,10 +52,6 @@ import type {
|
|
|
58
52
|
ToolResultContent,
|
|
59
53
|
} from "../providers/types.js";
|
|
60
54
|
import { NATIVE_WEB_SEARCH_TOOL_NAME } from "../providers/types.js";
|
|
61
|
-
import {
|
|
62
|
-
boundUnparseableToolArgs,
|
|
63
|
-
isUnparseableToolArgs,
|
|
64
|
-
} from "../providers/unparseable-tool-args.js";
|
|
65
55
|
import { recordWatchdogEvent } from "../telemetry/watchdog-events-store.js";
|
|
66
56
|
import {
|
|
67
57
|
ABORT_SETTLE_GRACE_MS,
|
|
@@ -74,7 +64,6 @@ import {
|
|
|
74
64
|
applyStreamingSubstitution,
|
|
75
65
|
applySubstitutions,
|
|
76
66
|
} from "../tools/sensitive-output-placeholders.js";
|
|
77
|
-
import { recoverSkillExecuteEnvelope } from "../tools/skills/execute.js";
|
|
78
67
|
import {
|
|
79
68
|
abortedToolResultText,
|
|
80
69
|
isPreemptedByNewMessage,
|
|
@@ -94,8 +83,6 @@ const log = getLogger("agent-loop");
|
|
|
94
83
|
|
|
95
84
|
/** Watchdog check name for both tool-gated reply outcomes. */
|
|
96
85
|
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";
|
|
99
86
|
|
|
100
87
|
/**
|
|
101
88
|
* The user-facing message a `send_user_message` block carries, or null when
|
|
@@ -709,8 +696,6 @@ interface AgentLoopRunOptionsBase {
|
|
|
709
696
|
injectionLedgerResets?: Set<string>;
|
|
710
697
|
signal?: AbortSignal;
|
|
711
698
|
requestId: string;
|
|
712
|
-
/** Locale for daemon-authored text emitted during this run. */
|
|
713
|
-
locale?: SupportedLocale;
|
|
714
699
|
/**
|
|
715
700
|
* Explicit model override (provider/model string) for every LLM call in
|
|
716
701
|
* this run. When omitted, the model is resolved through the normal
|
|
@@ -1026,31 +1011,12 @@ function toolResultEventFields(
|
|
|
1026
1011
|
interface NormalizedToolUse {
|
|
1027
1012
|
/** Assistant content with at most one `tool_use` block per call id. */
|
|
1028
1013
|
content: ContentBlock[];
|
|
1029
|
-
/** The
|
|
1014
|
+
/** The `tool_use` blocks in `content`, in order. */
|
|
1030
1015
|
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;
|
|
1035
1016
|
/** Coalesced copies: a call id and name for each block dropped. */
|
|
1036
1017
|
duplicates: Array<{ id: string; name: string }>;
|
|
1037
1018
|
}
|
|
1038
1019
|
|
|
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
|
-
|
|
1054
1020
|
/**
|
|
1055
1021
|
* Resolves an assistant reply's `tool_use` blocks into an executable set keyed
|
|
1056
1022
|
* by call id: a block with no id gets one, and a repeat of an id already in the
|
|
@@ -1064,10 +1030,8 @@ function normalizeToolUseBlocks(
|
|
|
1064
1030
|
): NormalizedToolUse {
|
|
1065
1031
|
const nextContent: ContentBlock[] = [];
|
|
1066
1032
|
const toolUseBlocks: ToolUseBlock[] = [];
|
|
1067
|
-
const executionInputsById = new Map<string, Record<string, unknown>>();
|
|
1068
1033
|
const duplicates: Array<{ id: string; name: string }> = [];
|
|
1069
1034
|
const seenIds = new Set<string>();
|
|
1070
|
-
let unparseableCount = 0;
|
|
1071
1035
|
|
|
1072
1036
|
for (const block of content) {
|
|
1073
1037
|
if (block.type !== "tool_use") {
|
|
@@ -1078,28 +1042,14 @@ function normalizeToolUseBlocks(
|
|
|
1078
1042
|
duplicates.push({ id: block.id, name: block.name });
|
|
1079
1043
|
continue;
|
|
1080
1044
|
}
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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);
|
|
1045
|
+
const normalized: ToolUseBlock =
|
|
1046
|
+
block.id.length === 0 ? { ...block, id: crypto.randomUUID() } : block;
|
|
1047
|
+
seenIds.add(normalized.id);
|
|
1048
|
+
nextContent.push(normalized);
|
|
1049
|
+
toolUseBlocks.push(normalized);
|
|
1094
1050
|
}
|
|
1095
1051
|
|
|
1096
|
-
return {
|
|
1097
|
-
content: nextContent,
|
|
1098
|
-
toolUseBlocks,
|
|
1099
|
-
executionInputsById,
|
|
1100
|
-
unparseableCount,
|
|
1101
|
-
duplicates,
|
|
1102
|
-
};
|
|
1052
|
+
return { content: nextContent, toolUseBlocks, duplicates };
|
|
1103
1053
|
}
|
|
1104
1054
|
|
|
1105
1055
|
/**
|
|
@@ -1536,7 +1486,6 @@ export class AgentLoop {
|
|
|
1536
1486
|
let newMessagesStart = history.length;
|
|
1537
1487
|
let toolUseTurns = 0;
|
|
1538
1488
|
let postModelCallContinues = 0;
|
|
1539
|
-
let consecutiveUnparseableToolTurns = 0;
|
|
1540
1489
|
// Whether the user has been told the OUTCOME of the work so far, not
|
|
1541
1490
|
// merely that work started. True only while the most recent tool-bearing
|
|
1542
1491
|
// response was `send_user_message` and nothing else: a response that sends
|
|
@@ -1792,7 +1741,6 @@ export class AgentLoop {
|
|
|
1792
1741
|
// request from a generation that died mid-flight. Declared here so that
|
|
1793
1742
|
// catch can reach it.
|
|
1794
1743
|
let streamedTokens = false;
|
|
1795
|
-
let unparseableToolRetryLimitReached = false;
|
|
1796
1744
|
|
|
1797
1745
|
try {
|
|
1798
1746
|
// ── Pre-call budget gate ─────────────────────────────────────
|
|
@@ -2780,44 +2728,6 @@ export class AgentLoop {
|
|
|
2780
2728
|
content: normalizedToolUse.content,
|
|
2781
2729
|
};
|
|
2782
2730
|
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
|
-
}
|
|
2821
2731
|
|
|
2822
2732
|
// At the no-tool stop boundary the retry decision is actionable: a
|
|
2823
2733
|
// recovery hook may repair history and ask to re-query (a tool-bearing
|
|
@@ -2826,7 +2736,6 @@ export class AgentLoop {
|
|
|
2826
2736
|
// persisting it; the per-run backstop keeps a misbehaving hook from
|
|
2827
2737
|
// spinning forever.
|
|
2828
2738
|
if (
|
|
2829
|
-
!unparseableToolRetryLimitReached &&
|
|
2830
2739
|
toolUseBlocks.length === 0 &&
|
|
2831
2740
|
postModelCallDecision === "continue"
|
|
2832
2741
|
) {
|
|
@@ -2888,40 +2797,11 @@ export class AgentLoop {
|
|
|
2888
2797
|
// Surface the finalized text if the client saw nothing live (a
|
|
2889
2798
|
// deferred stream, a hook-rewritten empty turn, or the tool-gated
|
|
2890
2799
|
// fallback where no `send_user_message` call ever reached the user).
|
|
2891
|
-
|
|
2892
|
-
|
|
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, {
|
|
2800
|
+
const textVisibility = textVisibilityOf(
|
|
2801
|
+
emitFinalAssistantText(assistantMessage.content, {
|
|
2921
2802
|
turnEnding: toolUseBlocks.length === 0,
|
|
2922
|
-
})
|
|
2923
|
-
|
|
2924
|
-
const textVisibility = textVisibilityOf(fallbackSurfaced);
|
|
2803
|
+
}),
|
|
2804
|
+
);
|
|
2925
2805
|
|
|
2926
2806
|
history.push(assistantMessage);
|
|
2927
2807
|
|
|
@@ -3034,8 +2914,7 @@ export class AgentLoop {
|
|
|
3034
2914
|
}
|
|
3035
2915
|
const result = await this.toolExecutor!(
|
|
3036
2916
|
toolUse.name,
|
|
3037
|
-
|
|
3038
|
-
toolUse.input,
|
|
2917
|
+
toolUse.input,
|
|
3039
2918
|
(chunk) => {
|
|
3040
2919
|
onEvent({
|
|
3041
2920
|
type: "tool_output_chunk",
|