@kuralle-agents/core 0.3.13 → 0.3.15
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 +7 -4
- package/dist/flow/controlEvaluator.d.ts +18 -0
- package/dist/flow/controlEvaluator.js +26 -0
- package/dist/flow/extraction.d.ts +7 -1
- package/dist/flow/extraction.js +27 -1
- package/dist/flow/flowControlTools.d.ts +3 -0
- package/dist/flow/flowControlTools.js +11 -0
- package/dist/flow/nodeBuilders.d.ts +3 -1
- package/dist/flow/nodeBuilders.js +2 -1
- package/dist/flow/runFlow.js +19 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/Runtime.js +2 -0
- package/dist/runtime/channels/TextDriver.js +26 -4
- package/dist/runtime/channels/VoiceDriver.js +1 -1
- package/dist/runtime/channels/voiceTools.d.ts +3 -1
- package/dist/runtime/channels/voiceTools.js +13 -1
- package/dist/runtime/ctx.d.ts +1 -0
- package/dist/runtime/ctx.js +1 -0
- package/dist/runtime/hostLoop.js +1 -1
- package/dist/tools/Tool.d.ts +8 -2
- package/dist/tools/Tool.js +13 -4
- package/dist/tools/effect/ToolExecutor.js +25 -3
- package/dist/tools/effect/defineTool.d.ts +5 -0
- package/dist/tools/effect/defineTool.js +3 -2
- package/dist/tools/effect/errors.d.ts +5 -0
- package/dist/tools/effect/errors.js +10 -0
- package/dist/tools/effect/index.d.ts +1 -1
- package/dist/tools/effect/index.js +1 -1
- package/dist/types/agentConfig.d.ts +4 -0
- package/dist/types/channel.d.ts +2 -0
- package/dist/types/effectTool.d.ts +1 -0
- package/dist/types/run-context.d.ts +2 -0
- package/dist/types/stream.d.ts +4 -0
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hasPendingUserInput } from '../runtime/channels/inputBuffer.js';
|
|
2
2
|
import { resolveCollectExtractionNode } from './nodeBuilders.js';
|
|
3
|
-
import { computeMissingFields, createExtractionSubmitTool, getCollectData, incrementCollectTurns, mergeTurnExtraction, projectCollectData, schemaSatisfied, } from './extraction.js';
|
|
3
|
+
import { computeMissingFields, createExtractionSubmitTool, getCollectData, incrementCollectTurns, emitExtractionTelemetry, mergeTurnExtraction, projectCollectData, schemaSatisfied, } from './extraction.js';
|
|
4
4
|
import { normalizeTransition } from './normalizeTransition.js';
|
|
5
5
|
function appendAssistantMessage(run, text) {
|
|
6
6
|
if (!text.trim()) {
|
|
@@ -57,7 +57,7 @@ export async function collectUntilComplete(node, run, driver, ctx) {
|
|
|
57
57
|
const turn = await (driver.runExtraction
|
|
58
58
|
? driver.runExtraction(resolved, ctx)
|
|
59
59
|
: driver.runAgentTurn(resolved, ctx));
|
|
60
|
-
mergeExtractionFromTurn(node, run, turn);
|
|
60
|
+
mergeExtractionFromTurn(node, run, turn, ctx);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
function humanizeField(field) {
|
|
@@ -92,8 +92,11 @@ function emitCollectAsk(node, run, ctx) {
|
|
|
92
92
|
ctx.emit({ type: 'turn-end' });
|
|
93
93
|
appendAssistantMessage(run, text);
|
|
94
94
|
}
|
|
95
|
-
function mergeExtractionFromTurn(node, run, turn) {
|
|
96
|
-
mergeTurnExtraction(node, run.state, turn.toolResults.map((record) => ({ name: record.name, result: record.result })));
|
|
95
|
+
function mergeExtractionFromTurn(node, run, turn, ctx) {
|
|
96
|
+
const { merged, incoming } = mergeTurnExtraction(node, run.state, turn.toolResults.map((record) => ({ name: record.name, result: record.result })));
|
|
97
|
+
if (merged && incoming) {
|
|
98
|
+
emitExtractionTelemetry(node, run.state, incoming, ctx.emit);
|
|
99
|
+
}
|
|
97
100
|
}
|
|
98
101
|
function peekLatestUserMessage(run) {
|
|
99
102
|
for (let i = run.messages.length - 1; i >= 0; i -= 1) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FlowState, ReplyNode } from '../types/flow.js';
|
|
2
|
+
import type { TurnResult } from '../types/channel.js';
|
|
3
|
+
import { type NormalizedTransition } from './normalizeTransition.js';
|
|
4
|
+
export interface ControlSignal {
|
|
5
|
+
node: ReplyNode;
|
|
6
|
+
turn: TurnResult;
|
|
7
|
+
state: FlowState;
|
|
8
|
+
interrupted: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type ControlDecision = {
|
|
11
|
+
kind: 'transition';
|
|
12
|
+
transition: NormalizedTransition;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'redispatch';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'stay';
|
|
17
|
+
};
|
|
18
|
+
export declare function evaluateReplyControl(signal: ControlSignal): Promise<ControlDecision>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalizeTransition } from './normalizeTransition.js';
|
|
2
|
+
function controlToTransition(control) {
|
|
3
|
+
switch (control.type) {
|
|
4
|
+
case 'handoff':
|
|
5
|
+
return { kind: 'handoff', to: control.target, reason: control.reason };
|
|
6
|
+
case 'end':
|
|
7
|
+
return { kind: 'end', reason: control.reason };
|
|
8
|
+
case 'escalate':
|
|
9
|
+
return { kind: 'escalate', reason: control.reason };
|
|
10
|
+
case 'recover':
|
|
11
|
+
return { kind: 'end', reason: control.reason ?? 'error_degraded' };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function evaluateReplyControl(signal) {
|
|
15
|
+
const { node, turn, state, interrupted } = signal;
|
|
16
|
+
if (interrupted) {
|
|
17
|
+
return { kind: 'redispatch' };
|
|
18
|
+
}
|
|
19
|
+
if (turn.control) {
|
|
20
|
+
return { kind: 'transition', transition: controlToTransition(turn.control) };
|
|
21
|
+
}
|
|
22
|
+
if (node.next) {
|
|
23
|
+
return { kind: 'transition', transition: normalizeTransition(await node.next(turn, state)) };
|
|
24
|
+
}
|
|
25
|
+
return { kind: 'stay' };
|
|
26
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CollectNode, FlowState } from '../types/flow.js';
|
|
2
2
|
import type { Tool } from '../types/effectTool.js';
|
|
3
|
+
import type { HarnessStreamPart } from '../types/stream.js';
|
|
3
4
|
export declare function getCollectData(state: FlowState, nodeId: string): Record<string, unknown>;
|
|
4
5
|
export declare function incrementCollectTurns(state: FlowState, nodeId: string): number;
|
|
5
6
|
export declare function computeMissingFields(node: CollectNode, data: Record<string, unknown>): string[];
|
|
@@ -10,7 +11,12 @@ export declare function createExtractionSubmitTool(node: CollectNode, missingFie
|
|
|
10
11
|
userMessage?: string;
|
|
11
12
|
retryNudge?: boolean;
|
|
12
13
|
}): Tool;
|
|
14
|
+
export interface MergeTurnExtractionResult {
|
|
15
|
+
merged: boolean;
|
|
16
|
+
incoming?: Record<string, unknown>;
|
|
17
|
+
}
|
|
13
18
|
export declare function mergeTurnExtraction(node: CollectNode, state: FlowState, toolResults: Array<{
|
|
14
19
|
name: string;
|
|
15
20
|
result: unknown;
|
|
16
|
-
}>):
|
|
21
|
+
}>): MergeTurnExtractionResult;
|
|
22
|
+
export declare function emitExtractionTelemetry(node: CollectNode, state: FlowState, incoming: Record<string, unknown>, emit: (part: HarnessStreamPart) => void): void;
|
package/dist/flow/extraction.js
CHANGED
|
@@ -95,6 +95,7 @@ function submitToolName(nodeId) {
|
|
|
95
95
|
export function mergeTurnExtraction(node, state, toolResults) {
|
|
96
96
|
const submitName = submitToolName(node.id);
|
|
97
97
|
let merged = false;
|
|
98
|
+
let lastIncoming;
|
|
98
99
|
const current = getCollectData(state, node.id);
|
|
99
100
|
for (const record of toolResults) {
|
|
100
101
|
if (record.name !== submitName) {
|
|
@@ -108,8 +109,33 @@ export function mergeTurnExtraction(node, state, toolResults) {
|
|
|
108
109
|
setCollectData(state, node.id, next);
|
|
109
110
|
Object.assign(current, next);
|
|
110
111
|
merged = true;
|
|
112
|
+
lastIncoming = incoming;
|
|
111
113
|
}
|
|
112
|
-
return merged;
|
|
114
|
+
return merged ? { merged: true, incoming: lastIncoming } : { merged: false };
|
|
115
|
+
}
|
|
116
|
+
export function emitExtractionTelemetry(node, state, incoming, emit) {
|
|
117
|
+
const fieldsAccepted = [];
|
|
118
|
+
const fieldsRejected = [];
|
|
119
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
120
|
+
if (fieldPopulated(value)) {
|
|
121
|
+
fieldsAccepted.push(key);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
fieldsRejected.push(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
emit({
|
|
128
|
+
type: 'custom',
|
|
129
|
+
name: 'flow.extraction.submission',
|
|
130
|
+
data: { node: node.id, fieldsAccepted, fieldsRejected },
|
|
131
|
+
});
|
|
132
|
+
const collected = getCollectData(state, node.id);
|
|
133
|
+
const missing = computeMissingFields(node, collected);
|
|
134
|
+
emit({
|
|
135
|
+
type: 'custom',
|
|
136
|
+
name: 'flow.extraction.update',
|
|
137
|
+
data: { nodeId: node.id, collected, missing },
|
|
138
|
+
});
|
|
113
139
|
}
|
|
114
140
|
function fieldPopulated(value) {
|
|
115
141
|
if (value === null || value === undefined) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Model-initiated flow-transition tools siloed from flow reply speaking dicts (H1). */
|
|
2
|
+
export const FLOW_TRANSITION_CONTROL_TOOL_NAMES = new Set([
|
|
3
|
+
'handoff',
|
|
4
|
+
'transfer_to_agent',
|
|
5
|
+
'final',
|
|
6
|
+
'escalate',
|
|
7
|
+
'recover',
|
|
8
|
+
]);
|
|
9
|
+
export function isFlowTransitionControlTool(name) {
|
|
10
|
+
return FLOW_TRANSITION_CONTROL_TOOL_NAMES.has(name);
|
|
11
|
+
}
|
|
@@ -9,5 +9,7 @@ export declare function buildNodePrompt(node: ReplyNode, state: FlowState): stri
|
|
|
9
9
|
* own instructions. Node instructions layer ON TOP — they never replace the
|
|
10
10
|
* base. Base resolves against the current state so dynamic base prompts work. */
|
|
11
11
|
export declare function composeSystem(base: Instructions | undefined, nodeSystem: string, state: FlowState): string;
|
|
12
|
-
export declare function resolveReplyNode(node: ReplyNode, state: FlowState
|
|
12
|
+
export declare function resolveReplyNode(node: ReplyNode, state: FlowState, options?: {
|
|
13
|
+
freeConversation?: boolean;
|
|
14
|
+
}): ResolvedNode;
|
|
13
15
|
export declare function resolveCollectExtractionNode(collectNode: CollectNode, missing: string[], state: FlowState, submitTool: Tool): ResolvedNode;
|
|
@@ -32,7 +32,7 @@ function buildNodeTools(node, state) {
|
|
|
32
32
|
}
|
|
33
33
|
return node.tools;
|
|
34
34
|
}
|
|
35
|
-
export function resolveReplyNode(node, state) {
|
|
35
|
+
export function resolveReplyNode(node, state, options) {
|
|
36
36
|
const tools = buildNodeTools(node, state);
|
|
37
37
|
return {
|
|
38
38
|
node,
|
|
@@ -41,6 +41,7 @@ export function resolveReplyNode(node, state) {
|
|
|
41
41
|
// Recover the raw executors from the node's `buildToolSet` tools so they run
|
|
42
42
|
// in-flow (with run context) — without also needing `agent.effectTools`.
|
|
43
43
|
localTools: rawToolsFromSet(tools),
|
|
44
|
+
...(options?.freeConversation && { freeConversation: true }),
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
export function resolveCollectExtractionNode(collectNode, missing, state, submitTool) {
|
package/dist/flow/runFlow.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isActionNode, isCollectNode, isDecideNode, isReplyNode, } from './nodeK
|
|
|
5
5
|
import { normalizeTransition, resolveNodeRef } from './normalizeTransition.js';
|
|
6
6
|
import { reduceTransition } from './reduceTransition.js';
|
|
7
7
|
import { resolveReplyNode } from './nodeBuilders.js';
|
|
8
|
+
import { evaluateReplyControl } from './controlEvaluator.js';
|
|
8
9
|
import { runNodeVerify, VerifyBlockedError } from './verify.js';
|
|
9
10
|
import { loadRecordedSteps } from '../runtime/durable/replay.js';
|
|
10
11
|
import { SuspendError } from '../runtime/durable/RunStore.js';
|
|
@@ -128,6 +129,24 @@ async function dispatchNode(node, run, driver, ctx) {
|
|
|
128
129
|
}
|
|
129
130
|
if (isReplyNode(node)) {
|
|
130
131
|
const turn = await driver.runAgentTurn(resolveReplyNode(node, run.state), ctx);
|
|
132
|
+
if (ctx.outOfBandControl) {
|
|
133
|
+
const decision = await evaluateReplyControl({
|
|
134
|
+
node,
|
|
135
|
+
turn,
|
|
136
|
+
state: run.state,
|
|
137
|
+
interrupted: !!turn.interrupted,
|
|
138
|
+
});
|
|
139
|
+
if (decision.kind === 'redispatch') {
|
|
140
|
+
const signal = await driver.awaitUser(ctx);
|
|
141
|
+
appendUserMessage(run, signal.input);
|
|
142
|
+
return dispatchNode(node, run, driver, ctx);
|
|
143
|
+
}
|
|
144
|
+
appendAssistantMessage(run, turn.text);
|
|
145
|
+
if (decision.kind === 'transition') {
|
|
146
|
+
return decision.transition;
|
|
147
|
+
}
|
|
148
|
+
return { kind: 'stay' };
|
|
149
|
+
}
|
|
131
150
|
appendAssistantMessage(run, turn.text);
|
|
132
151
|
if (turn.interrupted) {
|
|
133
152
|
const signal = await driver.awaitUser(ctx);
|
package/dist/index.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export type { Hooks } from './types/hooks.js';
|
|
|
67
67
|
export type { HarnessHooks } from './types/runtime.js';
|
|
68
68
|
export { defineAgent, defineFlow, reply, collect, action, decide, confirmGate, } from './authoring/index.js';
|
|
69
69
|
export { defineTool } from './types/effectTool.js';
|
|
70
|
-
export { buildToolSet, toolToAiSdk, ToolApprovalDeniedError } from './tools/effect/index.js';
|
|
70
|
+
export { buildToolSet, toolToAiSdk, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
|
|
71
71
|
export type { Tool as EffectTool } from './types/effectTool.js';
|
|
72
72
|
export type { AgentRoute } from './types/processors.js';
|
|
73
73
|
export type { AgentConfig, Instructions } from './types/agentConfig.js';
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,6 @@ export { CapabilityHost, TriageCapability, ExtractionCapability, HandoffCapabili
|
|
|
38
38
|
export { filterAuditEntries } from './audit/index.js';
|
|
39
39
|
export { defineAgent, defineFlow, reply, collect, action, decide, confirmGate, } from './authoring/index.js';
|
|
40
40
|
export { defineTool } from './types/effectTool.js';
|
|
41
|
-
export { buildToolSet, toolToAiSdk, ToolApprovalDeniedError } from './tools/effect/index.js';
|
|
41
|
+
export { buildToolSet, toolToAiSdk, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
|
|
42
42
|
export { parseConfirmation } from './flow/confirmParse.js';
|
|
43
43
|
export { createRuntime, Runtime, } from './runtime/Runtime.js';
|
package/dist/runtime/Runtime.js
CHANGED
|
@@ -74,6 +74,7 @@ export class Runtime {
|
|
|
74
74
|
tools: effectTools,
|
|
75
75
|
enforcer: policies.enforcer,
|
|
76
76
|
agentId: opened.agent.id,
|
|
77
|
+
onInterim: (message) => emit({ type: 'text-delta', text: message }),
|
|
77
78
|
});
|
|
78
79
|
const steps = await loadRecordedSteps(opened.runStore, opened.runState.runId);
|
|
79
80
|
const freshRunState = (await opened.runStore.getRunState(opened.runState.runId)) ?? opened.runState;
|
|
@@ -109,6 +110,7 @@ export class Runtime {
|
|
|
109
110
|
// Agent base layer (ADR 0001): composed into every node turn by the drivers.
|
|
110
111
|
runCtx.baseInstructions = opened.agent.instructions;
|
|
111
112
|
runCtx.globalTools = opened.agent.globalTools;
|
|
113
|
+
runCtx.outOfBandControl = opened.agent.experimental?.outOfBandControl ?? false;
|
|
112
114
|
await this.hooks?.onStart?.(runCtx);
|
|
113
115
|
const driver = opts.driver ?? new TextDriver();
|
|
114
116
|
let activeAgent = opened.agent;
|
|
@@ -7,6 +7,7 @@ import { runSilentExtraction } from './extractionTurn.js';
|
|
|
7
7
|
import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTurn.js';
|
|
8
8
|
import { resolveMaxSteps } from '../policies/limits.js';
|
|
9
9
|
import { appendGatherBlocks, resolveNodeGatherScope, runGatherPhase } from '../grounding/index.js';
|
|
10
|
+
import { isFlowTransitionControlTool } from '../../flow/flowControlTools.js';
|
|
10
11
|
export class TextDriver {
|
|
11
12
|
toolDefs;
|
|
12
13
|
maxSteps;
|
|
@@ -34,7 +35,7 @@ export class TextDriver {
|
|
|
34
35
|
const baseSystem = composeSystem(ctx.baseInstructions, nodeSystem, ctx.runState.state);
|
|
35
36
|
const system = appendGatherBlocks(baseSystem, [gather.retrievalBlock, gather.memoryBlock]);
|
|
36
37
|
const messages = [...ctx.runState.messages];
|
|
37
|
-
const aiTools = this.resolveTools(node, ctx
|
|
38
|
+
const aiTools = this.resolveTools(node, ctx);
|
|
38
39
|
const maxSteps = resolveMaxSteps(ctx.limits, this.maxSteps);
|
|
39
40
|
const toolCallsMade = [];
|
|
40
41
|
let draftText = '';
|
|
@@ -137,20 +138,41 @@ export class TextDriver {
|
|
|
137
138
|
const input = consumePendingUserInput(ctx.session);
|
|
138
139
|
return { type: 'message', input };
|
|
139
140
|
}
|
|
140
|
-
resolveTools(resolved,
|
|
141
|
-
const
|
|
141
|
+
resolveTools(resolved, ctx) {
|
|
142
|
+
const siloFlowControl = ctx.outOfBandControl && !resolved.freeConversation;
|
|
143
|
+
const merged = {
|
|
144
|
+
...this.toolDefs,
|
|
145
|
+
...(ctx.globalTools ?? {}),
|
|
146
|
+
...(resolved.localTools ?? {}),
|
|
147
|
+
};
|
|
142
148
|
const aiTools = { ...resolved.tools };
|
|
143
149
|
for (const [name, tool] of Object.entries(merged)) {
|
|
150
|
+
if (siloFlowControl && isFlowTransitionControlTool(name)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
144
153
|
if (tool && !aiTools[name]) {
|
|
145
154
|
const built = buildToolSet({ [name]: tool });
|
|
146
155
|
Object.assign(aiTools, built);
|
|
147
156
|
}
|
|
148
157
|
}
|
|
158
|
+
if (siloFlowControl) {
|
|
159
|
+
for (const name of Object.keys(aiTools)) {
|
|
160
|
+
if (isFlowTransitionControlTool(name)) {
|
|
161
|
+
delete aiTools[name];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
149
165
|
if (Object.keys(aiTools).length === 0 && Object.keys(merged).length === 0) {
|
|
150
166
|
return undefined;
|
|
151
167
|
}
|
|
152
168
|
if (Object.keys(aiTools).length === 0) {
|
|
153
|
-
|
|
169
|
+
const filteredMerged = siloFlowControl
|
|
170
|
+
? Object.fromEntries(Object.entries(merged).filter(([name]) => !isFlowTransitionControlTool(name)))
|
|
171
|
+
: merged;
|
|
172
|
+
if (Object.keys(filteredMerged).length === 0) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return buildToolSet(filteredMerged);
|
|
154
176
|
}
|
|
155
177
|
return aiTools;
|
|
156
178
|
}
|
|
@@ -35,7 +35,7 @@ export class VoiceDriver {
|
|
|
35
35
|
const nodeSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
|
|
36
36
|
const baseSystem = composeSystem(ctx.baseInstructions, nodeSystem, ctx.runState.state);
|
|
37
37
|
const system = appendGatherBlocks(baseSystem, [gather.retrievalBlock, gather.memoryBlock]);
|
|
38
|
-
const geminiTools = resolveVoiceGeminiTools(node, { ...this.toolDefs, ...(ctx.globalTools ?? {}) });
|
|
38
|
+
const geminiTools = resolveVoiceGeminiTools(node, { ...this.toolDefs, ...(ctx.globalTools ?? {}) }, { siloFlowControl: ctx.outOfBandControl && !node.freeConversation });
|
|
39
39
|
const toolCallsMade = [];
|
|
40
40
|
const maxSteps = resolveMaxSteps(ctx.limits, this.maxSteps);
|
|
41
41
|
let draftText = '';
|
|
@@ -3,5 +3,7 @@ import type { ResolvedNode } from '../../types/channel.js';
|
|
|
3
3
|
import type { AnyTool } from '../../types/effectTool.js';
|
|
4
4
|
import { buildToolSet } from '../../tools/effect/defineTool.js';
|
|
5
5
|
export declare function v2ToolsToGemini(tools: Record<string, AnyTool>): GeminiFunctionDeclaration[];
|
|
6
|
-
export declare function resolveVoiceGeminiTools(resolved: ResolvedNode, toolDefs: Record<string, AnyTool
|
|
6
|
+
export declare function resolveVoiceGeminiTools(resolved: ResolvedNode, toolDefs: Record<string, AnyTool>, options?: {
|
|
7
|
+
siloFlowControl?: boolean;
|
|
8
|
+
}): GeminiFunctionDeclaration[];
|
|
7
9
|
export { buildToolSet };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { toGeminiDeclarations } from '../../capabilities/adapters/gemini.js';
|
|
3
3
|
import { buildToolSet } from '../../tools/effect/defineTool.js';
|
|
4
|
+
import { isFlowTransitionControlTool } from '../../flow/flowControlTools.js';
|
|
4
5
|
function toolToDeclaration(name, tool) {
|
|
5
6
|
return {
|
|
6
7
|
name: tool.name || name,
|
|
@@ -13,14 +14,25 @@ export function v2ToolsToGemini(tools) {
|
|
|
13
14
|
const declarations = Object.entries(tools).map(([name, tool]) => toolToDeclaration(name, tool));
|
|
14
15
|
return toGeminiDeclarations(declarations);
|
|
15
16
|
}
|
|
16
|
-
export function resolveVoiceGeminiTools(resolved, toolDefs) {
|
|
17
|
+
export function resolveVoiceGeminiTools(resolved, toolDefs, options) {
|
|
18
|
+
const siloFlowControl = options?.siloFlowControl === true;
|
|
17
19
|
const merged = { ...toolDefs, ...(resolved.localTools ?? {}) };
|
|
18
20
|
const fromNode = toolSetToEffectTools(resolved.tools);
|
|
19
21
|
for (const [name, tool] of Object.entries(fromNode)) {
|
|
22
|
+
if (siloFlowControl && isFlowTransitionControlTool(name)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
20
25
|
if (!merged[name]) {
|
|
21
26
|
merged[name] = tool;
|
|
22
27
|
}
|
|
23
28
|
}
|
|
29
|
+
if (siloFlowControl) {
|
|
30
|
+
for (const name of Object.keys(merged)) {
|
|
31
|
+
if (isFlowTransitionControlTool(name)) {
|
|
32
|
+
delete merged[name];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
24
36
|
return v2ToolsToGemini(merged);
|
|
25
37
|
}
|
|
26
38
|
function toolSetToEffectTools(tools) {
|
package/dist/runtime/ctx.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface CtxDeps {
|
|
|
21
21
|
hookRunner?: HookRunner;
|
|
22
22
|
model: LanguageModel;
|
|
23
23
|
controlModel?: LanguageModel;
|
|
24
|
+
outOfBandControl?: boolean;
|
|
24
25
|
refinementPolicies?: RefinementCapability[];
|
|
25
26
|
validationPolicies?: ValidationCapability[];
|
|
26
27
|
inputProcessors?: InputProcessor[];
|
package/dist/runtime/ctx.js
CHANGED
|
@@ -100,6 +100,7 @@ function makeCtx(deps) {
|
|
|
100
100
|
hookRunner: deps.hookRunner ?? {},
|
|
101
101
|
model: deps.model,
|
|
102
102
|
controlModel: deps.controlModel ?? deps.model,
|
|
103
|
+
outOfBandControl: deps.outOfBandControl ?? false,
|
|
103
104
|
refinementPolicies: deps.refinementPolicies ?? [],
|
|
104
105
|
validationPolicies: deps.validationPolicies ?? [],
|
|
105
106
|
inputProcessors: deps.inputProcessors ?? [],
|
package/dist/runtime/hostLoop.js
CHANGED
|
@@ -74,7 +74,7 @@ async function runFreeConversation(agent, run, driver, ctx) {
|
|
|
74
74
|
incrementTurnCount(run);
|
|
75
75
|
assertWithinTurnLimit(run, ctx.limits);
|
|
76
76
|
const replyNode = buildAgentReplyNode(agent);
|
|
77
|
-
const turn = await driver.runAgentTurn(resolveReplyNode(replyNode, run.state), ctx);
|
|
77
|
+
const turn = await driver.runAgentTurn(resolveReplyNode(replyNode, run.state, { freeConversation: true }), ctx);
|
|
78
78
|
if (turn.text.trim()) {
|
|
79
79
|
const message = { role: 'assistant', content: turn.text };
|
|
80
80
|
run.messages = [...run.messages, message];
|
package/dist/tools/Tool.d.ts
CHANGED
|
@@ -25,10 +25,12 @@ export interface ToolDefinition<TInput = unknown, TResult = unknown> {
|
|
|
25
25
|
errorPolicy?: 'abort' | 'warn' | 'continue';
|
|
26
26
|
}
|
|
27
27
|
export interface ToolWithFiller<TInput = unknown, TResult = unknown> extends ToolDefinition<TInput, TResult> {
|
|
28
|
-
/**
|
|
28
|
+
/** @deprecated Use `interim` on effect tools (`defineTool`). */
|
|
29
29
|
filler?: string;
|
|
30
|
-
/**
|
|
30
|
+
/** @deprecated Use `interimAfterMs` on effect tools (`defineTool`). */
|
|
31
31
|
estimatedDurationMs?: number;
|
|
32
|
+
interim?: string;
|
|
33
|
+
interimAfterMs?: number;
|
|
32
34
|
}
|
|
33
35
|
type SchemaToolDefinition<TSchema extends ZodTypeAny, TResult = unknown> = {
|
|
34
36
|
description: string;
|
|
@@ -38,8 +40,12 @@ type SchemaToolDefinition<TSchema extends ZodTypeAny, TResult = unknown> = {
|
|
|
38
40
|
errorPolicy?: 'abort' | 'warn' | 'continue';
|
|
39
41
|
};
|
|
40
42
|
type SchemaToolWithFiller<TSchema extends ZodTypeAny, TResult = unknown> = SchemaToolDefinition<TSchema, TResult> & {
|
|
43
|
+
/** @deprecated Use `interim`. */
|
|
41
44
|
filler?: string;
|
|
45
|
+
/** @deprecated Use `interimAfterMs`. */
|
|
42
46
|
estimatedDurationMs?: number;
|
|
47
|
+
interim?: string;
|
|
48
|
+
interimAfterMs?: number;
|
|
43
49
|
};
|
|
44
50
|
export declare function createTool<TSchema extends ZodTypeAny, TResult = unknown>(definition: SchemaToolDefinition<TSchema, TResult>): Tool<z.infer<TSchema>, TResult> & ToolDefinition<z.infer<TSchema>, TResult>;
|
|
45
51
|
export declare function createToolWithFiller<TSchema extends ZodTypeAny, TResult = unknown>(definition: SchemaToolWithFiller<TSchema, TResult>): Tool<z.infer<TSchema>, TResult> & ToolWithFiller<z.infer<TSchema>, TResult>;
|
package/dist/tools/Tool.js
CHANGED
|
@@ -11,9 +11,18 @@ export function createToolWithFiller(definition) {
|
|
|
11
11
|
// @ts-expect-error — same deferred-generic limitation as createTool (see above).
|
|
12
12
|
const t = aiTool({ description, inputSchema: zodSchema(inputSchema), execute });
|
|
13
13
|
const extended = Object.assign(t, definition);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
extended.
|
|
14
|
+
const interim = definition.interim ?? definition.filler;
|
|
15
|
+
const interimAfterMs = definition.interimAfterMs ?? definition.estimatedDurationMs;
|
|
16
|
+
if (interim) {
|
|
17
|
+
extended.interim = interim;
|
|
18
|
+
if (definition.filler)
|
|
19
|
+
extended.filler = definition.filler;
|
|
20
|
+
}
|
|
21
|
+
if (interimAfterMs != null) {
|
|
22
|
+
extended.interimAfterMs = interimAfterMs;
|
|
23
|
+
if (definition.estimatedDurationMs != null) {
|
|
24
|
+
extended.estimatedDurationMs = definition.estimatedDurationMs;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
18
27
|
return extended;
|
|
19
28
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { debug } from '../../debug.js';
|
|
3
3
|
import { cancelledPlaceholder, inProgressPlaceholder, PairingTracker, } from './pairing.js';
|
|
4
|
+
import { ToolTimeoutError } from './errors.js';
|
|
4
5
|
import { ToolValidationError, validateAndSanitize, validateOutput } from './schema.js';
|
|
5
6
|
export class CoreToolExecutor {
|
|
6
7
|
tools;
|
|
@@ -93,10 +94,13 @@ export class CoreToolExecutor {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
let interimTimer;
|
|
97
|
+
let timeoutTimer;
|
|
96
98
|
let interimSent = false;
|
|
97
99
|
const onAbort = () => {
|
|
98
100
|
if (interimTimer)
|
|
99
101
|
clearTimeout(interimTimer);
|
|
102
|
+
if (timeoutTimer)
|
|
103
|
+
clearTimeout(timeoutTimer);
|
|
100
104
|
};
|
|
101
105
|
abortSignal?.addEventListener('abort', onAbort, { once: true });
|
|
102
106
|
try {
|
|
@@ -129,11 +133,27 @@ export class CoreToolExecutor {
|
|
|
129
133
|
abortSignal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
|
|
130
134
|
})
|
|
131
135
|
: null;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
const timeoutMs = def.timeoutMs;
|
|
137
|
+
const timeoutPromise = timeoutMs != null && timeoutMs > 0
|
|
138
|
+
? new Promise((_, reject) => {
|
|
139
|
+
timeoutTimer = setTimeout(() => {
|
|
140
|
+
reject(new ToolTimeoutError(name, timeoutMs));
|
|
141
|
+
}, timeoutMs);
|
|
142
|
+
if (typeof timeoutTimer === 'object' && 'unref' in timeoutTimer) {
|
|
143
|
+
timeoutTimer.unref();
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
: null;
|
|
147
|
+
const racers = [executePromise];
|
|
148
|
+
if (abortPromise)
|
|
149
|
+
racers.push(abortPromise);
|
|
150
|
+
if (timeoutPromise)
|
|
151
|
+
racers.push(timeoutPromise);
|
|
152
|
+
const rawResult = racers.length > 1 ? await Promise.race(racers) : await executePromise;
|
|
135
153
|
if (interimTimer)
|
|
136
154
|
clearTimeout(interimTimer);
|
|
155
|
+
if (timeoutTimer)
|
|
156
|
+
clearTimeout(timeoutTimer);
|
|
137
157
|
const validated = await validateOutput(def.output, rawResult, name);
|
|
138
158
|
callRecord.result = validated;
|
|
139
159
|
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
@@ -149,6 +169,8 @@ export class CoreToolExecutor {
|
|
|
149
169
|
catch (error) {
|
|
150
170
|
if (interimTimer)
|
|
151
171
|
clearTimeout(interimTimer);
|
|
172
|
+
if (timeoutTimer)
|
|
173
|
+
clearTimeout(timeoutTimer);
|
|
152
174
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
153
175
|
const placeholder = cancelledPlaceholder(requestId, name);
|
|
154
176
|
callRecord.success = false;
|
|
@@ -13,6 +13,11 @@ export declare function defineTool<S extends z.ZodTypeAny | StandardSchemaV1 | u
|
|
|
13
13
|
interruptible?: boolean;
|
|
14
14
|
interim?: string;
|
|
15
15
|
interimAfterMs?: number;
|
|
16
|
+
/** @deprecated Use `interim`. */
|
|
17
|
+
filler?: string;
|
|
18
|
+
/** @deprecated Use `interimAfterMs`. */
|
|
19
|
+
estimatedDurationMs?: number;
|
|
20
|
+
timeoutMs?: number;
|
|
16
21
|
execute: (args: InferToolInput<S>, ctx?: ToolContext) => Promise<R> | AsyncIterable<R>;
|
|
17
22
|
}): Tool<InferToolInput<S>, R>;
|
|
18
23
|
export declare function toolToAiSdk<TInput = unknown, TOutput = unknown>(def: Tool<TInput, TOutput>): AiTool<TInput, TOutput>;
|
|
@@ -7,8 +7,9 @@ export function defineTool(config) {
|
|
|
7
7
|
output: config.output,
|
|
8
8
|
needsApproval: config.needsApproval,
|
|
9
9
|
interruptible: config.interruptible,
|
|
10
|
-
interim: config.interim,
|
|
11
|
-
interimAfterMs: config.interimAfterMs,
|
|
10
|
+
interim: config.interim ?? config.filler,
|
|
11
|
+
interimAfterMs: config.interimAfterMs ?? config.estimatedDurationMs,
|
|
12
|
+
timeoutMs: config.timeoutMs,
|
|
12
13
|
execute: config.execute,
|
|
13
14
|
};
|
|
14
15
|
}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* by a human (the `__approval` signal resolves with `approved: false`). Catch it
|
|
4
4
|
* inside the calling flow `action` node to route gracefully (e.g. escalate or end).
|
|
5
5
|
*/
|
|
6
|
+
export declare class ToolTimeoutError extends Error {
|
|
7
|
+
readonly toolName: string;
|
|
8
|
+
readonly timeoutMs: number;
|
|
9
|
+
constructor(toolName: string, timeoutMs: number);
|
|
10
|
+
}
|
|
6
11
|
export declare class ToolApprovalDeniedError extends Error {
|
|
7
12
|
readonly toolName: string;
|
|
8
13
|
readonly by?: string;
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
* by a human (the `__approval` signal resolves with `approved: false`). Catch it
|
|
4
4
|
* inside the calling flow `action` node to route gracefully (e.g. escalate or end).
|
|
5
5
|
*/
|
|
6
|
+
export class ToolTimeoutError extends Error {
|
|
7
|
+
toolName;
|
|
8
|
+
timeoutMs;
|
|
9
|
+
constructor(toolName, timeoutMs) {
|
|
10
|
+
super(`Tool "${toolName}" timeout after ${timeoutMs}ms`);
|
|
11
|
+
this.name = 'ToolTimeoutError';
|
|
12
|
+
this.toolName = toolName;
|
|
13
|
+
this.timeoutMs = timeoutMs;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
6
16
|
export class ToolApprovalDeniedError extends Error {
|
|
7
17
|
toolName;
|
|
8
18
|
by;
|
|
@@ -4,4 +4,4 @@ export type { CoreToolExecutorConfig, CoreExecuteArgs } from './ToolExecutor.js'
|
|
|
4
4
|
export { PairingTracker, cancelledPlaceholder, inProgressPlaceholder, } from './pairing.js';
|
|
5
5
|
export type { ToolCallPair, ToolPairStatus, ToolRequestRecord, ToolResponseRecord, CancelledToolResult, InProgressToolResult, } from './pairing.js';
|
|
6
6
|
export { validateAndSanitize, validateOutput, ToolValidationError } from './schema.js';
|
|
7
|
-
export { ToolApprovalDeniedError } from './errors.js';
|
|
7
|
+
export { ToolApprovalDeniedError, ToolTimeoutError } from './errors.js';
|
|
@@ -2,4 +2,4 @@ export { defineTool, toolToAiSdk, buildToolSet } from './defineTool.js';
|
|
|
2
2
|
export { CoreToolExecutor } from './ToolExecutor.js';
|
|
3
3
|
export { PairingTracker, cancelledPlaceholder, inProgressPlaceholder, } from './pairing.js';
|
|
4
4
|
export { validateAndSanitize, validateOutput, ToolValidationError } from './schema.js';
|
|
5
|
-
export { ToolApprovalDeniedError } from './errors.js';
|
|
5
|
+
export { ToolApprovalDeniedError, ToolTimeoutError } from './errors.js';
|
|
@@ -35,5 +35,9 @@ export interface AgentConfig {
|
|
|
35
35
|
memory?: AgentMemory;
|
|
36
36
|
guardrails?: Guardrails;
|
|
37
37
|
limits?: Limits;
|
|
38
|
+
experimental?: {
|
|
39
|
+
/** Flow reply nodes: silo flow-transition control tools + deterministic evaluator (ADR 0003 H1). Default OFF. */
|
|
40
|
+
outOfBandControl?: boolean;
|
|
41
|
+
};
|
|
38
42
|
}
|
|
39
43
|
export declare function defineAgent(config: AgentConfig): AgentConfig;
|
package/dist/types/channel.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface ResolvedNode {
|
|
|
7
7
|
prompt: string;
|
|
8
8
|
tools: ToolSet;
|
|
9
9
|
localTools?: Record<string, AnyTool>;
|
|
10
|
+
/** Free-conversation reply (host loop): keeps model control tools even when outOfBandControl is on. */
|
|
11
|
+
freeConversation?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export interface ChannelDriver {
|
|
12
14
|
runAgentTurn(node: ResolvedNode, ctx: RunContext): Promise<TurnResult>;
|
|
@@ -9,6 +9,7 @@ export interface Tool<TInput = unknown, TOutput = unknown> {
|
|
|
9
9
|
interruptible?: boolean;
|
|
10
10
|
interim?: string;
|
|
11
11
|
interimAfterMs?: number;
|
|
12
|
+
timeoutMs?: number;
|
|
12
13
|
execute: (args: TInput, ctx?: ToolContext) => Promise<TOutput> | AsyncIterable<TOutput>;
|
|
13
14
|
}
|
|
14
15
|
export type AnyTool = Tool<any, any>;
|
|
@@ -53,6 +53,8 @@ export interface RunContext {
|
|
|
53
53
|
model: LanguageModel;
|
|
54
54
|
/** Control-path model (routing, decide, extraction) at temperature 0. */
|
|
55
55
|
controlModel: LanguageModel;
|
|
56
|
+
/** When true, flow reply nodes use the out-of-band control evaluator (ADR 0003 H1). */
|
|
57
|
+
outOfBandControl: boolean;
|
|
56
58
|
refinementPolicies: RefinementCapability[];
|
|
57
59
|
validationPolicies: ValidationCapability[];
|
|
58
60
|
inputProcessors: InputProcessor[];
|
package/dist/types/stream.d.ts
CHANGED
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.15",
|
|
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.15"
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"chrono-node": "^2.6.0",
|