@kuralle-agents/core 0.13.0 → 0.15.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 (160) hide show
  1. package/README.md +4 -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 +1 -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/collectUntilComplete.js +30 -7
  11. package/dist/flow/degrade.js +8 -4
  12. package/dist/flow/emitInteractive.d.ts +2 -2
  13. package/dist/flow/emitInteractive.js +6 -3
  14. package/dist/flow/extraction.d.ts +21 -2
  15. package/dist/flow/extraction.js +62 -7
  16. package/dist/flow/reduceTransition.d.ts +2 -2
  17. package/dist/flow/reduceTransition.js +7 -3
  18. package/dist/flow/runFlow.d.ts +11 -0
  19. package/dist/flow/runFlow.js +68 -12
  20. package/dist/foundation/AgentStateController.d.ts +1 -1
  21. package/dist/foundation/ToolExecutor.d.ts +2 -2
  22. package/dist/foundation/index.d.ts +0 -11
  23. package/dist/foundation/index.js +0 -5
  24. package/dist/index.d.ts +9 -14
  25. package/dist/index.js +5 -6
  26. package/dist/outcomes/streamPart.d.ts +2 -2
  27. package/dist/outcomes/streamPart.js +2 -3
  28. package/dist/prompts/PromptBuilder.d.ts +3 -3
  29. package/dist/prompts/PromptBuilder.js +6 -6
  30. package/dist/prompts/index.d.ts +1 -1
  31. package/dist/prompts/types.d.ts +7 -2
  32. package/dist/runtime/InMemoryRetrievalCache.d.ts +1 -1
  33. package/dist/runtime/KnowledgeProvider.d.ts +3 -2
  34. package/dist/runtime/KnowledgeProvider.js +54 -30
  35. package/dist/runtime/Runtime.d.ts +9 -1
  36. package/dist/runtime/Runtime.js +123 -30
  37. package/dist/runtime/TraceRecorder.d.ts +22 -4
  38. package/dist/runtime/TraceRecorder.js +65 -29
  39. package/dist/runtime/buildAgentToolSurface.js +1 -1
  40. package/dist/runtime/channels/TextDriver.js +12 -9
  41. package/dist/runtime/channels/executeModelTool.d.ts +6 -0
  42. package/dist/runtime/channels/executeModelTool.js +70 -10
  43. package/dist/runtime/channels/extractionTurn.js +1 -1
  44. package/dist/runtime/channels/index.d.ts +0 -3
  45. package/dist/runtime/channels/index.js +0 -6
  46. package/dist/runtime/channels/streaming/hostControlSpeak.js +5 -1
  47. package/dist/runtime/channels/streaming/speakGated.js +33 -12
  48. package/dist/runtime/citations/index.d.ts +3 -3
  49. package/dist/runtime/closeRun.js +3 -2
  50. package/dist/runtime/controlFlowSignal.d.ts +24 -0
  51. package/dist/runtime/controlFlowSignal.js +29 -0
  52. package/dist/runtime/ctx.d.ts +3 -4
  53. package/dist/runtime/ctx.js +39 -5
  54. package/dist/runtime/durable/RunStore.js +1 -1
  55. package/dist/runtime/grounding/gather.d.ts +1 -1
  56. package/dist/runtime/grounding/inMemoryKnowledge.d.ts +1 -1
  57. package/dist/runtime/grounding/knowledge.d.ts +1 -1
  58. package/dist/runtime/hostLoop.js +39 -10
  59. package/dist/runtime/index.d.ts +1 -2
  60. package/dist/runtime/index.js +1 -1
  61. package/dist/runtime/outcomeMarking.d.ts +2 -2
  62. package/dist/runtime/outcomeMarking.js +2 -1
  63. package/dist/runtime/policies/agentTurn.d.ts +1 -1
  64. package/dist/runtime/runHookSafely.d.ts +3 -0
  65. package/dist/runtime/runHookSafely.js +19 -0
  66. package/dist/scheduler/index.d.ts +2 -2
  67. package/dist/scheduler/index.js +1 -1
  68. package/dist/services/TracingService.js +2 -3
  69. package/dist/skills/collectSkills.d.ts +11 -3
  70. package/dist/skills/collectSkills.js +51 -8
  71. package/dist/skills/compositeSkillStore.d.ts +26 -0
  72. package/dist/skills/compositeSkillStore.js +54 -0
  73. package/dist/skills/defineSkill.d.ts +21 -0
  74. package/dist/skills/defineSkill.js +26 -0
  75. package/dist/skills/fsSkillStore.d.ts +3 -0
  76. package/dist/skills/fsSkillStore.js +79 -0
  77. package/dist/skills/index.d.ts +4 -0
  78. package/dist/skills/index.js +4 -0
  79. package/dist/skills/parseSkillFrontmatter.d.ts +14 -0
  80. package/dist/skills/parseSkillFrontmatter.js +183 -0
  81. package/dist/skills/wireAgentSkills.d.ts +2 -1
  82. package/dist/skills/wireAgentSkills.js +2 -2
  83. package/dist/testing/mocks.d.ts +3 -3
  84. package/dist/testing/mocks.js +7 -3
  85. package/dist/tools/controlResults.d.ts +14 -0
  86. package/dist/tools/controlResults.js +15 -0
  87. package/dist/tools/effect/ToolExecutor.d.ts +6 -0
  88. package/dist/tools/effect/ToolExecutor.js +71 -35
  89. package/dist/tools/effect/defineTool.d.ts +9 -0
  90. package/dist/tools/effect/defineTool.js +1 -0
  91. package/dist/tools/effect/errors.d.ts +2 -5
  92. package/dist/tools/effect/errors.js +2 -5
  93. package/dist/tools/fs/createFsTool.js +2 -1
  94. package/dist/tools/index.d.ts +0 -1
  95. package/dist/tools/index.js +0 -1
  96. package/dist/types/effectTool.d.ts +2 -0
  97. package/dist/types/flow.d.ts +1 -1
  98. package/dist/types/guardrails.d.ts +5 -0
  99. package/dist/types/hooks.d.ts +2 -2
  100. package/dist/types/index.d.ts +25 -16
  101. package/dist/types/index.js +8 -13
  102. package/dist/types/knowledge.d.ts +213 -0
  103. package/dist/types/processors.d.ts +0 -58
  104. package/dist/types/run-context.d.ts +3 -7
  105. package/dist/types/runtime.d.ts +3 -74
  106. package/dist/types/session.d.ts +1 -1
  107. package/dist/types/skills.d.ts +15 -1
  108. package/dist/types/stream.d.ts +133 -71
  109. package/dist/types/stream.js +34 -1
  110. package/dist/types/trace.d.ts +2 -0
  111. package/guides/AGENTS.md +3 -3
  112. package/guides/FLOWS.md +1 -1
  113. package/guides/GETTING_STARTED.md +1 -1
  114. package/guides/RUNTIME.md +20 -15
  115. package/guides/TOOLS.md +1 -1
  116. package/package.json +3 -13
  117. package/dist/foundation/AgentDefinition.d.ts +0 -17
  118. package/dist/foundation/ConversationEventLog.d.ts +0 -71
  119. package/dist/foundation/ConversationEventLog.js +0 -1
  120. package/dist/foundation/ConversationState.d.ts +0 -33
  121. package/dist/foundation/ConversationState.js +0 -1
  122. package/dist/foundation/DefaultConversationEventLog.d.ts +0 -27
  123. package/dist/foundation/DefaultConversationEventLog.js +0 -205
  124. package/dist/foundation/DefaultConversationState.d.ts +0 -36
  125. package/dist/foundation/DefaultConversationState.js +0 -103
  126. package/dist/foundation/DefaultToolExecutor.d.ts +0 -57
  127. package/dist/foundation/DefaultToolExecutor.js +0 -132
  128. package/dist/foundation/createFoundation.d.ts +0 -32
  129. package/dist/foundation/createFoundation.js +0 -33
  130. package/dist/hooks/HookRunner.d.ts +0 -37
  131. package/dist/hooks/HookRunner.js +0 -111
  132. package/dist/hooks/builtin/logging.d.ts +0 -5
  133. package/dist/hooks/builtin/logging.js +0 -98
  134. package/dist/hooks/builtin/metrics.d.ts +0 -7
  135. package/dist/hooks/builtin/metrics.js +0 -60
  136. package/dist/hooks/builtin/observability.d.ts +0 -20
  137. package/dist/hooks/builtin/observability.js +0 -572
  138. package/dist/hooks/helpers.d.ts +0 -22
  139. package/dist/hooks/helpers.js +0 -183
  140. package/dist/hooks/index.d.ts +0 -8
  141. package/dist/hooks/index.js +0 -6
  142. package/dist/realtime/RealtimeAudioClient.d.ts +0 -166
  143. package/dist/realtime/RealtimeAudioClient.js +0 -14
  144. package/dist/realtime/RealtimeSessionHandle.d.ts +0 -6
  145. package/dist/realtime/RealtimeSessionHandle.js +0 -1
  146. package/dist/realtime/index.d.ts +0 -2
  147. package/dist/realtime/index.js +0 -1
  148. package/dist/runtime/channels/VoiceDriver.d.ts +0 -31
  149. package/dist/runtime/channels/VoiceDriver.js +0 -321
  150. package/dist/runtime/channels/voiceTools.d.ts +0 -9
  151. package/dist/runtime/channels/voiceTools.js +0 -51
  152. package/dist/tools/errorHandling.d.ts +0 -35
  153. package/dist/tools/errorHandling.js +0 -145
  154. package/dist/types/voice.d.ts +0 -517
  155. package/dist/types/voice.js +0 -14
  156. package/dist/utils/isRecord.d.ts +0 -1
  157. package/dist/utils/isRecord.js +0 -3
  158. package/dist/utils/messageNormalization.d.ts +0 -2
  159. package/dist/utils/messageNormalization.js +0 -137
  160. /package/dist/{foundation/AgentDefinition.js → types/knowledge.js} +0 -0
