@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
@@ -7,10 +7,16 @@ import { createEventBus, createTurnHandle } from '../events/TurnHandle.js';
7
7
  import { CoreToolExecutor } from '../tools/effect/index.js';
8
8
  import { buildAgentToolSurface } from './buildAgentToolSurface.js';
9
9
  import { hostLoop } from './hostLoop.js';
10
+ import { isHandoffOscillating } from './handoffOscillation.js';
11
+ import { applyHandoffContinuation } from './handoffContinuation.js';
10
12
  import { isDegradableRuntimeError } from '../flow/degradableErrors.js';
11
13
  import { SAFE_DEGRADED_MESSAGE } from '../flow/degrade.js';
12
14
  import { adaptHostSelect } from './hostClassifyAdapter.js';
13
15
  import { openRun, sessionDerivedRunId } from './openRun.js';
16
+ function resolveOutOfBandControl(agent) {
17
+ const hasFlows = (agent.flows?.length ?? 0) > 0;
18
+ return agent.experimental?.outOfBandControl ?? hasFlows;
19
+ }
14
20
  import { closeRun } from './closeRun.js';
15
21
  import { SessionRunStore } from './durable/SessionRunStore.js';
16
22
  import { loadRecordedSteps } from './durable/replay.js';
@@ -19,8 +25,16 @@ import { resolveAgentPolicies } from './policies/resolvePolicies.js';
19
25
  import { buildAutoRetrieveProvider, buildKnowledgeProvider, buildMemoryService, runMemoryIngest, } from './grounding/index.js';
20
26
  import { SessionMutex } from './SessionMutex.js';
21
27
  import { compactMessages } from './compaction.js';
28
+ import { readLastPromptTokens, readCumulativeUsage, computeTurnTraceUsage } from './turnTokenUsage.js';
22
29
  import { isContextOverflowError, recoverFromContextOverflow } from './contextOverflow.js';
30
+ import { projectGoalsPromptFromState, updateGoalsFromTurn } from './goals.js';
23
31
  import { buildEscalationRequest, recordEscalationOutcome, ESCALATION_NOTIFIED_KEY, } from '../escalation/escalation.js';
