@vellumai/assistant 0.12.2-dev.202609181515.7e76c92 → 0.12.2-dev.202609181615.645c0a5
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/package.json +1 -1
- package/src/__tests__/agent-loop-output-hooks.test.ts +150 -1
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +8 -2
- package/src/__tests__/conversation-agent-loop.test.ts +120 -2
- package/src/agent/loop.ts +76 -11
- package/src/calls/__tests__/voice-session-bridge.test.ts +342 -14
- package/src/calls/voice-escalation-target.ts +11 -0
- package/src/calls/voice-session-bridge.ts +140 -51
- package/src/daemon/conversation-agent-loop.ts +46 -18
- package/src/daemon/conversation.ts +80 -32
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +154 -15
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +172 -0
- package/src/live-voice/live-activity-reporter.ts +115 -6
- package/src/live-voice/live-voice-session.ts +90 -15
- package/src/live-voice/protocol.ts +10 -2
- package/src/plugin-api/vision-support.test.ts +22 -0
- package/src/plugin-api/vision-support.ts +17 -5
- package/src/runtime/routes/conversation-routes.ts +1 -1
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
8
8
|
|
|
9
|
-
import type { AgentEvent } from "../agent/loop.js";
|
|
9
|
+
import type { AgentEvent, PreparedModelCall } from "../agent/loop.js";
|
|
10
10
|
import { AgentLoop } from "../agent/loop.js";
|
|
11
11
|
import type {
|
|
12
12
|
PostModelCallContext,
|
|
@@ -18,12 +18,14 @@ import type {
|
|
|
18
18
|
ContentBlock,
|
|
19
19
|
Message,
|
|
20
20
|
ProviderResponse,
|
|
21
|
+
ToolDefinition,
|
|
21
22
|
} from "../providers/types.js";
|
|
22
23
|
import {
|
|
23
24
|
createMockProvider,
|
|
24
25
|
textResponse,
|
|
25
26
|
toolUseResponse,
|
|
26
27
|
} from "./helpers/mock-provider.js";
|
|
28
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
27
29
|
|
|
28
30
|
const userMessage: Message = {
|
|
29
31
|
role: "user",
|
|
@@ -91,6 +93,7 @@ function registerOutputHookPlugin(hooks: {
|
|
|
91
93
|
describe("agent loop output hooks", () => {
|
|
92
94
|
beforeEach(() => {
|
|
93
95
|
resetPluginRegistryAndRegisterDefaults();
|
|
96
|
+
setConfig("llm", {});
|
|
94
97
|
});
|
|
95
98
|
|
|
96
99
|
test("post-model-call transforms the persisted message content", async () => {
|
|
@@ -403,6 +406,152 @@ describe("agent loop output hooks", () => {
|
|
|
403
406
|
expect(calls[0].options?.config?.overrideProfile).toBe("fast-profile");
|
|
404
407
|
});
|
|
405
408
|
|
|
409
|
+
test("prepared model call carries the post-hook route and wire tool surface", async () => {
|
|
410
|
+
registerOutputHookPlugin({
|
|
411
|
+
preModelCall: (ctx) => {
|
|
412
|
+
ctx.systemPrompt = `${ctx.systemPrompt ?? ""} [EDITED]`;
|
|
413
|
+
ctx.modelProfile = "fast-profile";
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
const tool: ToolDefinition = {
|
|
417
|
+
name: "dynamic_tool",
|
|
418
|
+
description: "Dynamic",
|
|
419
|
+
input_schema: { type: "object" },
|
|
420
|
+
};
|
|
421
|
+
const { provider, calls } = createMockProvider([textResponse("hi")]);
|
|
422
|
+
Object.assign(provider, { supportsNativeWebSearch: true });
|
|
423
|
+
const loop = new AgentLoop({
|
|
424
|
+
provider,
|
|
425
|
+
systemPrompt: "base prompt",
|
|
426
|
+
conversationId: "test-conversation",
|
|
427
|
+
config: { enableNativeWebSearch: true },
|
|
428
|
+
resolveTools: () => [tool],
|
|
429
|
+
});
|
|
430
|
+
let prepared: PreparedModelCall | undefined;
|
|
431
|
+
|
|
432
|
+
await loop.run({
|
|
433
|
+
requestId: "test-request",
|
|
434
|
+
messages: [userMessage],
|
|
435
|
+
onEvent: collect([]),
|
|
436
|
+
callSite: "mainAgent",
|
|
437
|
+
overrideProfile: "conversation-profile",
|
|
438
|
+
forceOverrideProfile: true,
|
|
439
|
+
onModelCallPrepared: (value) => {
|
|
440
|
+
prepared = value;
|
|
441
|
+
},
|
|
442
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(prepared).toMatchObject({
|
|
446
|
+
callSite: "mainAgent",
|
|
447
|
+
overrideProfile: "fast-profile",
|
|
448
|
+
forceOverrideProfile: true,
|
|
449
|
+
systemPrompt: "base prompt [EDITED]",
|
|
450
|
+
});
|
|
451
|
+
expect(prepared?.tools.map((item) => item.name)).toEqual([
|
|
452
|
+
"dynamic_tool",
|
|
453
|
+
"web_search",
|
|
454
|
+
]);
|
|
455
|
+
expect(calls[0].tools).toEqual(prepared?.tools);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("prepared model call carries the finalized cache policy", async () => {
|
|
459
|
+
setConfig("llm", {
|
|
460
|
+
callSites: { mainAgent: { disableCache: true } },
|
|
461
|
+
});
|
|
462
|
+
const { provider } = createMockProvider([textResponse("hi")]);
|
|
463
|
+
const loop = new AgentLoop({
|
|
464
|
+
provider,
|
|
465
|
+
systemPrompt: "base prompt",
|
|
466
|
+
conversationId: "test-conversation",
|
|
467
|
+
});
|
|
468
|
+
let prepared: PreparedModelCall | undefined;
|
|
469
|
+
|
|
470
|
+
await loop.run({
|
|
471
|
+
requestId: "test-request",
|
|
472
|
+
messages: [userMessage],
|
|
473
|
+
onEvent: collect([]),
|
|
474
|
+
callSite: "mainAgent",
|
|
475
|
+
onModelCallPrepared: (value) => {
|
|
476
|
+
prepared = value;
|
|
477
|
+
},
|
|
478
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(prepared?.disableCache).toBe(true);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("prepared model call preserves explicit system-prompt removal", async () => {
|
|
485
|
+
registerOutputHookPlugin({
|
|
486
|
+
preModelCall: (ctx) => {
|
|
487
|
+
ctx.systemPrompt = null;
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
const { provider, calls } = createMockProvider([textResponse("hi")]);
|
|
491
|
+
const loop = new AgentLoop({
|
|
492
|
+
provider,
|
|
493
|
+
systemPrompt: "base prompt",
|
|
494
|
+
conversationId: "test-conversation",
|
|
495
|
+
});
|
|
496
|
+
let prepared: PreparedModelCall | undefined;
|
|
497
|
+
|
|
498
|
+
await loop.run({
|
|
499
|
+
requestId: "test-request",
|
|
500
|
+
messages: [userMessage],
|
|
501
|
+
onEvent: collect([]),
|
|
502
|
+
callSite: "mainAgent",
|
|
503
|
+
onModelCallPrepared: (value) => {
|
|
504
|
+
prepared = value;
|
|
505
|
+
},
|
|
506
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(prepared?.systemPrompt).toBeNull();
|
|
510
|
+
expect(calls[0].options?.systemPrompt).toBeUndefined();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("inference routing preserves the semantic call site for hooks and events", async () => {
|
|
514
|
+
const preModelCallSites: Array<string | null> = [];
|
|
515
|
+
const postModelCallSites: Array<string | null> = [];
|
|
516
|
+
registerOutputHookPlugin({
|
|
517
|
+
preModelCall: (ctx) => {
|
|
518
|
+
preModelCallSites.push(ctx.callSite);
|
|
519
|
+
},
|
|
520
|
+
postModelCall: (ctx) => {
|
|
521
|
+
postModelCallSites.push(ctx.callSite);
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
const { provider, calls } = createMockProvider([textResponse("hi")]);
|
|
525
|
+
const loop = new AgentLoop({
|
|
526
|
+
provider,
|
|
527
|
+
systemPrompt: "system",
|
|
528
|
+
conversationId: "test-conversation",
|
|
529
|
+
});
|
|
530
|
+
const events: AgentEvent[] = [];
|
|
531
|
+
let prepared: PreparedModelCall | undefined;
|
|
532
|
+
|
|
533
|
+
await loop.run({
|
|
534
|
+
requestId: "test-request",
|
|
535
|
+
messages: [userMessage],
|
|
536
|
+
onEvent: collect(events),
|
|
537
|
+
callSite: "callAgent",
|
|
538
|
+
inferenceCallSite: "mainAgent",
|
|
539
|
+
onModelCallPrepared: (value) => {
|
|
540
|
+
prepared = value;
|
|
541
|
+
},
|
|
542
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
expect(calls[0].options?.config?.callSite).toBe("mainAgent");
|
|
546
|
+
expect(prepared?.callSite).toBe("mainAgent");
|
|
547
|
+
expect(preModelCallSites).toEqual(["callAgent"]);
|
|
548
|
+
expect(postModelCallSites).toEqual(["callAgent"]);
|
|
549
|
+
expect(events).toContainEqual({
|
|
550
|
+
type: "llm_call_started",
|
|
551
|
+
callSite: "callAgent",
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
406
555
|
test("pre-model-call seeds modelProfile from the resolved override and clearing it drops the override", async () => {
|
|
407
556
|
// GIVEN a hook that observes the seeded override and then clears it
|
|
408
557
|
let seeded: string | null | undefined;
|
|
@@ -169,6 +169,7 @@ describe("runAgentLoopImpl disk pressure gate", () => {
|
|
|
169
169
|
const events: AssistantEvent[] = [];
|
|
170
170
|
const activityStates: unknown[][] = [];
|
|
171
171
|
const drainQueue = mock(async (_reason: unknown) => {});
|
|
172
|
+
const onFirstModelCallPrepared = mock(() => {});
|
|
172
173
|
const ctx = makeCtx({
|
|
173
174
|
emitActivityState: (...args: unknown[]) => {
|
|
174
175
|
activityStates.push(args);
|
|
@@ -176,8 +177,12 @@ describe("runAgentLoopImpl disk pressure gate", () => {
|
|
|
176
177
|
drainQueue,
|
|
177
178
|
});
|
|
178
179
|
|
|
179
|
-
await runAgentLoopImpl(
|
|
180
|
-
|
|
180
|
+
await runAgentLoopImpl(
|
|
181
|
+
ctx,
|
|
182
|
+
"background task",
|
|
183
|
+
"msg-1",
|
|
184
|
+
(event) => events.push(event),
|
|
185
|
+
{ onFirstModelCallPrepared },
|
|
181
186
|
);
|
|
182
187
|
|
|
183
188
|
expect(events.find((event) => event.type === "error")).toMatchObject({
|
|
@@ -195,5 +200,6 @@ describe("runAgentLoopImpl disk pressure gate", () => {
|
|
|
195
200
|
expect(ctx.abortController).toBeNull();
|
|
196
201
|
expect(ctx.currentRequestId).toBeUndefined();
|
|
197
202
|
expect(drainQueue).toHaveBeenCalledWith("loop_complete");
|
|
203
|
+
expect(onFirstModelCallPrepared).not.toHaveBeenCalled();
|
|
198
204
|
});
|
|
199
205
|
});
|
|
@@ -24,7 +24,12 @@ import { getConversationDirName } from "../persistence/conversation-directories.
|
|
|
24
24
|
import type { UserPromptSubmitContext } from "../plugin-api/types.js";
|
|
25
25
|
import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
|
|
26
26
|
import { registerPlugin } from "../plugins/registry.js";
|
|
27
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
Message,
|
|
29
|
+
Provider,
|
|
30
|
+
SendMessageOptions,
|
|
31
|
+
ToolDefinition,
|
|
32
|
+
} from "../providers/types.js";
|
|
28
33
|
import { ContextOverflowError } from "../providers/types.js";
|
|
29
34
|
import {
|
|
30
35
|
resolveUsageAttribution,
|
|
@@ -715,7 +720,7 @@ mock.module("../persistence/llm-request-log-store.js", () => ({
|
|
|
715
720
|
// ── Imports (after mocks) ────────────────────────────────────────────
|
|
716
721
|
|
|
717
722
|
import { AgentLoop } from "../agent/loop.js";
|
|
718
|
-
import
|
|
723
|
+
import { Conversation } from "../daemon/conversation.js";
|
|
719
724
|
import {
|
|
720
725
|
applyCompactionResult,
|
|
721
726
|
runAgentLoopImpl,
|
|
@@ -1100,6 +1105,82 @@ beforeEach(() => {
|
|
|
1100
1105
|
resetPluginRegistryAndRegisterDefaults();
|
|
1101
1106
|
});
|
|
1102
1107
|
|
|
1108
|
+
describe("prompt cache warming", () => {
|
|
1109
|
+
test("attributes provider usage to the conversation", async () => {
|
|
1110
|
+
const sendMessage = mock(
|
|
1111
|
+
async (_messages: Message[], _options?: SendMessageOptions) =>
|
|
1112
|
+
textResponse("unused"),
|
|
1113
|
+
);
|
|
1114
|
+
const conversation = Object.assign(
|
|
1115
|
+
Object.create(Conversation.prototype) as object,
|
|
1116
|
+
{
|
|
1117
|
+
conversationId: "conv-cache-warm-test",
|
|
1118
|
+
messages: [],
|
|
1119
|
+
provider: { name: "mock-provider", sendMessage },
|
|
1120
|
+
agentLoop: { getResolvedTools: () => [] },
|
|
1121
|
+
buildCurrentSystemPrompt: () => "system prompt",
|
|
1122
|
+
},
|
|
1123
|
+
) as unknown as Conversation;
|
|
1124
|
+
|
|
1125
|
+
await conversation.warmPromptCache();
|
|
1126
|
+
|
|
1127
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
1128
|
+
expect(sendMessage.mock.calls[0]?.[1]?.config).toMatchObject({
|
|
1129
|
+
callSite: "mainAgent",
|
|
1130
|
+
conversationId: "conv-cache-warm-test",
|
|
1131
|
+
max_tokens: 16,
|
|
1132
|
+
selectionSeed: "conv-cache-warm-test",
|
|
1133
|
+
});
|
|
1134
|
+
expect(
|
|
1135
|
+
sendMessage.mock.calls[0]?.[1]?.config?.usageTracking,
|
|
1136
|
+
).toBeUndefined();
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
test("stays non-rejecting when request preparation fails", async () => {
|
|
1140
|
+
const sendMessage = mock(async () => textResponse("unused"));
|
|
1141
|
+
const conversation = Object.assign(
|
|
1142
|
+
Object.create(Conversation.prototype) as object,
|
|
1143
|
+
{
|
|
1144
|
+
conversationId: "conv-cache-warm-test",
|
|
1145
|
+
messages: [],
|
|
1146
|
+
provider: { name: "mock-provider", sendMessage },
|
|
1147
|
+
agentLoop: {
|
|
1148
|
+
getResolvedTools: () => {
|
|
1149
|
+
throw new Error("tool resolution failed");
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1152
|
+
buildCurrentSystemPrompt: () => "system prompt",
|
|
1153
|
+
},
|
|
1154
|
+
) as unknown as Conversation;
|
|
1155
|
+
|
|
1156
|
+
await expect(conversation.warmPromptCache()).resolves.toBeUndefined();
|
|
1157
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
test("does not rebuild a system prompt explicitly removed by a hook", async () => {
|
|
1161
|
+
const sendMessage = mock(
|
|
1162
|
+
async (_messages: Message[], _options?: SendMessageOptions) =>
|
|
1163
|
+
textResponse("unused"),
|
|
1164
|
+
);
|
|
1165
|
+
const buildCurrentSystemPrompt = mock(() => "rebuilt prompt");
|
|
1166
|
+
const conversation = Object.assign(
|
|
1167
|
+
Object.create(Conversation.prototype) as object,
|
|
1168
|
+
{
|
|
1169
|
+
conversationId: "conv-cache-warm-test",
|
|
1170
|
+
messages: [],
|
|
1171
|
+
provider: { name: "mock-provider", sendMessage },
|
|
1172
|
+
agentLoop: { getResolvedTools: () => [] },
|
|
1173
|
+
buildCurrentSystemPrompt,
|
|
1174
|
+
},
|
|
1175
|
+
) as unknown as Conversation;
|
|
1176
|
+
|
|
1177
|
+
await conversation.warmPromptCache({ systemPrompt: null, tools: [] });
|
|
1178
|
+
|
|
1179
|
+
expect(buildCurrentSystemPrompt).not.toHaveBeenCalled();
|
|
1180
|
+
expect(sendMessage.mock.calls[0]?.[1]?.systemPrompt).toBeUndefined();
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1103
1184
|
describe("session-agent-loop", () => {
|
|
1104
1185
|
describe("user-prompt-submit hook failures", () => {
|
|
1105
1186
|
test("passes the effective profile to hooks even when it was already announced", async () => {
|
|
@@ -2165,6 +2246,43 @@ describe("session-agent-loop", () => {
|
|
|
2165
2246
|
const call = recordRequestLogMock.mock.calls[0] as unknown as unknown[];
|
|
2166
2247
|
expect(call[5]).toBe("callAgent");
|
|
2167
2248
|
});
|
|
2249
|
+
|
|
2250
|
+
test("reports only the first finalized model call", async () => {
|
|
2251
|
+
const tool: ToolDefinition = {
|
|
2252
|
+
name: "echo",
|
|
2253
|
+
description: "Echo",
|
|
2254
|
+
input_schema: { type: "object" },
|
|
2255
|
+
};
|
|
2256
|
+
const onFirstModelCallPrepared = mock(() => {});
|
|
2257
|
+
const ctx = makeCtx({
|
|
2258
|
+
providerResponses: [
|
|
2259
|
+
toolUseResponse("tool-1", "echo", {}),
|
|
2260
|
+
textResponse("done"),
|
|
2261
|
+
],
|
|
2262
|
+
loopTools: [tool],
|
|
2263
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
2264
|
+
});
|
|
2265
|
+
const turnSignal = ctx.abortController?.signal;
|
|
2266
|
+
|
|
2267
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
|
|
2268
|
+
callSite: "callAgent",
|
|
2269
|
+
inferenceCallSite: "mainAgent",
|
|
2270
|
+
overrideProfile: "quality-optimized",
|
|
2271
|
+
forceOverrideProfile: true,
|
|
2272
|
+
onFirstModelCallPrepared,
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
expect(onFirstModelCallPrepared).toHaveBeenCalledTimes(1);
|
|
2276
|
+
expect(onFirstModelCallPrepared).toHaveBeenCalledWith({
|
|
2277
|
+
callSite: "mainAgent",
|
|
2278
|
+
overrideProfile: "quality-optimized",
|
|
2279
|
+
forceOverrideProfile: true,
|
|
2280
|
+
signal: turnSignal,
|
|
2281
|
+
systemPrompt: "system prompt",
|
|
2282
|
+
tools: [tool],
|
|
2283
|
+
});
|
|
2284
|
+
expect(ctx.currentCallSite).toBe("callAgent");
|
|
2285
|
+
});
|
|
2168
2286
|
});
|
|
2169
2287
|
|
|
2170
2288
|
describe("usage accounting", () => {
|
package/src/agent/loop.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { AnsweredQuestion } from "../api/events/question-answered.js";
|
|
2
|
+
import { resolveCallSiteConfig } from "../config/llm-resolver.js";
|
|
3
|
+
import { getConfig } from "../config/loader.js";
|
|
2
4
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
3
5
|
import { SEND_USER_MESSAGE_TOOL_NAME } from "../config/send-user-message-constants.js";
|
|
4
6
|
import { recordEstimate } from "../context/estimator-calibration.js";
|
|
@@ -664,6 +666,18 @@ type AgentLoopContextWindowResolver = () => {
|
|
|
664
666
|
overflowRecovery: { enabled: boolean; safetyMarginRatio: number };
|
|
665
667
|
};
|
|
666
668
|
|
|
669
|
+
/** Final request surface after the pre-model hook has settled. */
|
|
670
|
+
export interface PreparedModelCall {
|
|
671
|
+
callSite?: LLMCallSite;
|
|
672
|
+
overrideProfile?: string;
|
|
673
|
+
forceOverrideProfile: boolean;
|
|
674
|
+
/** Present when the finalized route opts out of prompt caching. */
|
|
675
|
+
disableCache?: true;
|
|
676
|
+
signal?: AbortSignal;
|
|
677
|
+
systemPrompt: string | null;
|
|
678
|
+
tools: ToolDefinition[];
|
|
679
|
+
}
|
|
680
|
+
|
|
667
681
|
interface AgentLoopRunOptionsBase {
|
|
668
682
|
/** Input history the run starts from; the loop appends its output onto a copy. */
|
|
669
683
|
messages: Message[];
|
|
@@ -691,7 +705,10 @@ interface AgentLoopRunOptionsBase {
|
|
|
691
705
|
onCheckpoint?: (
|
|
692
706
|
checkpoint: CheckpointInfo,
|
|
693
707
|
) => CheckpointDecision | Promise<CheckpointDecision>;
|
|
708
|
+
/** Semantic call site exposed to hooks, events, and loop behavior. */
|
|
694
709
|
callSite?: LLMCallSite;
|
|
710
|
+
/** Provider-resolution call site when it differs from turn semantics. */
|
|
711
|
+
inferenceCallSite?: LLMCallSite;
|
|
695
712
|
/**
|
|
696
713
|
* Route this run's user-facing text through the `send_user_message` tool
|
|
697
714
|
* instead of streamed assistant text. The daemon sets it for main-agent runs
|
|
@@ -741,6 +758,8 @@ interface AgentLoopRunOptionsBase {
|
|
|
741
758
|
*/
|
|
742
759
|
forceOverrideProfile?: boolean;
|
|
743
760
|
resolveOverrideProfile?: () => string | undefined;
|
|
761
|
+
/** Observe a finalized model request without delaying provider dispatch. */
|
|
762
|
+
onModelCallPrepared?: (prepared: PreparedModelCall) => void;
|
|
744
763
|
/**
|
|
745
764
|
* When `true`, the loop owns turn-start and mid-loop compaction. The pre-call
|
|
746
765
|
* budget gate runs before the very first provider call — subsuming the
|
|
@@ -1430,18 +1449,21 @@ export class AgentLoop {
|
|
|
1430
1449
|
requestId,
|
|
1431
1450
|
onCheckpoint,
|
|
1432
1451
|
callSite,
|
|
1452
|
+
inferenceCallSite,
|
|
1433
1453
|
suppressAssistantText = false,
|
|
1434
1454
|
supportsDynamicUi = true,
|
|
1435
1455
|
trust,
|
|
1436
1456
|
overrideProfile,
|
|
1437
1457
|
forceOverrideProfile = false,
|
|
1438
1458
|
resolveOverrideProfile,
|
|
1459
|
+
onModelCallPrepared,
|
|
1439
1460
|
compactInPlace = false,
|
|
1440
1461
|
isNonInteractive = false,
|
|
1441
1462
|
model: runModel,
|
|
1442
1463
|
latencyTracker,
|
|
1443
1464
|
injectionLedgerResets,
|
|
1444
1465
|
} = options;
|
|
1466
|
+
const providerCallSite = inferenceCallSite ?? callSite;
|
|
1445
1467
|
// Snapshot the system prompt once per run. The instance field is mutable
|
|
1446
1468
|
// (the conversation may update it between turns), but a single run must
|
|
1447
1469
|
// use one consistent prompt — an aborted run left detached after the
|
|
@@ -1887,7 +1909,7 @@ export class AgentLoop {
|
|
|
1887
1909
|
// unexecutable client tool. The advisor consult's `advisorProfile` can
|
|
1888
1910
|
// route `subagentSpawn` to a provider/model whose native-search support
|
|
1889
1911
|
// differs from the construction-time default, so the gate resolves the
|
|
1890
|
-
// routed target (
|
|
1912
|
+
// routed target (providerCallSite + overrideProfile) via
|
|
1891
1913
|
// `supportsNativeWebSearchFor` rather than the static
|
|
1892
1914
|
// `this.provider.supportsNativeWebSearch` snapshot; providers without
|
|
1893
1915
|
// the routing-aware probe fall back to the static flag. This is a SERVER
|
|
@@ -1899,7 +1921,7 @@ export class AgentLoop {
|
|
|
1899
1921
|
.supportsNativeWebSearchFor
|
|
1900
1922
|
? this.provider.supportsNativeWebSearchFor(
|
|
1901
1923
|
buildNativeWebSearchProbeOptions(
|
|
1902
|
-
|
|
1924
|
+
providerCallSite,
|
|
1903
1925
|
resolveEffectiveOverrideProfile(),
|
|
1904
1926
|
forceOverrideProfile,
|
|
1905
1927
|
this.conversationId,
|
|
@@ -1918,11 +1940,11 @@ export class AgentLoop {
|
|
|
1918
1940
|
// 1. Per-run explicit (`runModel`)
|
|
1919
1941
|
// 2. Call-site resolved values (filled by
|
|
1920
1942
|
// `RetryProvider.normalizeSendMessageOptions` from
|
|
1921
|
-
// `resolveCallSiteConfig(
|
|
1943
|
+
// `resolveCallSiteConfig(providerCallSite, llm)`)
|
|
1922
1944
|
// 3. Conversation defaults (`this.config.*`, from the resolved
|
|
1923
1945
|
// default call-site config)
|
|
1924
1946
|
//
|
|
1925
|
-
// When `
|
|
1947
|
+
// When `providerCallSite` is present we deliberately leave
|
|
1926
1948
|
// `max_tokens`/`thinking`/`effort`/`speed` *unset* in `providerConfig`
|
|
1927
1949
|
// so the normalizer can fill them from the call-site resolution. The
|
|
1928
1950
|
// normalizer only writes these fields when they're undefined; if we
|
|
@@ -1930,10 +1952,10 @@ export class AgentLoop {
|
|
|
1930
1952
|
// for these knobs is silently ignored.
|
|
1931
1953
|
//
|
|
1932
1954
|
// `toolChoice` and `cacheTtl` are not part of the call-site schema, so
|
|
1933
|
-
// they always come from `this.config` regardless of `
|
|
1955
|
+
// they always come from `this.config` regardless of `providerCallSite`.
|
|
1934
1956
|
const providerConfig: Record<string, unknown> = {};
|
|
1935
1957
|
|
|
1936
|
-
if (!
|
|
1958
|
+
if (!providerCallSite) {
|
|
1937
1959
|
providerConfig.max_tokens = this.config.maxTokens;
|
|
1938
1960
|
}
|
|
1939
1961
|
|
|
@@ -1941,7 +1963,7 @@ export class AgentLoop {
|
|
|
1941
1963
|
providerConfig.model = runModel;
|
|
1942
1964
|
}
|
|
1943
1965
|
|
|
1944
|
-
if (!
|
|
1966
|
+
if (!providerCallSite) {
|
|
1945
1967
|
const thinking = normalizeThinkingConfigForWire(this.config.thinking);
|
|
1946
1968
|
if (thinking !== undefined) {
|
|
1947
1969
|
providerConfig.thinking = thinking;
|
|
@@ -1980,9 +2002,9 @@ export class AgentLoop {
|
|
|
1980
2002
|
// defaults when absent).
|
|
1981
2003
|
// User-initiated conversation turns default to `mainAgent` in the
|
|
1982
2004
|
// agent loop's caller; other invocation contexts (heartbeat, filing,
|
|
1983
|
-
// analyze, etc.) pass their own
|
|
1984
|
-
if (
|
|
1985
|
-
providerConfig.callSite =
|
|
2005
|
+
// analyze, etc.) pass their own provider-resolution site.
|
|
2006
|
+
if (providerCallSite) {
|
|
2007
|
+
providerConfig.callSite = providerCallSite;
|
|
1986
2008
|
providerConfig.usageTracking = "manual";
|
|
1987
2009
|
// Per-conversation seed for deterministic `mix`-profile expansion.
|
|
1988
2010
|
// Sourced from the loop's own conversation id so every LLM call in a
|
|
@@ -2010,7 +2032,7 @@ export class AgentLoop {
|
|
|
2010
2032
|
// `activeProfile` and any call-site named profile. Threading it on
|
|
2011
2033
|
// every send (rather than once at construction) keeps subagents that
|
|
2012
2034
|
// share an `AgentLoop` instance but ought to inherit a different
|
|
2013
|
-
// profile correct
|
|
2035
|
+
// profile correct, matching how the provider call site is plumbed.
|
|
2014
2036
|
const effectiveOverrideProfile = resolveEffectiveOverrideProfile();
|
|
2015
2037
|
if (effectiveOverrideProfile) {
|
|
2016
2038
|
providerConfig.overrideProfile = effectiveOverrideProfile;
|
|
@@ -2234,6 +2256,49 @@ export class AgentLoop {
|
|
|
2234
2256
|
);
|
|
2235
2257
|
}
|
|
2236
2258
|
|
|
2259
|
+
if (onModelCallPrepared && !signal?.aborted) {
|
|
2260
|
+
const preparedOverrideProfile =
|
|
2261
|
+
typeof providerConfig.overrideProfile === "string" &&
|
|
2262
|
+
providerConfig.overrideProfile.length > 0
|
|
2263
|
+
? providerConfig.overrideProfile
|
|
2264
|
+
: undefined;
|
|
2265
|
+
try {
|
|
2266
|
+
const preparedForceOverrideProfile =
|
|
2267
|
+
providerConfig.forceOverrideProfile === true;
|
|
2268
|
+
const disableCache =
|
|
2269
|
+
providerCallSite !== undefined &&
|
|
2270
|
+
resolveCallSiteConfig(providerCallSite, getConfig().llm, {
|
|
2271
|
+
...(preparedOverrideProfile !== undefined
|
|
2272
|
+
? { overrideProfile: preparedOverrideProfile }
|
|
2273
|
+
: {}),
|
|
2274
|
+
...(preparedForceOverrideProfile
|
|
2275
|
+
? { forceOverrideProfile: true }
|
|
2276
|
+
: {}),
|
|
2277
|
+
...(this.conversationId !== undefined
|
|
2278
|
+
? { selectionSeed: this.conversationId }
|
|
2279
|
+
: {}),
|
|
2280
|
+
}).disableCache === true;
|
|
2281
|
+
onModelCallPrepared({
|
|
2282
|
+
...(providerCallSite !== undefined
|
|
2283
|
+
? { callSite: providerCallSite }
|
|
2284
|
+
: {}),
|
|
2285
|
+
...(preparedOverrideProfile !== undefined
|
|
2286
|
+
? { overrideProfile: preparedOverrideProfile }
|
|
2287
|
+
: {}),
|
|
2288
|
+
forceOverrideProfile: preparedForceOverrideProfile,
|
|
2289
|
+
...(disableCache ? { disableCache: true as const } : {}),
|
|
2290
|
+
...(signal !== undefined ? { signal } : {}),
|
|
2291
|
+
systemPrompt: providerOptions.systemPrompt ?? null,
|
|
2292
|
+
tools: currentTools,
|
|
2293
|
+
});
|
|
2294
|
+
} catch (preparedError) {
|
|
2295
|
+
rlog.warn(
|
|
2296
|
+
{ err: preparedError },
|
|
2297
|
+
"Prepared model-call observer failed; continuing with provider dispatch",
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2237
2302
|
// Announce the LLM-call boundary so downstream handlers (the
|
|
2238
2303
|
// daemon's persistence pipeline) can reserve an empty assistant row
|
|
2239
2304
|
// and stamp the resulting `messageId` onto every streaming event the
|