@kuralle-agents/core 0.3.14 → 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.
@@ -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
- }>): boolean;
21
+ }>): MergeTurnExtractionResult;
22
+ export declare function emitExtractionTelemetry(node: CollectNode, state: FlowState, incoming: Record<string, unknown>, emit: (part: HarnessStreamPart) => void): void;
@@ -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';
@@ -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;
@@ -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
- /** Optional filler message to display while tool is executing */
28
+ /** @deprecated Use `interim` on effect tools (`defineTool`). */
29
29
  filler?: string;
30
- /** Optional estimated duration in ms for showing filler */
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>;
@@ -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
- if (definition.filler)
15
- extended.filler = definition.filler;
16
- if (definition.estimatedDurationMs)
17
- extended.estimatedDurationMs = definition.estimatedDurationMs;
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 rawResult = abortPromise
133
- ? await Promise.race([executePromise, abortPromise])
134
- : await executePromise;
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>;
@@ -59,6 +59,10 @@ export type HarnessStreamPart = {
59
59
  } | {
60
60
  type: 'error';
61
61
  error: string;
62
+ } | {
63
+ type: 'custom';
64
+ name: string;
65
+ data: unknown;
62
66
  } | {
63
67
  type: 'done';
64
68
  sessionId: string;
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.14",
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.14"
100
+ "@kuralle-agents/realtime-audio": "0.3.15"
101
101
  },
102
102
  "dependencies": {
103
103
  "chrono-node": "^2.6.0",