@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
@@ -9,6 +9,7 @@ import { hasHostControlTargets } from './hostControlTools.js';
9
9
  import { isValidControl, resolveHostControl, startHostControlGuard, } from './hostControlGuard.js';
10
10
  import { resolveDispatchMode, isAdvisoryDispatch } from './dispatchMode.js';
11
11
  import { adaptHostSelect } from './hostClassifyAdapter.js';
12
+ import { persistTurnUsageFromTurn } from './turnTokenUsage.js';
12
13
  export async function hostLoop(options) {
13
14
  const { agent, run, driver, ctx } = options;
14
15
  const classify = options.classify ??
@@ -35,7 +36,7 @@ export async function hostLoop(options) {
35
36
  return { kind: 'paused' };
36
37
  }
37
38
  if (error instanceof LimitsExceededError) {
38
- ctx.emit({ type: 'error', error: error.message });
39
+ ctx.emit({ channel: 'client', type: 'error', payload: { error: error.message } });
39
40
  return { kind: 'ended', reason: error.message };
40
41
  }
41
42
  throw error;
@@ -67,6 +68,12 @@ async function runActiveFlow(flow, run, driver, ctx, agent) {
67
68
  ctx.resetCallsites();
68
69
  const result = await runFlow(flow, run, driver, ctx, agent);
69
70
  if (result.kind === 'handoff') {
71
+ // A handoff fired from inside a flow: the source flow is abandoned for this turn.
72
+ // Clear the active-flow pointers so the target agent does not try (and fail) to
73
+ // resume a flow that belongs to the source agent (G17).
74
+ run.activeFlow = undefined;
75
+ run.activeNode = undefined;
76
+ await ctx.runStore.putRunState(run);
70
77
  return { kind: 'handoff', to: result.to, reason: result.reason };
71
78
  }
72
79
  if (result.kind === 'awaitingUser') {
@@ -111,12 +118,16 @@ async function runFreeConversation(agent, run, driver, ctx, classify) {
111
118
  resolved.hostControl = { dispatchMode, advisoryDispatch };
112
119
  }
113
120
  const turn = await driver.runAgentTurn(resolved, ctx);
121
+ await persistTurnUsageFromTurn(ctx, turn);
114
122
  if (turn.control && isValidControl(turn.control, agent, run)) {
115
123
  emitHostGuardTelemetry(ctx, { invoked: false, reason: 'main-control' });
116
124
  return await executeHostControl(agent, run, driver, ctx, turn.control);
117
125
  }
118
126
  if (turn.text.trim()) {
119
127
  emitHostGuardTelemetry(ctx, { invoked: false, reason: 'answered' });
128
+ if (turn.toolMessages?.length) {
129
+ run.messages = [...run.messages, ...turn.toolMessages];
130
+ }
120
131
  const message = { role: 'assistant', content: turn.text };
121
132
  run.messages = [...run.messages, message];
122
133
  await ctx.runStore.putRunState(run);
@@ -149,7 +160,11 @@ function guardVerdictToTelemetryVerdict(verdict) {
149
160
  return 'keep';
150
161
  }
151
162
  function emitHostGuardTelemetry(ctx, data) {
152
- ctx.emit({ type: 'custom', name: 'host-guard', data });
163
+ ctx.emit({
164
+ channel: 'internal',
165
+ type: 'custom',
166
+ payload: { name: 'host-guard', data },
167
+ });
153
168
  }
154
169
  function guardVerdictToControl(verdict, agent) {
155
170
  const selection = verdictToSelection(verdict, agent);
@@ -163,7 +178,11 @@ function guardVerdictToControl(verdict, agent) {
163
178
  }
164
179
  async function executeHostControl(agent, run, driver, ctx, control) {
165
180
  if (!control) {
166
- ctx.emit({ type: 'error', error: 'No valid host control target resolved' });
181
+ ctx.emit({
182
+ channel: 'client',
183
+ type: 'error',
184
+ payload: { error: 'No valid host control target resolved' },
185
+ });
167
186
  return { kind: 'ended', reason: 'dispatch_failed' };
168
187
  }
169
188
  if (control.type === 'enterFlow') {
@@ -171,18 +190,30 @@ async function executeHostControl(agent, run, driver, ctx, control) {
171
190
  if (flow) {
172
191
  return await runActiveFlow(flow, run, driver, ctx, agent);
173
192
  }
174
- ctx.emit({ type: 'error', error: `Flow not found: ${control.flowName}` });
193
+ ctx.emit({
194
+ channel: 'client',
195
+ type: 'error',
196
+ payload: { error: `Flow not found: ${control.flowName}` },
197
+ });
175
198
  return { kind: 'ended', reason: 'flow_not_found' };
176
199
  }
177
200
  if (control.type === 'handoff') {
178
- ctx.emit({ type: 'handoff', targetAgent: control.target, reason: control.reason });
201
+ ctx.emit({
202
+ channel: 'internal',
203
+ type: 'handoff',
204
+ payload: { targetAgent: control.target, reason: control.reason },
205
+ });
179
206
  return { kind: 'handoff', to: control.target, reason: control.reason };
180
207
  }
181
208
  if (control.type === 'end') {
182
209
  return { kind: 'ended', reason: control.reason };
183
210
  }
184
211
  if (control.type === 'escalate') {
185
- ctx.emit({ type: 'handoff', targetAgent: 'human', reason: control.reason });
212
+ // No handoff part is emitted here. Runtime owns it: it emits one for any
213
+ // target in `terminalHandoffTargets` (default ['human']) on this exact
214
+ // return value. Emitting here too produced two handoff spans per escalation
215
+ // and a meaningless `human -> human` self-edge, which then mis-attributed
216
+ // every later span in the turn to `human`.
186
217
  return { kind: 'handoff', to: 'human', reason: control.reason, category: control.category };
187
218
  }
188
219
  if (control.type === 'recover') {
@@ -1,8 +1,9 @@
1
- export { createRuntime, Runtime, type HarnessConfig, type RunOptions, } from './Runtime.js';
1
+ export { createRuntime, Runtime, type HarnessConfig, type RunOptions, type TracingConfig, } from './Runtime.js';
2
2
  export type { RuntimeLike } from './RuntimeLike.js';
3
+ export { TraceRecorder, runOnce, type TraceRecorderOptions } from './TraceRecorder.js';
4
+ export type { AgentSpan, AgentTrace, SpanKind } from '../types/trace.js';
3
5
  export { SessionWorkingMemory } from './WorkingMemory.js';
4
- export { TextDriver, VoiceDriver } from './channels/index.js';
5
- export type { VoiceDriverConfig } from './channels/index.js';
6
+ export { TextDriver } from './channels/index.js';
6
7
  export { setPendingUserInput, consumePendingUserInput, consumeAllPendingUserInput, peekPendingUserInput, hasPendingUserInput, } from './channels/index.js';
7
8
  export { userInputToText, mergeUserInputContents, hasMediaParts, transcribeAudioParts, type UserInputContent, } from './userInput.js';
8
9
  export { isAbortSignal, type InterruptionEvent, type AbortOptions, type CancellationReason, } from '../types/index.js';
@@ -1,6 +1,7 @@
1
1
  export { createRuntime, Runtime, } from './Runtime.js';
2
+ export { TraceRecorder, runOnce } from './TraceRecorder.js';
2
3
  export { SessionWorkingMemory } from './WorkingMemory.js';
3
- export { TextDriver, VoiceDriver } from './channels/index.js';
4
+ export { TextDriver } from './channels/index.js';
4
5
  // Pending-input buffer helpers — required by custom ChannelDriver authors to
5
6
  // implement awaitUser the same FIFO-aware way the built-in drivers do (the
6
7
  // buffer is an ordered queue since 0.3.13/H3, not a single slot).
@@ -21,6 +21,8 @@ export interface OpenRunOptions {
21
21
  seedMessages?: ModelMessage[];
22
22
  historyDelta?: ModelMessage[];
23
23
  signalDelivery?: SignalDelivery;
24
+ /** Stable key for this inbound user message; duplicate webhook retries are ignored (H2). */
25
+ idempotencyKey?: string;
24
26
  transcriptionModel?: TranscriptionModel;
25
27
  defaultAgentId: string;
26
28
  sessionStore: SessionStore;
@@ -3,6 +3,8 @@ import { transcribeAudioParts } from './userInput.js';
3
3
  import { setPendingUserInput } from './channels/inputBuffer.js';
4
4
  import { SessionRunStore } from './durable/SessionRunStore.js';
5
5
  import { recordSignalDelivery } from './durable/replay.js';
6
+ import { resetTurnCount } from './policies/limits.js';
7
+ import { mutateSessionWithRetry } from '../session/utils.js';
6
8
  export function sessionDerivedRunId(sessionId) {
7
9
  return sessionId;
8
10
  }
@@ -26,16 +28,18 @@ export async function openRun(agentsById, options) {
26
28
  };
27
29
  await runStore.initRun(runState);
28
30
  if (initialMessages.length > 0) {
29
- session.messages = [...initialMessages];
30
- await options.sessionStore.save(session);
31
+ await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
32
+ latest.messages = [...initialMessages];
33
+ });
31
34
  }
32
35
  }
33
36
  if (options.historyDelta?.length) {
34
37
  runState.messages = [...runState.messages, ...options.historyDelta];
35
38
  runState.updatedAt = Date.now();
36
39
  await runStore.putRunState(runState);
37
- session.messages = [...session.messages, ...options.historyDelta];
38
- await options.sessionStore.save(session);
40
+ await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
41
+ latest.messages = [...latest.messages, ...options.historyDelta];
42
+ });
39
43
  }
40
44
  if (options.signalDelivery) {
41
45
  await recordSignalDelivery(runStore, runState, options.signalDelivery);
@@ -53,22 +57,47 @@ export async function openRun(agentsById, options) {
53
57
  const hasInput = typeof effectiveInput === 'string'
54
58
  ? effectiveInput.length > 0
55
59
  : Array.isArray(effectiveInput) && effectiveInput.length > 0;
60
+ const isResume = Boolean(options.signalDelivery);
61
+ const isFlowContinuation = Boolean(runState.activeFlow);
62
+ const isFreshLogicalRun = (hasInput || Boolean(options.wake)) && !isResume && !isFlowContinuation;
63
+ if (isFreshLogicalRun) {
64
+ runState.runEpoch = (runState.runEpoch ?? 0) + 1;
65
+ await runStore.pruneStepsBeforeEpoch(runId, runState.runEpoch);
66
+ resetTurnCount(runState);
67
+ if (Array.isArray(runState.state.__completedFlows)) {
68
+ runState.state.__completedFlows = [];
69
+ }
70
+ runState.updatedAt = Date.now();
71
+ await runStore.putRunState(runState);
72
+ }
56
73
  if (hasInput && effectiveInput !== undefined) {
74
+ if (options.idempotencyKey) {
75
+ const processed = runState.processedInboundKeys ?? [];
76
+ if (processed.includes(options.idempotencyKey)) {
77
+ const agent = agentsById.get(runState.activeAgentId);
78
+ if (!agent) {
79
+ throw new Error(`Unknown activeAgentId "${runState.activeAgentId}"`);
80
+ }
81
+ const latestSession = (await options.sessionStore.get(options.sessionId)) ?? session;
82
+ return { session: latestSession, runState, runStore, agent };
83
+ }
84
+ runState.processedInboundKeys = [...processed, options.idempotencyKey];
85
+ }
57
86
  runState.updatedAt = Date.now();
58
87
  if (runState.activeFlow) {
59
88
  await runStore.putRunState(runState);
60
- const sessionAfterPersist = (await options.sessionStore.get(options.sessionId)) ?? session;
61
- setPendingUserInput(sessionAfterPersist, effectiveInput);
62
- await options.sessionStore.save(sessionAfterPersist);
89
+ await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
90
+ setPendingUserInput(latest, effectiveInput);
91
+ });
63
92
  }
64
93
  else {
65
94
  const userMessage = { role: 'user', content: effectiveInput };
66
95
  runState.messages = [...runState.messages, userMessage];
67
96
  runState.updatedAt = Date.now();
68
97
  await runStore.putRunState(runState);
69
- const sessionAfterPersist = (await options.sessionStore.get(options.sessionId)) ?? session;
70
- sessionAfterPersist.messages = [...sessionAfterPersist.messages, userMessage];
71
- await options.sessionStore.save(sessionAfterPersist);
98
+ await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
99
+ latest.messages = [...latest.messages, userMessage];
100
+ });
72
101
  }
73
102
  }
74
103
  if (options.wake && !hasInput) {
@@ -84,18 +113,18 @@ export async function openRun(agentsById, options) {
84
113
  runState.messages = [...runState.messages, wakeMessage];
85
114
  runState.updatedAt = Date.now();
86
115
  await runStore.putRunState(runState);
87
- const sessionAfterPersist = (await options.sessionStore.get(options.sessionId)) ?? session;
88
- sessionAfterPersist.messages = [...sessionAfterPersist.messages, wakeMessage];
89
- await options.sessionStore.save(sessionAfterPersist);
116
+ await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
117
+ latest.messages = [...latest.messages, wakeMessage];
118
+ });
90
119
  }
91
120
  const agent = agentsById.get(runState.activeAgentId);
92
121
  if (!agent) {
93
122
  throw new Error(`Unknown activeAgentId "${runState.activeAgentId}"`);
94
123
  }
95
- const latestSession = (await options.sessionStore.get(options.sessionId)) ?? session;
96
- latestSession.currentAgent = runState.activeAgentId;
97
- latestSession.activeAgentId = runState.activeAgentId;
98
- await options.sessionStore.save(latestSession);
124
+ const latestSession = await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
125
+ latest.currentAgent = runState.activeAgentId;
126
+ latest.activeAgentId = runState.activeAgentId;
127
+ });
99
128
  return { session: latestSession, runState, runStore, agent };
