@kuralle-agents/core 0.12.0 → 0.14.0

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.
Files changed (210) hide show
  1. package/README.md +89 -4
  2. package/dist/ai-sdk/uiMessageStream.d.ts +2 -2
  3. package/dist/ai-sdk/uiMessageStream.js +42 -29
  4. package/dist/capabilities/LivePromptAssembler.d.ts +1 -1
  5. package/dist/capabilities/LivePromptAssembler.js +2 -1
  6. package/dist/capabilities/index.d.ts +1 -1
  7. package/dist/eval/EvalRunner.js +4 -4
  8. package/dist/eval/simulation.js +4 -3
  9. package/dist/events/TurnHandle.d.ts +3 -3
  10. package/dist/flow/collectDigression.d.ts +5 -2
  11. package/dist/flow/collectDigression.js +48 -10
  12. package/dist/flow/collectUntilComplete.js +13 -5
  13. package/dist/flow/degrade.js +8 -4
  14. package/dist/flow/emitInteractive.d.ts +2 -2
  15. package/dist/flow/emitInteractive.js +6 -3
  16. package/dist/flow/extraction.d.ts +3 -2
  17. package/dist/flow/extraction.js +14 -4
  18. package/dist/flow/reduceTransition.d.ts +2 -2
  19. package/dist/flow/reduceTransition.js +7 -3
  20. package/dist/flow/runFlow.js +97 -17
  21. package/dist/foundation/AgentStateController.d.ts +1 -1
  22. package/dist/foundation/ToolExecutor.d.ts +2 -2
  23. package/dist/foundation/index.d.ts +0 -11
  24. package/dist/foundation/index.js +0 -5
  25. package/dist/index.d.ts +15 -15
  26. package/dist/index.js +9 -6
  27. package/dist/outcomes/streamPart.d.ts +2 -2
  28. package/dist/outcomes/streamPart.js +2 -3
  29. package/dist/prompts/PromptBuilder.d.ts +3 -3
  30. package/dist/prompts/PromptBuilder.js +13 -6
  31. package/dist/prompts/index.d.ts +1 -1
  32. package/dist/prompts/types.d.ts +9 -2
  33. package/dist/runtime/InMemoryRetrievalCache.d.ts +25 -0
  34. package/dist/runtime/InMemoryRetrievalCache.js +59 -0
  35. package/dist/runtime/KnowledgeProvider.d.ts +3 -2
  36. package/dist/runtime/KnowledgeProvider.js +60 -31
  37. package/dist/runtime/Runtime.d.ts +47 -1
  38. package/dist/runtime/Runtime.js +273 -36
  39. package/dist/runtime/TokenAccumulator.d.ts +7 -0
  40. package/dist/runtime/TokenAccumulator.js +7 -0
  41. package/dist/runtime/TraceRecorder.d.ts +51 -0
  42. package/dist/runtime/TraceRecorder.js +326 -0
  43. package/dist/runtime/buildAgentToolSurface.js +1 -1
  44. package/dist/runtime/channels/TextDriver.js +57 -31
  45. package/dist/runtime/channels/executeModelTool.d.ts +14 -1
  46. package/dist/runtime/channels/executeModelTool.js +132 -3
  47. package/dist/runtime/channels/extractionTurn.js +1 -1
  48. package/dist/runtime/channels/index.d.ts +0 -3
  49. package/dist/runtime/channels/index.js +0 -6
  50. package/dist/runtime/channels/inputBuffer.d.ts +1 -0
  51. package/dist/runtime/channels/inputBuffer.js +9 -0
  52. package/dist/runtime/channels/streaming/hostControlSpeak.js +5 -1
  53. package/dist/runtime/channels/streaming/speakGated.js +24 -12
  54. package/dist/runtime/citations/index.d.ts +3 -3
  55. package/dist/runtime/closeRun.js +14 -10
  56. package/dist/runtime/compaction.d.ts +2 -0
  57. package/dist/runtime/compaction.js +3 -2
  58. package/dist/runtime/controlFlowSignal.d.ts +24 -0
  59. package/dist/runtime/controlFlowSignal.js +29 -0
  60. package/dist/runtime/ctx.d.ts +3 -4
  61. package/dist/runtime/ctx.js +147 -60
  62. package/dist/runtime/durable/RunStore.d.ts +16 -0
  63. package/dist/runtime/durable/RunStore.js +7 -1
  64. package/dist/runtime/durable/SessionRunStore.d.ts +7 -2
  65. package/dist/runtime/durable/SessionRunStore.js +136 -34
  66. package/dist/runtime/durable/idempotency.d.ts +1 -0
  67. package/dist/runtime/durable/idempotency.js +3 -0
  68. package/dist/runtime/durable/replay.js +7 -2
  69. package/dist/runtime/durable/types.d.ts +11 -0
  70. package/dist/runtime/goals.d.ts +18 -0
  71. package/dist/runtime/goals.js +158 -0
  72. package/dist/runtime/grounding/gather.d.ts +1 -1
  73. package/dist/runtime/grounding/inMemoryKnowledge.d.ts +1 -1
  74. package/dist/runtime/grounding/knowledge.d.ts +1 -1
  75. package/dist/runtime/grounding/knowledge.js +1 -1
  76. package/dist/runtime/handoffContinuation.d.ts +20 -0
  77. package/dist/runtime/handoffContinuation.js +30 -0
  78. package/dist/runtime/handoffOscillation.d.ts +6 -0
  79. package/dist/runtime/handoffOscillation.js +17 -0
  80. package/dist/runtime/hostLoop.js +37 -6
  81. package/dist/runtime/index.d.ts +4 -3
  82. package/dist/runtime/index.js +2 -1
  83. package/dist/runtime/openRun.d.ts +2 -0
  84. package/dist/runtime/openRun.js +46 -17
  85. package/dist/runtime/outcomeMarking.d.ts +2 -2
  86. package/dist/runtime/outcomeMarking.js +2 -1
  87. package/dist/runtime/policies/agentTurn.d.ts +1 -1
  88. package/dist/runtime/policies/limits.d.ts +1 -0
  89. package/dist/runtime/policies/limits.js +3 -0
  90. package/dist/runtime/runHookSafely.d.ts +3 -0
  91. package/dist/runtime/runHookSafely.js +19 -0
  92. package/dist/runtime/select.d.ts +5 -1
  93. package/dist/runtime/select.js +30 -1
  94. package/dist/runtime/turnTokenUsage.d.ts +28 -0
  95. package/dist/runtime/turnTokenUsage.js +70 -0
  96. package/dist/scheduler/index.d.ts +2 -2
  97. package/dist/scheduler/index.js +1 -1
  98. package/dist/services/TracingService.js +2 -3
  99. package/dist/session/SessionStore.d.ts +6 -0
  100. package/dist/session/SessionStore.js +12 -1
  101. package/dist/session/stores/MemoryStore.d.ts +1 -1
  102. package/dist/session/stores/MemoryStore.js +16 -2
  103. package/dist/session/testing.d.ts +6 -1
  104. package/dist/session/testing.js +34 -0
  105. package/dist/session/utils.d.ts +3 -0
  106. package/dist/session/utils.js +23 -0
  107. package/dist/skills/collectSkills.d.ts +11 -3
  108. package/dist/skills/collectSkills.js +51 -8
  109. package/dist/skills/compositeSkillStore.d.ts +26 -0
  110. package/dist/skills/compositeSkillStore.js +54 -0
  111. package/dist/skills/defineSkill.d.ts +21 -0
  112. package/dist/skills/defineSkill.js +26 -0
  113. package/dist/skills/fsSkillStore.d.ts +3 -0
  114. package/dist/skills/fsSkillStore.js +79 -0
  115. package/dist/skills/index.d.ts +4 -0
  116. package/dist/skills/index.js +4 -0
  117. package/dist/skills/parseSkillFrontmatter.d.ts +14 -0
  118. package/dist/skills/parseSkillFrontmatter.js +183 -0
  119. package/dist/skills/wireAgentSkills.d.ts +2 -1
  120. package/dist/skills/wireAgentSkills.js +2 -2
  121. package/dist/testing/mocks.d.ts +3 -3
  122. package/dist/testing/mocks.js +7 -3
  123. package/dist/tools/controlResults.d.ts +14 -0
  124. package/dist/tools/controlResults.js +15 -0
  125. package/dist/tools/effect/ToolExecutor.d.ts +6 -0
  126. package/dist/tools/effect/ToolExecutor.js +75 -36
  127. package/dist/tools/effect/defineTool.d.ts +11 -0
  128. package/dist/tools/effect/defineTool.js +3 -0
  129. package/dist/tools/effect/errors.d.ts +2 -5
  130. package/dist/tools/effect/errors.js +2 -5
  131. package/dist/tools/index.d.ts +0 -1
  132. package/dist/tools/index.js +0 -1
  133. package/dist/tracing/MemoryTraceStore.d.ts +16 -0
  134. package/dist/tracing/MemoryTraceStore.js +56 -0
  135. package/dist/tracing/OtelTraceSink.d.ts +127 -0
  136. package/dist/tracing/OtelTraceSink.js +101 -0
  137. package/dist/tracing/TraceStore.d.ts +19 -0
  138. package/dist/tracing/TraceStore.js +32 -0
  139. package/dist/tracing/index.d.ts +3 -0
  140. package/dist/tracing/index.js +3 -0
  141. package/dist/tracing/testing.d.ts +3 -0
  142. package/dist/tracing/testing.js +45 -0
  143. package/dist/types/agentConfig.d.ts +2 -1
  144. package/dist/types/channel.d.ts +14 -1
  145. package/dist/types/effectTool.d.ts +6 -0
  146. package/dist/types/flow.d.ts +1 -1
  147. package/dist/types/guardrails.d.ts +5 -0
  148. package/dist/types/hooks.d.ts +2 -2
  149. package/dist/types/index.d.ts +25 -15
  150. package/dist/types/index.js +8 -12
  151. package/dist/types/knowledge.d.ts +213 -0
  152. package/dist/types/processors.d.ts +0 -58
  153. package/dist/types/run-context.d.ts +16 -7
  154. package/dist/types/runtime.d.ts +3 -74
  155. package/dist/types/session.d.ts +11 -1
  156. package/dist/types/skills.d.ts +15 -1
  157. package/dist/types/stream.d.ts +138 -68
  158. package/dist/types/stream.js +34 -1
  159. package/dist/types/trace.d.ts +52 -0
  160. package/guides/AGENTS.md +3 -3
  161. package/guides/EXAMPLE_VERIFICATION.md +4 -4
  162. package/guides/FLOWS.md +1 -1
  163. package/guides/GETTING_STARTED.md +1 -1
  164. package/guides/RUNTIME.md +20 -15
  165. package/guides/TOOLS.md +1 -1
  166. package/package.json +13 -14
  167. package/dist/foundation/AgentDefinition.d.ts +0 -17
  168. package/dist/foundation/ConversationEventLog.d.ts +0 -71
  169. package/dist/foundation/ConversationState.d.ts +0 -33
  170. package/dist/foundation/ConversationState.js +0 -1
  171. package/dist/foundation/DefaultConversationEventLog.d.ts +0 -27
  172. package/dist/foundation/DefaultConversationEventLog.js +0 -205
  173. package/dist/foundation/DefaultConversationState.d.ts +0 -36
  174. package/dist/foundation/DefaultConversationState.js +0 -103
  175. package/dist/foundation/DefaultToolExecutor.d.ts +0 -57
  176. package/dist/foundation/DefaultToolExecutor.js +0 -132
  177. package/dist/foundation/createFoundation.d.ts +0 -32
  178. package/dist/foundation/createFoundation.js +0 -33
  179. package/dist/hooks/HookRunner.d.ts +0 -37
  180. package/dist/hooks/HookRunner.js +0 -111
  181. package/dist/hooks/builtin/logging.d.ts +0 -5
  182. package/dist/hooks/builtin/logging.js +0 -98
  183. package/dist/hooks/builtin/metrics.d.ts +0 -7
  184. package/dist/hooks/builtin/metrics.js +0 -60
  185. package/dist/hooks/builtin/observability.d.ts +0 -20
  186. package/dist/hooks/builtin/observability.js +0 -572
  187. package/dist/hooks/helpers.d.ts +0 -22
  188. package/dist/hooks/helpers.js +0 -183
  189. package/dist/hooks/index.d.ts +0 -8
  190. package/dist/hooks/index.js +0 -6
  191. package/dist/realtime/RealtimeAudioClient.d.ts +0 -166
  192. package/dist/realtime/RealtimeAudioClient.js +0 -14
  193. package/dist/realtime/RealtimeSessionHandle.d.ts +0 -6
  194. package/dist/realtime/RealtimeSessionHandle.js +0 -1
  195. package/dist/realtime/index.d.ts +0 -2
  196. package/dist/realtime/index.js +0 -1
  197. package/dist/runtime/channels/VoiceDriver.d.ts +0 -31
  198. package/dist/runtime/channels/VoiceDriver.js +0 -321
  199. package/dist/runtime/channels/voiceTools.d.ts +0 -9
  200. package/dist/runtime/channels/voiceTools.js +0 -51
  201. package/dist/tools/errorHandling.d.ts +0 -35
  202. package/dist/tools/errorHandling.js +0 -145
  203. package/dist/types/voice.d.ts +0 -517
  204. package/dist/types/voice.js +0 -14
  205. package/dist/utils/isRecord.d.ts +0 -1
  206. package/dist/utils/isRecord.js +0 -3
  207. package/dist/utils/messageNormalization.d.ts +0 -2
  208. package/dist/utils/messageNormalization.js +0 -137
  209. /package/dist/{foundation/AgentDefinition.js → types/knowledge.js} +0 -0
  210. /package/dist/{foundation/ConversationEventLog.js → types/trace.js} +0 -0
