@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.
- package/dist/ai-assistant.api.json +336 -3
- package/dist/ai-assistant.d.ts +100 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +30 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +45 -2
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +16 -1
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +7 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +40 -19
- package/dist/esm/components/chat-driver/chat-driver.test.js +86 -0
- package/dist/esm/config/define-stateful-agent.js +20 -0
- package/dist/esm/main/main.js +5 -3
- package/dist/esm/state/debug-event-log.js +1 -1
- package/package.json +16 -16
- package/src/components/chat-driver/chat-driver.test.ts +114 -2
- package/src/components/chat-driver/chat-driver.ts +76 -7
- package/src/config/config.ts +49 -1
- package/src/config/define-stateful-agent.ts +58 -1
- package/src/main/main.ts +7 -1
- package/src/state/debug-event-log.ts +1 -1
|
@@ -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<
|
|
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<
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
|
|
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');
|
package/src/config/config.ts
CHANGED
|
@@ -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 {
|
|
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
|
];
|