32
+ import { runOnce as recordRunOnce } from './TraceRecorder.js';
33
+ import { TraceRecorder } from './TraceRecorder.js';
34
+ import { MemoryTraceStore } from '../tracing/MemoryTraceStore.js';
35
+ import { mutateSessionWithRetry } from '../session/utils.js';
36
+ import { isTraceStore } from '../tracing/TraceStore.js';
37
+ import { runHookSafely } from './runHookSafely.js';
24
38
  export class Runtime {
25
39
  config;
26
40
  agentsById;
@@ -31,6 +45,9 @@ export class Runtime {
31
45
  hooks;
32
46
  activeTurnAborts = new Map();
33
47
  sessionMutex = new SessionMutex();
48
+ traceStore;
49
+ traceSinks;
50
+ pendingTraceWrites = new Set();
34
51
  constructor(config) {
35
52
  this.config = config;
36
53
  this.agentsById = indexAgents(config.agents);
@@ -39,12 +56,28 @@ export class Runtime {
39
56
  this.maxHandoffs = config.maxHandoffs ?? 5;
40
57
  this.terminalHandoffTargets = new Set(config.terminalHandoffTargets ?? ['human']);
41
58
  this.hooks = config.hooks;
59
+ const configuredSinks = config.tracing?.sinks ?? [];
60
+ const configuredStore = config.tracing?.store ?? configuredSinks.find(isTraceStore);
61
+ this.traceStore = config.tracing?.enabled === false
62
+ ? undefined
63
+ : configuredStore ?? new MemoryTraceStore();
64
+ this.traceSinks = this.traceStore
65
+ ? [this.traceStore, ...configuredSinks.filter((sink) => sink !== this.traceStore)]
66
+ : [];
42
67
  }
43
68
  run(opts) {
44
69
  if (opts.wake && opts.input !== undefined) {
45
70
  throw new Error('RunOptions.wake and RunOptions.input are mutually exclusive');
46
71
  }
47
72
  const sessionId = opts.sessionId || randomUUID();
73
+ const recorder = this.shouldTrace(sessionId, opts.input)
74
+ ? new TraceRecorder({
75
+ sessionId,
76
+ agentId: opts.agentId ?? this.config.defaultAgentId,
77
+ input: opts.input,
78
+ onSpan: (span) => this.writeSpan(span),
79
+ })
80
+ : undefined;
48
81
  const bus = createEventBus();
49
82
  const abortController = new AbortController();
50
83
  this.activeTurnAborts.set(sessionId, abortController);
@@ -54,8 +87,11 @@ export class Runtime {
54
87
  const execute = async () => {
55
88
  let runCtx;
56
89
  const emit = (part) => {
90
+ recorder?.record(part);
91
+ if (part.type === 'done')
92
+ this.flushTraceSinks();
57
93
  bus.emit(part);
58
- void this.hooks?.onStreamPart?.(runCtx, part);
94
+ void runHookSafely('onStreamPart', () => this.hooks?.onStreamPart?.(runCtx, part));
59
95
  };
60
96
  const opened = await openRun(this.agentsById, {
61
97
  sessionId,
@@ -67,10 +103,12 @@ export class Runtime {
67
103
  seedMessages: opts.seedMessages,
68
104
  historyDelta: opts.historyDelta,
69
105
  signalDelivery: opts.signalDelivery,
106
+ idempotencyKey: opts.idempotencyKey,
70
107
  transcriptionModel: this.config.transcriptionModel,
71
108
  defaultAgentId: this.config.defaultAgentId,
72
109
  sessionStore: this.sessionStore,
73
110
  });
111
+ recorder?.setInitiatingAgent(opened.agent.id);
74
112
  const policies = resolveAgentPolicies(opened.agent);
75
113
  const knowledgeProvider = this.config.knowledge
76
114
  ? buildKnowledgeProvider(this.config.knowledge)
@@ -86,13 +124,24 @@ export class Runtime {
86
124
  agentId: opened.agent.id,
87
125
  onInterim: (message) => {
88
126
  const id = crypto.randomUUID();
89
- emit({ type: 'text-start', id });
90
- emit({ type: 'text-delta', id, delta: message });
91
- emit({ type: 'text-end', id });
127
+ emit({ channel: 'client', type: 'text-start', payload: { id } });
128
+ emit({ channel: 'client', type: 'text-delta', payload: { id, delta: message } });
129
+ emit({ channel: 'client', type: 'text-end', payload: { id } });
130
+ },
131
+ onChunk: (chunk, toolName, toolCallId) => {
132
+ emit({
133
+ channel: 'internal',
134
+ type: 'tool-result',
135
+ payload: { toolName, result: chunk, toolCallId, preliminary: true },
136
+ });
92
137
  },
93
138
  });
94
139
  const steps = await loadRecordedSteps(opened.runStore, opened.runState.runId);
95
140
  const freshRunState = (await opened.runStore.getRunState(opened.runState.runId)) ?? opened.runState;
141
+ // Snapshot cumulative token usage as this turn opens, so the trace can report
142
+ // THIS turn's consumption (delta), not the running session total (see the
143
+ // per-turn scope requirement in the observability guide).
144
+ const usageBaseline = readCumulativeUsage(freshRunState.state);
96
145
  const model = opened.agent.model ?? this.defaultModel;
97
146
  if (!model) {
98
147
  throw new Error('Runtime requires agent.model or config.defaultModel');
@@ -120,16 +169,20 @@ export class Runtime {
120
169
  : undefined,
121
170
  fs: openingSurface.resolvedWorkspace?.fs,
122
171
  });
172
+ // Session retrieval cache (G6): created once per run, persists across
173
+ // in-session handoffs (runCtx survives the handoff branch). RAG-only —
174
+ // a knowledge-less runtime leaves it undefined.
175
+ runCtx.retrievalCache = knowledgeProvider?.createSessionCache();
123
176
  // Agent base layer (ADR 0001): composed into every node turn by the drivers.
124
177
  runCtx.baseInstructions = opened.agent.instructions;
125
178
  runCtx.globalTools = openingSurface.globalTools;
126
- runCtx.outOfBandControl = opened.agent.experimental?.outOfBandControl ?? false;
179
+ runCtx.outOfBandControl = resolveOutOfBandControl(opened.agent);
127
180
  runCtx.skillPrompt = openingSurface.skillPrompt;
128
- runCtx.workingMemoryPrompt = openingSurface.workingMemoryPrompt;
181
+ runCtx.workingMemoryPrompt = appendGoalsPrompt(openingSurface.workingMemoryPrompt, opened.session.workingMemory);
129
182
  runCtx.workingMemoryTools = openingSurface.workingMemoryTools;
130
- await this.hooks?.onStart?.(runCtx);
183
+ await runHookSafely('onStart', () => this.hooks?.onStart?.(runCtx));
131
184
  if (opts.wake) {
132
- emit({ type: 'wake', reason: opts.wake.reason });
185
+ emit({ channel: 'internal', type: 'wake', payload: { reason: opts.wake.reason } });
133
186
  }
134
187
  const driver = opts.driver ?? new TextDriver();
135
188
  let activeAgent = opened.agent;
@@ -159,7 +212,11 @@ export class Runtime {
159
212
  }
160
213
  if (loopResult.kind === 'handoff') {
161
214
  if (this.terminalHandoffTargets.has(loopResult.to)) {
162
- emit({ type: 'handoff', targetAgent: loopResult.to, reason: loopResult.reason });
215
+ emit({
216
+ channel: 'internal',
217
+ type: 'handoff',
218
+ payload: { targetAgent: loopResult.to, reason: loopResult.reason },
219
+ });
163
220
  runCtx.runState.status = 'paused';
164
221
  await runCtx.runStore.putRunState(runCtx.runState);
165
222
  await this.dispatchEscalation(runCtx, activeAgent, { reason: loopResult.reason ?? 'handoff_to_human', category: loopResult.category }, emit, { setLatch: false });
@@ -169,6 +226,14 @@ export class Runtime {
169
226
  if (handoffCount > this.maxHandoffs) {
170
227
  throw new Error(`maxHandoffs exceeded (${this.maxHandoffs})`);
171
228
  }
229
+ // Cross-turn ping-pong safeguard (handoffCount resets each turn, so it
230
+ // can't catch A↔B oscillation spread across turns). This is a bound
231
+ // ABOVE maxHandoffs: within-run runaway is caught by the maxHandoffs
232
+ // check above; oscillation only fires for same-pair accumulation in the
233
+ // persisted handoffHistory beyond that, so it never pre-empts maxHandoffs.
234
+ if (isHandoffOscillating(opened.session.handoffHistory, runCtx.runState.activeAgentId, loopResult.to, this.maxHandoffs + 1)) {
235
+ throw new Error(`Handoff oscillation detected between "${runCtx.runState.activeAgentId}" and "${loopResult.to}"`);
236
+ }
172
237
  const target = this.agentsById.get(loopResult.to);
173
238
  if (!target) {
174
239
  throw new Error(`Handoff target agent not found: ${loopResult.to}`);
@@ -179,6 +244,20 @@ export class Runtime {
179
244
  reason: loopResult.reason ?? 'handoff',
180
245
  timestamp: new Date(),
181
246
  });
247
+ const handoffTarget = loopResult.to;
248
+ const routeFilter = activeAgent.routes?.find((r) => r.agent === handoffTarget)?.filter;
249
+ const inputFilter = routeFilter ?? this.config.handoffInputFilter;
250
+ if (inputFilter) {
251
+ const filtered = await inputFilter({
252
+ messages: runCtx.runState.messages,
253
+ workingMemory: runCtx.session.workingMemory,
254
+ sourceAgentId: runCtx.runState.activeAgentId,
255
+ targetAgentId: handoffTarget,
256
+ reason: loopResult.reason,
257
+ });
258
+ runCtx.runState.messages = filtered.messages;
259
+ runCtx.session.workingMemory = filtered.workingMemory;
260
+ }
182
261
  runCtx.runState.activeAgentId = loopResult.to;
183
262
  activeAgent = target;
184
263
  const targetSurface = await buildAgentToolSurface(target, opened.session, {
@@ -191,12 +270,51 @@ export class Runtime {
191
270
  : undefined;
192
271
  runCtx.globalTools = targetSurface.globalTools;
193
272
  runCtx.skillPrompt = targetSurface.skillPrompt;
194
- runCtx.workingMemoryPrompt = targetSurface.workingMemoryPrompt;
273
+ runCtx.workingMemoryPrompt = appendGoalsPrompt(targetSurface.workingMemoryPrompt, runCtx.session.workingMemory);
195
274
  runCtx.workingMemoryTools = targetSurface.workingMemoryTools;
196
275
  runCtx.fs = targetSurface.resolvedWorkspace?.fs;
197
276
  runCtx.memoryService = this.config.memoryService
198
277
  ? buildMemoryService(this.config.memoryService, target)
199
278
  : undefined;
279
+ const targetPolicies = resolveAgentPolicies(target);
280
+ const targetModel = target.model ?? this.defaultModel;
281
+ if (!targetModel) {
282
+ throw new Error('Runtime requires agent.model or config.defaultModel');
283
+ }
284
+ runCtx.baseInstructions =
285
+ (this.config.silentHandoff ?? true)
286
+ ? applyHandoffContinuation(target.instructions)
287
+ : target.instructions;
288
+ runCtx.model = targetModel;
289
+ runCtx.controlModel = target.controlModel ?? targetModel;
290
+ runCtx.outOfBandControl = resolveOutOfBandControl(target);
291
+ runCtx.limits = targetPolicies.limits;
292
+ runCtx.refinementPolicies = targetPolicies.refinementPolicies;
293
+ runCtx.validationPolicies = targetPolicies.validationPolicies;
294
+ runCtx.inputProcessors = targetPolicies.inputProcessors;
295
+ runCtx.outputProcessors = targetPolicies.outputProcessors;
296
+ runCtx.toolExecutor = new CoreToolExecutor({
297
+ tools: targetSurface.executorTools,
298
+ enforcer: targetPolicies.enforcer,
299
+ agentId: target.id,
300
+ onInterim: (message) => {
301
+ const id = crypto.randomUUID();
302
+ emit({ channel: 'client', type: 'text-start', payload: { id } });
303
+ emit({
304
+ channel: 'client',
305
+ type: 'text-delta',
306
+ payload: { id, delta: message },
307
+ });
308
+ emit({ channel: 'client', type: 'text-end', payload: { id } });
309
+ },
310
+ onChunk: (chunk, toolName, toolCallId) => {
311
+ emit({
312
+ channel: 'internal',
313
+ type: 'tool-result',
314
+ payload: { toolName, result: chunk, toolCallId, preliminary: true },
315
+ });
316
+ },
317
+ });
200
318
  await runCtx.runStore.putRunState(runCtx.runState);
201
319
  continue;
202
320
  }
@@ -221,14 +339,18 @@ export class Runtime {
221
339
  await this.applyCompaction(runCtx, activeAgent, emit, false);
222
340
  }
223
341
  catch (error) {
224
- await this.hooks?.onError?.(runCtx, error);
342
+ await runHookSafely('onError', () => this.hooks?.onError?.(runCtx, error));
225
343
  if (isDegradableRuntimeError(error)) {
226
344
  const message = error instanceof Error ? error.message : String(error);
227
- emit({ type: 'error', error: message });
345
+ emit({ channel: 'client', type: 'error', payload: { error: message } });
228
346
  const degradedId = crypto.randomUUID();
229
- emit({ type: 'text-start', id: degradedId });
230
- emit({ type: 'text-delta', id: degradedId, delta: SAFE_DEGRADED_MESSAGE });
231
- emit({ type: 'text-end', id: degradedId });
347
+ emit({ channel: 'client', type: 'text-start', payload: { id: degradedId } });
348
+ emit({
349
+ channel: 'client',
350
+ type: 'text-delta',
351
+ payload: { id: degradedId, delta: SAFE_DEGRADED_MESSAGE },
352
+ });
353
+ emit({ channel: 'client', type: 'text-end', payload: { id: degradedId } });
232
354
  runCtx.runState.messages = [
233
355
  ...runCtx.runState.messages,
234
356
  { role: 'assistant', content: SAFE_DEGRADED_MESSAGE },
@@ -254,10 +376,22 @@ export class Runtime {
254
376
  outcomeReason: loopResult.kind === 'ended' ? loopResult.reason : undefined,
255
377
  memoryIngest: async () => {
256
378
  await runMemoryIngest(runCtx);
379
+ if (this.config.trackGoals) {
380
+ const controlModel = this.agentsById.get(runCtx.runState.activeAgentId)?.controlModel ??
381
+ runCtx.controlModel;
382
+ await updateGoalsFromTurn(runCtx, controlModel);
383
+ }
384
+ },
385
+ });
386
+ await runHookSafely('onEnd', () => this.hooks?.onEnd?.(runCtx));
387
+ emit({
388
+ channel: 'client',
389
+ type: 'done',
390
+ payload: {
391
+ sessionId: opened.session.id,
392
+ usage: computeTurnTraceUsage(usageBaseline, runCtx.runState.state),
257
393
  },
258
394
  });
259
- await this.hooks?.onEnd?.(runCtx);
260
- emit({ type: 'done', sessionId: opened.session.id });
261
395
  }
262
396
  return { text: collectAssistantText(runCtx.runState.messages), toolResults: [] };
263
397
  };
@@ -276,12 +410,35 @@ export class Runtime {
276
410
  run: gated,
277
411
  });
278
412
  }
413
+ async runOnce(opts) {
414
+ const existing = opts.sessionId ? await this.sessionStore.get(opts.sessionId) : null;
415
+ // Caller's explicit agentId wins, matching run() (`opts.agentId ?? defaultAgentId`).
416
+ // Persisted state is the fallback, not an override — otherwise the two public
417
+ // entry points disagree about which agent handles the same turn.
418
+ const agentId = opts.agentId ?? existing?.activeAgentId ?? existing?.currentAgent ?? this.config.defaultAgentId;
419
+ return recordRunOnce(this, { ...opts, agentId });
420
+ }
279
421
  stream(opts) {
280
422
  return this.run(opts);
281
423
  }
282
424
  async getSession(sessionId) {
283
425
  return this.sessionStore.get(sessionId);
284
426
  }
427
+ async getTrace(traceId) {
428
+ await this.settleTraceWrites();
429
+ return this.traceStore?.getTrace(traceId) ?? null;
430
+ }
431
+ async listTraces(sessionId) {
432
+ await this.settleTraceWrites();
433
+ return this.traceStore?.listTraces(sessionId) ?? [];
434
+ }
435
+ getTraceStore() {
436
+ return this.traceStore;
437
+ }
438
+ /** The agent used when neither the caller nor persisted state names one. */
439
+ getDefaultAgentId() {
440
+ return this.config.defaultAgentId;
441
+ }
285
442
  getSessionStore() {
286
443
  return this.sessionStore;
287
444
  }
@@ -308,6 +465,65 @@ export class Runtime {
308
465
  markedBy: opts?.markedBy ?? 'http',
309
466
  });
310
467
  }
468
+ shouldTrace(sessionId, input) {
469
+ if (this.traceSinks.length === 0)
470
+ return false;
471
+ const sampling = this.config.tracing?.sampling;
472
+ if (typeof sampling === 'function') {
473
+ try {
474
+ return sampling({ sessionId, input });
475
+ }
476
+ catch {
477
+ return false;
478
+ }
479
+ }
480
+ if (sampling === undefined)
481
+ return true;
482
+ return sampling > 0 && (sampling >= 1 || Math.random() < sampling);
483
+ }
484
+ writeSpan(original) {
485
+ let span = original;
486
+ try {
487
+ if (this.config.tracing?.redact) {
488
+ span = this.config.tracing.redact(structuredClone(original));
489
+ }
490
+ }
491
+ catch {
492
+ return;
493
+ }
494
+ if (!span)
495
+ return;
496
+ for (const sink of this.traceSinks) {
497
+ try {
498
+ const result = sink.write(span);
499
+ if (result instanceof Promise) {
500
+ const pending = result.catch(() => { }).finally(() => this.pendingTraceWrites.delete(pending));
501
+ this.pendingTraceWrites.add(pending);
502
+ }
503
+ }
504
+ catch {
505
+ // Traces are derived observability and never participate in run correctness.
506
+ }
507
+ }
508
+ }
509
+ async settleTraceWrites() {
510
+ await Promise.allSettled([...this.pendingTraceWrites]);
511
+ await Promise.allSettled(this.traceSinks.map((sink) => sink.flush?.()));
512
+ }
513
+ flushTraceSinks() {
514
+ for (const sink of this.traceSinks) {
515
+ try {
516
+ const result = sink.flush?.();
517
+ if (result) {
518
+ const pending = result.catch(() => { }).finally(() => this.pendingTraceWrites.delete(pending));
519
+ this.pendingTraceWrites.add(pending);
520
+ }
521
+ }
522
+ catch {
523
+ // Export flushes are observational and never affect the run.
524
+ }
525
+ }
526
+ }
311
527
  /**
312
528
  * Compact `runState.messages` when over the configured trigger (or always,
313
529
  * when `force`). Persists both the run state and the session message mirror.
@@ -327,27 +543,33 @@ export class Runtime {
327
543
  model,
328
544
  config,
329
545
  force,
546
+ lastPromptTokens: readLastPromptTokens(runCtx.runState.state),
330
547
  });
331
548
  if (!result.compacted) {
332
549
  if (force) {
333
- emit({ type: 'compaction-skipped', reason: result.reason });
550
+ emit({
551
+ channel: 'internal',
552
+ type: 'compaction-skipped',
553
+ payload: { reason: result.reason },
554
+ });
334
555
  }
335
556
  return false;
336
557
  }
337
558
  runCtx.runState.messages = result.messages;
338
559
  runCtx.runState.updatedAt = Date.now();
339
560
  await runCtx.runStore.putRunState(runCtx.runState);
340
- const latest = await this.sessionStore.get(runCtx.session.id);
341
- if (latest) {
561
+ await mutateSessionWithRetry(this.sessionStore, runCtx.session.id, (latest) => {
342
562
  latest.messages = [...result.messages];
343
- await this.sessionStore.save(latest);
344
- }
563
+ });
345
564
  runCtx.session.messages = [...result.messages];
346
565
  emit({
566
+ channel: 'internal',
347
567
  type: 'context-compacted',
348
- beforeTokens: result.beforeTokens,
349
- afterTokens: result.afterTokens,
350
- summarizedCount: result.summarizedCount,
568
+ payload: {
569
+ beforeTokens: result.beforeTokens,
570
+ afterTokens: result.afterTokens,
571
+ summarizedCount: result.summarizedCount,
572
+ },
351
573
  });
352
574
  return true;
353
575
  }
@@ -362,9 +584,12 @@ export class Runtime {
362
584
  runCtx.runState.messages = runCtx.session.messages;
363
585
  const compacted = await this.applyCompaction(runCtx, agent, emit, true);
364
586
  emit({
587
+ channel: 'internal',
365
588
  type: 'context-overflow-recovered',
366
- strippedCount: recovery.strippedCount,
367
- compacted,
589
+ payload: {
590
+ strippedCount: recovery.strippedCount,
591
+ compacted,
592
+ },
368
593
  });
369
594
  }
370
595
  async getConversationLength(sessionId) {
@@ -409,18 +634,23 @@ export class Runtime {
409
634
  error: error instanceof Error ? error.message : String(error),
410
635
  };
411
636
  }
412
- recordEscalationOutcome(runCtx.session, info.category ?? 'user-request', outcome);
413
637
  if (opts.setLatch) {
414
638
  runCtx.runState.state[ESCALATION_NOTIFIED_KEY] = true;
415
639
  }
416
640
  await runCtx.runStore.putRunState(runCtx.runState);
417
- await this.sessionStore.save(runCtx.session);
641
+ const latestSession = await mutateSessionWithRetry(this.sessionStore, runCtx.session.id, (latest) => {
642
+ recordEscalationOutcome(latest, info.category ?? 'user-request', outcome);
643
+ });
644
+ runCtx.session.metadata = latestSession.metadata;
418
645
  emit({
646
+ channel: 'internal',
419
647
  type: 'escalation',
420
- reason: info.reason,
421
- category: info.category,
422
- outcome: outcome.status,
423
- summary: request.summary,
648
+ payload: {
649
+ reason: info.reason,
650
+ category: info.category,
651
+ outcome: outcome.status,
652
+ summary: request.summary,
653
+ },
424
654
  });
425
655
  }
426
656
  /**
@@ -451,9 +681,9 @@ export class Runtime {
451
681
  delete runState.state[ESCALATION_NOTIFIED_KEY];
452
682
  runState.updatedAt = Date.now();
453
683
  await runStore.putRunState(runState);
454
- const latest = (await this.sessionStore.get(sessionId)) ?? session;
455
- latest.messages = [...runState.messages];
456
- await this.sessionStore.save(latest);
684
+ await mutateSessionWithRetry(this.sessionStore, sessionId, (latest) => {
685
+ latest.messages = [...runState.messages];
686
+ });
457
687
  }
458
688
  }
459
689
  export function createRuntime(config) {
@@ -476,3 +706,10 @@ function collectAssistantText(messages) {
476
706
  }
477
707
  return '';
478
708
  }
709
+ function appendGoalsPrompt(workingMemoryPrompt, workingMemory) {
710
+ const goalsPrompt = projectGoalsPromptFromState(workingMemory);
711
+ if (!goalsPrompt) {
712
+ return workingMemoryPrompt;
713
+ }
714
+ return [workingMemoryPrompt, goalsPrompt].filter((part) => part && part.trim()).join('\n\n');
715
+ }
@@ -12,6 +12,13 @@ export declare class TokenAccumulator {
12
12
  private _peakUtil;
13
13
  private _cumCacheRead;
14
14
  constructor(contextWindow?: number | undefined);
15
+ /** Rehydrate cumulative totals from persisted session state (cross-turn). */
16
+ restoreCumulative(saved: {
17
+ inputTokens: number;
18
+ outputTokens: number;
19
+ totalTokens: number;
20
+ cacheReadTokens?: number;
21
+ }): void;
15
22
  record(usage: TurnUsageInput): TurnUsage;
16
23
  get cumulative(): {
17
24
  inputTokens: number;
@@ -12,6 +12,13 @@ export class TokenAccumulator {
12
12
  constructor(contextWindow) {
13
13
  this.contextWindow = contextWindow;
14
14
  }
15
+ /** Rehydrate cumulative totals from persisted session state (cross-turn). */
16
+ restoreCumulative(saved) {
17
+ this._cumInput = saved.inputTokens;
18
+ this._cumOutput = saved.outputTokens;
19
+ this._cumTotal = saved.totalTokens;
20
+ this._cumCacheRead = saved.cacheReadTokens ?? 0;
21
+ }
15
22
  record(usage) {
16
23
  this._cumInput += usage.inputTokens;
17
24
  this._cumOutput += usage.outputTokens;
@@ -0,0 +1,51 @@
1
+ import type { TurnResult } from '../types/channel.js';
2
+ import type { StreamPart, TurnHandle } from '../types/stream.js';
3
+ import type { AgentSpan, AgentTrace } from '../types/trace.js';
4
+ import type { RunOptions } from './Runtime.js';
5
+ export interface TraceRecorderOptions {
6
+ sessionId?: string;
7
+ agentId?: string;
8
+ input?: unknown;
9
+ onSpan?: (span: AgentSpan) => void;
10
+ }
11
+ export declare class TraceRecorder {
12
+ private readonly trace;
13
+ private readonly root;
14
+ private currentFlow?;
15
+ private currentNode?;
16
+ private readonly openTools;
17
+ private readonly toolCallIds;
18
+ private readonly emitted;
19
+ private readonly onSpan?;
20
+ private currentAgentId?;
21
+ constructor(options?: TraceRecorderOptions);
22
+ setInitiatingAgent(agentId: string): void;
23
+ record(part: StreamPart): void;
24
+ finish(result: TurnResult): AgentTrace;
25
+ private openSpan;
26
+ private takeToolSpan;
27
+ private closeTools;
28
+ private closeNode;
29
+ private closeFlow;
30
+ private setSessionId;
31
+ private close;
32
+ private emitSpan;
33
+ }
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 {
42
+ run(opts: RunOptions): TurnHandle;
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>;