@@ -37,12 +37,16 @@ export function createMockSession(partial = {}) {
37
37
  handoffHistory: partial.handoffHistory ?? [],
38
38
  };
39
39
  }
40
+ function isAsyncIterable(value) {
41
+ return Symbol.asyncIterator in value;
42
+ }
40
43
  export function createMockRuntime(parts, options = {}) {
41
44
  const sessions = options.sessions ?? new Map();
42
45
  const run = (opts) => {
43
46
  if (typeof parts === 'function') {
44
47
  throw parts();
45
48
  }
49
+ const eventSource = parts;
46
50
  options.onRun?.({
47
51
  sessionId: opts.sessionId,
48
52
  input: opts.input,
@@ -50,13 +54,13 @@ export function createMockRuntime(parts, options = {}) {
50
54
  seedMessages: opts.seedMessages,
51
55
  });
52
56
  async function* events() {
53
- if (Symbol.asyncIterator in Object(parts)) {
54
- for await (const part of parts) {
57
+ if (isAsyncIterable(eventSource)) {
58
+ for await (const part of eventSource) {
55
59
  yield part;
56
60
  }
57
61
  return;
58
62
  }
59
- for (const part of parts) {
63
+ for (const part of eventSource) {
60
64
  yield part;
61
65
  }
62
66
  }
@@ -13,3 +13,17 @@ export declare function toolErrorResult(error: unknown): {
13
13
  error: true;
14
14
  message: string;
15
15
  };
16
+ export interface ToolDeniedResult {
17
+ __denied: true;
18
+ toolName: string;
19
+ deniedBy?: string;
20
+ message: string;
21
+ }
22
+ /**
23
+ * What the model sees when a human declines a `needsApproval` tool it asked to run.
24
+ *
25
+ * A result rather than an error, so the agent can tell the user the request was declined
26
+ * instead of the turn dying. `__denied` (not `error: true`) keeps it distinguishable from a
27
+ * genuine failure — nothing malfunctioned.
28
+ */
29
+ export declare function toolDeniedResult(toolName: string, deniedBy?: string): ToolDeniedResult;
@@ -16,3 +16,18 @@ export function toolErrorResult(error) {
16
16
  const message = error instanceof Error ? error.message : String(error);
17
17
  return { error: true, message };
18
18
  }
19
+ /**
20
+ * What the model sees when a human declines a `needsApproval` tool it asked to run.
21
+ *
22
+ * A result rather than an error, so the agent can tell the user the request was declined
23
+ * instead of the turn dying. `__denied` (not `error: true`) keeps it distinguishable from a
24
+ * genuine failure — nothing malfunctioned.
25
+ */
26
+ export function toolDeniedResult(toolName, deniedBy) {
27
+ return {
28
+ __denied: true,
29
+ toolName,
30
+ deniedBy,
31
+ message: `The "${toolName}" action was not approved${deniedBy ? ` by ${deniedBy}` : ''}. Tell the user it was declined; do not retry it.`,
32
+ };
33
+ }
@@ -9,6 +9,11 @@ export interface CoreToolExecutorConfig {
9
9
  parallelExecution?: boolean;
10
10
  agentId?: string;
11
11
  onInterim?: (message: string, toolName: string) => void;
12
+ /**
13
+ * Called for each chunk an async-iterable tool yields, as it arrives. The aggregate is
14
+ * still the tool's result; this is progress, not output.
15
+ */
16
+ onChunk?: (chunk: unknown, toolName: string, toolCallId: string) => void;
12
17
  }
13
18
  export interface CoreExecuteArgs {
14
19
  name: string;
@@ -26,6 +31,7 @@ export declare class CoreToolExecutor implements EffectToolExecutor {
26
31
  private readonly parallelExecution;
27
32
  private readonly agentId;
28
33
  private readonly onInterim?;
34
+ private readonly onChunk?;
29
35
  private readonly pairing;
30
36
  private executionGate;
31
37
  private callHistory;
@@ -3,12 +3,44 @@ import { debug } from '../../debug.js';
3
3
  import { cancelledPlaceholder, inProgressPlaceholder, PairingTracker, } from './pairing.js';
4
4
  import { ToolTimeoutError } from './errors.js';
5
5
  import { ToolValidationError, validateAndSanitize, validateOutput } from './schema.js';
6
+ import { isControlFlowSignal } from '../../runtime/controlFlowSignal.js';
7
+ /**
8
+ * Merges the caller's abort with a timeout so the tool sees one signal. `AbortSignal.any` is
9
+ * present on Node 20+, Bun, and workerd; the manual path keeps older runtimes working.
10
+ * Mirrors `composeSignals` in `@kuralle-agents/fs`.
11
+ */
12
+ function composeSignals(signal, timeoutSignal) {
13
+ if (!signal)
14
+ return timeoutSignal;
15
+ if (!timeoutSignal)
16
+ return signal;
17
+ if (typeof AbortSignal.any === 'function') {
18
+ return AbortSignal.any([signal, timeoutSignal]);
19
+ }
20
+ const controller = new AbortController();
21
+ const onAbort = () => controller.abort();
22
+ signal.addEventListener('abort', onAbort, { once: true });
23
+ timeoutSignal.addEventListener('abort', onAbort, { once: true });
24
+ if (signal.aborted || timeoutSignal.aborted)
25
+ onAbort();
26
+ return controller.signal;
27
+ }
28
+ function rejectOnAbort(signal, makeError) {
29
+ return new Promise((_, reject) => {
30
+ if (signal.aborted) {
31
+ reject(makeError());
32
+ return;
33
+ }
34
+ signal.addEventListener('abort', () => reject(makeError()), { once: true });
35
+ });
36
+ }
6
37
  export class CoreToolExecutor {
7
38
  tools;
8
39
  enforcer;
9
40
  parallelExecution;
10
41
  agentId;
11
42
  onInterim;
43
+ onChunk;
12
44
  pairing = new PairingTracker();
13
45
  executionGate = Promise.resolve();
14
46
  callHistory = [];
@@ -18,6 +50,7 @@ export class CoreToolExecutor {
18
50
  this.parallelExecution = config.parallelExecution ?? false;
19
51
  this.agentId = config.agentId ?? 'agent';
20
52
  this.onInterim = config.onInterim;
53
+ this.onChunk = config.onChunk;
21
54
  }
22
55
  getPairs() {
23
56
  return this.pairing.getAllPairs();
@@ -26,7 +59,10 @@ export class CoreToolExecutor {
26
59
  return this.tools.get(name);
27
60
  }
28
61
  async execute(args) {
29
- if (!this.parallelExecution) {
62
+ const registryDef = this.tools.get(args.name);
63
+ const def = args.def ?? registryDef;
64
+ const parallelSafe = def?.parallelSafe === true || def?.replay === false;
65
+ if (!this.parallelExecution && !parallelSafe) {
30
66
  return this.withSerialGate(() => this.executeInner(args));
31
67
  }
32
68
  return this.executeInner(args);
@@ -94,19 +130,14 @@ export class CoreToolExecutor {
94
130
  }
95
131
  }
96
132
  let interimTimer;
97
- let timeoutTimer;
98
- let interimSent = false;
99
133
  const onAbort = () => {
100
134
  if (interimTimer)
101
135
  clearTimeout(interimTimer);
102
- if (timeoutTimer)
103
- clearTimeout(timeoutTimer);
104
136
  };
105
137
  abortSignal?.addEventListener('abort', onAbort, { once: true });
106
138
  try {
107
139
  if (def.interim && def.interimAfterMs != null && def.interimAfterMs >= 0) {
108
140
  interimTimer = setTimeout(() => {
109
- interimSent = true;
110
141
  this.onInterim?.(def.interim, name);
111
142
  this.pairing.closePair(requestId, 'in_progress', inProgressPlaceholder(requestId, name, def.interim));
112
143
  }, def.interimAfterMs);
@@ -114,40 +145,36 @@ export class CoreToolExecutor {
114
145
  interimTimer.unref();
115
146
  }
116
147
  }
148
+ const timeoutMs = def.timeoutMs;
149
+ // The tool receives the timeout as an abort, so a cooperative tool stops working when
150
+ // we stop waiting for it. Racing alone abandoned the promise while the work continued.
151
+ const timeoutSignal = timeoutMs != null && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
152
+ const effectiveSignal = composeSignals(abortSignal, timeoutSignal);
117
153
  const executeCtx = toolCtx
118
- ? { ...toolCtx, abortSignal }
119
- : abortSignal
120
- ? { abortSignal }
154
+ ? { ...toolCtx, abortSignal: effectiveSignal }
155
+ : effectiveSignal
156
+ ? { abortSignal: effectiveSignal }
121
157
  : undefined;
122
158
  const executePromise = Promise.resolve(def.execute(sanitizedArgs, executeCtx)).then(async (result) => {
123
159
  if (result && typeof result[Symbol.asyncIterator] === 'function') {
124
160
  const chunks = [];
125
161
  for await (const chunk of result) {
126
162
  chunks.push(chunk);
163
+ // Surface progress as it arrives. Observational only — the journal records the
164
+ // aggregate below, so a replayed step emits nothing and stays deterministic.
165
+ this.onChunk?.(chunk, name, requestId);
127
166
  }
128
167
  return chunks.length === 1 ? chunks[0] : chunks;
129
168
  }
130
169
  return result;
131
170
  });
171
+ // The signals above cancel a cooperative tool; these racers stop us waiting on one that
172
+ // ignores them. Both are needed — neither alone bounds the call.
132
173
  const abortPromise = abortSignal && def.interruptible !== false
133
- ? new Promise((_, reject) => {
134
- if (abortSignal.aborted) {
135
- reject(new DOMException('Aborted', 'AbortError'));
136
- return;
137
- }
138
- abortSignal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
139
- })
174
+ ? rejectOnAbort(abortSignal, () => new DOMException('Aborted', 'AbortError'))
140
175
  : null;
141
- const timeoutMs = def.timeoutMs;
142
- const timeoutPromise = timeoutMs != null && timeoutMs > 0
143
- ? new Promise((_, reject) => {
144
- timeoutTimer = setTimeout(() => {
145
- reject(new ToolTimeoutError(name, timeoutMs));
146
- }, timeoutMs);
147
- if (typeof timeoutTimer === 'object' && 'unref' in timeoutTimer) {
148
- timeoutTimer.unref();
149
- }
150
- })
176
+ const timeoutPromise = timeoutSignal
177
+ ? rejectOnAbort(timeoutSignal, () => new ToolTimeoutError(name, timeoutMs))
151
178
  : null;
152
179
  const racers = [executePromise];
153
180
  if (abortPromise)
@@ -157,25 +184,16 @@ export class CoreToolExecutor {
157
184
  const rawResult = racers.length > 1 ? await Promise.race(racers) : await executePromise;
158
185
  if (interimTimer)
159
186
  clearTimeout(interimTimer);
160
- if (timeoutTimer)
161
- clearTimeout(timeoutTimer);
162
187
  const validated = await validateOutput(def.output, rawResult, name);
163
188
  callRecord.result = validated;
164
189
  callRecord.durationMs = Date.now() - callRecord.timestamp;
165
190
  this.callHistory.push(callRecord);
166
- if (!interimSent) {
167
- this.pairing.closePair(requestId, 'completed', validated);
168
- }
169
- else {
170
- this.pairing.closePair(requestId, 'completed', validated);
171
- }
191
+ this.pairing.closePair(requestId, 'completed', validated);
172
192
  return validated;
173
193
  }
174
194
  catch (error) {
175
195
  if (interimTimer)
176
196
  clearTimeout(interimTimer);
177
- if (timeoutTimer)
178
- clearTimeout(timeoutTimer);
179
197
  if (error instanceof DOMException && error.name === 'AbortError') {
180
198
  const placeholder = cancelledPlaceholder(requestId, name);
181
199
  callRecord.success = false;
@@ -189,6 +207,27 @@ export class CoreToolExecutor {
189
207
  this.pairing.closePair(requestId, 'validation_failed', undefined, error.message);
190
208
  throw error;
191
209
  }
210
+ if (error instanceof ToolTimeoutError || isControlFlowSignal(error)) {
211
+ // A timeout and a suspend are decisions about the run, not results the tool may
212
+ // reinterpret. They reach the caller unchanged.
213
+ throw error;
214
+ }
215
+ if (def.onError) {
216
+ // Recovery runs before the failure is recorded, so a handled error is journaled as
217
+ // the success it became. A throwing handler falls through to the generic path below.
218
+ try {
219
+ const recovered = await def.onError(error instanceof Error ? error : new Error(String(error)), sanitizedArgs);
220
+ const validatedRecovery = await validateOutput(def.output, recovered, name);
221
+ callRecord.result = validatedRecovery;
222
+ callRecord.durationMs = Date.now() - callRecord.timestamp;
223
+ this.callHistory.push(callRecord);
224
+ this.pairing.closePair(requestId, 'completed', validatedRecovery);
225
+ return validatedRecovery;
226
+ }
227
+ catch (recoveryError) {
228
+ error = recoveryError;
229
+ }
230
+ }
192
231
  const err = error instanceof Error ? error : new Error(String(error));
193
232
  callRecord.success = false;
194
233
  callRecord.error = err;
@@ -19,7 +19,18 @@ export declare function defineTool<S extends z.ZodTypeAny | StandardSchemaV1 | u
19
19
  estimatedDurationMs?: number;
20
20
  timeoutMs?: number;
21
21
  replay?: boolean;
22
+ parallelSafe?: boolean;
23
+ idempotencyKey?: (args: InferToolInput<S>) => string;
22
24
  execute: (args: InferToolInput<S>, ctx?: ToolContext) => Promise<R> | AsyncIterable<R>;
25
+ /**
26
+ * Turns a thrown error into a result the model can act on, instead of a generic failure.
27
+ * Return a value to recover; rethrow (or omit this) to let the error propagate.
28
+ *
29
+ * Runs only for genuine failures — never for a timeout, an abort, an input/output schema
30
+ * violation, or a control-flow signal, all of which must stay distinguishable from a
31
+ * result the tool chose to return.
32
+ */
33
+ onError?: (error: Error, args: InferToolInput<S>) => Promise<R> | R;
23
34
  }): Tool<InferToolInput<S>, R>;
24
35
  export declare function toolToAiSdk<TInput = unknown, TOutput = unknown>(def: Tool<TInput, TOutput>): AiTool<TInput, TOutput>;
25
36
  export declare function buildToolSet(tools: Record<string, AnyTool>): ToolSet;
@@ -11,7 +11,10 @@ export function defineTool(config) {
11
11
  interimAfterMs: config.interimAfterMs ?? config.estimatedDurationMs,
12
12
  timeoutMs: config.timeoutMs,
13
13
  replay: config.replay,
14
+ parallelSafe: config.parallelSafe,
15
+ idempotencyKey: config.idempotencyKey,
14
16
  execute: config.execute,
17
+ onError: config.onError,
15
18
  };
16
19
  }
17
20
  export function toolToAiSdk(def) {
@@ -1,13 +1,10 @@
1
- /**
2
- * Thrown by `ctx.tool(...)` when a tool declared `needsApproval: true` is denied
3
- * by a human (the `__approval` signal resolves with `approved: false`). Catch it
4
- * inside the calling flow `action` node to route gracefully (e.g. escalate or end).
5
- */
1
+ /** Thrown when a tool execution exceeds its configured timeout. */
6
2
  export declare class ToolTimeoutError extends Error {
7
3
  readonly toolName: string;
8
4
  readonly timeoutMs: number;
9
5
  constructor(toolName: string, timeoutMs: number);
10
6
  }
7
+ /** Thrown when a human denies a tool call that requires approval. */
11
8
  export declare class ToolApprovalDeniedError extends Error {
12
9
  readonly toolName: string;
13
10
  readonly by?: string;
@@ -1,8 +1,4 @@
1
- /**
2
- * Thrown by `ctx.tool(...)` when a tool declared `needsApproval: true` is denied
3
- * by a human (the `__approval` signal resolves with `approved: false`). Catch it
4
- * inside the calling flow `action` node to route gracefully (e.g. escalate or end).
5
- */
1
+ /** Thrown when a tool execution exceeds its configured timeout. */
6
2
  export class ToolTimeoutError extends Error {
7
3
  toolName;
8
4
  timeoutMs;
@@ -13,6 +9,7 @@ export class ToolTimeoutError extends Error {
13
9
  this.timeoutMs = timeoutMs;
14
10
  }
15
11
  }
12
+ /** Thrown when a human denies a tool call that requires approval. */
16
13
  export class ToolApprovalDeniedError extends Error {
17
14
  toolName;
18
15
  by;
@@ -6,5 +6,4 @@ export { isFinalResult } from './final.js';
6
6
  export type { FinalResult } from './final.js';
7
7
  export { createHttpTool } from './http.js';
8
8
  export type { HttpToolConfig, HttpToolResult, HttpParam, ParamType, HttpMethod, AuthConfig, } from './http.types.js';
9
- export { withErrorHandling, executeWithRetry, createCircuitBreaker, withTimeout, isPermanentError, isCircuitOpenError, CircuitOpenError, ToolTimeoutError, type ToolErrorConfig, type ToolResult, } from './errorHandling.js';
10
9
  export { DEFAULT_ERROR_MESSAGES, getUserFriendlyError, categorizeError, createErrorResponse, type ErrorCategory, } from './errorMessages.js';
@@ -2,5 +2,4 @@ export { createTool, createToolWithFiller } from './Tool.js';
2
2
  export { createHandoffTool, isHandoffResult } from './handoff.js';
3
3
  export { isFinalResult } from './final.js';
4
4
  export { createHttpTool } from './http.js';
5
- export { withErrorHandling, executeWithRetry, createCircuitBreaker, withTimeout, isPermanentError, isCircuitOpenError, CircuitOpenError, ToolTimeoutError, } from './errorHandling.js';
6
5
  export { DEFAULT_ERROR_MESSAGES, getUserFriendlyError, categorizeError, createErrorResponse, } from './errorMessages.js';
@@ -0,0 +1,16 @@
1
+ import type { AgentSpan, AgentTrace } from '../types/trace.js';
2
+ import { type TraceListWindow, type TraceStore } from './TraceStore.js';
3
+ export interface MemoryTraceStoreOptions {
4
+ retentionMs?: number;
5
+ }
6
+ export declare class MemoryTraceStore implements TraceStore {
7
+ private readonly options;
8
+ private readonly spans;
9
+ constructor(options?: MemoryTraceStoreOptions);
10
+ write(span: AgentSpan): void;
11
+ putSpan(span: AgentSpan): void;
12
+ getTrace(traceId: string): Promise<AgentTrace | null>;
13
+ listTraces(sessionId: string, window?: TraceListWindow): Promise<AgentTrace[]>;
14
+ cleanup(maxAgeMs: number): Promise<number>;
15
+ private cleanupSync;
16
+ }
@@ -0,0 +1,56 @@
1
+ import { cloneSpan, traceFromSpans, } from './TraceStore.js';
2
+ export class MemoryTraceStore {
3
+ options;
4
+ spans = new Map();
5
+ constructor(options = {}) {
6
+ this.options = options;
7
+ }
8
+ write(span) {
9
+ this.putSpan(span);
10
+ }
11
+ putSpan(span) {
12
+ let trace = this.spans.get(span.traceId);
13
+ if (!trace) {
14
+ trace = new Map();
15
+ this.spans.set(span.traceId, trace);
16
+ }
17
+ trace.set(span.spanId, cloneSpan(span));
18
+ if (this.options.retentionMs !== undefined)
19
+ this.cleanupSync(this.options.retentionMs);
20
+ }
21
+ async getTrace(traceId) {
22
+ const spans = this.spans.get(traceId);
23
+ return traceFromSpans(spans ? [...spans.values()] : []);
24
+ }
25
+ async listTraces(sessionId, window) {
26
+ const traces = [...this.spans.values()]
27
+ .map((spans) => traceFromSpans([...spans.values()]))
28
+ .filter((trace) => trace !== null)
29
+ .filter((trace) => trace.sessionId === sessionId)
30
+ .filter((trace) => inWindow(trace.startedAt, window))
31
+ .sort((a, b) => b.startedAt - a.startedAt);
32
+ return window?.limit === undefined ? traces : traces.slice(0, window.limit);
33
+ }
34
+ async cleanup(maxAgeMs) {
35
+ return this.cleanupSync(maxAgeMs);
36
+ }
37
+ cleanupSync(maxAgeMs) {
38
+ const cutoff = Date.now() - maxAgeMs;
39
+ let removed = 0;
40
+ for (const [traceId, spans] of this.spans) {
41
+ const root = [...spans.values()].find((span) => span.kind === 'turn');
42
+ if ((root?.endTime ?? root?.startTime ?? 0) < cutoff) {
43
+ this.spans.delete(traceId);
44
+ removed += 1;
45
+ }
46
+ }
47
+ return removed;
48
+ }
49
+ }
50
+ function inWindow(timestamp, window) {
51
+ if (window?.from && timestamp < window.from.getTime())
52
+ return false;
53
+ if (window?.to && timestamp > window.to.getTime())
54
+ return false;
55
+ return true;
56
+ }
@@ -0,0 +1,127 @@
1
+ import type { AgentSpan } from '../types/trace.js';
2
+ import type { TraceSink } from './TraceStore.js';
3
+ export interface OtelTraceSinkOptions {
4
+ endpoint: string;
5
+ headers?: Record<string, string>;
6
+ serviceName?: string;
7
+ batchSize?: number;
8
+ fetch?: typeof fetch;
9
+ }
10
+ export declare class OtelTraceSink implements TraceSink {
11
+ private readonly options;
12
+ private readonly pending;
13
+ private exporting?;
14
+ constructor(options: OtelTraceSinkOptions);
15
+ write(span: AgentSpan): Promise<void> | void;
16
+ flush(): Promise<void>;
17
+ private export;
18
+ }
19
+ export declare function otelSink(options: OtelTraceSinkOptions): OtelTraceSink;
20
+ export interface LangfuseSinkOptions {
21
+ endpoint?: string;
22
+ publicKey: string;
23
+ secretKey: string;
24
+ serviceName?: string;
25
+ batchSize?: number;
26
+ fetch?: typeof fetch;
27
+ }
28
+ export declare function langfuseSink(options: LangfuseSinkOptions): OtelTraceSink;
29
+ export declare function toOtlpPayload(spans: AgentSpan[], serviceName?: string): {
30
+ resourceSpans: {
31
+ resource: {
32
+ attributes: {
33
+ key: string;
34
+ value: {
35
+ stringValue: string;
36
+ };
37
+ }[];
38
+ };
39
+ scopeSpans: {
40
+ scope: {
41
+ name: string;
42
+ };
43
+ spans: {
44
+ name: string;
45
+ kind: number;
46
+ startTimeUnixNano: string;
47
+ endTimeUnixNano: string;
48
+ attributes: ({
49
+ key: string;
50
+ value: {
51
+ boolValue: boolean;
52
+ intValue?: undefined;
53
+ doubleValue?: undefined;
54
+ stringValue?: undefined;
55
+ };
56
+ } | {
57
+ key: string;
58
+ value: {
59
+ intValue: string;
60
+ boolValue?: undefined;
61
+ doubleValue?: undefined;
62
+ stringValue?: undefined;
63
+ };
64
+ } | {
65
+ key: string;
66
+ value: {
67
+ doubleValue: number;
68
+ boolValue?: undefined;
69
+ intValue?: undefined;
70
+ stringValue?: undefined;
71
+ };
72
+ } | {
73
+ key: string;
74
+ value: {
75
+ stringValue: string;
76
+ boolValue?: undefined;
77
+ intValue?: undefined;
78
+ doubleValue?: undefined;
79
+ };
80
+ })[];
81
+ events: {
82
+ name: string;
83
+ timeUnixNano: string;
84
+ attributes: ({
85
+ key: string;
86
+ value: {
87
+ boolValue: boolean;
88
+ intValue?: undefined;
89
+ doubleValue?: undefined;
90
+ stringValue?: undefined;
91
+ };
92
+ } | {
93
+ key: string;
94
+ value: {
95
+ intValue: string;
96
+ boolValue?: undefined;
97
+ doubleValue?: undefined;
98
+ stringValue?: undefined;
99
+ };
100
+ } | {
101
+ key: string;
102
+ value: {
103
+ doubleValue: number;
104
+ boolValue?: undefined;
105
+ intValue?: undefined;
106
+ stringValue?: undefined;
107
+ };
108
+ } | {
109
+ key: string;
110
+ value: {
111
+ stringValue: string;
112
+ boolValue?: undefined;
113
+ intValue?: undefined;
114
+ doubleValue?: undefined;
115
+ };
116
+ })[];
117
+ }[];
118
+ status: {
119
+ code: number;
120
+ };
121
+ parentSpanId?: string | undefined;
122
+ traceId: string;
123
+ spanId: string;
124
+ }[];
125
+ }[];
126
+ }[];
127
+ };
@@ -0,0 +1,101 @@
1
+ export class OtelTraceSink {
2
+ options;
3
+ pending = [];
4
+ exporting;
5
+ constructor(options) {
6
+ this.options = options;
7
+ }
8
+ write(span) {
9
+ this.pending.push(structuredClone(span));
10
+ if (this.pending.length >= (this.options.batchSize ?? 32))
11
+ return this.flush();
12
+ }
13
+ async flush() {
14
+ if (this.exporting)
15
+ await this.exporting;
16
+ if (this.pending.length === 0)
17
+ return;
18
+ const spans = this.pending.splice(0);
19
+ this.exporting = this.export(spans).finally(() => { this.exporting = undefined; });
20
+ await this.exporting;
21
+ }
22
+ async export(spans) {
23
+ const transport = this.options.fetch ?? globalThis.fetch;
24
+ const response = await transport(otlpUrl(this.options.endpoint), {
25
+ method: 'POST',
26
+ headers: { 'content-type': 'application/json', ...this.options.headers },
27
+ body: JSON.stringify(toOtlpPayload(spans, this.options.serviceName)),
28
+ });
29
+ if (!response.ok)
30
+ throw new Error(`OTLP trace export failed: ${response.status}`);
31
+ }
32
+ }
33
+ export function otelSink(options) {
34
+ return new OtelTraceSink(options);
35
+ }
36
+ export function langfuseSink(options) {
37
+ return new OtelTraceSink({
38
+ endpoint: options.endpoint ?? 'https://cloud.langfuse.com/api/public/otel',
39
+ headers: {
40
+ Authorization: `Basic ${btoa(`${options.publicKey}:${options.secretKey}`)}`,
41
+ 'x-langfuse-ingestion-version': '4',
42
+ },
43
+ serviceName: options.serviceName,
44
+ batchSize: options.batchSize,
45
+ fetch: options.fetch,
46
+ });
47
+ }
48
+ export function toOtlpPayload(spans, serviceName = 'kuralle-agent') {
49
+ return {
50
+ resourceSpans: [{
51
+ resource: { attributes: [{ key: 'service.name', value: { stringValue: serviceName } }] },
52
+ scopeSpans: [{
53
+ scope: { name: '@kuralle-agents/core' },
54
+ spans: spans.map(toOtlpSpan),
55
+ }],
56
+ }],
57
+ };
58
+ }
59
+ function toOtlpSpan(span) {
60
+ return {
61
+ traceId: normalizeHex(span.traceId, 32),
62
+ spanId: normalizeHex(span.spanId, 16),
63
+ ...(span.parentSpanId ? { parentSpanId: normalizeHex(span.parentSpanId, 16) } : {}),
64
+ name: span.name,
65
+ kind: 1,
66
+ startTimeUnixNano: millisecondsToNanos(span.startTime),
67
+ endTimeUnixNano: millisecondsToNanos(span.endTime ?? span.startTime),
68
+ attributes: [
69
+ otlpAttribute('kuralle.kind', span.kind),
70
+ ...Object.entries(span.attributes)
71
+ .filter(([, value]) => value !== undefined)
72
+ .map(([key, value]) => otlpAttribute(`kuralle.${key}`, value)),
73
+ ],
74
+ events: (span.events ?? []).map((event) => ({
75
+ name: event.name,
76
+ timeUnixNano: millisecondsToNanos(event.time),
77
+ attributes: Object.entries(event.attributes ?? {}).map(([key, value]) => otlpAttribute(`kuralle.${key}`, value)),
78
+ })),
79
+ status: { code: span.status === 'error' ? 2 : 1 },
80
+ };
81
+ }
82
+ function otlpAttribute(key, value) {
83
+ if (typeof value === 'boolean')
84
+ return { key, value: { boolValue: value } };
85
+ if (typeof value === 'number' && Number.isInteger(value))
86
+ return { key, value: { intValue: String(value) } };
87
+ if (typeof value === 'number')
88
+ return { key, value: { doubleValue: value } };
89
+ return { key, value: { stringValue: typeof value === 'string' ? value : JSON.stringify(value) } };
90
+ }
91
+ function normalizeHex(value, length) {
92
+ const hex = value.toLowerCase().replace(/[^0-9a-f]/g, '');
93
+ return hex.padStart(length, '0').slice(-length);
94
+ }
95
+ function millisecondsToNanos(value) {
96
+ return (BigInt(Math.trunc(value)) * 1000000n).toString();
97
+ }
98
+ function otlpUrl(endpoint) {
99
+ const trimmed = endpoint.replace(/\/$/, '');
100
+ return trimmed.endsWith('/v1/traces') ? trimmed : `${trimmed}/v1/traces`;
101
+ }