@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.2-dev.202609181515.7e76c92",
3
+ "version": "0.12.2-dev.202609181615.645c0a5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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(ctx, "background task", "msg-1", (event) =>
180
- events.push(event),
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 { Message, Provider, ToolDefinition } from "../providers/types.js";
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 type { Conversation } from "../daemon/conversation.js";
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 (callSite + overrideProfile) via
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
- callSite,
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(callSite, llm)`)
1943
+ // `resolveCallSiteConfig(providerCallSite, llm)`)
1922
1944
  // 3. Conversation defaults (`this.config.*`, from the resolved
1923
1945
  // default call-site config)
1924
1946
  //
1925
- // When `callSite` is present we deliberately leave
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 `callSite`.
1955
+ // they always come from `this.config` regardless of `providerCallSite`.
1934
1956
  const providerConfig: Record<string, unknown> = {};
1935
1957
 
1936
- if (!callSite) {
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 (!callSite) {
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 `callSite`.
1984
- if (callSite) {
1985
- providerConfig.callSite = 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 — and matches how `callSite` is plumbed.
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