100
129
  }
101
130
  async function loadOrCreateSession(options) {
@@ -1,9 +1,9 @@
1
1
  import type { Session } from '../types/session.js';
2
2
  import type { SessionStore } from '../session/SessionStore.js';
3
3
  import type { ConversationOutcome, ConversationOutcomeMarkedBy, ConversationOutcomeRecord } from '../outcomes/types.js';
4
- import type { HarnessStreamPart } from '../types/stream.js';
4
+ import type { StreamPart } from '../types/stream.js';
5
5
  export declare function isTerminalOutcome(outcome: ConversationOutcome): boolean;
6
6
  export declare function markSessionOutcome(sessionStore: SessionStore, session: Session, outcome: ConversationOutcome, opts?: {
7
7
  reason?: string;
8
8
  markedBy?: ConversationOutcomeMarkedBy;
9
- }, emit?: (part: HarnessStreamPart) => void): Promise<ConversationOutcomeRecord>;
9
+ }, emit?: (part: StreamPart) => void): Promise<ConversationOutcomeRecord>;
@@ -22,8 +22,9 @@ export async function markSessionOutcome(sessionStore, session, outcome, opts =
22
22
  await sessionStore.save(session);
23
23
  if (emit) {
24
24
  emit({
25
+ channel: 'client',
25
26
  type: 'conversation-outcome',
26
- outcome: record.outcome,
27
+ payload: { outcome: record.outcome },
27
28
  });
28
29
  }
29
30
  return record;
@@ -1,6 +1,6 @@
1
1
  import type { TurnControl } from '../../types/channel.js';
2
2
  import type { ToolCallRecord } from '../../types/session.js';
3
- import type { SourceRef } from '../../types/voice.js';
3
+ import type { SourceRef } from '../../types/knowledge.js';
4
4
  import type { RunContext } from '../../types/run-context.js';
5
5
  export interface PreTurnResult {
6
6
  proceed: boolean;
@@ -4,5 +4,6 @@ export declare class LimitsExceededError extends Error {
4
4
  constructor(message: string);
5
5
  }
6
6
  export declare function incrementTurnCount(run: RunState): number;
7
+ export declare function resetTurnCount(run: RunState): void;
7
8
  export declare function assertWithinTurnLimit(run: RunState, limits?: Limits): void;
8
9
  export declare function resolveMaxSteps(limits: Limits | undefined, fallback: number): number;
@@ -15,6 +15,9 @@ export function incrementTurnCount(run) {
15
15
  run.updatedAt = Date.now();
16
16
  return next;
17
17
  }
18
+ export function resetTurnCount(run) {
19
+ run.state[TURN_COUNT_KEY] = 0;
20
+ }
18
21
  export function assertWithinTurnLimit(run, limits) {
19
22
  const maxTurns = limits?.maxTurns;
20
23
  if (maxTurns == null) {
@@ -0,0 +1,3 @@
1
+ type HookCallback = () => void | Promise<void>;
2
+ export declare function runHookSafely(name: string, callback: HookCallback | undefined): Promise<void>;
3
+ export {};
@@ -0,0 +1,19 @@
1
+ export function runHookSafely(name, callback) {
2
+ if (!callback)
3
+ return Promise.resolve();
4
+ try {
5
+ return Promise.resolve(callback()).catch((error) => reportHookError(name, error));
6
+ }
7
+ catch (error) {
8
+ reportHookError(name, error);
9
+ return Promise.resolve();
10
+ }
11
+ }
12
+ function reportHookError(name, error) {
13
+ try {
14
+ console.error(`Hook ${name} failed`, error);
15
+ }
16
+ catch {
17
+ // Hook failure reporting must not affect run correctness.
18
+ }
19
+ }
@@ -1,4 +1,4 @@
1
- import type { LanguageModel } from 'ai';
1
+ import type { LanguageModel, ModelMessage } from 'ai';
2
2
  import type { AgentConfig } from '../types/agentConfig.js';
3
3
  import type { Flow } from '../types/flow.js';
4
4
  import type { RunState } from './durable/types.js';
@@ -20,12 +20,15 @@ export interface HostGuardVerdict {
20
20
  reason?: string;
21
21
  confidence?: number;
22
22
  }
23
+ export declare const DEFAULT_CLASSIFIER_CONTEXT_MESSAGES = 6;
23
24
  export interface ClassifyHostOptions {
24
25
  agent: AgentConfig;
25
26
  run: RunState;
26
27
  model: LanguageModel;
27
28
  allowKeep: boolean;
28
29
  excludeFlowNames?: string[];
30
+ /** How many recent messages to include in the classifier prompt. Default: 6. */
31
+ contextMessageLimit?: number;
29
32
  }
30
33
  export declare function classifyHostTarget(options: ClassifyHostOptions): Promise<HostGuardVerdict>;
31
34
  /** @deprecated Use classifyHostTarget — kept as alias for test injection. */
@@ -33,4 +36,5 @@ export declare function selectHostTarget(options: Omit<ClassifyHostOptions, 'all
33
36
  alwaysRoute?: boolean;
34
37
  }): Promise<HostSelection>;
35
38
  export declare function verdictToSelection(verdict: HostGuardVerdict, agent: AgentConfig): HostSelection | undefined;
39
+ export declare function formatRecentConversation(messages: ModelMessage[], limit?: number): string;
36
40
  export { availableHostFlows };
@@ -14,11 +14,13 @@ const dispatcherSchema = z.object({
14
14
  agentId: z.union([z.string(), z.null()]),
15
15
  reason: z.union([z.string(), z.null()]),
16
16
  });
17
+ export const DEFAULT_CLASSIFIER_CONTEXT_MESSAGES = 6;
17
18
  export async function classifyHostTarget(options) {
18
19
  const { agent, run, model, allowKeep } = options;
19
20
  const flows = agent.flows ?? [];
20
21
  const routes = agent.routes ?? [];
21
22
  const latestUser = latestUserMessage(run.messages);
23
+ const recentContext = formatRecentConversation(run.messages, options.contextMessageLimit ?? DEFAULT_CLASSIFIER_CONTEXT_MESSAGES);
22
24
  if (!latestUser) {
23
25
  return { action: 'keep', confidence: 1 };
24
26
  }
@@ -51,7 +53,9 @@ export async function classifyHostTarget(options) {
51
53
  system: 'You are an internal routing classifier. Choose exactly one action. ' +
52
54
  'Output schema fields only — never user-facing prose. ' +
53
55
  'Reason over semantic descriptions only; never match keywords or substrings.',
54
- prompt: `User message:\n${latestUser}\n\n` +
56
+ prompt: (recentContext
57
+ ? `Recent conversation:\n${recentContext}\n\n`
58
+ : `User message:\n${latestUser}\n\n`) +
55
59
  (completedFlows.length > 0 ? `Completed flows: ${completedFlows.join(', ')}\n\n` : '') +
56
60
  (flowLines ? `Available flows:\n${flowLines}\n\n` : '') +
57
61
  (routeLines ? `Routes:\n${routeLines}\n\n` : '') +
@@ -159,6 +163,31 @@ function formatRouteLine(route, index) {
159
163
  const target = route.agent ? `agent "${route.agent}"` : route.flow ? `flow "${route.flow}"` : 'keep';
160
164
  return `- route ${index + 1} → ${target} when: ${route.when}`;
161
165
  }
166
+ function formatMessageContent(message) {
167
+ if (typeof message.content === 'string') {
168
+ return message.content;
169
+ }
170
+ if (Array.isArray(message.content)) {
171
+ return message.content
172
+ .filter((part) => part.type === 'text')
173
+ .map((part) => part.text)
174
+ .join('');
175
+ }
176
+ return '';
177
+ }
178
+ export function formatRecentConversation(messages, limit = DEFAULT_CLASSIFIER_CONTEXT_MESSAGES) {
179
+ const slice = messages.slice(-limit);
180
+ return slice
181
+ .map((message) => {
182
+ const content = formatMessageContent(message);
183
+ if (!content.trim()) {
184
+ return undefined;
185
+ }
186
+ return `${message.role}: ${content}`;
187
+ })
188
+ .filter((line) => line != null)
189
+ .join('\n');
190
+ }
162
191
  function latestUserMessage(messages) {
163
192
  for (let i = messages.length - 1; i >= 0; i -= 1) {
164
193
  const message = messages[i];
@@ -0,0 +1,28 @@
1
+ import type { TurnResult } from '../types/channel.js';
2
+ import type { RunContext } from '../types/run-context.js';
3
+ export declare const TOKEN_USAGE_STATE_KEY = "__tokenUsage";
4
+ export declare const LAST_PROMPT_TOKENS_KEY = "__lastPromptTokens";
5
+ export interface PersistedTokenUsage {
6
+ inputTokens: number;
7
+ outputTokens: number;
8
+ totalTokens: number;
9
+ cacheReadTokens?: number;
10
+ }
11
+ /** Record real turn usage onto run state (survives across turns via persistence). */
12
+ export declare function persistTurnUsageFromTurn(ctx: RunContext, turn: TurnResult): Promise<void>;
13
+ export declare function readLastPromptTokens(state: Record<string, unknown>): number | undefined;
14
+ /** The session-cumulative usage persisted on run state (or undefined before any turn). */
15
+ export declare function readCumulativeUsage(state: Record<string, unknown>): PersistedTokenUsage | undefined;
16
+ export interface TraceTurnUsage {
17
+ /** Input tokens consumed by THIS turn (cumulative delta since the turn opened). */
18
+ inputTokens?: number;
19
+ /** Output tokens generated by THIS turn (cumulative delta since the turn opened). */
20
+ outputTokens?: number;
21
+ /** Context-window occupancy — the last prompt's token count (not a per-turn delta). */
22
+ contextTokens?: number;
23
+ }
24
+ /** Per-turn token usage for a trace's `done` event. `baseline` is the cumulative
25
+ * usage captured when the turn OPENED, so input/output are strictly this turn's
26
+ * consumption (correct for cost attribution — never the running session total).
27
+ * `contextTokens` is the current window occupancy, which is inherently a snapshot. */
28
+ export declare function computeTurnTraceUsage(baseline: PersistedTokenUsage | undefined, state: Record<string, unknown>): TraceTurnUsage;
@@ -0,0 +1,70 @@
1
+ import { TokenAccumulator } from './TokenAccumulator.js';
2
+ export const TOKEN_USAGE_STATE_KEY = '__tokenUsage';
3
+ export const LAST_PROMPT_TOKENS_KEY = '__lastPromptTokens';
4
+ function readPersistedUsage(state) {
5
+ const saved = state[TOKEN_USAGE_STATE_KEY];
6
+ if (!saved || typeof saved !== 'object') {
7
+ return undefined;
8
+ }
9
+ const usage = saved;
10
+ if (typeof usage.inputTokens !== 'number' ||
11
+ typeof usage.outputTokens !== 'number' ||
12
+ typeof usage.totalTokens !== 'number') {
13
+ return undefined;
14
+ }
15
+ return usage;
16
+ }
17
+ function hydrateAccumulator(state) {
18
+ const acc = new TokenAccumulator();
19
+ const saved = readPersistedUsage(state);
20
+ if (saved) {
21
+ acc.restoreCumulative(saved);
22
+ }
23
+ return acc;
24
+ }
25
+ /** Record real turn usage onto run state (survives across turns via persistence). */
26
+ export async function persistTurnUsageFromTurn(ctx, turn) {
27
+ if (!turn.usage) {
28
+ return;
29
+ }
30
+ const acc = hydrateAccumulator(ctx.runState.state);
31
+ const turnIndex = acc.turns.length + 1;
32
+ acc.record({
33
+ turn: turnIndex,
34
+ inputTokens: turn.usage.inputTokens,
35
+ outputTokens: turn.usage.outputTokens,
36
+ totalTokens: turn.usage.totalTokens,
37
+ cacheReadTokens: turn.usage.cacheReadTokens,
38
+ latencyMs: 0,
39
+ });
40
+ ctx.runState.state[TOKEN_USAGE_STATE_KEY] = acc.cumulative;
41
+ ctx.runState.state[LAST_PROMPT_TOKENS_KEY] =
42
+ turn.usage.contextTokens ?? turn.usage.inputTokens;
43
+ ctx.runState.updatedAt = Date.now();
44
+ await ctx.runStore.putRunState(ctx.runState);
45
+ }
46
+ export function readLastPromptTokens(state) {
47
+ const value = state[LAST_PROMPT_TOKENS_KEY];
48
+ return typeof value === 'number' ? value : undefined;
49
+ }
50
+ /** The session-cumulative usage persisted on run state (or undefined before any turn). */
51
+ export function readCumulativeUsage(state) {
52
+ return readPersistedUsage(state);
53
+ }
54
+ /** Per-turn token usage for a trace's `done` event. `baseline` is the cumulative
55
+ * usage captured when the turn OPENED, so input/output are strictly this turn's
56
+ * consumption (correct for cost attribution — never the running session total).
57
+ * `contextTokens` is the current window occupancy, which is inherently a snapshot. */
58
+ export function computeTurnTraceUsage(baseline, state) {
59
+ const contextTokens = readLastPromptTokens(state);
60
+ const now = readPersistedUsage(state);
61
+ if (!now) {
62
+ return contextTokens === undefined ? {} : { contextTokens };
63
+ }
64
+ const base = baseline ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
65
+ return {
66
+ inputTokens: Math.max(0, now.inputTokens - base.inputTokens),
67
+ outputTokens: Math.max(0, now.outputTokens - base.outputTokens),
68
+ ...(contextTokens === undefined ? {} : { contextTokens }),
69
+ };
70
+ }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { Tool } from '../types/effectTool.js';
3
- import type { HarnessStreamPart, TurnHandle } from '../types/stream.js';
3
+ import type { StreamPart, TurnHandle } from '../types/stream.js';
4
4
  /**
5
5
  * Deferred-work scheduling for proactive (agent-initiated) turns.
6
6
  *
@@ -51,7 +51,7 @@ export interface WakeDelivery {
51
51
  reason: string;
52
52
  payload?: Record<string, unknown>;
53
53
  /** Full stream of the wake turn (text, tool events, interactive parts…). */
54
- parts: HarnessStreamPart[];
54
+ parts: StreamPart[];
55
55
  /** Concatenated assistant text of the wake turn. */
56
56
  text: string;
57
57
  }
@@ -60,7 +60,7 @@ export function createWakeJobRunner(runtime, opts) {
60
60
  for await (const part of handle.events) {
61
61
  parts.push(part);
62
62
  if (part.type === 'text-delta') {
63
- text += part.delta;
63
+ text += part.payload.delta;
64
64
  }
65
65
  }
66
66
  const result = await handle;
@@ -11,9 +11,8 @@ export class TracingService {
11
11
  }
12
12
  startSpan(name, attributes, parentSpanId) {
13
13
  if (!this.config) {
14
- // Fallback or throw? Helpers threw.
15
- // We will throw to maintain behavior, but maybe warn in future.
16
- throw new Error('Tracing not initialized. Call initTracing() first.');
14
+ throw new Error('TracingService not initialized. Construct it with a config — ' +
15
+ 'new TracingService(config) or call .init(config) before startSpan().');
17
16
  }
18
17
  const span = {
19
18
  id: this.generateSpanId(),
@@ -1,5 +1,11 @@
1
1
  import type { Session } from '../types/index.js';
2
2
  import type { AuditListOptions, ConversationAuditEntry } from '../audit/types.js';
3
+ export declare class StaleWriteError extends Error {
4
+ readonly sessionId: string;
5
+ readonly expectedVersion: number;
6
+ readonly actualVersion: number;
7
+ constructor(sessionId: string, expectedVersion: number, actualVersion: number);
8
+ }
3
9
  export interface SessionListWindow {
4
10
  from?: Date;
5
11
  to?: Date;
@@ -1 +1,12 @@
1
- export {};
1
+ export class StaleWriteError extends Error {
2
+ sessionId;
3
+ expectedVersion;
4
+ actualVersion;
5
+ constructor(sessionId, expectedVersion, actualVersion) {
6
+ super(`Stale write for session ${sessionId}: expected version ${expectedVersion}, stored version is ${actualVersion}`);
7
+ this.name = 'StaleWriteError';
8
+ this.sessionId = sessionId;
9
+ this.expectedVersion = expectedVersion;
10
+ this.actualVersion = actualVersion;
11
+ }
12
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Session } from '../../types/index.js';
2
2
  import type { AuditListOptions, ConversationAuditEntry } from '../../audit/types.js';
3
- import type { SessionListWindow, SessionStore } from '../SessionStore.js';
3
+ import { type SessionListWindow, type SessionStore } from '../SessionStore.js';
4
4
  export declare class MemoryStore implements SessionStore {
5
5
  private sessions;
6
6
  get(id: string): Promise<Session | null>;
@@ -1,3 +1,4 @@
1
+ import { StaleWriteError } from '../SessionStore.js';
1
2
  export class MemoryStore {
2
3
  sessions = new Map();
3
4
  async get(id) {
@@ -5,8 +6,21 @@ export class MemoryStore {
5
6
  return session ? safeClone(session) : null;
6
7
  }
7
8
  async save(session) {
8
- session.updatedAt = new Date();
9
- this.sessions.set(session.id, safeClone(session));
9
+ const existing = this.sessions.get(session.id);
10
+ const expected = session.version ?? 0;
11
+ if (existing !== undefined) {
12
+ const stored = existing.version ?? 0;
13
+ if (stored !== expected) {
14
+ throw new StaleWriteError(session.id, expected, stored);
15
+ }
16
+ }
17
+ else if (expected !== 0) {
18
+ throw new StaleWriteError(session.id, expected, 0);
19
+ }
20
+ const toSave = safeClone(session);
21
+ toSave.updatedAt = new Date();
22
+ toSave.version = expected + 1;
23
+ this.sessions.set(session.id, toSave);
10
24
  }
11
25
  async delete(id) {
12
26
  this.sessions.delete(id);
@@ -1,7 +1,12 @@
1
- import type { SessionStore } from './SessionStore.js';
1
+ import { type SessionStore } from './SessionStore.js';
2
2
  export type SessionStoreFactory = () => SessionStore | Promise<SessionStore>;
3
3
  /**
4
4
  * Registers the shared SessionStore contract tests. Must be invoked at the
5
5
  * top level of a bun test file.
6
6
  */
7
7
  export declare function runSessionStoreContract(factory: SessionStoreFactory): void;
8
+ /**
9
+ * Registers optimistic-concurrency (CAS) contract tests for SessionStore adapters.
10
+ * Two concurrent saves with the same expected version: exactly one succeeds.
11
+ */
12
+ export declare function runSessionStoreCasContract(factory: SessionStoreFactory): void;