@@ -34,6 +34,13 @@ import { TraceRecorder } from './TraceRecorder.js';
34
34
  import { MemoryTraceStore } from '../tracing/MemoryTraceStore.js';
35
35
  import { mutateSessionWithRetry } from '../session/utils.js';
36
36
  import { isTraceStore } from '../tracing/TraceStore.js';
37
+ import { runHookSafely } from './runHookSafely.js';
38
+ /**
39
+ * What the user is told when the run hands off to a human and the app has not configured an
40
+ * escalation handler to say something better. Silence is the wrong default: an escalation
41
+ * that emits no text is indistinguishable from the agent having crashed.
42
+ */
43
+ export const HANDOFF_TO_HUMAN_MESSAGE = "I'm bringing a colleague into this — they'll pick it up from here.";
37
44
  export class Runtime {
38
45
  config;
39
46
  agentsById;
@@ -72,6 +79,7 @@ export class Runtime {
72
79
  const recorder = this.shouldTrace(sessionId, opts.input)
73
80
  ? new TraceRecorder({
74
81
  sessionId,
82
+ agentId: opts.agentId ?? this.config.defaultAgentId,
75
83
  input: opts.input,
76
84
  onSpan: (span) => this.writeSpan(span),
77
85
  })
@@ -84,12 +92,19 @@ export class Runtime {
84
92
  }
85
93
  const execute = async () => {
86
94
  let runCtx;
95
+ // Whether the user has been told anything at all this turn. A terminal handoff with
96
+ // no escalation handler configured used to emit zero text — seven consecutive turns
97
+ // were observed producing empty output while silently firing transfer_to_agent, which
98
+ // is indistinguishable from an outage.
99
+ let sawUserText = false;
87
100
  const emit = (part) => {
101
+ if (part.type === 'text-delta' && part.payload.delta)
102
+ sawUserText = true;
88
103
  recorder?.record(part);
89
104
  if (part.type === 'done')
90
105
  this.flushTraceSinks();
91
106
  bus.emit(part);
92
- void this.hooks?.onStreamPart?.(runCtx, part);
107
+ void runHookSafely('onStreamPart', () => this.hooks?.onStreamPart?.(runCtx, part));
93
108
  };
94
109
  const opened = await openRun(this.agentsById, {
95
110
  sessionId,
@@ -106,6 +121,7 @@ export class Runtime {
106
121
  defaultAgentId: this.config.defaultAgentId,
107
122
  sessionStore: this.sessionStore,
108
123
  });
124
+ recorder?.setInitiatingAgent(opened.agent.id);
109
125
  const policies = resolveAgentPolicies(opened.agent);
110
126
  const knowledgeProvider = this.config.knowledge
111
127
  ? buildKnowledgeProvider(this.config.knowledge)
@@ -121,9 +137,16 @@ export class Runtime {
121
137
  agentId: opened.agent.id,
122
138
  onInterim: (message) => {
123
139
  const id = crypto.randomUUID();
124
- emit({ type: 'text-start', id });
125
- emit({ type: 'text-delta', id, delta: message });
126
- emit({ type: 'text-end', id });
140
+ emit({ channel: 'client', type: 'text-start', payload: { id } });
141
+ emit({ channel: 'client', type: 'text-delta', payload: { id, delta: message } });
142
+ emit({ channel: 'client', type: 'text-end', payload: { id } });
143
+ },
144
+ onChunk: (chunk, toolName, toolCallId) => {
145
+ emit({
146
+ channel: 'internal',
147
+ type: 'tool-result',
148
+ payload: { toolName, result: chunk, toolCallId, preliminary: true },
149
+ });
127
150
  },
128
151
  });
129
152
  const steps = await loadRecordedSteps(opened.runStore, opened.runState.runId);
@@ -170,9 +193,9 @@ export class Runtime {
170
193
  runCtx.skillPrompt = openingSurface.skillPrompt;
171
194
  runCtx.workingMemoryPrompt = appendGoalsPrompt(openingSurface.workingMemoryPrompt, opened.session.workingMemory);
172
195
  runCtx.workingMemoryTools = openingSurface.workingMemoryTools;
173
- await this.hooks?.onStart?.(runCtx);
196
+ await runHookSafely('onStart', () => this.hooks?.onStart?.(runCtx));
174
197
  if (opts.wake) {
175
- emit({ type: 'wake', reason: opts.wake.reason });
198
+ emit({ channel: 'internal', type: 'wake', payload: { reason: opts.wake.reason } });
176
199
  }
177
200
  const driver = opts.driver ?? new TextDriver();
178
201
  let activeAgent = opened.agent;
@@ -202,8 +225,38 @@ export class Runtime {
202
225
  }
203
226
  if (loopResult.kind === 'handoff') {
204
227
  if (this.terminalHandoffTargets.has(loopResult.to)) {
205
- emit({ type: 'handoff', targetAgent: loopResult.to, reason: loopResult.reason });
228
+ // This emit is load-bearing for TERMINAL targets specifically. A terminal
229
+ // handoff breaks out of the loop here, before hostLoop's own emit and before
230
+ // handoffHistory is appended — so this is the only handoff part a client ever
231
+ // sees for an escalation. Removing it as "redundant" silently produced zero
232
+ // handoff parts for every escalation; escalation.test.ts catches it.
233
+ //
234
+ // The self-edge seen live (handoff human->human, two spans on one turn) is a
235
+ // real but SEPARATE defect on the non-terminal path — fix it there, not by
236
+ // deleting this.
237
+ emit({
238
+ channel: 'internal',
239
+ type: 'handoff',
240
+ payload: { targetAgent: loopResult.to, reason: loopResult.reason },
241
+ });
206
242
  runCtx.runState.status = 'paused';
243
+ // Never hand off in silence. dispatchEscalation returns immediately when no
244
+ // escalation handler is configured, so without this the turn ends with no
245
+ // assistant text at all and the user cannot tell an escalation from a crash.
246
+ if (!sawUserText) {
247
+ const handoffId = crypto.randomUUID();
248
+ emit({ channel: 'client', type: 'text-start', payload: { id: handoffId } });
249
+ emit({
250
+ channel: 'client',
251
+ type: 'text-delta',
252
+ payload: { id: handoffId, delta: HANDOFF_TO_HUMAN_MESSAGE },
253
+ });
254
+ emit({ channel: 'client', type: 'text-end', payload: { id: handoffId } });
255
+ runCtx.runState.messages = [
256
+ ...runCtx.runState.messages,
257
+ { role: 'assistant', content: HANDOFF_TO_HUMAN_MESSAGE },
258
+ ];
259
+ }
207
260
  await runCtx.runStore.putRunState(runCtx.runState);
208
261
  await this.dispatchEscalation(runCtx, activeAgent, { reason: loopResult.reason ?? 'handoff_to_human', category: loopResult.category }, emit, { setLatch: false });
209
262
  break;
@@ -285,9 +338,20 @@ export class Runtime {
285
338
  agentId: target.id,
286
339
  onInterim: (message) => {
287
340
  const id = crypto.randomUUID();
288
- emit({ type: 'text-start', id });
289
- emit({ type: 'text-delta', id, delta: message });
290
- emit({ type: 'text-end', id });
341
+ emit({ channel: 'client', type: 'text-start', payload: { id } });
342
+ emit({
343
+ channel: 'client',
344
+ type: 'text-delta',
345
+ payload: { id, delta: message },
346
+ });
347
+ emit({ channel: 'client', type: 'text-end', payload: { id } });
348
+ },
349
+ onChunk: (chunk, toolName, toolCallId) => {
350
+ emit({
351
+ channel: 'internal',
352
+ type: 'tool-result',
353
+ payload: { toolName, result: chunk, toolCallId, preliminary: true },
354
+ });
291
355
  },
292
356
  });
293
357
  await runCtx.runStore.putRunState(runCtx.runState);
@@ -314,14 +378,18 @@ export class Runtime {
314
378
  await this.applyCompaction(runCtx, activeAgent, emit, false);
315
379
  }
316
380
  catch (error) {
317
- await this.hooks?.onError?.(runCtx, error);
381
+ await runHookSafely('onError', () => this.hooks?.onError?.(runCtx, error));
318
382
  if (isDegradableRuntimeError(error)) {
319
383
  const message = error instanceof Error ? error.message : String(error);
320
- emit({ type: 'error', error: message });
384
+ emit({ channel: 'client', type: 'error', payload: { error: message } });
321
385
  const degradedId = crypto.randomUUID();
322
- emit({ type: 'text-start', id: degradedId });
323
- emit({ type: 'text-delta', id: degradedId, delta: SAFE_DEGRADED_MESSAGE });
324
- emit({ type: 'text-end', id: degradedId });
386
+ emit({ channel: 'client', type: 'text-start', payload: { id: degradedId } });
387
+ emit({
388
+ channel: 'client',
389
+ type: 'text-delta',
390
+ payload: { id: degradedId, delta: SAFE_DEGRADED_MESSAGE },
391
+ });
392
+ emit({ channel: 'client', type: 'text-end', payload: { id: degradedId } });
325
393
  runCtx.runState.messages = [
326
394
  ...runCtx.runState.messages,
327
395
  { role: 'assistant', content: SAFE_DEGRADED_MESSAGE },
@@ -354,11 +422,14 @@ export class Runtime {
354
422
  }
355
423
  },
356
424
  });
357
- await this.hooks?.onEnd?.(runCtx);
425
+ await runHookSafely('onEnd', () => this.hooks?.onEnd?.(runCtx));
358
426
  emit({
427
+ channel: 'client',
359
428
  type: 'done',
360
- sessionId: opened.session.id,
361
- usage: computeTurnTraceUsage(usageBaseline, runCtx.runState.state),
429
+ payload: {
430
+ sessionId: opened.session.id,
431
+ usage: computeTurnTraceUsage(usageBaseline, runCtx.runState.state),
432
+ },
362
433
  });
363
434
  }
364
435
  return { text: collectAssistantText(runCtx.runState.messages), toolResults: [] };
@@ -378,8 +449,13 @@ export class Runtime {
378
449
  run: gated,
379
450
  });
380
451
  }
381
- runOnce(opts) {
382
- return recordRunOnce(this, opts);
452
+ async runOnce(opts) {
453
+ const existing = opts.sessionId ? await this.sessionStore.get(opts.sessionId) : null;
454
+ // Caller's explicit agentId wins, matching run() (`opts.agentId ?? defaultAgentId`).
455
+ // Persisted state is the fallback, not an override — otherwise the two public
456
+ // entry points disagree about which agent handles the same turn.
457
+ const agentId = opts.agentId ?? existing?.activeAgentId ?? existing?.currentAgent ?? this.config.defaultAgentId;
458
+ return recordRunOnce(this, { ...opts, agentId });
383
459
  }
384
460
  stream(opts) {
385
461
  return this.run(opts);
@@ -398,6 +474,10 @@ export class Runtime {
398
474
  getTraceStore() {
399
475
  return this.traceStore;
400
476
  }
477
+ /** The agent used when neither the caller nor persisted state names one. */
478
+ getDefaultAgentId() {
479
+ return this.config.defaultAgentId;
480
+ }
401
481
  getSessionStore() {
402
482
  return this.sessionStore;
403
483
  }
@@ -506,7 +586,11 @@ export class Runtime {
506
586
  });
507
587
  if (!result.compacted) {
508
588
  if (force) {
509
- emit({ type: 'compaction-skipped', reason: result.reason });
589
+ emit({
590
+ channel: 'internal',
591
+ type: 'compaction-skipped',
592
+ payload: { reason: result.reason },
593
+ });
510
594
  }
511
595
  return false;
512
596
  }
@@ -518,10 +602,13 @@ export class Runtime {
518
602
  });
519
603
  runCtx.session.messages = [...result.messages];
520
604
  emit({
605
+ channel: 'internal',
521
606
  type: 'context-compacted',
522
- beforeTokens: result.beforeTokens,
523
- afterTokens: result.afterTokens,
524
- summarizedCount: result.summarizedCount,
607
+ payload: {
608
+ beforeTokens: result.beforeTokens,
609
+ afterTokens: result.afterTokens,
610
+ summarizedCount: result.summarizedCount,
611
+ },
525
612
  });
526
613
  return true;
527
614
  }
@@ -536,9 +623,12 @@ export class Runtime {
536
623
  runCtx.runState.messages = runCtx.session.messages;
537
624
  const compacted = await this.applyCompaction(runCtx, agent, emit, true);
538
625
  emit({
626
+ channel: 'internal',
539
627
  type: 'context-overflow-recovered',
540
- strippedCount: recovery.strippedCount,
541
- compacted,
628
+ payload: {
629
+ strippedCount: recovery.strippedCount,
630
+ compacted,
631
+ },
542
632
  });
543
633
  }
544
634
  async getConversationLength(sessionId) {
@@ -592,11 +682,14 @@ export class Runtime {
592
682
  });
593
683
  runCtx.session.metadata = latestSession.metadata;
594
684
  emit({
685
+ channel: 'internal',
595
686
  type: 'escalation',
596
- reason: info.reason,
597
- category: info.category,
598
- outcome: outcome.status,
599
- summary: request.summary,
687
+ payload: {
688
+ reason: info.reason,
689
+ category: info.category,
690
+ outcome: outcome.status,
691
+ summary: request.summary,
692
+ },
600
693
  });
601
694
  }
602
695
  /**
@@ -1,9 +1,10 @@
1
1
  import type { TurnResult } from '../types/channel.js';
2
- import type { HarnessStreamPart, TurnHandle } from '../types/stream.js';
2
+ import type { StreamPart, TurnHandle } from '../types/stream.js';
3
3
  import type { AgentSpan, AgentTrace } from '../types/trace.js';
4
4
  import type { RunOptions } from './Runtime.js';
5
5
  export interface TraceRecorderOptions {
6
6
  sessionId?: string;
7
+ agentId?: string;
7
8
  input?: unknown;
8
9
  onSpan?: (span: AgentSpan) => void;
9
10
  }
@@ -16,8 +17,10 @@ export declare class TraceRecorder {
16
17
  private readonly toolCallIds;
17
18
  private readonly emitted;
18
19
  private readonly onSpan?;
20
+ private currentAgentId?;
19
21
  constructor(options?: TraceRecorderOptions);
20
- record(part: HarnessStreamPart): void;
22
+ setInitiatingAgent(agentId: string): void;
23
+ record(part: StreamPart): void;
21
24
  finish(result: TurnResult): AgentTrace;
22
25
  private openSpan;
23
26
  private takeToolSpan;
@@ -28,6 +31,21 @@ export declare class TraceRecorder {
28
31
  private close;
29
32
  private emitSpan;
30
33
  }
31
- export declare function runOnce(runtime: {
34
+ /**
35
+ * What the standalone {@link runOnce} needs from a runtime. `run` is required;
36
+ * the optional accessors let it resolve the same agent `Runtime.runOnce` would.
37
+ *
38
+ * It must NOT call `runtime.runOnce` — that method delegates here, so delegating
39
+ * back is infinite recursion.
40
+ */
41
+ export interface RunOnceRuntime {
32
42
  run(opts: RunOptions): TurnHandle;
33
- }, opts: RunOptions): Promise<AgentTrace>;
43
+ getSessionStore?(): {
44
+ get(id: string): Promise<{
45
+ activeAgentId?: string;
46
+ currentAgent?: string;
47
+ } | null>;
48
+ };
49
+ getDefaultAgentId?(): string;
50
+ }
51
+ export declare function runOnce(runtime: RunOnceRuntime, opts: RunOptions): Promise<AgentTrace>;
@@ -7,11 +7,13 @@ export class TraceRecorder {
7
7
  toolCallIds = new Map();
8
8
  emitted = new Set();
9
9
  onSpan;
10
+ currentAgentId;
10
11
  constructor(options = {}) {
11
12
  const startedAt = Date.now();
12
13
  const sessionId = options.sessionId ?? '';
13
14
  const traceId = crypto.randomUUID().replaceAll('-', '');
14
15
  this.onSpan = options.onSpan;
16
+ this.currentAgentId = options.agentId;
15
17
  this.root = {
16
18
  traceId,
17
19
  spanId: newSpanId(),
@@ -21,6 +23,7 @@ export class TraceRecorder {
21
23
  status: 'ok',
22
24
  attributes: {
23
25
  sessionId,
26
+ ...(options.agentId ? { agentId: options.agentId } : {}),
24
27
  ...(options.input !== undefined ? { input: toJsonValue(options.input) } : {}),
25
28
  },
26
29
  };
@@ -35,22 +38,26 @@ export class TraceRecorder {
35
38
  startedAt,
36
39
  };
37
40
  }
41
+ setInitiatingAgent(agentId) {
42
+ this.currentAgentId = agentId;
43
+ this.root.attributes.agentId = agentId;
44
+ }
38
45
  record(part) {
39
46
  try {
40
47
  const at = Date.now();
41
48
  switch (part.type) {
42
49
  case 'text-delta':
43
- this.trace.answer += part.delta;
50
+ this.trace.answer += part.payload.delta;
44
51
  break;
45
52
  case 'flow-enter':
46
53
  this.closeNode(at);
47
54
  this.closeFlow(at);
48
55
  this.currentFlow = this.openSpan({
49
- name: `flow:${part.flow}`,
56
+ name: `flow:${part.payload.flow}`,
50
57
  kind: 'flow',
51
58
  parentSpanId: this.root.spanId,
52
59
  at,
53
- attributes: { activeFlow: part.flow },
60
+ attributes: { activeFlow: part.payload.flow },
54
61
  });
55
62
  break;
56
63
  case 'flow-end':
@@ -60,13 +67,13 @@ export class TraceRecorder {
60
67
  case 'node-enter':
61
68
  this.closeNode(at);
62
69
  this.currentNode = this.openSpan({
63
- name: `node:${part.nodeName}`,
70
+ name: `node:${part.payload.nodeName}`,
64
71
  kind: 'node',
65
72
  parentSpanId: this.currentFlow?.spanId ?? this.root.spanId,
66
73
  at,
67
74
  attributes: {
68
75
  ...(this.currentFlow ? { activeFlow: activeFlowName(this.currentFlow) } : {}),
69
- nodeId: part.nodeName,
76
+ nodeId: part.payload.nodeName,
70
77
  },
71
78
  });
72
79
  break;
@@ -75,37 +82,40 @@ export class TraceRecorder {
75
82
  break;
76
83
  case 'tool-call': {
77
84
  const span = this.openSpan({
78
- name: `tool:${part.toolName}`,
85
+ name: `tool:${part.payload.toolName}`,
79
86
  kind: 'tool',
80
87
  parentSpanId: this.currentNode?.spanId ?? this.root.spanId,
81
88
  at,
82
89
  attributes: {
83
90
  ...(this.currentFlow ? { activeFlow: activeFlowName(this.currentFlow) } : {}),
84
91
  ...(this.currentNode ? { nodeId: nodeName(this.currentNode) } : {}),
85
- toolName: part.toolName,
86
- input: toJsonValue(part.args),
92
+ toolName: part.payload.toolName,
93
+ input: toJsonValue(part.payload.args),
87
94
  },
88
95
  });
89
- this.toolCallIds.set(span, part.toolCallId);
96
+ this.toolCallIds.set(span, part.payload.toolCallId);
90
97
  this.openTools.push(span);
91
98
  this.trace.usedTool = true;
92
- this.trace.toolCalls.push({ name: part.toolName, args: toJsonValue(part.args) });
99
+ this.trace.toolCalls.push({
100
+ name: part.payload.toolName,
101
+ args: toJsonValue(part.payload.args),
102
+ });
93
103
  break;
94
104
  }
95
105
  case 'tool-result': {
96
106
  this.trace.usedTool = true;
97
- const result = toJsonValue(part.result);
98
- this.trace.toolResults.push({ name: part.toolName, result });
99
- const span = this.takeToolSpan(part.toolName, part.toolCallId) ??
107
+ const result = toJsonValue(part.payload.result);
108
+ this.trace.toolResults.push({ name: part.payload.toolName, result });
109
+ const span = this.takeToolSpan(part.payload.toolName, part.payload.toolCallId) ??
100
110
  this.openSpan({
101
- name: `tool:${part.toolName}`,
111
+ name: `tool:${part.payload.toolName}`,
102
112
  kind: 'tool',
103
113
  parentSpanId: this.currentNode?.spanId ?? this.root.spanId,
104
114
  at,
105
115
  attributes: {
106
116
  ...(this.currentFlow ? { activeFlow: activeFlowName(this.currentFlow) } : {}),
107
117
  ...(this.currentNode ? { nodeId: nodeName(this.currentNode) } : {}),
108
- toolName: part.toolName,
118
+ toolName: part.payload.toolName,
109
119
  },
110
120
  });
111
121
  span.attributes.output = result;
@@ -114,35 +124,43 @@ export class TraceRecorder {
114
124
  break;
115
125
  }
116
126
  case 'handoff': {
127
+ const handoffFrom = this.currentAgentId;
117
128
  const span = this.openSpan({
118
- name: `handoff:${part.targetAgent}`,
129
+ name: `handoff:${part.payload.targetAgent}`,
119
130
  kind: 'handoff',
120
131
  parentSpanId: this.root.spanId,
121
132
  at,
122
- attributes: { handoffTo: part.targetAgent },
133
+ attributes: {
134
+ ...(handoffFrom ? { handoffFrom } : {}),
135
+ handoffTo: part.payload.targetAgent,
136
+ },
123
137
  });
124
138
  span.endTime = at;
125
139
  this.emitSpan(span);
140
+ this.currentAgentId = part.payload.targetAgent;
126
141
  break;
127
142
  }
128
143
  case 'error': {
129
144
  const span = this.openTools.at(-1) ?? this.currentNode ?? this.currentFlow ?? this.root;
130
145
  span.status = 'error';
131
- span.attributes.error = part.error;
146
+ span.attributes.error = part.payload.error;
132
147
  this.root.status = 'error';
133
- this.root.attributes.error = part.error;
148
+ this.root.attributes.error = part.payload.error;
134
149
  break;
135
150
  }
136
151
  case 'done':
137
- this.setSessionId(part.sessionId);
152
+ this.setSessionId(part.payload.sessionId);
138
153
  this.root.attributes.output = this.trace.answer;
139
- if (part.usage) {
140
- if (typeof part.usage.inputTokens === 'number')
141
- this.root.attributes.tokensIn = part.usage.inputTokens;
142
- if (typeof part.usage.outputTokens === 'number')
143
- this.root.attributes.tokensOut = part.usage.outputTokens;
144
- if (typeof part.usage.contextTokens === 'number')
145
- this.root.attributes.contextTokens = part.usage.contextTokens;
154
+ if (part.payload.usage) {
155
+ if (typeof part.payload.usage.inputTokens === 'number') {
156
+ this.root.attributes.tokensIn = part.payload.usage.inputTokens;
157
+ }
158
+ if (typeof part.payload.usage.outputTokens === 'number') {
159
+ this.root.attributes.tokensOut = part.payload.usage.outputTokens;
160
+ }
161
+ if (typeof part.payload.usage.contextTokens === 'number') {
162
+ this.root.attributes.contextTokens = part.payload.usage.contextTokens;
163
+ }
146
164
  }
147
165
  this.close(at);
148
166
  break;
@@ -174,7 +192,11 @@ export class TraceRecorder {
174
192
  kind: args.kind,
175
193
  startTime: args.at,
176
194
  status: 'ok',
177
- attributes: { sessionId: this.trace.sessionId, ...args.attributes },
195
+ attributes: {
196
+ sessionId: this.trace.sessionId,
197
+ ...(this.currentAgentId ? { agentId: this.currentAgentId } : {}),
198
+ ...args.attributes,
199
+ },
178
200
  };
179
201
  this.trace.spans.push(span);
180
202
  return span;
@@ -239,8 +261,22 @@ export class TraceRecorder {
239
261
  }
240
262
  }
241
263
  export async function runOnce(runtime, opts) {
264
+ // Resolve attribution the same way Runtime.runOnce does: caller's agentId
265
+ // first, then persisted state, then the runtime default. Without this the
266
+ // standalone helper produced spans with no `agentId` and handoff spans with
267
+ // no `handoffFrom` — the exact feature it is documented to provide.
268
+ let agentId = opts.agentId;
269
+ if (!agentId && opts.sessionId && runtime.getSessionStore) {
270
+ const existing = await runtime.getSessionStore().get(opts.sessionId);
271
+ agentId = existing?.activeAgentId ?? existing?.currentAgent;
272
+ }
273
+ agentId ??= runtime.getDefaultAgentId?.();
242
274
  const handle = runtime.run(opts);
243
- const recorder = new TraceRecorder({ sessionId: opts.sessionId, input: opts.input });
275
+ const recorder = new TraceRecorder({
276
+ sessionId: opts.sessionId,
277
+ agentId,
278
+ input: opts.input,
279
+ });
244
280
  for await (const part of handle.events) {
245
281
  recorder.record(part);
246
282
  }
@@ -28,7 +28,7 @@ export async function buildAgentToolSurface(agent, session, deps) {
28
28
  let skillPrompt;
29
29
  let skillTools = {};
30
30
  if (agent.skills) {
31
- const wired = await wireAgentSkills(agent);
31
+ const wired = await wireAgentSkills(agent, resolvedWorkspace?.fs);
32
32
  if (wired) {
33
33
  skillTools = wired.tools;
34
34
  Object.assign(executorTools, wired.tools);
@@ -29,16 +29,19 @@ export class TextDriver {
29
29
  if (!preTurn.proceed) {
30
30
  const blocked = preTurn.blockedMessage ?? 'Input blocked by guardrails';
31
31
  ctx.emit({
32
+ channel: 'internal',
32
33
  type: 'safety-blocked',
33
- moderator: preTurn.blockedBy ?? 'input-guardrails',
34
- rationale: preTurn.blockedReason ?? 'input blocked',
35
- userFacingMessage: blocked,
34
+ payload: {
35
+ moderator: preTurn.blockedBy ?? 'input-guardrails',
36
+ rationale: preTurn.blockedReason ?? 'input blocked',
37
+ userFacingMessage: blocked,
38
+ },
36
39
  });
37
40
  const id = crypto.randomUUID();
38
- ctx.emit({ type: 'text-start', id });
39
- ctx.emit({ type: 'text-delta', id, delta: blocked });
40
- ctx.emit({ type: 'text-end', id });
41
- ctx.emit({ type: 'turn-end' });
41
+ ctx.emit({ channel: 'client', type: 'text-start', payload: { id } });
42
+ ctx.emit({ channel: 'client', type: 'text-delta', payload: { id, delta: blocked } });
43
+ ctx.emit({ channel: 'client', type: 'text-end', payload: { id } });
44
+ ctx.emit({ channel: 'internal', type: 'turn-end', payload: {} });
42
45
  return { text: blocked, toolResults: [] };
43
46
  }
44
47
  const scope = resolveNodeGatherScope(replyNode, ctx.runState.state, ctx.runState.messages);
@@ -75,7 +78,7 @@ export class TextDriver {
75
78
  if (part.type === 'error') {
76
79
  const err = part.error;
77
80
  const message = err instanceof Error ? err.message : String(err);
78
- ctx.emit({ type: 'error', error: message });
81
+ ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
79
82
  throw err instanceof Error ? err : new Error(message);
80
83
  }
81
84
  }
@@ -160,7 +163,7 @@ export class TextDriver {
160
163
  if (turnUsage && turnUsage.totalTokens > 0) {
161
164
  out.usage = turnUsage;
162
165
  }
163
- ctx.emit({ type: 'turn-end' });
166
+ ctx.emit({ channel: 'internal', type: 'turn-end', payload: {} });
164
167
  return out;
165
168
  }
166
169
  // Non-speaking field extraction for collect nodes (shared helper so text and
@@ -11,6 +11,12 @@ export interface ModelToolCallOutcome {
11
11
  result: unknown;
12
12
  control?: TurnControl;
13
13
  failed: boolean;
14
+ /**
15
+ * Set when the call unwound with a control-flow signal rather than a failure. Held as a
16
+ * value so a parallel batch can still finalize its siblings; the dispatcher rethrows it
17
+ * once the batch has settled. Never surfaced to the model.
18
+ */
19
+ signal?: unknown;
14
20
  }
15
21
  export declare function executeModelToolCall(ctx: RunContext, call: ModelToolCall, localTools?: Record<string, AnyTool>, durableOpts?: {
16
22
  callsite?: string;