@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
@@ -1,7 +1,4 @@
1
1
  export type { ChannelDriver } from '../../types/channel.js';
2
2
  export { TextDriver, buildNodePrompt } from './TextDriver.js';
3
3
  export type { TextDriverConfig } from './TextDriver.js';
4
- export { VoiceDriver } from './VoiceDriver.js';
5
- export type { VoiceDriverConfig } from './VoiceDriver.js';
6
- export { resolveVoiceGeminiTools, v2ToolsToGemini } from './voiceTools.js';
7
4
  export { setPendingUserInput, consumePendingUserInput, consumeAllPendingUserInput, peekPendingUserInput, hasPendingUserInput, } from './inputBuffer.js';
@@ -1,8 +1,2 @@
1
1
  export { TextDriver, buildNodePrompt } from './TextDriver.js';
2
- // PAUSED: the realtime VoiceDriver is not on the primary (text) path and is not
3
- // re-exported from the package's headline API. It stays here for the realtime
4
- // stack (`@kuralle-agents/realtime-audio`) via the `/runtime` subpath. Text is
5
- // the primary primitive; cascaded voice runs over text (see livekit-plugin).
6
- export { VoiceDriver } from './VoiceDriver.js';
7
- export { resolveVoiceGeminiTools, v2ToolsToGemini } from './voiceTools.js';
8
2
  export { setPendingUserInput, consumePendingUserInput, consumeAllPendingUserInput, peekPendingUserInput, hasPendingUserInput, } from './inputBuffer.js';
@@ -7,3 +7,4 @@ export declare function consumePendingUserInput(session: Session): UserInputCont
7
7
  export declare function consumeAllPendingUserInput(session: Session): UserInputContent | undefined;
8
8
  export declare function peekPendingUserInput(session: Session): UserInputContent | undefined;
9
9
  export declare function hasPendingUserInput(session: Session): boolean;
