@kuralle-agents/core 0.3.4 → 0.3.6
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/flow/collectUntilComplete.js +43 -2
- package/dist/flow/nodeBuilders.d.ts +5 -0
- package/dist/flow/nodeBuilders.js +8 -0
- package/dist/runtime/Runtime.js +3 -0
- package/dist/runtime/channels/TextDriver.d.ts +1 -0
- package/dist/runtime/channels/TextDriver.js +15 -6
- package/dist/runtime/channels/VoiceDriver.d.ts +1 -0
- package/dist/runtime/channels/VoiceDriver.js +14 -4
- package/dist/runtime/channels/extractionTurn.d.ts +14 -0
- package/dist/runtime/channels/extractionTurn.js +87 -0
- package/dist/types/agentConfig.d.ts +6 -0
- package/dist/types/channel.d.ts +7 -0
- package/dist/types/flow.d.ts +7 -0
- package/dist/types/run-context.d.ts +7 -0
- package/package.json +2 -2
|
@@ -32,6 +32,9 @@ export async function collectUntilComplete(node, run, driver, ctx) {
|
|
|
32
32
|
appendUserMessage(run, signal.input);
|
|
33
33
|
}
|
|
34
34
|
else if (ctx.turnInputConsumed) {
|
|
35
|
+
// No fresh input to extract this turn: ask (deterministically) for the
|
|
36
|
+
// fields still missing and wait. Never run extraction over stale context.
|
|
37
|
+
emitCollectAsk(node, run, ctx);
|
|
35
38
|
return { kind: 'stay' };
|
|
36
39
|
}
|
|
37
40
|
ctx.turnInputConsumed = true;
|
|
@@ -46,11 +49,49 @@ export async function collectUntilComplete(node, run, driver, ctx) {
|
|
|
46
49
|
userMessage: peekLatestUserMessage(run),
|
|
47
50
|
});
|
|
48
51
|
const resolved = resolveCollectExtractionNode(node, missing, run.state, submitTool);
|
|
49
|
-
|
|
52
|
+
// Non-speaking extraction: the model's prose is DISCARDED (never emitted or
|
|
53
|
+
// appended), so a collect turn cannot author narration that contradicts flow
|
|
54
|
+
// state. Falls back to runAgentTurn for drivers without runExtraction; its
|
|
55
|
+
// text is likewise dropped here. The user-facing question is the deterministic
|
|
56
|
+
// `ask` emitted above — never model-authored.
|
|
57
|
+
const turn = await (driver.runExtraction
|
|
58
|
+
? driver.runExtraction(resolved, ctx)
|
|
59
|
+
: driver.runAgentTurn(resolved, ctx));
|
|
50
60
|
mergeExtractionFromTurn(node, run, turn);
|
|
51
|
-
appendAssistantMessage(run, turn.text);
|
|
52
61
|
}
|
|
53
62
|
}
|
|
63
|
+
function humanizeField(field) {
|
|
64
|
+
return field
|
|
65
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
66
|
+
.replace(/[_-]+/g, ' ')
|
|
67
|
+
.trim()
|
|
68
|
+
.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
/** Deterministic, framework-authored question for the still-missing fields. Uses
|
|
71
|
+
* the node's `ask` when provided, else a safe default that never references a
|
|
72
|
+
* downstream outcome (order/delivery/payment/website). */
|
|
73
|
+
function renderCollectAsk(node, missing, state) {
|
|
74
|
+
if (node.ask) {
|
|
75
|
+
return node.ask(missing, state);
|
|
76
|
+
}
|
|
77
|
+
if (missing.length === 1) {
|
|
78
|
+
return `Could you share your ${humanizeField(missing[0])}?`;
|
|
79
|
+
}
|
|
80
|
+
if (missing.length > 1) {
|
|
81
|
+
return `Could you share: ${missing.map(humanizeField).join(', ')}?`;
|
|
82
|
+
}
|
|
83
|
+
return 'Could you tell me a little more?';
|
|
84
|
+
}
|
|
85
|
+
function emitCollectAsk(node, run, ctx) {
|
|
86
|
+
const missing = computeMissingFields(node, getCollectData(run.state, node.id));
|
|
87
|
+
const text = renderCollectAsk(node, missing, run.state);
|
|
88
|
+
if (!text.trim()) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
ctx.emit({ type: 'text-delta', text });
|
|
92
|
+
ctx.emit({ type: 'turn-end' });
|
|
93
|
+
appendAssistantMessage(run, text);
|
|
94
|
+
}
|
|
54
95
|
function mergeExtractionFromTurn(node, run, turn) {
|
|
55
96
|
mergeTurnExtraction(node, run.state, turn.toolResults.map((record) => ({ name: record.name, result: record.result })));
|
|
56
97
|
}
|
|
@@ -4,5 +4,10 @@ import type { ResolvedNode } from '../types/channel.js';
|
|
|
4
4
|
import type { Tool } from '../types/effectTool.js';
|
|
5
5
|
export declare function resolveInstructions(instructions: Instructions, state: FlowState): string;
|
|
6
6
|
export declare function buildNodePrompt(node: ReplyNode, state: FlowState): string;
|
|
7
|
+
/** Compose the agent base layer (ADR 0001) into a node's system prompt: the
|
|
8
|
+
* agent's base instructions (persona / safety / grounding) prefix the node's
|
|
9
|
+
* own instructions. Node instructions layer ON TOP — they never replace the
|
|
10
|
+
* base. Base resolves against the current state so dynamic base prompts work. */
|
|
11
|
+
export declare function composeSystem(base: Instructions | undefined, nodeSystem: string, state: FlowState): string;
|
|
7
12
|
export declare function resolveReplyNode(node: ReplyNode, state: FlowState): ResolvedNode;
|
|
8
13
|
export declare function resolveCollectExtractionNode(collectNode: CollectNode, missing: string[], state: FlowState, submitTool: Tool): ResolvedNode;
|
|
@@ -15,6 +15,14 @@ export function resolveInstructions(instructions, state) {
|
|
|
15
15
|
export function buildNodePrompt(node, state) {
|
|
16
16
|
return resolveInstructions(node.instructions, state);
|
|
17
17
|
}
|
|
18
|
+
/** Compose the agent base layer (ADR 0001) into a node's system prompt: the
|
|
19
|
+
* agent's base instructions (persona / safety / grounding) prefix the node's
|
|
20
|
+
* own instructions. Node instructions layer ON TOP — they never replace the
|
|
21
|
+
* base. Base resolves against the current state so dynamic base prompts work. */
|
|
22
|
+
export function composeSystem(base, nodeSystem, state) {
|
|
23
|
+
const baseText = base ? resolveInstructions(base, state) : '';
|
|
24
|
+
return [baseText, nodeSystem].filter((s) => s && s.trim()).join('\n\n');
|
|
25
|
+
}
|
|
18
26
|
function buildNodeTools(node, state) {
|
|
19
27
|
if (!node.tools) {
|
|
20
28
|
return {};
|
package/dist/runtime/Runtime.js
CHANGED
|
@@ -97,6 +97,9 @@ export class Runtime {
|
|
|
97
97
|
? buildMemoryService(this.config.memoryService, opened.agent)
|
|
98
98
|
: undefined,
|
|
99
99
|
});
|
|
100
|
+
// Agent base layer (ADR 0001): composed into every node turn by the drivers.
|
|
101
|
+
runCtx.baseInstructions = opened.agent.instructions;
|
|
102
|
+
runCtx.globalTools = opened.agent.globalTools;
|
|
100
103
|
await this.hooks?.onStart?.(runCtx);
|
|
101
104
|
const driver = opts.driver ?? new TextDriver();
|
|
102
105
|
let activeAgent = opened.agent;
|
|
@@ -13,6 +13,7 @@ export declare class TextDriver implements ChannelDriver {
|
|
|
13
13
|
private readonly maxSteps;
|
|
14
14
|
constructor(config?: TextDriverConfig);
|
|
15
15
|
runAgentTurn(node: ResolvedNode, ctx: RunContext): Promise<TurnResult>;
|
|
16
|
+
runExtraction(node: ResolvedNode, ctx: RunContext): Promise<TurnResult>;
|
|
16
17
|
runStructured(node: DecideNode, ctx: RunContext): Promise<unknown>;
|
|
17
18
|
awaitUser(ctx: RunContext): Promise<UserSignal>;
|
|
18
19
|
private resolveTools;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { streamText, generateObject } from 'ai';
|
|
2
|
-
import { buildNodePrompt, resolveInstructions } from '../../flow/nodeBuilders.js';
|
|
2
|
+
import { buildNodePrompt, resolveInstructions, composeSystem } from '../../flow/nodeBuilders.js';
|
|
3
3
|
import { buildToolSet } from '../../tools/effect/index.js';
|
|
4
4
|
import { classifyControl } from '../../flow/classifyControl.js';
|
|
5
5
|
import { consumePendingUserInput } from './inputBuffer.js';
|
|
6
|
+
import { runSilentExtraction } from './extractionTurn.js';
|
|
6
7
|
import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTurn.js';
|
|
7
8
|
import { resolveMaxSteps } from '../policies/limits.js';
|
|
8
9
|
import { appendGatherBlocks, runGatherPhase } from '../grounding/index.js';
|
|
@@ -28,10 +29,11 @@ export class TextDriver {
|
|
|
28
29
|
const gather = await runGatherPhase(ctx);
|
|
29
30
|
const out = { text: '', toolResults: [] };
|
|
30
31
|
const model = replyNode.model ?? ctx.model;
|
|
31
|
-
const
|
|
32
|
+
const nodeSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
|
|
33
|
+
const baseSystem = composeSystem(ctx.baseInstructions, nodeSystem, ctx.runState.state);
|
|
32
34
|
const system = appendGatherBlocks(baseSystem, [gather.retrievalBlock, gather.memoryBlock]);
|
|
33
35
|
const messages = [...ctx.runState.messages];
|
|
34
|
-
const aiTools = this.resolveTools(node);
|
|
36
|
+
const aiTools = this.resolveTools(node, ctx.globalTools);
|
|
35
37
|
const maxSteps = resolveMaxSteps(ctx.limits, this.maxSteps);
|
|
36
38
|
const toolCallsMade = [];
|
|
37
39
|
let draftText = '';
|
|
@@ -126,8 +128,15 @@ export class TextDriver {
|
|
|
126
128
|
ctx.emit({ type: 'turn-end' });
|
|
127
129
|
return out;
|
|
128
130
|
}
|
|
131
|
+
// Non-speaking field extraction for collect nodes (shared helper so text and
|
|
132
|
+
// voice are identical). The model's prose is discarded; the user-facing
|
|
133
|
+
// question is emitted deterministically by the flow engine (CollectNode.ask).
|
|
134
|
+
runExtraction(node, ctx) {
|
|
135
|
+
const model = node.node.model ?? ctx.model;
|
|
136
|
+
return runSilentExtraction(node, ctx, model, resolveMaxSteps(ctx.limits, this.maxSteps));
|
|
137
|
+
}
|
|
129
138
|
async runStructured(node, ctx) {
|
|
130
|
-
const base = resolveInstructions(node.instructions, ctx.runState.state);
|
|
139
|
+
const base = composeSystem(ctx.baseInstructions, resolveInstructions(node.instructions, ctx.runState.state), ctx.runState.state);
|
|
131
140
|
const schema = node.schema;
|
|
132
141
|
// When the node offers choices (e.g. via withChoices), constrain the model
|
|
133
142
|
// to return exactly one option id. Otherwise an unconstrained string schema
|
|
@@ -151,8 +160,8 @@ export class TextDriver {
|
|
|
151
160
|
const input = consumePendingUserInput(ctx.session);
|
|
152
161
|
return { type: 'message', input };
|
|
153
162
|
}
|
|
154
|
-
resolveTools(resolved) {
|
|
155
|
-
const merged = { ...this.toolDefs, ...(resolved.localTools ?? {}) };
|
|
163
|
+
resolveTools(resolved, globalTools) {
|
|
164
|
+
const merged = { ...this.toolDefs, ...(globalTools ?? {}), ...(resolved.localTools ?? {}) };
|
|
156
165
|
const aiTools = { ...resolved.tools };
|
|
157
166
|
for (const [name, tool] of Object.entries(merged)) {
|
|
158
167
|
if (tool && !aiTools[name]) {
|
|
@@ -20,6 +20,7 @@ export declare class VoiceDriver implements ChannelDriver {
|
|
|
20
20
|
constructor(config: VoiceDriverConfig);
|
|
21
21
|
runAgentTurn(node: ResolvedNode, ctx: RunContext): Promise<TurnResult>;
|
|
22
22
|
runStructured(node: DecideNode, ctx: RunContext): Promise<unknown>;
|
|
23
|
+
runExtraction(node: ResolvedNode, ctx: RunContext): Promise<TurnResult>;
|
|
23
24
|
awaitUser(ctx: RunContext): Promise<UserSignal>;
|
|
24
25
|
reconfigure(config: Partial<RealtimeSessionConfig>): Promise<void>;
|
|
25
26
|
getHeardCharCount(): number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { generateObject } from 'ai';
|
|
2
|
-
import {
|
|
2
|
+
import { runSilentExtraction } from './extractionTurn.js';
|
|
3
|
+
import { buildNodePrompt, resolveInstructions, composeSystem } from '../../flow/nodeBuilders.js';
|
|
3
4
|
import { classifyControl } from '../../flow/classifyControl.js';
|
|
4
5
|
import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTurn.js';
|
|
5
6
|
import { resolveMaxSteps } from '../policies/limits.js';
|
|
@@ -30,9 +31,10 @@ export class VoiceDriver {
|
|
|
30
31
|
}
|
|
31
32
|
const gather = await runGatherPhase(ctx);
|
|
32
33
|
const out = { text: '', toolResults: [] };
|
|
33
|
-
const
|
|
34
|
+
const nodeSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
|
|
35
|
+
const baseSystem = composeSystem(ctx.baseInstructions, nodeSystem, ctx.runState.state);
|
|
34
36
|
const system = appendGatherBlocks(baseSystem, [gather.retrievalBlock, gather.memoryBlock]);
|
|
35
|
-
const geminiTools = resolveVoiceGeminiTools(node, this.toolDefs);
|
|
37
|
+
const geminiTools = resolveVoiceGeminiTools(node, { ...this.toolDefs, ...(ctx.globalTools ?? {}) });
|
|
36
38
|
const toolCallsMade = [];
|
|
37
39
|
const maxSteps = resolveMaxSteps(ctx.limits, this.maxSteps);
|
|
38
40
|
let draftText = '';
|
|
@@ -62,7 +64,7 @@ export class VoiceDriver {
|
|
|
62
64
|
return out;
|
|
63
65
|
}
|
|
64
66
|
async runStructured(node, ctx) {
|
|
65
|
-
const system = resolveInstructions(node.instructions, ctx.runState.state);
|
|
67
|
+
const system = composeSystem(ctx.baseInstructions, resolveInstructions(node.instructions, ctx.runState.state), ctx.runState.state);
|
|
66
68
|
const schema = node.schema;
|
|
67
69
|
const { object } = await generateObject({
|
|
68
70
|
model: ctx.model,
|
|
@@ -73,6 +75,14 @@ export class VoiceDriver {
|
|
|
73
75
|
});
|
|
74
76
|
return object;
|
|
75
77
|
}
|
|
78
|
+
// Non-speaking collect extraction: uses the shared text-model extraction path
|
|
79
|
+
// rather than the realtime audio provider, so the agent never SPEAKS during
|
|
80
|
+
// field collection (which is where ungrounded narration leaked). Identical
|
|
81
|
+
// behavior to TextDriver — voice and text emit the same structural events; the
|
|
82
|
+
// user-facing question is the deterministic CollectNode.ask, synthesized after.
|
|
83
|
+
runExtraction(node, ctx) {
|
|
84
|
+
return runSilentExtraction(node, ctx, ctx.model, resolveMaxSteps(ctx.limits, this.maxSteps));
|
|
85
|
+
}
|
|
76
86
|
async awaitUser(ctx) {
|
|
77
87
|
if (this.pendingBargeInInput != null) {
|
|
78
88
|
const input = this.pendingBargeInInput;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type LanguageModel } from 'ai';
|
|
2
|
+
import type { ResolvedNode, TurnResult } from '../../types/channel.js';
|
|
3
|
+
import type { RunContext } from '../../types/run-context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Shared, NON-SPEAKING field extraction for `collect` nodes, used by every
|
|
6
|
+
* ChannelDriver so text and voice behave identically. It runs the model with the
|
|
7
|
+
* node's submit tool to pull structured fields, but never emits a `text-delta`,
|
|
8
|
+
* never emits `turn-end`, and never appends model prose — the model's words are
|
|
9
|
+
* discarded by construction. The user-facing question is emitted deterministically
|
|
10
|
+
* by the flow engine (`CollectNode.ask`), not by the model. This is the structural
|
|
11
|
+
* invariant that stops a collect turn from narrating outcomes that contradict
|
|
12
|
+
* flow state, regardless of which model is used.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runSilentExtraction(node: ResolvedNode, ctx: RunContext, model: LanguageModel, maxSteps: number): Promise<TurnResult>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { streamText } from 'ai';
|
|
2
|
+
import { buildToolSet } from '../../tools/effect/index.js';
|
|
3
|
+
import { buildNodePrompt, composeSystem } from '../../flow/nodeBuilders.js';
|
|
4
|
+
/**
|
|
5
|
+
* Shared, NON-SPEAKING field extraction for `collect` nodes, used by every
|
|
6
|
+
* ChannelDriver so text and voice behave identically. It runs the model with the
|
|
7
|
+
* node's submit tool to pull structured fields, but never emits a `text-delta`,
|
|
8
|
+
* never emits `turn-end`, and never appends model prose — the model's words are
|
|
9
|
+
* discarded by construction. The user-facing question is emitted deterministically
|
|
10
|
+
* by the flow engine (`CollectNode.ask`), not by the model. This is the structural
|
|
11
|
+
* invariant that stops a collect turn from narrating outcomes that contradict
|
|
12
|
+
* flow state, regardless of which model is used.
|
|
13
|
+
*/
|
|
14
|
+
export async function runSilentExtraction(node, ctx, model, maxSteps) {
|
|
15
|
+
const replyNode = node.node;
|
|
16
|
+
const nodeSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
|
|
17
|
+
const system = composeSystem(ctx.baseInstructions, nodeSystem, ctx.runState.state);
|
|
18
|
+
const messages = [...ctx.runState.messages];
|
|
19
|
+
const aiTools = resolveExtractionTools(node);
|
|
20
|
+
const out = { text: '', toolResults: [] };
|
|
21
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
22
|
+
const result = streamText({ model, system, messages, tools: aiTools, abortSignal: ctx.abortSignal });
|
|
23
|
+
for await (const part of result.fullStream) {
|
|
24
|
+
// Intentionally NOT handling 'text-delta' — extraction never speaks.
|
|
25
|
+
if (part.type === 'error') {
|
|
26
|
+
const err = part.error;
|
|
27
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28
|
+
ctx.emit({ type: 'error', error: message });
|
|
29
|
+
throw err instanceof Error ? err : new Error(message);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const finishReason = await result.finishReason;
|
|
33
|
+
const response = await result.response;
|
|
34
|
+
messages.push(...response.messages);
|
|
35
|
+
if (finishReason !== 'tool-calls') {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
const toolCalls = await result.toolCalls;
|
|
39
|
+
for (const call of toolCalls) {
|
|
40
|
+
const localTool = node.localTools?.[call.toolName];
|
|
41
|
+
const toolResult = await ctx.tool(call.toolName, call.input, {
|
|
42
|
+
toolCallId: call.toolCallId,
|
|
43
|
+
...(localTool && {
|
|
44
|
+
def: localTool,
|
|
45
|
+
toolCtx: {
|
|
46
|
+
session: ctx.session,
|
|
47
|
+
runState: ctx.runState,
|
|
48
|
+
tool: ctx.tool.bind(ctx),
|
|
49
|
+
now: ctx.now.bind(ctx),
|
|
50
|
+
uuid: ctx.uuid.bind(ctx),
|
|
51
|
+
emit: ctx.emit.bind(ctx),
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
out.toolResults.push({
|
|
56
|
+
name: call.toolName,
|
|
57
|
+
args: call.input,
|
|
58
|
+
result: toolResult,
|
|
59
|
+
toolCallId: call.toolCallId,
|
|
60
|
+
});
|
|
61
|
+
messages.push({
|
|
62
|
+
role: 'tool',
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: 'tool-result',
|
|
66
|
+
toolCallId: call.toolCallId,
|
|
67
|
+
toolName: call.toolName,
|
|
68
|
+
output: { type: 'json', value: toolResult },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** Tools available to the extraction turn = the node's submit tool only (built
|
|
77
|
+
* from the resolved node, independent of any driver-level tool defs) so text and
|
|
78
|
+
* voice resolve an identical toolset. */
|
|
79
|
+
function resolveExtractionTools(resolved) {
|
|
80
|
+
const aiTools = { ...resolved.tools };
|
|
81
|
+
for (const [name, tool] of Object.entries(resolved.localTools ?? {})) {
|
|
82
|
+
if (tool && !aiTools[name]) {
|
|
83
|
+
Object.assign(aiTools, buildToolSet({ [name]: tool }));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return Object.keys(aiTools).length > 0 ? aiTools : undefined;
|
|
87
|
+
}
|
|
@@ -16,6 +16,12 @@ export interface AgentConfig {
|
|
|
16
16
|
model?: LanguageModel;
|
|
17
17
|
tools?: ToolSet;
|
|
18
18
|
effectTools?: Record<string, AnyTool>;
|
|
19
|
+
/** Safe, always-available tools made model-visible in EVERY speaking node turn
|
|
20
|
+
* (the agent "base layer", ADR 0001) — e.g. a returns/FAQ knowledge-base
|
|
21
|
+
* lookup the user might ask for mid-flow. This is an explicit allow-list:
|
|
22
|
+
* NEVER put consequential/mutating tools here (they must stay flow-gated), and
|
|
23
|
+
* they are not exposed during non-speaking collect extraction. */
|
|
24
|
+
globalTools?: Record<string, AnyTool>;
|
|
19
25
|
flows?: Flow[];
|
|
20
26
|
routes?: Route[];
|
|
21
27
|
routing?: RoutingPolicy;
|
package/dist/types/channel.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface ChannelDriver {
|
|
|
14
14
|
runStructured?(node: Extract<FlowNode, {
|
|
15
15
|
kind: 'decide';
|
|
16
16
|
}>, ctx: RunContext): Promise<unknown>;
|
|
17
|
+
/** Non-speaking field extraction for `collect` nodes: runs the submit tool to
|
|
18
|
+
* pull structured fields but MUST NOT emit any user-facing text (no
|
|
19
|
+
* text-delta, no spoken transcript). The returned `text` is ignored by the
|
|
20
|
+
* flow engine. This is the structural backstop that stops a collect turn from
|
|
21
|
+
* authoring narration that contradicts flow state. Drivers without it fall
|
|
22
|
+
* back to runAgentTurn, whose text the engine then discards. */
|
|
23
|
+
runExtraction?(node: ResolvedNode, ctx: RunContext): Promise<TurnResult>;
|
|
17
24
|
}
|
|
18
25
|
export interface TurnResult {
|
|
19
26
|
text: string;
|
package/dist/types/flow.d.ts
CHANGED
|
@@ -42,7 +42,14 @@ export interface CollectNode {
|
|
|
42
42
|
id: string;
|
|
43
43
|
schema: StandardSchemaV1;
|
|
44
44
|
required?: string[];
|
|
45
|
+
/** Extraction-only guidance for the (non-speaking) field extraction turn.
|
|
46
|
+
* This text is NEVER shown to the user — see `ask` for user-facing copy. */
|
|
45
47
|
instructions?: (missing: string[], state: FlowState) => Instructions;
|
|
48
|
+
/** Deterministic, framework-emitted question shown when fields are still
|
|
49
|
+
* missing. Collect extraction never speaks model-authored text, so this is
|
|
50
|
+
* the only user-facing copy a collect node produces. Must not claim any
|
|
51
|
+
* downstream outcome (order placed, delivery scheduled, payment, website). */
|
|
52
|
+
ask?: (missing: string[], state: FlowState) => string;
|
|
46
53
|
choices?: ChoiceOption[];
|
|
47
54
|
maxTurns?: number;
|
|
48
55
|
onComplete: (data: unknown, state: FlowState) => Transition | Promise<Transition>;
|
|
@@ -8,6 +8,7 @@ import type { RefinementCapability } from '../capabilities/RefinementCapability.
|
|
|
8
8
|
import type { ValidationCapability } from '../capabilities/ValidationCapability.js';
|
|
9
9
|
import type { Limits } from './guardrails.js';
|
|
10
10
|
import type { AnyTool } from './effectTool.js';
|
|
11
|
+
import type { Instructions } from './agentConfig.js';
|
|
11
12
|
export interface EffectToolExecutor {
|
|
12
13
|
execute(args: {
|
|
13
14
|
name: string;
|
|
@@ -57,6 +58,12 @@ export interface RunContext {
|
|
|
57
58
|
* context. Reset to false on every `createRunContext` (i.e. every turn).
|
|
58
59
|
*/
|
|
59
60
|
turnInputConsumed?: boolean;
|
|
61
|
+
/** Agent base layer (ADR 0001), set when entering a flow. `baseInstructions`
|
|
62
|
+
* is composed as a prefix into every node turn's system prompt (persona /
|
|
63
|
+
* safety / grounding floor); `globalTools` are safe tools made model-visible
|
|
64
|
+
* in every speaking turn. */
|
|
65
|
+
baseInstructions?: Instructions;
|
|
66
|
+
globalTools?: Record<string, AnyTool>;
|
|
60
67
|
tool(name: string, args: unknown, options?: {
|
|
61
68
|
toolCallId?: string;
|
|
62
69
|
def?: AnyTool;
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "git+https://github.com/kuralle/kuralle-agents.git",
|
|
7
7
|
"directory": "packages/kuralle-core"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.6",
|
|
10
10
|
"description": "A framework for structured conversational AI agents",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"dotenv": "^16.4.0",
|
|
98
98
|
"typescript": "^5.3.0",
|
|
99
99
|
"zod": "^3.23.0",
|
|
100
|
-
"@kuralle-agents/realtime-audio": "0.3.
|
|
100
|
+
"@kuralle-agents/realtime-audio": "0.3.6"
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"chrono-node": "^2.6.0",
|