@kuralle-agents/core 0.3.14 → 0.3.16
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/choiceMatch.d.ts +17 -0
- package/dist/flow/choiceMatch.js +119 -0
- package/dist/flow/collectUntilComplete.js +7 -4
- package/dist/flow/extraction.d.ts +7 -1
- package/dist/flow/extraction.js +27 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/Runtime.js +1 -0
- package/dist/runtime/channels/TextDriver.js +4 -21
- package/dist/runtime/channels/VoiceDriver.js +3 -19
- package/dist/runtime/select.js +30 -0
- 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/effectTool.d.ts +1 -0
- package/dist/types/stream.d.ts +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ModelMessage } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { DecideNode } from '../types/flow.js';
|
|
4
|
+
import type { ChoiceOption } from '../types/selection.js';
|
|
5
|
+
import type { RunContext } from '../types/run-context.js';
|
|
6
|
+
/** Reserved enum member: model declines to pick a listed choice (→ author stay/unmatched). */
|
|
7
|
+
export declare const CHOICE_NONE = "__none";
|
|
8
|
+
export declare function isConstrainedChoiceEnumSchema(schema: unknown): boolean;
|
|
9
|
+
export declare function isChoiceFieldSchema(schema: unknown): schema is z.ZodObject<{
|
|
10
|
+
choice: z.ZodString;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function buildChoiceEnumSchema(choices: ChoiceOption[]): z.ZodObject<{
|
|
13
|
+
choice: z.ZodEnum<[string, ...string[]]>;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function latestUserMessageText(messages: ModelMessage[]): string | undefined;
|
|
16
|
+
export declare function matchChoiceFromInput(input: string, choices: ChoiceOption[]): string | undefined;
|
|
17
|
+
export declare function resolveStructuredDecide(node: DecideNode, ctx: RunContext, system: string): Promise<unknown>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { generateObject } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/** Reserved enum member: model declines to pick a listed choice (→ author stay/unmatched). */
|
|
4
|
+
export const CHOICE_NONE = '__none';
|
|
5
|
+
function normalize(raw) {
|
|
6
|
+
return raw.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
7
|
+
}
|
|
8
|
+
function keywordTerms(text) {
|
|
9
|
+
return normalize(text)
|
|
10
|
+
.split(/[^a-z0-9]+/)
|
|
11
|
+
.filter((term) => term.length > 4);
|
|
12
|
+
}
|
|
13
|
+
export function isConstrainedChoiceEnumSchema(schema) {
|
|
14
|
+
if (!(schema instanceof z.ZodObject)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return schema.shape.choice instanceof z.ZodEnum;
|
|
18
|
+
}
|
|
19
|
+
export function isChoiceFieldSchema(schema) {
|
|
20
|
+
if (!(schema instanceof z.ZodObject)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const keys = Object.keys(schema.shape);
|
|
24
|
+
if (keys.length !== 1 || keys[0] !== 'choice') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return schema.shape.choice instanceof z.ZodString;
|
|
28
|
+
}
|
|
29
|
+
export function buildChoiceEnumSchema(choices) {
|
|
30
|
+
const ids = choices.map((c) => c.id);
|
|
31
|
+
if (ids.length === 0) {
|
|
32
|
+
return z.object({ choice: z.enum([CHOICE_NONE]) });
|
|
33
|
+
}
|
|
34
|
+
const members = [ids[0], ...ids.slice(1), CHOICE_NONE];
|
|
35
|
+
return z.object({ choice: z.enum(members) });
|
|
36
|
+
}
|
|
37
|
+
export function latestUserMessageText(messages) {
|
|
38
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
39
|
+
const message = messages[i];
|
|
40
|
+
if (message?.role === 'user') {
|
|
41
|
+
if (typeof message.content === 'string') {
|
|
42
|
+
return message.content;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(message.content)) {
|
|
45
|
+
const text = message.content
|
|
46
|
+
.filter((part) => part.type === 'text')
|
|
47
|
+
.map((part) => part.text)
|
|
48
|
+
.join('');
|
|
49
|
+
if (text) {
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
export function matchChoiceFromInput(input, choices) {
|
|
58
|
+
const normalized = normalize(input);
|
|
59
|
+
if (!normalized) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const idMatches = choices.filter((c) => normalize(c.id) === normalized).map((c) => c.id);
|
|
63
|
+
if (idMatches.length === 1) {
|
|
64
|
+
return idMatches[0];
|
|
65
|
+
}
|
|
66
|
+
if (idMatches.length > 1) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const labelMatches = choices.filter((c) => normalize(c.label) === normalized).map((c) => c.id);
|
|
70
|
+
if (labelMatches.length === 1) {
|
|
71
|
+
return labelMatches[0];
|
|
72
|
+
}
|
|
73
|
+
if (labelMatches.length > 1) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const keywordMatches = [];
|
|
77
|
+
for (const choice of choices) {
|
|
78
|
+
const terms = keywordTerms(choice.label);
|
|
79
|
+
if (terms.length > 0 && terms.some((term) => normalized.includes(term))) {
|
|
80
|
+
keywordMatches.push(choice.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (keywordMatches.length === 1) {
|
|
84
|
+
return keywordMatches[0];
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
export async function resolveStructuredDecide(node, ctx, system) {
|
|
89
|
+
const schema = node.schema;
|
|
90
|
+
const useConstrainedChoices = (node.choices?.length ?? 0) > 0 && isChoiceFieldSchema(schema);
|
|
91
|
+
if (!useConstrainedChoices) {
|
|
92
|
+
const { object } = await generateObject({
|
|
93
|
+
model: ctx.controlModel,
|
|
94
|
+
schema,
|
|
95
|
+
system,
|
|
96
|
+
messages: ctx.runState.messages,
|
|
97
|
+
temperature: 0,
|
|
98
|
+
abortSignal: ctx.abortSignal,
|
|
99
|
+
});
|
|
100
|
+
return object;
|
|
101
|
+
}
|
|
102
|
+
const input = latestUserMessageText(ctx.runState.messages);
|
|
103
|
+
if (input) {
|
|
104
|
+
const matched = matchChoiceFromInput(input, node.choices);
|
|
105
|
+
if (matched) {
|
|
106
|
+
return { choice: matched };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const choiceSchema = buildChoiceEnumSchema(node.choices);
|
|
110
|
+
const { object } = await generateObject({
|
|
111
|
+
model: ctx.controlModel,
|
|
112
|
+
schema: choiceSchema,
|
|
113
|
+
system,
|
|
114
|
+
messages: ctx.runState.messages,
|
|
115
|
+
temperature: 0,
|
|
116
|
+
abortSignal: ctx.abortSignal,
|
|
117
|
+
});
|
|
118
|
+
return object;
|
|
119
|
+
}
|
|
@@ -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) {
|
|
@@ -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) {
|
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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { streamText
|
|
1
|
+
import { streamText } from 'ai';
|
|
2
2
|
import { buildNodePrompt, resolveInstructions, composeSystem } from '../../flow/nodeBuilders.js';
|
|
3
3
|
import { buildToolSet } from '../../tools/effect/index.js';
|
|
4
4
|
import { executeModelToolCall, toolResultMessage } from './executeModelTool.js';
|
|
@@ -8,6 +8,7 @@ import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTu
|
|
|
8
8
|
import { resolveMaxSteps } from '../policies/limits.js';
|
|
9
9
|
import { appendGatherBlocks, resolveNodeGatherScope, runGatherPhase } from '../grounding/index.js';
|
|
10
10
|
import { isFlowTransitionControlTool } from '../../flow/flowControlTools.js';
|
|
11
|
+
import { resolveStructuredDecide } from '../../flow/choiceMatch.js';
|
|
11
12
|
export class TextDriver {
|
|
12
13
|
toolDefs;
|
|
13
14
|
maxSteps;
|
|
@@ -113,26 +114,8 @@ export class TextDriver {
|
|
|
113
114
|
return runSilentExtraction(node, ctx, ctx.controlModel, resolveMaxSteps(ctx.limits, this.maxSteps));
|
|
114
115
|
}
|
|
115
116
|
async runStructured(node, ctx) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
// When the node offers choices (e.g. via withChoices), constrain the model
|
|
119
|
-
// to return exactly one option id. Otherwise an unconstrained string schema
|
|
120
|
-
// lets the model reply with free-form prose that `decide()` can't match,
|
|
121
|
-
// stalling the flow at every interactive node.
|
|
122
|
-
const system = node.choices?.length
|
|
123
|
-
? `${base}\n\nYou MUST pick exactly ONE option by its id. Valid ids: ${node.choices
|
|
124
|
-
.map((c) => c.id)
|
|
125
|
-
.join(', ')}. Respond with only the chosen id, nothing else.`
|
|
126
|
-
: base;
|
|
127
|
-
const { object } = await generateObject({
|
|
128
|
-
model: ctx.controlModel,
|
|
129
|
-
schema,
|
|
130
|
-
system,
|
|
131
|
-
messages: ctx.runState.messages,
|
|
132
|
-
temperature: 0,
|
|
133
|
-
abortSignal: ctx.abortSignal,
|
|
134
|
-
});
|
|
135
|
-
return object;
|
|
117
|
+
const system = composeSystem(ctx.baseInstructions, resolveInstructions(node.instructions, ctx.runState.state), ctx.runState.state);
|
|
118
|
+
return resolveStructuredDecide(node, ctx, system);
|
|
136
119
|
}
|
|
137
120
|
async awaitUser(ctx) {
|
|
138
121
|
const input = consumePendingUserInput(ctx.session);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { generateObject } from 'ai';
|
|
2
1
|
import { runSilentExtraction } from './extractionTurn.js';
|
|
2
|
+
import { resolveStructuredDecide } from '../../flow/choiceMatch.js';
|
|
3
3
|
import { buildNodePrompt, resolveInstructions, composeSystem } from '../../flow/nodeBuilders.js';
|
|
4
4
|
import { executeModelToolCall } from './executeModelTool.js';
|
|
5
5
|
import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTurn.js';
|
|
@@ -65,24 +65,8 @@ export class VoiceDriver {
|
|
|
65
65
|
return out;
|
|
66
66
|
}
|
|
67
67
|
async runStructured(node, ctx) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
// Parity with TextDriver: when the node offers choices, constrain the model
|
|
71
|
-
// to a single option id so an unconstrained string can't stall the decide.
|
|
72
|
-
const system = node.choices?.length
|
|
73
|
-
? `${base}\n\nYou MUST pick exactly ONE option by its id. Valid ids: ${node.choices
|
|
74
|
-
.map((c) => c.id)
|
|
75
|
-
.join(', ')}. Respond with only the chosen id, nothing else.`
|
|
76
|
-
: base;
|
|
77
|
-
const { object } = await generateObject({
|
|
78
|
-
model: ctx.controlModel,
|
|
79
|
-
schema,
|
|
80
|
-
system,
|
|
81
|
-
messages: ctx.runState.messages,
|
|
82
|
-
temperature: 0,
|
|
83
|
-
abortSignal: ctx.abortSignal,
|
|
84
|
-
});
|
|
85
|
-
return object;
|
|
68
|
+
const system = composeSystem(ctx.baseInstructions, resolveInstructions(node.instructions, ctx.runState.state), ctx.runState.state);
|
|
69
|
+
return resolveStructuredDecide(node, ctx, system);
|
|
86
70
|
}
|
|
87
71
|
// Non-speaking collect extraction: uses the shared text-model extraction path
|
|
88
72
|
// rather than the realtime audio provider, so the agent never SPEAKS during
|
package/dist/runtime/select.js
CHANGED
|
@@ -24,6 +24,10 @@ export async function selectHostTarget(options) {
|
|
|
24
24
|
if (availableFlows.length === 1 && agentRoutes.length === 0) {
|
|
25
25
|
return { kind: 'enterFlow', flow: availableFlows[0] };
|
|
26
26
|
}
|
|
27
|
+
const deterministic = deterministicRouteMatch(latestUser, routes, availableFlows);
|
|
28
|
+
if (deterministic) {
|
|
29
|
+
return deterministic;
|
|
30
|
+
}
|
|
27
31
|
const flowLines = availableFlows
|
|
28
32
|
.map((flow) => `- flow "${flow.name}": ${flow.description}`)
|
|
29
33
|
.join('\n');
|
|
@@ -72,6 +76,32 @@ export async function selectHostTarget(options) {
|
|
|
72
76
|
}
|
|
73
77
|
return keywordRouteFallback(latestUser, routes, availableFlows);
|
|
74
78
|
}
|
|
79
|
+
function deterministicRouteMatch(message, routes, availableFlows) {
|
|
80
|
+
const lower = message.toLowerCase();
|
|
81
|
+
const hits = [];
|
|
82
|
+
for (const route of routes) {
|
|
83
|
+
const terms = route.when
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.split(/[^a-z0-9]+/)
|
|
86
|
+
.filter((term) => term.length > 3);
|
|
87
|
+
if (!terms.some((term) => lower.includes(term))) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (route.flow) {
|
|
91
|
+
const flow = availableFlows.find((candidate) => candidate.name === route.flow);
|
|
92
|
+
if (flow) {
|
|
93
|
+
hits.push({ kind: 'enterFlow', flow });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (route.agent) {
|
|
97
|
+
hits.push({ kind: 'route', agentId: route.agent });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (hits.length === 1) {
|
|
101
|
+
return hits[0];
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
75
105
|
function keywordRouteFallback(message, routes, availableFlows) {
|
|
76
106
|
const lower = message.toLowerCase();
|
|
77
107
|
for (const route of routes) {
|
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';
|
|
@@ -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>;
|
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.16",
|
|
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.16"
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"chrono-node": "^2.6.0",
|