10
+ export declare function syncPendingUserInput(source: Session, target: Session): void;
@@ -33,3 +33,12 @@ export function peekPendingUserInput(session) {
33
33
  export function hasPendingUserInput(session) {
34
34
  return queue(session).length > 0;
35
35
  }
36
+ export function syncPendingUserInput(source, target) {
37
+ const pending = queue(source);
38
+ if (pending.length > 0) {
39
+ target.workingMemory[PENDING_INPUT_KEY] = [...pending];
40
+ }
41
+ else {
42
+ delete target.workingMemory[PENDING_INPUT_KEY];
43
+ }
44
+ }
@@ -72,7 +72,11 @@ export async function speakWithHostControl(args) {
72
72
  toolControl = getToolControl();
73
73
  if (toolControl) {
74
74
  if (spoken.text) {
75
- ctx.emit({ type: 'text-cancel', id: turnId, reason: 'host-control' });
75
+ ctx.emit({
76
+ channel: 'client',
77
+ type: 'text-cancel',
78
+ payload: { id: turnId, reason: 'host-control' },
79
+ });
76
80
  }
77
81
  return { text: '', control: toolControl };
78
82
  }
@@ -1,16 +1,16 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { SentenceAggregator } from './SentenceAggregator.js';
3
3
  function emitMessage(ctx, id, text) {
4
- ctx.emit({ type: 'text-start', id });
5
- ctx.emit({ type: 'text-delta', id, delta: text });
6
- ctx.emit({ type: 'text-end', id });
4
+ ctx.emit({ channel: 'client', type: 'text-start', payload: { id } });
5
+ ctx.emit({ channel: 'client', type: 'text-delta', payload: { id, delta: text } });
6
+ ctx.emit({ channel: 'client', type: 'text-end', payload: { id } });
7
7
  }
8
8
  async function emitBlockedSafeMessage(ctx, turnId, started, outcome) {
9
9
  if (started) {
10
10
  ctx.emit({
11
+ channel: 'client',
11
12
  type: 'text-cancel',
12
- id: turnId,
13
- reason: outcome.reason ?? 'blocked',
13
+ payload: { id: turnId, reason: outcome.reason ?? 'blocked' },
14
14
  });
15
15
  }
16
16
  const safeId = randomUUID();
@@ -32,7 +32,7 @@ export async function speakGated(args) {
32
32
  }
33
33
  catch (err) {
34
34
  const message = err instanceof Error ? err.message : String(err);
35
- ctx.emit({ type: 'error', error: message });
35
+ ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
36
36
  throw err instanceof Error ? err : new Error(message);
37
37
  }
38
38
  const decision = await runGate(full, true);
@@ -48,13 +48,17 @@ export async function speakGated(args) {
48
48
  let emitted = '';
49
49
  const openOnce = () => {
50
50
  if (!started) {
51
- ctx.emit({ type: 'text-start', id: turnId });
51
+ ctx.emit({ channel: 'client', type: 'text-start', payload: { id: turnId } });
52
52
  started = true;
53
53
  }
54
54
  };
55
55
  const emitCleared = (chunk) => {
56
56
  openOnce();
57
- ctx.emit({ type: 'text-delta', id: turnId, delta: chunk });
57
+ ctx.emit({
58
+ channel: 'client',
59
+ type: 'text-delta',
60
+ payload: { id: turnId, delta: chunk },
61
+ });
58
62
  emitted += chunk;
59
63
  };
60
64
  const gateSentence = async (sentence, final) => {
@@ -69,7 +73,11 @@ export async function speakGated(args) {
69
73
  for await (const { delta } of source) {
70
74
  if (mode === 'token') {
71
75
  openOnce();
72
- ctx.emit({ type: 'text-delta', id: turnId, delta });
76
+ ctx.emit({
77
+ channel: 'client',
78
+ type: 'text-delta',
79
+ payload: { id: turnId, delta },
80
+ });
73
81
  emitted += delta;
74
82
  continue;
75
83
  }
@@ -88,15 +96,19 @@ export async function speakGated(args) {
88
96
  }
89
97
  }
90
98
  if (started) {
91
- ctx.emit({ type: 'text-end', id: turnId });
99
+ ctx.emit({ channel: 'client', type: 'text-end', payload: { id: turnId } });
92
100
  }
93
101
  return { text: emitted };
94
102
  }
95
103
  catch (err) {
96
104
  const message = err instanceof Error ? err.message : String(err);
97
- ctx.emit({ type: 'error', error: message });
105
+ ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
98
106
  if (started) {
99
- ctx.emit({ type: 'text-cancel', id: turnId, reason: message });
107
+ ctx.emit({
108
+ channel: 'client',
109
+ type: 'text-cancel',
110
+ payload: { id: turnId, reason: message },
111
+ });
100
112
  }
101
113
  throw err instanceof Error ? err : new Error(message);
102
114
  }
@@ -1,15 +1,15 @@
1
- import type { AgentKnowledgeOverrides, HarnessStreamPart, KnowledgeRetrievalResult, RetrievalCacheAdapter, SourceRef } from '../../types/index.js';
1
+ import type { AgentKnowledgeOverrides, StreamPart, KnowledgeRetrievalResult, RetrievalCacheAdapter, SourceRef } from '../../types/index.js';
2
2
  export interface CitationRetrievalProvider {
3
3
  retrieve(query: string, cache: RetrievalCacheAdapter | undefined, agentOverrides?: AgentKnowledgeOverrides, isVoice?: boolean): Promise<{
4
4
  results: KnowledgeRetrievalResult[];
5
5
  citations?: SourceRef[];
6
- events: HarnessStreamPart[];
6
+ events: StreamPart[];
7
7
  }>;
8
8
  }
9
9
  export interface CitationRetrievalResult {
10
10
  results: KnowledgeRetrievalResult[];
11
11
  citations: SourceRef[];
12
- events: HarnessStreamPart[];
12
+ events: StreamPart[];
13
13
  }
14
14
  export declare function retrieveWithCitations(provider: CitationRetrievalProvider, query: string, cache: RetrievalCacheAdapter | undefined, agentOverrides?: AgentKnowledgeOverrides, isVoice?: boolean): Promise<CitationRetrievalResult>;
15
15
  export declare function normalizeCitations(results: KnowledgeRetrievalResult[], nativeCitations?: readonly SourceRef[]): SourceRef[];
@@ -1,4 +1,7 @@
1
1
  import { isTerminalOutcome, markSessionOutcome } from './outcomeMarking.js';
2
+ import { mutateSessionWithRetry } from '../session/utils.js';
3
+ import { syncPendingUserInput } from './channels/inputBuffer.js';
4
+ import { runHookSafely } from './runHookSafely.js';
2
5
  export async function closeRun(options) {
3
6
  const { session, runState, runStore, sessionStore, hooks, ctx } = options;
4
7
  runState.updatedAt = Date.now();
@@ -14,17 +17,18 @@ export async function closeRun(options) {
14
17
  outcomeRecord = await markSessionOutcome(sessionStore, session, options.terminalOutcome, { reason: options.outcomeReason, markedBy: 'hook' }, ctx.emit);
15
18
  }
16
19
  if (outcomeRecord && isTerminalOutcome(outcomeRecord.outcome)) {
17
- await hooks?.onConversationEnd?.({
20
+ await runHookSafely('onConversationEnd', () => hooks?.onConversationEnd?.({
18
21
  session,
19
22
  outcome: outcomeRecord,
20
- });
23
+ }));
21
24
  }
22
- const latest = (await sessionStore.get(session.id)) ?? session;
23
- latest.currentAgent = runState.activeAgentId;
24
- latest.activeAgentId = runState.activeAgentId;
25
- // Sync the session-level message mirror to the canonical run record — it
26
- // otherwise lacks assistant turns and keeps pre-guardrail (unredacted)
27
- // user input written at openRun.
28
- latest.messages = [...runState.messages];
29
- await sessionStore.save(latest);
25
+ await mutateSessionWithRetry(sessionStore, session.id, (latest) => {
26
+ latest.currentAgent = runState.activeAgentId;
27
+ latest.activeAgentId = runState.activeAgentId;
28
+ // Sync the session-level message mirror to the canonical run record — it
29
+ // otherwise lacks assistant turns and keeps pre-guardrail (unredacted)
30
+ // user input written at openRun.
31
+ latest.messages = [...runState.messages];
32
+ syncPendingUserInput(ctx.session, latest);
33
+ });
30
34
  }
@@ -42,6 +42,8 @@ export interface CompactMessagesOptions {
42
42
  /** Skip the threshold check (overflow recovery). */
43
43
  force?: boolean;
44
44
  abortSignal?: AbortSignal;
45
+ /** Real last-turn prompt tokens when available (replaces chars/4 for threshold). */
46
+ lastPromptTokens?: number;
45
47
  }
46
48
  /**
47
49
  * Pure compaction step: returns a new message list when compaction applied.
@@ -64,10 +64,11 @@ function truncate(text, max) {
64
64
  * pairs are never split across the summary boundary.
65
65
  */
66
66
  export async function compactMessages(options) {
67
- const { messages, model, config, force, abortSignal } = options;
67
+ const { messages, model, config, force, abortSignal, lastPromptTokens } = options;
68
68
  const triggerTokens = config.triggerTokens ?? DEFAULT_COMPACTION_TRIGGER_TOKENS;
69
69
  const keepRecent = config.keepRecentMessages ?? DEFAULT_COMPACTION_KEEP_RECENT;
70
- const beforeTokens = estimateMessagesTokens(messages);
70
+ const estimatedTokens = estimateMessagesTokens(messages);
71
+ const beforeTokens = lastPromptTokens ?? estimatedTokens;
71
72
  if (!force && beforeTokens < triggerTokens) {
72
73
  return { compacted: false, reason: 'under-threshold', beforeTokens };
73
74
  }
@@ -0,0 +1,24 @@
1
+ import { ToolApprovalDeniedError } from '../tools/effect/errors.js';
2
+ /**
3
+ * The run is not finished: stop the turn, keep the journal, resume when a signal arrives.
4
+ * It unwinds the stack like an error but is not a failure, so it must never be reported to
5
+ * the model as a tool error, shown to the user as an error, or journaled as one.
6
+ *
7
+ * Only `SuspendError` qualifies. An approval *denial* is deliberately not a signal — see
8
+ * `isApprovalDenial`.
9
+ */
10
+ export declare function isControlFlowSignal(error: unknown): boolean;
11
+ /**
12
+ * A human answered "no". Neither a failure nor a suspend — the opposite of a suspend, in
13
+ * fact: a suspend defers the decision, a denial resolves it.
14
+ *
15
+ * Two rules follow, and they differ by who is driving the call:
16
+ *
17
+ * - It must never be degraded into "something went wrong on my side". Nothing went wrong;
18
+ * saying so would be a lie to the user.
19
+ * - On the **model** path it becomes a tool result, because there is no author code to
20
+ * catch it and the agent needs to be able to tell the user the request was declined.
21
+ * On the **flow** path it keeps propagating, because an `action` node's author chose to
22
+ * call the tool and can catch it or branch on `ctx.approve()` instead.
23
+ */
24
+ export declare function isApprovalDenial(error: unknown): error is ToolApprovalDeniedError;
@@ -0,0 +1,29 @@
1
+ import { SuspendError } from './durable/RunStore.js';
2
+ import { ToolApprovalDeniedError } from '../tools/effect/errors.js';
3
+ /**
4
+ * The run is not finished: stop the turn, keep the journal, resume when a signal arrives.
5
+ * It unwinds the stack like an error but is not a failure, so it must never be reported to
6
+ * the model as a tool error, shown to the user as an error, or journaled as one.
7
+ *
8
+ * Only `SuspendError` qualifies. An approval *denial* is deliberately not a signal — see
9
+ * `isApprovalDenial`.
10
+ */
11
+ export function isControlFlowSignal(error) {
12
+ return error instanceof SuspendError;
13
+ }
14
+ /**
15
+ * A human answered "no". Neither a failure nor a suspend — the opposite of a suspend, in
16
+ * fact: a suspend defers the decision, a denial resolves it.
17
+ *
18
+ * Two rules follow, and they differ by who is driving the call:
19
+ *
20
+ * - It must never be degraded into "something went wrong on my side". Nothing went wrong;
21
+ * saying so would be a lie to the user.
22
+ * - On the **model** path it becomes a tool result, because there is no author code to
23
+ * catch it and the agent needs to be able to tell the user the request was declined.
24
+ * On the **flow** path it keeps propagating, because an `action` node's author chose to
25
+ * call the tool and can catch it or branch on `ctx.approve()` instead.
26
+ */
27
+ export function isApprovalDenial(error) {
28
+ return error instanceof ToolApprovalDeniedError;
29
+ }
@@ -1,7 +1,7 @@
1
1
  import type { LanguageModel } from 'ai';
2
2
  import type { Session } from '../types/session.js';
3
- import type { EffectToolExecutor, HookRunner, MemoryService, AutoRetrieveProvider, RunContext } from '../types/run-context.js';
4
- import type { HarnessStreamPart } from '../types/stream.js';
3
+ import type { EffectToolExecutor, MemoryService, AutoRetrieveProvider, RunContext } from '../types/run-context.js';
4
+ import type { StreamPart } from '../types/stream.js';
5
5
  import type { RefinementCapability } from '../capabilities/RefinementCapability.js';
6
6
  import type { ValidationCapability } from '../capabilities/ValidationCapability.js';
7
7
  import type { InputProcessor, OutputProcessor } from '../types/processors.js';
@@ -19,7 +19,6 @@ export interface CtxDeps {
19
19
  runStore: RunStore;
20
20
  steps: StepRecord[];
21
21
  toolExecutor: EffectToolExecutor;
22
- hookRunner?: HookRunner;
23
22
  model: LanguageModel;
24
23
  controlModel?: LanguageModel;
25
24
  outOfBandControl?: boolean;
@@ -34,7 +33,7 @@ export interface CtxDeps {
34
33
  bargeIn?: AbortSignal;
35
34
  abortSignal?: AbortSignal;
36
35
  clock?: EffectClock;
37
- emit?: (part: HarnessStreamPart) => void;
36
+ emit?: (part: StreamPart) => void;
38
37
  }
39
38
  export declare function createRunContext(deps: CtxDeps): Promise<RunContext>;
40
39
  export {};
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { SuspendError } from './durable/RunStore.js';
3
- import { clockEffectKey, pauseEffectKey, toolEffectKey, } from './durable/idempotency.js';
3
+ import { clockEffectKey, idempotencyKey, logicalRunId, pauseEffectKey, } from './durable/idempotency.js';
4
4
  import { findStepByKey } from './durable/replay.js';
5
5
  import { ToolApprovalDeniedError } from '../tools/effect/errors.js';
6
6
  const APPROVAL_SIGNAL = '__approval';
@@ -12,58 +12,149 @@ function makeCtx(deps) {
12
12
  uuid: () => randomUUID(),
13
13
  };
14
14
  const emit = deps.emit ?? (() => { });
15
+ // Mutable holder: handoff reassigns runCtx.toolExecutor; ctx.tool must see the swap.
16
+ const toolExecutorHolder = { executor: deps.toolExecutor };
17
+ let pendingAppendTail = Promise.resolve();
18
+ const serializePendingAppend = (operation) => {
19
+ const previous = pendingAppendTail;
20
+ let release;
21
+ pendingAppendTail = new Promise((resolve) => {
22
+ release = resolve;
23
+ });
24
+ return previous.then(async () => {
25
+ try {
26
+ return await operation();
27
+ }
28
+ finally {
29
+ release();
30
+ }
31
+ });
32
+ };
15
33
  const consumeCallsite = () => {
16
34
  const site = String(effectOrdinal);
17
35
  effectOrdinal += 1;
18
36
  return site;
19
37
  };
20
- const appendLiveStep = async (key, kind, name, result, signalId) => {
38
+ const isFinishedStep = (hit) => {
39
+ if (hit.status === 'finished') {
40
+ return true;
41
+ }
42
+ if (hit.status === 'running') {
43
+ return false;
44
+ }
45
+ if (hit.status === 'error' || hit.error) {
46
+ return false;
47
+ }
48
+ return hit.result !== undefined;
49
+ };
50
+ const updateLocalStep = (key, patch) => {
51
+ const idx = steps.findIndex((step) => step.key === key);
52
+ if (idx >= 0) {
53
+ steps[idx] = { ...steps[idx], ...patch };
54
+ }
55
+ };
56
+ const appendPendingStep = async (key, kind, name, index, signalId) => {
21
57
  const startedAt = Date.now();
22
58
  const record = {
23
- index: steps.length,
59
+ index,
24
60
  key,
25
61
  kind,
26
62
  name,
27
63
  signalId,
28
- result,
64
+ status: 'running',
29
65
  startedAt,
30
- finishedAt: startedAt,
66
+ epoch: deps.runState.runEpoch ?? 0,
31
67
  };
32
68
  await deps.runStore.appendStep(deps.runState.runId, record);
33
- steps.push(record);
34
- deps.runState.updatedAt = startedAt;
35
- await deps.runStore.putRunState(deps.runState);
69
+ const localIdx = steps.findIndex((step) => step.index === index);
70
+ if (localIdx >= 0) {
71
+ steps[localIdx] = record;
72
+ }
73
+ else {
74
+ while (steps.length < index) {
75
+ steps.push({
76
+ index: steps.length,
77
+ key: `__reserve:${deps.runState.runId}:${steps.length}`,
78
+ kind: 'tool',
79
+ name: '__reserve',
80
+ status: 'running',
81
+ startedAt,
82
+ epoch: deps.runState.runEpoch ?? 0,
83
+ });
84
+ }
85
+ if (steps.length === index) {
86
+ steps.push(record);
87
+ }
88
+ else {
89
+ steps[index] = record;
90
+ }
91
+ }
36
92
  };
37
- const replayOrExecute = async (key, kind, name, execute) => {
93
+ const replayOrExecute = async (key, kind, name, execute, options) => {
38
94
  const hit = findStepByKey(steps, key);
39
95
  if (hit) {
40
- if (hit.error) {
96
+ if (hit.status === 'error' || hit.error) {
41
97
  throw Object.assign(new Error(hit.error.message), { name: hit.error.name });
42
98
  }
43
- return hit.result;
99
+ if (isFinishedStep(hit)) {
100
+ return hit.result;
101
+ }
44
102
  }
103
+ const isRetry = hit?.status === 'running';
104
+ const stepIndex = options?.index ?? hit?.index;
105
+ if (!isRetry) {
106
+ const append = async () => {
107
+ let index = stepIndex;
108
+ if (index === undefined) {
109
+ if (deps.runStore.reserveSteps) {
110
+ index = (await deps.runStore.reserveSteps(deps.runState.runId, 1))[0];
111
+ }
112
+ else {
113
+ // ctx.tool owns ordinal assignment: use the store's atomic reservation when available,
114
+ // and serialize legacy stores that do not expose reserveSteps within this context.
115
+ index = steps.length;
116
+ }
117
+ }
118
+ await appendPendingStep(key, kind, name, index);
119
+ };
120
+ if (stepIndex === undefined && !deps.runStore.reserveSteps) {
121
+ await serializePendingAppend(append);
122
+ }
123
+ else {
124
+ await append();
125
+ }
126
+ }
127
+ let result;
45
128
  try {
46
- const result = await execute();
47
- await appendLiveStep(key, kind, name, result);
48
- return result;
129
+ result = await execute();
49
130
  }
50
131
  catch (error) {
51
132
  const err = error instanceof Error ? error : new Error(String(error));
52
- const startedAt = Date.now();
53
- const record = {
54
- index: steps.length,
55
- key,
56
- kind,
57
- name,
133
+ const finishedAt = Date.now();
134
+ await deps.runStore.finalizeStep(deps.runState.runId, key, {
135
+ status: 'error',
58
136
  error: { name: err.name, message: err.message },
59
- startedAt,
60
- finishedAt: startedAt,
61
- };
62
- await deps.runStore.appendStep(deps.runState.runId, record);
63
- steps.push(record);
137
+ finishedAt,
138
+ });
139
+ updateLocalStep(key, {
140
+ status: 'error',
141
+ error: { name: err.name, message: err.message },
142
+ finishedAt,
143
+ });
64
144
  throw err;
65
145
  }
146
+ const finishedAt = Date.now();
147
+ await deps.runStore.finalizeStep(deps.runState.runId, key, {
148
+ status: 'finished',
149
+ result,
150
+ finishedAt,
151
+ });
152
+ updateLocalStep(key, { status: 'finished', result, finishedAt });
153
+ deps.runState.updatedAt = finishedAt;
154
+ await deps.runStore.putRunState(deps.runState);
155
+ return result;
66
156
  };
157
+ const effectRunId = () => logicalRunId(deps.runState.runId, deps.runState.runEpoch);
67
158
  const suspendForSignal = async (signalName, callsite, meta) => {
68
159
  deps.runState.waitingFor = {
69
160
  signalName,
@@ -75,18 +166,20 @@ function makeCtx(deps) {
75
166
  deps.runState.status = 'paused';
76
167
  deps.runState.updatedAt = Date.now();
77
168
  await deps.runStore.putRunState(deps.runState);
78
- emit({ type: 'paused', waitingFor: signalName });
169
+ emit({ channel: 'internal', type: 'paused', payload: { waitingFor: signalName } });
79
170
  throw new SuspendError(signalName);
80
171
  };
81
172
  const pauseEffect = async (signalName, meta) => {
82
173
  const callsite = consumeCallsite();
83
- const key = pauseEffectKey(deps.runState.runId, callsite, signalName);
174
+ const key = pauseEffectKey(effectRunId(), callsite, signalName);
84
175
  const hit = findStepByKey(steps, key);
85
176
  if (hit) {
86
- if (hit.error) {
177
+ if (hit.status === 'error' || hit.error) {
87
178
  throw Object.assign(new Error(hit.error.message), { name: hit.error.name });
88
179
  }
89
- return hit.result;
180
+ if (hit.status === 'finished' || hit.result !== undefined) {
181
+ return hit.result;
182
+ }
90
183
  }
91
184
  await suspendForSignal(signalName, callsite, meta);
92
185
  throw new Error('unreachable');
@@ -96,8 +189,12 @@ function makeCtx(deps) {
96
189
  runState: deps.runState,
97
190
  runStore: deps.runStore,
98
191
  emit,
99
- toolExecutor: deps.toolExecutor,
100
- hookRunner: deps.hookRunner ?? {},
192
+ get toolExecutor() {
193
+ return toolExecutorHolder.executor;
194
+ },
195
+ set toolExecutor(executor) {
196
+ toolExecutorHolder.executor = executor;
197
+ },
101
198
  model: deps.model,
102
199
  controlModel: deps.controlModel ?? deps.model,
103
200
  outOfBandControl: deps.outOfBandControl ?? false,
@@ -121,6 +218,13 @@ function makeCtx(deps) {
121
218
  resetCallsites: () => {
122
219
  effectOrdinal = 0;
123
220
  },
221
+ reserveCallsites: (count) => {
222
+ const sites = [];
223
+ for (let i = 0; i < count; i++) {
224
+ sites.push(consumeCallsite());
225
+ }
226
+ return sites;
227
+ },
124
228
  tool: async (name, args, options) => {
125
229
  // needsApproval gate: a tool flagged `needsApproval` must be approved by a human
126
230
  // before it runs. Approval is a durable pause (the `__approval` signal); on resume
@@ -129,7 +233,7 @@ function makeCtx(deps) {
129
233
  // ordering is deterministic across replays. NOTE: the surrounding agent turn is not
130
234
  // itself a replayable effect — this is fully deterministic for flow `action` tools;
131
235
  // for model-issued tool calls, resume re-enters the agent turn.
132
- const def = options?.def ?? deps.toolExecutor.getTool?.(name);
236
+ const def = options?.def ?? toolExecutorHolder.executor.getTool?.(name);
133
237
  if (def?.needsApproval) {
134
238
  const decision = (await pauseEffect(APPROVAL_SIGNAL, {
135
239
  approval: { title: `Approve tool: ${name}` },
@@ -138,9 +242,12 @@ function makeCtx(deps) {
138
242
  throw new ToolApprovalDeniedError(name, decision.by);
139
243
  }
140
244
  }
141
- const callsite = consumeCallsite();
142
- const key = toolEffectKey(deps.runState.runId, callsite, name, args);
143
- const executeTool = () => deps.toolExecutor.execute({
245
+ const callsite = options?.callsite ?? consumeCallsite();
246
+ const logicalId = effectRunId();
247
+ const key = def?.idempotencyKey != null
248
+ ? def.idempotencyKey(args)
249
+ : idempotencyKey(logicalId, callsite, { name, args });
250
+ const executeTool = () => toolExecutorHolder.executor.execute({
144
251
  name,
145
252
  args,
146
253
  session: deps.session,
@@ -150,30 +257,10 @@ function makeCtx(deps) {
150
257
  toolCtx: options?.toolCtx,
151
258
  });
152
259
  if (def?.replay === false) {
153
- const auditKey = `${key}:${steps.length}`;
154
- try {
155
- const result = await executeTool();
156
- await appendLiveStep(auditKey, 'tool', name, result);
157
- return result;
158
- }
159
- catch (error) {
160
- const err = error instanceof Error ? error : new Error(String(error));
161
- const startedAt = Date.now();
162
- const record = {
163
- index: steps.length,
164
- key: auditKey,
165
- kind: 'tool',
166
- name,
167
- error: { name: err.name, message: err.message },
168
- startedAt,
169
- finishedAt: startedAt,
170
- };
171
- await deps.runStore.appendStep(deps.runState.runId, record);
172
- steps.push(record);
173
- throw err;
174
- }
260
+ const auditKey = `${key}:${steps.length}:${options?.index ?? callsite}`;
261
+ return replayOrExecute(auditKey, 'tool', name, executeTool, { index: options?.index });
175
262
  }
176
- return replayOrExecute(key, 'tool', name, executeTool);
263
+ return replayOrExecute(key, 'tool', name, executeTool, { index: options?.index });
177
264
  },
178
265
  approve: async (req) => {
179
266
  return pauseEffect(APPROVAL_SIGNAL, { approval: req });
@@ -186,12 +273,12 @@ function makeCtx(deps) {
186
273
  },
187
274
  now: async () => {
188
275
  const callsite = consumeCallsite();
189
- const key = clockEffectKey(deps.runState.runId, callsite, 'now');
276
+ const key = clockEffectKey(effectRunId(), callsite, 'now');
190
277
  return replayOrExecute(key, 'now', 'now', async () => clock.now());
191
278
  },
192
279
  uuid: async () => {
193
280
  const callsite = consumeCallsite();
194
- const key = clockEffectKey(deps.runState.runId, callsite, 'uuid');
281
+ const key = clockEffectKey(effectRunId(), callsite, 'uuid');
195
282
  return replayOrExecute(key, 'uuid', 'uuid', async () => clock.uuid());
196
283
  },
197
284
  };
@@ -12,9 +12,25 @@ export declare class SuspendError extends Error {
12
12
  export declare class RunNotFoundError extends Error {
13
13
  constructor(runId: string);
14
14
  }
15
+ export declare class StepNotFoundError extends Error {
16
+ constructor(runId: string, key: string);
17
+ }
18
+ export interface StepFinalizePatch {
19
+ status: 'finished' | 'error';
20
+ result?: unknown;
21
+ error?: {
22
+ name: string;
23
+ message: string;
24
+ };
25
+ finishedAt?: number;
26
+ }
15
27
  export interface RunStore {
16
28
  appendStep(runId: string, record: StepRecord): Promise<void>;
29
+ finalizeStep(runId: string, key: string, patch: StepFinalizePatch): Promise<void>;
17
30
  getSteps(runId: string): Promise<StepRecord[]>;
18
31
  getRunState(runId: string): Promise<RunState | null>;
19
32
  putRunState(state: RunState): Promise<void>;
33
+ initRun?(state: RunState): Promise<void>;
34
+ pruneStepsBeforeEpoch?(runId: string, keepEpoch: number): Promise<void>;
35
+ reserveSteps?(runId: string, count: number): Promise<number[]>;
20
36
  }
@@ -3,7 +3,7 @@ export class LogConflictError extends Error {
3
3
  expectedIndex;
4
4
  actualIndex;
5
5
  constructor(runId, expectedIndex, actualIndex) {
6
- super(`Log conflict for run ${runId}: expected append at index ${expectedIndex}, current length is ${actualIndex}`);
6
+ super(`Log conflict for run ${runId}: expected append at index ${expectedIndex}, current length is ${actualIndex}. For parallel durable effects, use ctx.tool directly or reserve callsites with ctx.reserveCallsites(count) before supplying explicit indices.`);
7
7
  this.name = 'LogConflictError';
8
8
  this.runId = runId;
9
9
  this.expectedIndex = expectedIndex;
@@ -24,3 +24,9 @@ export class RunNotFoundError extends Error {
24
24
  this.name = 'RunNotFoundError';
25
25
  }
26
26
  }
27
+ export class StepNotFoundError extends Error {
28
+ constructor(runId, key) {
29
+ super(`Step not found for run ${runId}: ${key}`);
30
+ this.name = 'StepNotFoundError';
31
+ }
32
+ }