@genesislcap/ai-assistant 14.455.0 → 14.455.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import type {
4
4
  ChatMessage,
5
5
  ChatRequestOptions,
6
6
  ChatToolCall,
7
+ ChatToolChoice,
7
8
  ChatToolDefinition,
8
9
  } from '@genesislcap/foundation-ai';
9
10
  import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
@@ -33,16 +34,20 @@ interface ScriptedProvider extends AIProvider {
33
34
  /** Tool names advertised to the model on each `chat()` call, in order. */
34
35
  advertisedPerCall: string[][];
35
36
  /** `toolChoice` seen on each `chat()` call, in order (sub-agents force it). */
36
- toolChoicePerCall: Array<'auto' | 'required' | undefined>;
37
+ toolChoicePerCall: Array<ChatToolChoice | undefined>;
38
+ /** `temperature` seen on each `chat()` call, in order. */
39
+ temperaturePerCall: Array<number | undefined>;
37
40
  }
38
41
 
39
42
  const scriptedProvider = (responses: ChatMessage[]): ScriptedProvider => {
40
43
  const queue = [...responses];
41
44
  const advertisedPerCall: string[][] = [];
42
- const toolChoicePerCall: Array<'auto' | 'required' | undefined> = [];
45
+ const toolChoicePerCall: Array<ChatToolChoice | undefined> = [];
46
+ const temperaturePerCall: Array<number | undefined> = [];
43
47
  return {
44
48
  advertisedPerCall,
45
49
  toolChoicePerCall,
50
+ temperaturePerCall,
46
51
  chat: async (
47
52
  _history: ChatMessage[],
48
53
  _userMessage: string,
@@ -50,6 +55,7 @@ const scriptedProvider = (responses: ChatMessage[]): ScriptedProvider => {
50
55
  ): Promise<ChatMessage> => {
51
56
  advertisedPerCall.push((options?.tools ?? []).map((t) => t.name));
52
57
  toolChoicePerCall.push(options?.toolChoice);
58
+ temperaturePerCall.push(options?.temperature);
53
59
  // Once the script is exhausted, end the turn with a plain text reply.
54
60
  return queue.shift() ?? { role: 'assistant', content: 'done' };
55
61
  },
@@ -723,3 +729,109 @@ subagent(
723
729
  );
724
730
 
725
731
  subagent.run();
732
+
733
+ // ---------------------------------------------------------------------------
734
+ // per-agent / per-state temperature & tool-call mode (GENC-1321)
735
+ //
736
+ // The driver resolves `temperature` and `toolChoice` the same way it resolves
737
+ // `provider`: a static value, or a function of the turn context (which carries
738
+ // the live state for stateful agents). These tests assert both forms reach the
739
+ // provider call, and that the per-turn (function) form re-resolves each call.
740
+ // ---------------------------------------------------------------------------
741
+
742
+ const settings = createLogicSuite('ChatDriver temperature & toolChoice');
743
+
744
+ settings('passes a static agent temperature and tool-call mode to the provider', async () => {
745
+ const provider = scriptedProvider([]);
746
+ const driver = makeDriver(agent({ name: 'a', temperature: 0.3, toolChoice: 'none' }), provider);
747
+
748
+ await driver.sendMessage('hi');
749
+
750
+ assert.is(provider.temperaturePerCall[0], 0.3);
751
+ assert.equal(provider.toolChoicePerCall[0], 'none');
752
+ // ...and surfaced in the per-turn debug snapshot (the effective values sent).
753
+ const snap = driver.getTurnSnapshots()[0];
754
+ assert.is(snap.temperature, 0.3);
755
+ assert.equal(snap.toolChoice, 'none');
756
+ });
757
+
758
+ settings('re-resolves the function form per turn (carrying turn context)', async () => {
759
+ const provider = scriptedProvider([callsTool('tool_a', 't1')]);
760
+ const config = agent({
761
+ name: 'b',
762
+ toolDefinitions: [def('tool_a')],
763
+ toolHandlers: { tool_a: async () => 'ok' },
764
+ // turnIndex: 0 on the first LLM call, > 0 on subsequent tool-loop iterations.
765
+ temperature: (ctx) => (ctx.turnIndex === 0 ? 0.1 : 0.9),
766
+ toolChoice: (ctx) => (ctx.turnIndex === 0 ? { tool: 'tool_a' } : 'auto'),
767
+ });
768
+
769
+ await makeDriver(config, provider).sendMessage('go');
770
+
771
+ // First call forces the named tool at a low temperature...
772
+ assert.is(provider.temperaturePerCall[0], 0.1);
773
+ assert.equal(provider.toolChoicePerCall[0], { tool: 'tool_a' });
774
+ // ...the follow-up call (after the tool result) sees the other branch.
775
+ assert.is(provider.temperaturePerCall[1], 0.9);
776
+ assert.equal(provider.toolChoicePerCall[1], 'auto');
777
+ });
778
+
779
+ settings(
780
+ 'leaves temperature and toolChoice unset when the agent does not configure them',
781
+ async () => {
782
+ const provider = scriptedProvider([]);
783
+
784
+ await makeDriver(agent({ name: 'c' }), provider).sendMessage('hi');
785
+
786
+ assert.is(provider.temperaturePerCall[0], undefined);
787
+ assert.is(provider.toolChoicePerCall[0], undefined);
788
+ },
789
+ );
790
+
791
+ settings.run();
792
+
793
+ // ---------------------------------------------------------------------------
794
+ // empty-response diagnostics (GENC-1321)
795
+ //
796
+ // The transport surfaces a provider diagnostic (Gemini: finishReason,
797
+ // thoughtsTokens, parts, blockReason) on the message; the driver must fold it
798
+ // into the empty-response meta events so the debug-log timeline shows *why* a
799
+ // turn came back blank — not just that it did.
800
+ // ---------------------------------------------------------------------------
801
+
802
+ const emptyDiag = createLogicSuite('ChatDriver empty-response diagnostics');
803
+
804
+ emptyDiag('folds the provider responseMeta into the empty-response meta events', async () => {
805
+ clearMetaEventRegistry();
806
+ const provider: AIProvider = {
807
+ chat: async (): Promise<ChatMessage> => ({
808
+ role: 'assistant',
809
+ content: '',
810
+ responseMeta: {
811
+ finishReason: 'STOP',
812
+ thoughtsTokens: 999,
813
+ parts: { functionCall: 0, thought: 0, text: 0 },
814
+ },
815
+ }),
816
+ };
817
+ const sessionKey = 'empty-diag';
818
+
819
+ await makeDriver(agent({ name: 'Static' }), provider, sessionKey).sendMessage('go');
820
+
821
+ // Both the in-turn retries and the final bail carry the diagnostic.
822
+ const retry = getMetaEvents(sessionKey).find(
823
+ (e) => e.type === 'turn.retry' && e.detail?.reason === 'empty-response',
824
+ );
825
+ assert.ok(retry, 'an empty-response turn.retry should be recorded');
826
+ assert.is(retry!.detail?.finishReason, 'STOP');
827
+ assert.is(retry!.detail?.thoughtsTokens, 999);
828
+
829
+ const err = getMetaEvents(sessionKey).find(
830
+ (e) => e.type === 'turn.error' && e.detail?.reason === 'empty-response',
831
+ );
832
+ assert.ok(err, 'an empty-response turn.error should be recorded after retries');
833
+ assert.is(err!.detail?.finishReason, 'STOP');
834
+ assert.is(err!.detail?.thoughtsTokens, 999);
835
+ });
836
+
837
+ emptyDiag.run();
@@ -6,6 +6,7 @@ import type {
6
6
  ChatMessage,
7
7
  ChatRequestOptions,
8
8
  ChatToolCall,
9
+ ChatToolChoice,
9
10
  ChatToolDefinition,
10
11
  ChatToolHandlers,
11
12
  InteractionRequestOptions,
@@ -19,6 +20,8 @@ import type {
19
20
  ProviderInput,
20
21
  SystemPromptContext,
21
22
  SystemPromptInput,
23
+ TemperatureInput,
24
+ ToolChoiceInput,
22
25
  ToolDefinitionsInput,
23
26
  ToolHandlersInput,
24
27
  } from '../../config/config';
@@ -98,6 +101,18 @@ export interface TurnSnapshot {
98
101
  systemPrompt?: string;
99
102
  /** Tool names sent to the LLM, in order — definitions are static per name so names alone suffice. */
100
103
  toolNames: string[];
104
+ /**
105
+ * Normalized `0`–`1` sampling temperature in effect for this call, if the
106
+ * agent (or its current state) configured one. Undefined → provider/model
107
+ * default. Mirrors the value resolved from `BaseAgentConfig.temperature`.
108
+ */
109
+ temperature?: number;
110
+ /**
111
+ * Tool-call mode actually sent to the provider this call — the effective
112
+ * value, including the sub-agent `'required'` default. Undefined → `'auto'`.
113
+ * Mirrors the value resolved from `BaseAgentConfig.toolChoice`.
114
+ */
115
+ toolChoice?: ChatToolChoice;
101
116
  /**
102
117
  * Per-turn display label resolved from the agent's `displayName`, e.g.
103
118
  * "Guided Booking (Counterparties)". `agentName` stays as the canonical
@@ -300,6 +315,17 @@ export class ChatDriver extends EventTarget implements AiDriver {
300
315
  * `undefined` means "use the registry default".
301
316
  */
302
317
  private activeProviderInput?: ProviderInput;
318
+ /**
319
+ * Active agent's temperature selector (static number or per-turn resolver),
320
+ * normalized to `0`–`1`. `undefined` means "use the provider/model default".
321
+ */
322
+ private activeTemperatureInput?: TemperatureInput;
323
+ /**
324
+ * Active agent's tool-call mode selector (static value or per-turn resolver).
325
+ * `undefined` falls back to the per-turn default (sub-agents force a tool
326
+ * call; top-level turns are `'auto'`).
327
+ */
328
+ private activeToolChoiceInput?: ToolChoiceInput;
303
329
  /**
304
330
  * Caches validated provider lookups per name within the current agent. Cleared
305
331
  * by `applyAgent` so each new agent's static/function-resolved names are
@@ -440,6 +466,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
440
466
  this.debugSnapshotter = config.getDebugSnapshot;
441
467
  this.subAgentsMap = new Map((config.subAgents ?? []).map((s) => [s.name, s]));
442
468
  this.activeProviderInput = config.provider;
469
+ this.activeTemperatureInput = config.temperature;
470
+ this.activeToolChoiceInput = config.toolChoice;
443
471
  this.resolvedProviderCache.clear();
444
472
  this.lastResolvedProviderName = undefined;
445
473
  // Static validation: resolve the name now so unknown-provider and missing-
@@ -513,6 +541,21 @@ export class ChatDriver extends EventTarget implements AiDriver {
513
541
  return provider;
514
542
  }
515
543
 
544
+ /**
545
+ * Resolve a per-turn config input that is either a static value or a function
546
+ * of the turn context — the value-or-resolver shape shared by `provider`,
547
+ * `temperature`, and `toolChoice`. Returns undefined when the input is unset.
548
+ */
549
+ private async resolveTurnInput<T>(
550
+ input: T | ((ctx: SystemPromptContext) => T | Promise<T>) | undefined,
551
+ ctx: SystemPromptContext,
552
+ ): Promise<T | undefined> {
553
+ if (input === undefined) return undefined;
554
+ return typeof input === 'function'
555
+ ? (input as (ctx: SystemPromptContext) => T | Promise<T>)(ctx)
556
+ : input;
557
+ }
558
+
516
559
  /**
517
560
  * Returns the early-stop result set by `completeSubAgent`, if any.
518
561
  * Called by a parent `ChatDriver` after running this instance as a sub-agent.
@@ -615,7 +658,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
615
658
  * before each LLM call — that's the latest point where the prompt, tool
616
659
  * surface, and agent state line up with what the model is about to see.
617
660
  */
618
- private recordTurnSnapshot(resolvedSystemPrompt: string | undefined): void {
661
+ private recordTurnSnapshot(
662
+ resolvedSystemPrompt: string | undefined,
663
+ temperature: number | undefined,
664
+ toolChoice: ChatToolChoice | undefined,
665
+ ): void {
619
666
  let agentSnapshot: unknown;
620
667
  if (this.debugSnapshotter) {
621
668
  try {
@@ -636,6 +683,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
636
683
  agentLabel: this.activeAgentLabel,
637
684
  systemPrompt: resolvedSystemPrompt,
638
685
  toolNames: this.toolDefinitions.map((t) => t.name),
686
+ temperature,
687
+ toolChoice,
639
688
  agentSnapshot,
640
689
  });
641
690
  if (this.turnSnapshots.length > this.maxTurnSnapshots) {
@@ -1463,7 +1512,22 @@ export class ChatDriver extends EventTarget implements AiDriver {
1463
1512
  ? `${baseSystemPrompt ?? ''}\n\nIMPORTANT: You must respond to the user's message. Call the appropriate tool or provide a text response — do not return an empty response.`
1464
1513
  : baseSystemPrompt;
1465
1514
 
1466
- this.recordTurnSnapshot(systemPrompt);
1515
+ // Resolve the per-turn temperature and tool-call mode the same way the
1516
+ // provider is resolved — static value or a function of the turn context
1517
+ // (which carries the live state for stateful agents). Resolved before the
1518
+ // snapshot so the debug log records the exact request config the model saw.
1519
+ // oxlint-disable-next-line no-await-in-loop
1520
+ const [resolvedTemperature, resolvedToolChoice] = await Promise.all([
1521
+ this.resolveTurnInput<number>(this.activeTemperatureInput, promptCtx),
1522
+ this.resolveTurnInput<ChatToolChoice>(this.activeToolChoiceInput, promptCtx),
1523
+ ]);
1524
+ // An agent/state-configured tool-call mode wins. Otherwise sub-agents must
1525
+ // finish by calling a tool (their completion tool) so the turn can't end
1526
+ // on a free-text answer; top-level agents stay 'auto'. (Transports no-op a
1527
+ // force when no tools are advertised.)
1528
+ const effectiveToolChoice = resolvedToolChoice ?? (this.isSubAgent ? 'required' : undefined);
1529
+
1530
+ this.recordTurnSnapshot(systemPrompt, resolvedTemperature, effectiveToolChoice);
1467
1531
 
1468
1532
  // Capture the pending user input, then clear the slots BEFORE the chat
1469
1533
  // call. `sendMessage` already appended the user message to `this.history`,
@@ -1484,11 +1548,10 @@ export class ChatDriver extends EventTarget implements AiDriver {
1484
1548
  // Per-turn signal: aborts on user cancel, and (via beginTurn's chain)
1485
1549
  // on driver dispose. Cancels the in-flight request either way.
1486
1550
  signal: this.turnController.signal,
1487
- // Sub-agents must finish by calling a tool (their completion tool), never
1488
- // by emitting a free-text turn force tool use so the provider can't
1489
- // return a bare text answer. Top-level agents stay on the default 'auto'.
1490
- // (Transports no-op the force when no tools are advertised.)
1491
- toolChoice: this.isSubAgent ? 'required' : undefined,
1551
+ toolChoice: effectiveToolChoice,
1552
+ // Agent/state-configured sampling temperature (normalized 0–1); each
1553
+ // transport translates it to its native range. Undefined provider default.
1554
+ temperature: resolvedTemperature,
1492
1555
  };
1493
1556
 
1494
1557
  // Resolve the active provider for this turn. Static names were validated
@@ -1594,6 +1657,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
1594
1657
  provider: this.lastResolvedProviderName,
1595
1658
  attempt: emptyResponseAttempts,
1596
1659
  maxAttempts: MAX_EMPTY_RESPONSE_RETRIES,
1660
+ // Provider diagnostic (Gemini: finishReason, thoughtsTokens, parts,
1661
+ // blockReason) so the timeline shows *why* the turn came back blank —
1662
+ // e.g. a high thoughtsTokens with a 'STOP' finish is "thought, then
1663
+ // stopped without answering".
1664
+ ...response.responseMeta,
1597
1665
  });
1598
1666
  iterations -= 1;
1599
1667
  continue;
@@ -1604,6 +1672,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
1604
1672
  provider: this.lastResolvedProviderName,
1605
1673
  attempts: emptyResponseAttempts,
1606
1674
  isSubAgent: this.isSubAgent,
1675
+ ...response.responseMeta,
1607
1676
  });
1608
1677
  if (this.isSubAgent) {
1609
1678
  this.failSubAgent('empty_response');
@@ -1,11 +1,12 @@
1
1
  import type {
2
2
  ChatInputDuringExecutionMode,
3
3
  ChatMessage,
4
+ ChatToolChoice,
4
5
  ChatToolDefinition,
5
6
  ChatToolHandlers,
6
7
  } from '@genesislcap/foundation-ai';
7
8
 
8
- export type { ChatInputDuringExecutionMode };
9
+ export type { ChatInputDuringExecutionMode, ChatToolChoice };
9
10
 
10
11
  /**
11
12
  * Context passed to `onActivate` / `onDeactivate` lifecycle hooks on an agent.
@@ -89,6 +90,32 @@ export type ToolHandlersInput =
89
90
  */
90
91
  export type ProviderInput = string | ((ctx: SystemPromptContext) => string | Promise<string>);
91
92
 
93
+ /**
94
+ * Sampling temperature for an agent, normalized to `0`–`1` (`0` = deterministic,
95
+ * `1` = the most random the active provider allows). Either a static number
96
+ * (resolved once) or a function resolved each tool-loop iteration — pick the
97
+ * function form to vary it by current state (e.g. a lower temperature for a
98
+ * precise extraction step, higher for brainstorming). Each transport translates
99
+ * the normalized value into its own native range. Omit to use the
100
+ * provider/model default. See `ChatRequestOptions.temperature`.
101
+ *
102
+ * @beta
103
+ */
104
+ export type TemperatureInput = number | ((ctx: SystemPromptContext) => number | Promise<number>);
105
+
106
+ /**
107
+ * Tool-call mode for an agent. Either a static `ChatToolChoice` (resolved
108
+ * once) or a function resolved each tool-loop iteration — pick the function form
109
+ * to vary it by current state (e.g. force a classifier tool in an intake step,
110
+ * then `'auto'` everywhere multi-step work happens). Omit to use the default
111
+ * (`'auto'`, except sub-agent turns which force `'required'`).
112
+ *
113
+ * @beta
114
+ */
115
+ export type ToolChoiceInput =
116
+ | ChatToolChoice
117
+ | ((ctx: SystemPromptContext) => ChatToolChoice | Promise<ChatToolChoice>);
118
+
92
119
  /**
93
120
  * Opts an agent in to manual selection from the assistant's agent picker.
94
121
  *
@@ -170,6 +197,27 @@ interface BaseAgentConfig {
170
197
  * @beta
171
198
  */
172
199
  provider?: ProviderInput;
200
+ /**
201
+ * Sampling temperature for this agent, normalized to `0`–`1`. Either a static
202
+ * number or a function resolved each tool-loop iteration — pick the function
203
+ * form to vary it by current state. Omit to use the provider/model default.
204
+ * Resolved and applied the same way as {@link BaseAgentConfig.provider}.
205
+ * See {@link TemperatureInput}.
206
+ *
207
+ * @beta
208
+ */
209
+ temperature?: TemperatureInput;
210
+ /**
211
+ * Tool-call mode for this agent — whether the model may, must, or must not
212
+ * call a tool this turn (and optionally which one). Either a static value or
213
+ * a function resolved each tool-loop iteration — pick the function form to
214
+ * vary it by current state (e.g. force a single tool at a known juncture).
215
+ * Omit to use the default (`'auto'`, except sub-agent turns which force
216
+ * `'required'`). See {@link ToolChoiceInput}.
217
+ *
218
+ * @beta
219
+ */
220
+ toolChoice?: ToolChoiceInput;
173
221
  /**
174
222
  * Optional primer history prepended to every call (not visible to the user).
175
223
  * Used to establish agent identity and behavioural rules.
@@ -1,4 +1,9 @@
1
- import type { ChatMessage, ChatToolDefinition, ChatToolHandlers } from '@genesislcap/foundation-ai';
1
+ import type {
2
+ ChatMessage,
3
+ ChatToolChoice,
4
+ ChatToolDefinition,
5
+ ChatToolHandlers,
6
+ } from '@genesislcap/foundation-ai';
2
7
  import { TOOL_FOLD_SYMBOL } from '../utils/tool-fold';
3
8
  import type {
4
9
  AgentConfig,
@@ -8,6 +13,8 @@ import type {
8
13
  ProviderInput,
9
14
  SystemPromptContext,
10
15
  SystemPromptInput,
16
+ TemperatureInput,
17
+ ToolChoiceInput,
11
18
  ToolDefinitionsInput,
12
19
  ToolHandlersInput,
13
20
  } from './config';
@@ -127,6 +134,25 @@ export interface StatefulAgentInit<S> {
127
134
  */
128
135
  provider?: string | ((ctx: StatefulAgentContext<S>) => string | Promise<string>);
129
136
 
137
+ /**
138
+ * Sampling temperature, normalized to `0`–`1`. Either a static number or a
139
+ * function resolved each tool-loop iteration with the current `state` — pick
140
+ * the function form to vary it per machine state (e.g. low for a precise
141
+ * extraction step, higher for free-form drafting). Omit for the default.
142
+ */
143
+ temperature?: number | ((ctx: StatefulAgentContext<S>) => number | Promise<number>);
144
+
145
+ /**
146
+ * Tool-call mode. Either a static `ChatToolChoice` or a function
147
+ * resolved each tool-loop iteration with the current `state` — pick the
148
+ * function form to force a specific tool in one state (e.g. a classifier in
149
+ * intake) and leave `'auto'` in states where multi-step work happens. Omit
150
+ * for the default.
151
+ */
152
+ toolChoice?:
153
+ | ChatToolChoice
154
+ | ((ctx: StatefulAgentContext<S>) => ChatToolChoice | Promise<ChatToolChoice>);
155
+
130
156
  /**
131
157
  * Optional getter for the debug-log snapshot. Defaults to auto-snapshotting
132
158
  * any property on `state` that looks like a foundation-state-machine
@@ -273,6 +299,35 @@ export function defineStatefulAgent<S>(opts: StatefulAgentInit<S>): AgentConfig
273
299
  }
274
300
  : opts.provider;
275
301
 
302
+ // `temperature` and `toolChoice` thread `state` in exactly like `provider`;
303
+ // static values pass through unchanged.
304
+ const wrappedTemperature: TemperatureInput | undefined =
305
+ typeof opts.temperature === 'function'
306
+ ? async (ctx: SystemPromptContext) => {
307
+ if (!state) {
308
+ throw new Error(`Stateful agent "${opts.name}" temperature called before init`);
309
+ }
310
+ return (opts.temperature as (ctx: StatefulAgentContext<S>) => number | Promise<number>)({
311
+ ...ctx,
312
+ state,
313
+ });
314
+ }
315
+ : opts.temperature;
316
+
317
+ const wrappedToolChoice: ToolChoiceInput | undefined =
318
+ typeof opts.toolChoice === 'function'
319
+ ? async (ctx: SystemPromptContext) => {
320
+ if (!state) {
321
+ throw new Error(`Stateful agent "${opts.name}" toolChoice called before init`);
322
+ }
323
+ return (
324
+ opts.toolChoice as (
325
+ ctx: StatefulAgentContext<S>,
326
+ ) => ChatToolChoice | Promise<ChatToolChoice>
327
+ )({ ...ctx, state });
328
+ }
329
+ : opts.toolChoice;
330
+
276
331
  const base = {
277
332
  name: opts.name,
278
333
  displayName: wrappedDisplayName,
@@ -283,6 +338,8 @@ export function defineStatefulAgent<S>(opts: StatefulAgentInit<S>): AgentConfig
283
338
  toolDefinitions: wrappedTools,
284
339
  toolHandlers: wrappedHandlers,
285
340
  provider: wrappedProvider,
341
+ temperature: wrappedTemperature,
342
+ toolChoice: wrappedToolChoice,
286
343
 
287
344
  onActivate: async (ctx: AgentLifecycleContext) => {
288
345
  state = await opts.init(ctx);
package/src/main/main.ts CHANGED
@@ -116,9 +116,11 @@ avoidTreeShaking(
116
116
  * - `toolHandlers` (functions),
117
117
  * - `onActivate` / `onDeactivate` (lifecycle hooks, functions),
118
118
  * - `getDebugSnapshot` (function),
119
- * - function-form `systemPrompt` / `toolDefinitions` / `displayName` / `provider`
119
+ * - function-form `systemPrompt` / `toolDefinitions` / `displayName` / `provider` /
120
+ * `temperature` / `toolChoice`
120
121
  * (downgraded to `undefined` in the snapshot — the live config on the driver
121
122
  * is still the source of truth; the slice only stores a serializable projection).
123
+ * Static forms (string / number / plain-object `toolChoice`) pass through.
122
124
  */
123
125
  function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
124
126
  const {
@@ -131,6 +133,8 @@ function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
131
133
  toolDefinitions,
132
134
  displayName,
133
135
  provider,
136
+ temperature,
137
+ toolChoice,
134
138
  ...rest
135
139
  } = agent;
136
140
  const stripped = {
@@ -139,6 +143,8 @@ function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
139
143
  toolDefinitions: typeof toolDefinitions === 'function' ? undefined : toolDefinitions,
140
144
  displayName: typeof displayName === 'function' ? undefined : displayName,
141
145
  provider: typeof provider === 'function' ? undefined : provider,
146
+ temperature: typeof temperature === 'function' ? undefined : temperature,
147
+ toolChoice: typeof toolChoice === 'function' ? undefined : toolChoice,
142
148
  };
143
149
  return subAgents?.length
144
150
  ? { ...stripped, subAgents: subAgents.map(stripHandlers) as AgentConfig[] }
@@ -247,7 +247,7 @@ export const DEBUG_LOG_README: readonly string[] = [
247
247
  "kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
248
248
  "kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
249
249
  "Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
250
- 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations, plus reason-specific diagnostics: attempts, finishMessage, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (model/provider for the upcoming turns), interaction.requested/resolved (blocking user widgets — explain quiet gaps), context.updated/threshold-crossed (token + cost), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
250
+ 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (model/provider for the upcoming turns), interaction.requested/resolved (blocking user widgets — explain quiet gaps), context.updated/threshold-crossed (token + cost), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
251
251
  "`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, token usage, session cost), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
252
252
  'To debug a failure: find the last turn.error or tool.failed, then read upward for the user message, the turn(s), and the agent/provider/state events that led into it.',
253
253
  ];