@vinhnt-sdk/core 0.1.1 → 0.1.3

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 (179) hide show
  1. package/README.md +194 -12
  2. package/dist/agent/agent-factory.d.ts +11 -0
  3. package/dist/agent/agent-factory.d.ts.map +1 -1
  4. package/dist/agent/agent-factory.js +11 -2
  5. package/dist/agent/agent-factory.js.map +1 -1
  6. package/dist/agent/agent-registry.d.ts +3 -0
  7. package/dist/agent/agent-registry.d.ts.map +1 -1
  8. package/dist/agent/agent-registry.js.map +1 -1
  9. package/dist/agent/create-agent-tool.d.ts +13 -13
  10. package/dist/agent/create-agent-tool.d.ts.map +1 -1
  11. package/dist/agent/delegate-batch-tool.d.ts.map +1 -1
  12. package/dist/agent/delegate-batch-tool.js +1 -1
  13. package/dist/agent/delegate-batch-tool.js.map +1 -1
  14. package/dist/agent/delegate-tool.js +1 -1
  15. package/dist/agent/delegate-tool.js.map +1 -1
  16. package/dist/agent/execution-engine.d.ts +1 -1
  17. package/dist/agent/execution-engine.d.ts.map +1 -1
  18. package/dist/agent/spawn-agent-tool.d.ts +10 -10
  19. package/dist/agent/spawn-agent-tool.d.ts.map +1 -1
  20. package/dist/event-bus/bridge.d.ts +2 -3
  21. package/dist/event-bus/bridge.d.ts.map +1 -1
  22. package/dist/event-bus/bridge.js +12 -5
  23. package/dist/event-bus/bridge.js.map +1 -1
  24. package/dist/event-bus/global-bus.d.ts +8 -0
  25. package/dist/event-bus/global-bus.d.ts.map +1 -1
  26. package/dist/event-bus/global-bus.js +48 -0
  27. package/dist/event-bus/global-bus.js.map +1 -1
  28. package/dist/event-bus/in-memory-bus.d.ts +14 -0
  29. package/dist/event-bus/in-memory-bus.d.ts.map +1 -1
  30. package/dist/event-bus/in-memory-bus.js +53 -0
  31. package/dist/event-bus/in-memory-bus.js.map +1 -1
  32. package/dist/event-bus/types.d.ts +17 -0
  33. package/dist/event-bus/types.d.ts.map +1 -1
  34. package/dist/fakes/fake-approval-store.d.ts +2 -2
  35. package/dist/fakes/fake-approval-store.d.ts.map +1 -1
  36. package/dist/fakes/fake-approval-store.js +28 -6
  37. package/dist/fakes/fake-approval-store.js.map +1 -1
  38. package/dist/fakes/fake-model.d.ts.map +1 -1
  39. package/dist/fakes/fake-model.js +2 -1
  40. package/dist/fakes/fake-model.js.map +1 -1
  41. package/dist/fakes/fake-store.d.ts +4 -1
  42. package/dist/fakes/fake-store.d.ts.map +1 -1
  43. package/dist/fakes/fake-store.js +33 -0
  44. package/dist/fakes/fake-store.js.map +1 -1
  45. package/dist/index.d.ts +232 -109
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +191 -66
  48. package/dist/index.js.map +1 -1
  49. package/dist/kernel/approval-handler.d.ts +1 -1
  50. package/dist/kernel/approval-handler.d.ts.map +1 -1
  51. package/dist/kernel/approval-handler.js +5 -1
  52. package/dist/kernel/approval-handler.js.map +1 -1
  53. package/dist/kernel/circuit-breaker.d.ts +19 -1
  54. package/dist/kernel/circuit-breaker.d.ts.map +1 -1
  55. package/dist/kernel/circuit-breaker.js +54 -10
  56. package/dist/kernel/circuit-breaker.js.map +1 -1
  57. package/dist/kernel/kernel-error.d.ts +3 -2
  58. package/dist/kernel/kernel-error.d.ts.map +1 -1
  59. package/dist/kernel/kernel-error.js +5 -6
  60. package/dist/kernel/kernel-error.js.map +1 -1
  61. package/dist/kernel/kernel-session.d.ts +13 -5
  62. package/dist/kernel/kernel-session.d.ts.map +1 -1
  63. package/dist/kernel/kernel-session.js +71 -13
  64. package/dist/kernel/kernel-session.js.map +1 -1
  65. package/dist/kernel/kernel-types.d.ts +82 -9
  66. package/dist/kernel/kernel-types.d.ts.map +1 -1
  67. package/dist/kernel/kernel-types.js.map +1 -1
  68. package/dist/kernel/kernel.d.ts +94 -20
  69. package/dist/kernel/kernel.d.ts.map +1 -1
  70. package/dist/kernel/kernel.js +591 -70
  71. package/dist/kernel/kernel.js.map +1 -1
  72. package/dist/kernel/model-caller.d.ts +30 -2
  73. package/dist/kernel/model-caller.d.ts.map +1 -1
  74. package/dist/kernel/model-caller.js +55 -20
  75. package/dist/kernel/model-caller.js.map +1 -1
  76. package/dist/kernel/permission-gate.d.ts +6 -4
  77. package/dist/kernel/permission-gate.d.ts.map +1 -1
  78. package/dist/kernel/permission-gate.js +16 -6
  79. package/dist/kernel/permission-gate.js.map +1 -1
  80. package/dist/kernel/run-context.d.ts +26 -0
  81. package/dist/kernel/run-context.d.ts.map +1 -0
  82. package/dist/kernel/run-context.js +13 -0
  83. package/dist/kernel/run-context.js.map +1 -0
  84. package/dist/kernel/run-loop.d.ts +35 -14
  85. package/dist/kernel/run-loop.d.ts.map +1 -1
  86. package/dist/kernel/run-loop.js +295 -87
  87. package/dist/kernel/run-loop.js.map +1 -1
  88. package/dist/kernel/self-correction.d.ts +2 -2
  89. package/dist/kernel/self-correction.d.ts.map +1 -1
  90. package/dist/kernel/self-correction.js +3 -2
  91. package/dist/kernel/self-correction.js.map +1 -1
  92. package/dist/kernel/state-machine.d.ts +2 -0
  93. package/dist/kernel/state-machine.d.ts.map +1 -1
  94. package/dist/kernel/state-machine.js +2 -0
  95. package/dist/kernel/state-machine.js.map +1 -1
  96. package/dist/kernel/step-executor.d.ts +11 -3
  97. package/dist/kernel/step-executor.d.ts.map +1 -1
  98. package/dist/kernel/step-executor.js +54 -24
  99. package/dist/kernel/step-executor.js.map +1 -1
  100. package/dist/kernel/sub-agent-runner.d.ts +10 -7
  101. package/dist/kernel/sub-agent-runner.d.ts.map +1 -1
  102. package/dist/kernel/sub-agent-runner.js +105 -31
  103. package/dist/kernel/sub-agent-runner.js.map +1 -1
  104. package/dist/kernel/tool-context-builder.d.ts +7 -2
  105. package/dist/kernel/tool-context-builder.d.ts.map +1 -1
  106. package/dist/kernel/tool-context-builder.js +23 -6
  107. package/dist/kernel/tool-context-builder.js.map +1 -1
  108. package/dist/kernel/tool-error-router.d.ts +1 -1
  109. package/dist/kernel/tool-error-router.d.ts.map +1 -1
  110. package/dist/kernel/tool-error-router.js +4 -3
  111. package/dist/kernel/tool-error-router.js.map +1 -1
  112. package/dist/logger.d.ts +5 -0
  113. package/dist/logger.d.ts.map +1 -1
  114. package/dist/logger.js +3 -0
  115. package/dist/logger.js.map +1 -1
  116. package/dist/model.d.ts +3 -1
  117. package/dist/model.d.ts.map +1 -1
  118. package/dist/model.js +2 -0
  119. package/dist/model.js.map +1 -1
  120. package/dist/plugin/manager.d.ts +3 -1
  121. package/dist/plugin/manager.d.ts.map +1 -1
  122. package/dist/plugin/manager.js +51 -3
  123. package/dist/plugin/manager.js.map +1 -1
  124. package/dist/plugin/plugin-file-loader.js +2 -2
  125. package/dist/plugin/plugin-file-loader.js.map +1 -1
  126. package/dist/plugin.d.ts +50 -4
  127. package/dist/plugin.d.ts.map +1 -1
  128. package/dist/plugin.js +14 -1
  129. package/dist/plugin.js.map +1 -1
  130. package/dist/session/null-store.d.ts +1 -0
  131. package/dist/session/null-store.d.ts.map +1 -1
  132. package/dist/session/null-store.js +7 -0
  133. package/dist/session/null-store.js.map +1 -1
  134. package/dist/session/run-coordinator.d.ts +9 -3
  135. package/dist/session/run-coordinator.d.ts.map +1 -1
  136. package/dist/session/run-coordinator.js +4 -0
  137. package/dist/session/run-coordinator.js.map +1 -1
  138. package/dist/skill/create-skill-tool.d.ts +5 -4
  139. package/dist/skill/create-skill-tool.d.ts.map +1 -1
  140. package/dist/skill/create-skill-tool.js +8 -5
  141. package/dist/skill/create-skill-tool.js.map +1 -1
  142. package/dist/skill/skill-def-registry.d.ts +6 -0
  143. package/dist/skill/skill-def-registry.d.ts.map +1 -1
  144. package/dist/skill/skill-def-registry.js +4 -0
  145. package/dist/skill/skill-def-registry.js.map +1 -1
  146. package/dist/system-context/sources/instructions-source.js +1 -1
  147. package/dist/system-context/sources/instructions-source.js.map +1 -1
  148. package/dist/tool/bridge.d.ts +7 -1
  149. package/dist/tool/bridge.d.ts.map +1 -1
  150. package/dist/tool/bridge.js +5 -0
  151. package/dist/tool/bridge.js.map +1 -1
  152. package/dist/tool/builtin-provider.d.ts +35 -0
  153. package/dist/tool/builtin-provider.d.ts.map +1 -0
  154. package/dist/tool/builtin-provider.js +75 -0
  155. package/dist/tool/builtin-provider.js.map +1 -0
  156. package/dist/tool/index.d.ts +21 -23
  157. package/dist/tool/index.d.ts.map +1 -1
  158. package/dist/tool/index.js +19 -18
  159. package/dist/tool/index.js.map +1 -1
  160. package/dist/tool/provider-helpers.d.ts.map +1 -1
  161. package/dist/tool/provider-helpers.js +3 -2
  162. package/dist/tool/provider-helpers.js.map +1 -1
  163. package/dist/tool/runtime.d.ts +23 -2
  164. package/dist/tool/runtime.d.ts.map +1 -1
  165. package/dist/tool/runtime.js +20 -16
  166. package/dist/tool/runtime.js.map +1 -1
  167. package/dist/tracer.d.ts +5 -0
  168. package/dist/tracer.d.ts.map +1 -1
  169. package/dist/tracer.js +4 -0
  170. package/dist/tracer.js.map +1 -1
  171. package/dist/workflow/agent-graph.js +3 -3
  172. package/dist/workflow/agent-graph.js.map +1 -1
  173. package/dist/workflow/workflow-helpers.js +2 -2
  174. package/dist/workflow/workflow-helpers.js.map +1 -1
  175. package/dist/workspace.d.ts +7 -0
  176. package/dist/workspace.d.ts.map +1 -1
  177. package/dist/workspace.js +4 -0
  178. package/dist/workspace.js.map +1 -1
  179. package/package.json +18 -62
@@ -1,20 +1,26 @@
1
+ import { ValidationError, AgentNotFoundError } from "@vinhnt-sdk/schema";
2
+ import { restoreRunFromStore, findActiveSessionIds } from "@vinhnt-sdk/session";
3
+ import { createRedactingLogger } from "@vinhnt-sdk/security";
1
4
  import { wildcardMatch } from "@vinhnt-sdk/schema";
2
5
  import { getBehaviourProfile } from "../agent/behaviour-profiles.js";
3
- import { DEFAULT_MAX_STEPS, DEFAULT_MAX_TOOL_CALLS_PER_STEP, DOOM_LOOP_THRESHOLD } from "./kernel-utils.js";
4
- import { RunStateMachine } from "./run-state.js";
5
- import { PermissionGate } from "./permission-gate.js";
6
- import { ModelCaller } from "./model-caller.js";
7
- import { StepExecutor } from "./step-executor.js";
8
- import { ToolSaga } from "./tool-saga.js";
9
- import { CircuitBreaker } from "./circuit-breaker.js";
10
- import { KernelError } from "./kernel-error.js";
6
+ import { DEFAULT_MAX_STEPS, DEFAULT_MAX_TOOL_CALLS_PER_STEP, DOOM_LOOP_THRESHOLD } from "@vinhnt-sdk/step-executor";
7
+ /** Default thinking prompt for reasoning steps. Exported for user override. */
8
+ export const DEFAULT_THINKING_PROMPT = "Analyze the user's request and the conversation context. Think step by step about what needs to be done. Output your reasoning.";
9
+ import { RunStateMachine } from "@vinhnt-sdk/step-executor";
10
+ import { PermissionGate } from "@vinhnt-sdk/step-executor";
11
+ import { ModelCaller } from "@vinhnt-sdk/llm";
12
+ import { StepExecutor } from "@vinhnt-sdk/step-executor";
13
+ import { ToolSaga } from "@vinhnt-sdk/tools";
14
+ import { createRunContext } from "@vinhnt-sdk/step-executor";
15
+ import { CircuitBreaker } from "@vinhnt-sdk/step-executor";
16
+ import { KernelError } from "@vinhnt-sdk/step-executor";
11
17
  import { runLoop as executeRunLoop } from "./run-loop.js";
12
18
  import { LifecycleManager } from "./lifecycle-manager.js";
13
- import { emitEvent as sessionEmit, addSessionMessage as sessionAddMessage, updateSessionOnComplete as sessionUpdateComplete, emitFail as sessionEmitFail, buildAgentIdentity, } from "./kernel-session.js";
19
+ import { emitEvent as sessionEmit, addSessionMessage as sessionAddMessage, emitCompletedWithSession as sessionEmitCompleted, emitFail as sessionEmitFail, buildAgentIdentity, } from "./kernel-session.js";
14
20
  import { runAgent as runSubAgent, runAgentsParallel as runSubAgentsParallel, createSubAgentAndRegister, } from "./sub-agent-runner.js";
15
21
  import { normalizeConfig } from "./kernel-types.js";
16
- export { CircuitBreaker, CircuitBreakerOpenError } from "./circuit-breaker.js";
17
- export { KernelError } from "./kernel-error.js";
22
+ export { CircuitBreaker, CircuitBreakerOpenError } from "@vinhnt-sdk/step-executor";
23
+ export { KernelError } from "@vinhnt-sdk/step-executor";
18
24
  /**
19
25
  * Core agent orchestration — run loop, permission gate, tool execution, sub-agent spawning.
20
26
  * Composition root for all run-time services. Each call to `run()` creates a tracked
@@ -45,6 +51,7 @@ export class AgentKernel {
45
51
  toolProviderRegistry;
46
52
  sessionTitleGenerator;
47
53
  saga;
54
+ runSagas = new Map();
48
55
  stepExecutor;
49
56
  currentAgent;
50
57
  currentDepth = 0;
@@ -59,27 +66,37 @@ export class AgentKernel {
59
66
  sessionDeps;
60
67
  subAgentDeps;
61
68
  runSessionStates = new Map();
69
+ /** runId → sessionId for pending-input persistence (RV-21). */
70
+ runSessionIds = new Map();
71
+ /** FIFO mirror of the state-machine input queue, used to mark promotion (RV-21). */
72
+ runPendingEntries = new Map();
73
+ /** Per-session monotonic admitted-seq allocator, seeded from the store lazily (RV-21). */
74
+ nextAdmittedSeq = new Map();
75
+ /** Per-session serialized write chain so admitted seqs stay FIFO-consistent (RV-21). */
76
+ inputWriteChains = new Map();
77
+ /** Per-run execution context — keeps parallel runs' agent/depth/chain/tools isolated. */
78
+ runContexts = new Map();
62
79
  lifecycleManager;
63
80
  constructor(config) {
64
81
  const normalized = normalizeConfig(config);
65
82
  // Validate config values
66
83
  if (normalized.maxSteps !== undefined && normalized.maxSteps < 1) {
67
- throw new Error("maxSteps must be >= 1");
84
+ throw new ValidationError("maxSteps must be >= 1");
68
85
  }
69
86
  if (normalized.maxTokens !== undefined && normalized.maxTokens < 1) {
70
- throw new Error("maxTokens must be >= 1");
87
+ throw new ValidationError("maxTokens must be >= 1");
71
88
  }
72
89
  if (normalized.stepTimeout !== undefined && normalized.stepTimeout < 1000) {
73
- throw new Error("stepTimeout must be >= 1000ms");
90
+ throw new ValidationError("stepTimeout must be >= 1000ms");
74
91
  }
75
92
  if (normalized.maxToolCallsPerStep !== undefined && normalized.maxToolCallsPerStep < 1) {
76
- throw new Error("maxToolCallsPerStep must be >= 1");
93
+ throw new ValidationError("maxToolCallsPerStep must be >= 1");
77
94
  }
78
95
  if (normalized.doomLoopThreshold !== undefined && normalized.doomLoopThreshold < 1) {
79
- throw new Error("doomLoopThreshold must be >= 1");
96
+ throw new ValidationError("doomLoopThreshold must be >= 1");
80
97
  }
81
98
  if (normalized.compactionThreshold !== undefined && (normalized.compactionThreshold < 0 || normalized.compactionThreshold > 1)) {
82
- throw new Error("compactionThreshold must be between 0 and 1");
99
+ throw new ValidationError("compactionThreshold must be between 0 and 1");
83
100
  }
84
101
  this.store = normalized.store;
85
102
  this.maxSteps = normalized.maxSteps ?? DEFAULT_MAX_STEPS;
@@ -102,15 +119,36 @@ export class AgentKernel {
102
119
  this.doomLoopThreshold = normalized.doomLoopThreshold ?? DOOM_LOOP_THRESHOLD;
103
120
  this.compactionThreshold = normalized.compactionThreshold;
104
121
  this.termination = normalized.termination;
105
- this.circuitBreaker = normalized.circuitBreaker ?? (normalized.circuitBreakerOptions ? new CircuitBreaker(normalized.circuitBreakerOptions) : new CircuitBreaker());
122
+ this.circuitBreaker = normalized.circuitBreaker ?? new CircuitBreaker({
123
+ ...(normalized.maxRetries !== undefined
124
+ ? { maxRetries: normalized.maxRetries }
125
+ : normalized.circuitBreakerOptions?.maxRetries !== undefined
126
+ ? { maxRetries: normalized.circuitBreakerOptions.maxRetries }
127
+ : {}),
128
+ ...(normalized.retryBackoffMs !== undefined
129
+ ? { backoffMs: normalized.retryBackoffMs }
130
+ : normalized.circuitBreakerOptions?.backoffMs !== undefined
131
+ ? { backoffMs: normalized.circuitBreakerOptions.backoffMs }
132
+ : {}),
133
+ ...(normalized.maxRetryBackoffMs !== undefined
134
+ ? { maxBackoffMs: normalized.maxRetryBackoffMs }
135
+ : normalized.circuitBreakerOptions?.maxBackoffMs !== undefined
136
+ ? { maxBackoffMs: normalized.circuitBreakerOptions.maxBackoffMs }
137
+ : {}),
138
+ ...normalized.circuitBreakerOptions,
139
+ });
106
140
  this.saga = new ToolSaga();
107
141
  this.stateMachine = new RunStateMachine();
108
142
  this.permissionGate = new PermissionGate({
109
143
  store: normalized.store,
110
- eventBus: normalized.eventBus,
144
+ ...(normalized.eventBus !== undefined ? { eventBus: normalized.eventBus } : {}),
145
+ // Core's PluginManager structurally satisfies the step-executor hook
146
+ // contract (named generic fireHook — castable, matches by design).
111
147
  pluginManager: normalized.pluginManager,
112
148
  approvalStore: normalized.permissions?.approvalStore,
113
- autoApprovalEnabled: normalized.permissions?.autoApprovalEnabled,
149
+ ...(normalized.permissions?.autoApprovalEnabled !== undefined
150
+ ? { autoApprovalEnabled: normalized.permissions.autoApprovalEnabled }
151
+ : {}),
114
152
  });
115
153
  if (normalized.permissions?.globalPermissionRules) {
116
154
  this.permissionGate.setGlobalRules(normalized.permissions.globalPermissionRules);
@@ -123,18 +161,32 @@ export class AgentKernel {
123
161
  }
124
162
  this.maxTokens = normalized.maxTokens ?? 4096;
125
163
  const maxTokens = this.maxTokens;
126
- const thinkingPrompt = normalized.thinkingPrompt ?? "Analyze the user's request and the conversation context. Think step by step about what needs to be done. Output your reasoning.";
164
+ const thinkingPrompt = normalized.thinkingPrompt ?? DEFAULT_THINKING_PROMPT;
165
+ // P1-N: wire the redacting logger into the kernel so every log line that
166
+ // passes through it (e.g. model-caller diagnostics) is scrubbed of secrets.
167
+ const rawLogger = normalized.logger;
168
+ const redactingLogger = rawLogger
169
+ ? {
170
+ debug: createRedactingLogger(rawLogger.debug.bind(rawLogger)),
171
+ info: createRedactingLogger(rawLogger.info.bind(rawLogger)),
172
+ warn: createRedactingLogger(rawLogger.warn.bind(rawLogger)),
173
+ error: createRedactingLogger(rawLogger.error.bind(rawLogger)),
174
+ }
175
+ : undefined;
127
176
  this.modelCaller = new ModelCaller({
128
177
  defaultModel: normalized.model,
129
178
  modelRegistry: normalized.modelRegistry,
130
179
  maxTokens,
131
180
  thinkingBudget: this.thinkingBudget,
132
181
  thinkingPrompt,
182
+ // Core's PluginManager structurally satisfies the model-caller hook
183
+ // contract (named generic fireHook — castable, matches by design).
133
184
  pluginManager: normalized.pluginManager,
185
+ logger: redactingLogger,
134
186
  emitEvent: (event, persist) => this.emitEvent(event, persist),
135
187
  modelForRun: (runId) => this.stateMachine.getModelForRun(runId),
136
188
  setModelForRun: (runId, model) => this.stateMachine.setModelForRun(runId, model),
137
- getAvailableTools: () => this.getAvailableTools(),
189
+ getAvailableTools: (runId) => this.getAvailableTools(runId),
138
190
  });
139
191
  const self = this;
140
192
  this.stepExecutor = new StepExecutor({
@@ -152,7 +204,9 @@ export class AgentKernel {
152
204
  ...(normalized.workspaceRoot !== undefined ? { workspaceRoot: normalized.workspaceRoot } : {}),
153
205
  currentAgent: this.currentAgent,
154
206
  saga: this.saga,
155
- findTool: (name) => self.findTool(name),
207
+ agentForRun: (runId) => this.runContexts.get(runId)?.agent,
208
+ sagaForRun: (runId) => this.runContexts.get(runId)?.saga ?? this.saga,
209
+ findTool: (name, runId) => self.findTool(name, runId),
156
210
  hasTool: (name) => self.hasTool(name),
157
211
  });
158
212
  if (normalized.tools) {
@@ -160,9 +214,9 @@ export class AgentKernel {
160
214
  }
161
215
  this.sessionDeps = {
162
216
  store: this.store,
163
- eventBus: this.eventBus,
164
- pluginManager: this.pluginManager,
165
- sessionStore: this.sessionStore,
217
+ ...(this.eventBus !== undefined ? { eventBus: this.eventBus } : {}),
218
+ ...(this.pluginManager !== undefined ? { pluginManager: this.pluginManager } : {}),
219
+ ...(this.sessionStore !== undefined ? { sessionStore: this.sessionStore } : {}),
166
220
  modelCaller: this.modelCaller,
167
221
  saga: this.saga,
168
222
  ...(normalized.noStore ? { noStore: true } : {}),
@@ -174,14 +228,46 @@ export class AgentKernel {
174
228
  store: this.store,
175
229
  modelCaller: this.modelCaller,
176
230
  maxSubAgentDepth: this.maxSubAgentDepth,
177
- currentAgentRef: { get value() { return self.currentAgent; }, set value(v) { self.currentAgent = v; } },
178
- currentDepthRef: { get value() { return self.currentDepth; }, set value(v) { self.currentDepth = v; } },
179
- agentChainRef: { get value() { return self.agentChain; }, set value(v) { self.agentChain = v; } },
231
+ currentAgentRef: {
232
+ get value() { return self.activeRunContext()?.agent ?? self.currentAgent; },
233
+ set value(v) {
234
+ const rc = self.activeRunContext();
235
+ if (rc) {
236
+ rc.agent = v;
237
+ rc.cachedTools = null;
238
+ rc.cachedToolsAgentId = undefined;
239
+ }
240
+ else {
241
+ self.currentAgent = v;
242
+ }
243
+ },
244
+ },
245
+ currentDepthRef: {
246
+ get value() { return self.activeRunContext()?.depth ?? self.currentDepth; },
247
+ set value(v) {
248
+ const rc = self.activeRunContext();
249
+ if (rc)
250
+ rc.depth = v;
251
+ else
252
+ self.currentDepth = v;
253
+ },
254
+ },
255
+ agentChainRef: {
256
+ get value() { return self.activeRunContext()?.agentChain ?? self.agentChain; },
257
+ set value(v) {
258
+ const rc = self.activeRunContext();
259
+ if (rc)
260
+ rc.agentChain = v;
261
+ else
262
+ self.agentChain = v;
263
+ },
264
+ },
180
265
  sessionStateRef: {
181
266
  get value() { return self["sessionState"]; },
182
267
  set value(v) { self["sessionState"] = v; },
183
268
  },
184
269
  runFn: (prompt, ctx, sessionId, _agentOverride) => this.run(prompt, ctx, sessionId, undefined, _agentOverride),
270
+ runContextFor: (runId) => this.runContexts.get(runId),
185
271
  };
186
272
  this.lifecycleManager = new LifecycleManager();
187
273
  }
@@ -223,12 +309,16 @@ export class AgentKernel {
223
309
  addSessionMessage(sid, role, content, extra) {
224
310
  return sessionAddMessage(this.sessionDeps, sid, role, content, extra);
225
311
  }
226
- getAvailableTools() {
227
- const agentId = this.currentAgent?.id;
228
- if (this.cachedTools && this.cachedToolsAgentId === agentId) {
229
- return this.cachedTools;
312
+ getAvailableTools(runId) {
313
+ const rc = runId ? this.runContexts.get(runId) : undefined;
314
+ const target = rc?.agent ?? this.currentAgent;
315
+ const agentId = target?.id;
316
+ const cachedTools = rc ? rc.cachedTools : this.cachedTools;
317
+ const cachedToolsAgentId = rc ? rc.cachedToolsAgentId : this.cachedToolsAgentId;
318
+ if (cachedTools && cachedToolsAgentId === agentId) {
319
+ return cachedTools;
230
320
  }
231
- const caps = this.currentAgent?.capabilities?.tools;
321
+ const caps = target?.capabilities?.tools;
232
322
  // Tool source priority: toolProviderRegistry > toolRegistry > tools[]
233
323
  let pool;
234
324
  if (this.toolProviderRegistry) {
@@ -241,7 +331,7 @@ export class AgentKernel {
241
331
  // belongs to plus core tools (tools in no registered domain). Undefined
242
332
  // (legacy default) means no filtering — all tools visible. Per-domain
243
333
  // permission defaults (e.g. an MCP server set to "deny") remove the tool.
244
- const domains = this.currentAgent?.domains;
334
+ const domains = target?.domains;
245
335
  if (domains !== undefined) {
246
336
  pool = pool.filter((t) => {
247
337
  const domain = this.domainFor(t.id);
@@ -262,7 +352,7 @@ export class AgentKernel {
262
352
  pool = pool.filter((t) => caps.some((c) => wildcardMatch(c, t.id)));
263
353
  }
264
354
  // Apply behaviour mode profile (if not "build" mode, profile rules restrict tools)
265
- const behaviourMode = this.currentAgent?.behaviourMode ?? "build";
355
+ const behaviourMode = target?.behaviourMode ?? "build";
266
356
  if (behaviourMode !== "build") {
267
357
  const profile = getBehaviourProfile(behaviourMode);
268
358
  pool = pool.filter((t) => {
@@ -275,15 +365,21 @@ export class AgentKernel {
275
365
  });
276
366
  }
277
367
  const result = pool.filter((t) => {
278
- const perm = this.permissionGate.checkTool(t.id, t.risk, undefined, this.currentAgent);
368
+ const perm = this.permissionGate.checkTool(t.id, t.risk, undefined, target);
279
369
  return perm.allowed || perm.needsApproval;
280
370
  });
281
- this.cachedTools = result;
282
- this.cachedToolsAgentId = agentId;
371
+ if (rc) {
372
+ rc.cachedTools = result;
373
+ rc.cachedToolsAgentId = agentId;
374
+ }
375
+ else {
376
+ this.cachedTools = result;
377
+ this.cachedToolsAgentId = agentId;
378
+ }
283
379
  return result;
284
380
  }
285
- findTool(name) {
286
- return this.getAvailableTools().find((t) => t.id === name);
381
+ findTool(name, runId) {
382
+ return this.getAvailableTools(runId).find((t) => t.id === name);
287
383
  }
288
384
  /** Whether a tool is registered in the underlying pool (regardless of per-agent filtering). */
289
385
  hasTool(name) {
@@ -296,10 +392,10 @@ export class AgentKernel {
296
392
  /** Switch to a named agent from the registry. Syncs permissions. */
297
393
  async useAgent(agentId) {
298
394
  if (!this.agentRegistry)
299
- throw new Error("No agent registry configured");
395
+ throw new KernelError("internal_error", "No agent registry configured");
300
396
  const agent = await this.agentRegistry.get(agentId);
301
397
  if (!agent)
302
- throw new Error(`Agent '${agentId}' not found`);
398
+ throw new AgentNotFoundError(agentId);
303
399
  this.currentAgent = agent;
304
400
  }
305
401
  /** Directly set the active agent config (bypasses registry lookup). */
@@ -310,7 +406,7 @@ export class AgentKernel {
310
406
  /** Create and register a new sub-agent from inline params. */
311
407
  async spawnAgent(params) {
312
408
  if (!this.agentRegistry)
313
- throw new Error("No agent registry configured");
409
+ throw new KernelError("internal_error", "No agent registry configured");
314
410
  const ref = { value: this.currentAgent };
315
411
  return createSubAgentAndRegister({ agentRegistry: this.agentRegistry, currentAgentRef: ref }, params);
316
412
  }
@@ -350,10 +446,17 @@ export class AgentKernel {
350
446
  return this.store;
351
447
  }
352
448
  /** Delegate a prompt to a named sub-agent. Returns the agent's response text. */
353
- async runAgent(agentId, prompt, ctx, sessionId) {
449
+ async runAgent(agentId, prompt, ctx, sessionId, parentRunId, signal) {
354
450
  if (!this.agentRegistry)
355
- throw new Error("No agent registry configured");
356
- return runSubAgent(this.subAgentDeps, agentId, prompt, ctx, sessionId);
451
+ throw new KernelError("internal_error", "No agent registry configured");
452
+ return runSubAgent(this.subAgentDeps, agentId, prompt, ctx, sessionId, parentRunId, signal);
453
+ }
454
+ /** Build the system head (agent identity + configured system prompt) for a run. */
455
+ buildSystemPrompt(agent) {
456
+ const identity = buildAgentIdentity(agent);
457
+ const systemPrompt = agent?.systemPrompt;
458
+ const parts = [identity, systemPrompt].filter(Boolean);
459
+ return parts.length > 0 ? parts.join("\n\n") : undefined;
357
460
  }
358
461
  /**
359
462
  * Start a new agent run. Creates a run ID, wires abort + session, and returns
@@ -374,18 +477,20 @@ export class AgentKernel {
374
477
  };
375
478
  }
376
479
  this.runSessionStates.set(runId, this.sessionState);
480
+ if (sessionId)
481
+ this.runSessionIds.set(runId, sessionId);
377
482
  const agent = agentOverride ?? this.currentAgent;
378
483
  if (agent) {
379
484
  this.modelCaller.resolveAgentModel(agent, runId);
380
485
  }
381
- // Build effective prompt with agent identity and system prompt
382
- const identity = buildAgentIdentity(agent);
383
- const systemPrompt = agent?.systemPrompt;
384
- const parts = [identity, systemPrompt, prompt].filter(Boolean);
385
- const effectivePrompt = parts.join("\n\n");
486
+ // RV-40: keep the system head separate from the user prompt so the model
487
+ // receives a real `system` message instead of a flattened user turn.
488
+ const systemPrompt = this.buildSystemPrompt(agent);
386
489
  // Per-run saga (no instance-level mutation)
387
490
  const runSaga = new ToolSaga();
388
- const promise = this.runLoop(effectivePrompt, runId, ctx, abort, sessionId, userContentParts, agentOverride, runSaga).finally(() => {
491
+ const promise = this.runLoop(prompt, runId, ctx, abort, sessionId, userContentParts, agentOverride, runSaga, undefined, systemPrompt)
492
+ .then(() => undefined)
493
+ .finally(() => {
389
494
  this.stateMachine.runIdStack.pop();
390
495
  this.runSessionStates.delete(runId);
391
496
  });
@@ -400,14 +505,249 @@ export class AgentKernel {
400
505
  throw err;
401
506
  }
402
507
  }
508
+ /**
509
+ * Resume a previously persisted run from durable storage (snapshot + messages).
510
+ * Restores the run's session state (history, step counter, token counts) and
511
+ * continues the run loop from where it stopped. Emits no `run.started` event
512
+ * (the run has already started) and does not re-inject the original prompt.
513
+ *
514
+ * @param runId The run to resume — must have persisted events in the store.
515
+ * @param prompt The steering prompt for the resumed run (appended as the
516
+ * latest user turn).
517
+ * @param ctx Request context.
518
+ * @param sessionId Optional session id — falls back to the persisted one.
519
+ */
520
+ async resumeRun(runId, prompt, ctx, sessionId, agentOverride) {
521
+ const restored = await restoreRunFromStore(this.store, this.sessionStore, runId);
522
+ if (!restored) {
523
+ return {
524
+ runId,
525
+ completed: Promise.reject(new Error(`No durable state found for run "${runId}" — nothing to resume`)),
526
+ abort: () => { },
527
+ };
528
+ }
529
+ const resolvedSessionId = restored.sessionId ?? sessionId;
530
+ this.stateMachine.runIdStack.push(runId);
531
+ try {
532
+ const parentRunId = this.stateMachine.runIdStack.at(-2);
533
+ const abort = this.stateMachine.createRun(runId, resolvedSessionId, parentRunId);
534
+ if (!abort) {
535
+ return {
536
+ runId,
537
+ completed: Promise.reject(new Error(`Session "${resolvedSessionId}" is busy — concurrent run rejected`)),
538
+ abort: () => { },
539
+ };
540
+ }
541
+ this.runSessionStates.set(runId, restored.sessionState);
542
+ if (resolvedSessionId)
543
+ this.runSessionIds.set(runId, resolvedSessionId);
544
+ const agent = agentOverride ?? this.currentAgent;
545
+ if (agent) {
546
+ this.modelCaller.resolveAgentModel(agent, runId);
547
+ }
548
+ // RV-40: keep the system head separate from the steering prompt.
549
+ const systemPrompt = this.buildSystemPrompt(agent);
550
+ const runSaga = new ToolSaga();
551
+ const promise = this.runLoop(prompt, runId, ctx, abort, resolvedSessionId, undefined, agentOverride, runSaga, true, systemPrompt)
552
+ .then(() => undefined)
553
+ .finally(() => {
554
+ this.stateMachine.runIdStack.pop();
555
+ this.runSessionStates.delete(runId);
556
+ });
557
+ return {
558
+ runId,
559
+ completed: promise,
560
+ abort: () => { abort.abort(); },
561
+ };
562
+ }
563
+ catch (err) {
564
+ this.stateMachine.runIdStack.pop();
565
+ throw err;
566
+ }
567
+ }
568
+ /** Reconstruct session IDs with non-terminal runs from the event store (for restart recovery). */
569
+ async getActiveSessionIds() {
570
+ return findActiveSessionIds(this.store);
571
+ }
572
+ /**
573
+ * Start a new agent run with full lifecycle management.
574
+ * Returns an AgentRunHandle with cancel, status, and event streaming.
575
+ *
576
+ * @example
577
+ * ```typescript
578
+ * const handle = kernel.createRunHandle("Write a hello world program", ctx);
579
+ *
580
+ * // Wait for completion
581
+ * const result = await handle.completed;
582
+ * console.log(result.status); // "succeeded"
583
+ *
584
+ * // Or cancel
585
+ * handle.cancel();
586
+ *
587
+ * // Stream events
588
+ * for await (const event of handle.events()) {
589
+ * console.log(event.type);
590
+ * }
591
+ * ```
592
+ */
593
+ createRunHandle(prompt, ctx, sessionId, userContentParts, agentOverride) {
594
+ const runId = crypto.randomUUID();
595
+ this.stateMachine.runIdStack.push(runId);
596
+ let cancelled = false;
597
+ let completed = false;
598
+ let result;
599
+ const eventHandlers = new Set();
600
+ const abortController = new AbortController();
601
+ const self = this;
602
+ // Create the run promise
603
+ const runPromise = (async () => {
604
+ try {
605
+ const parentRunId = this.stateMachine.runIdStack.at(-2);
606
+ const abort = this.stateMachine.createRun(runId, sessionId, parentRunId);
607
+ if (!abort) {
608
+ throw new KernelError("session_busy", `Session "${sessionId}" is busy`);
609
+ }
610
+ // Wire the handle's cancel() into the controller the run loop listens on.
611
+ abortController.signal.addEventListener("abort", () => abort.abort(), { once: true });
612
+ // RV-7: if cancel() was called before this listener attached (sync call
613
+ // right after createRunHandle), the abort signal is already fired — replay
614
+ // it so the run converges on the single `cancelled` terminal outcome.
615
+ if (abortController.signal.aborted)
616
+ abort.abort();
617
+ this.runSessionStates.set(runId, this.sessionState);
618
+ if (sessionId)
619
+ this.runSessionIds.set(runId, sessionId);
620
+ const agent = agentOverride ?? this.currentAgent;
621
+ if (agent) {
622
+ this.modelCaller.resolveAgentModel(agent, runId);
623
+ }
624
+ // RV-40: keep the system head separate from the user prompt.
625
+ const systemPrompt = this.buildSystemPrompt(agent);
626
+ // Per-run saga (no instance-level mutation)
627
+ const runSaga = new ToolSaga();
628
+ // Emit agent.started event
629
+ const startEvent = {
630
+ type: "agent.started",
631
+ timestamp: new Date().toISOString(),
632
+ runId,
633
+ prompt,
634
+ ...(agent?.profile?.model !== undefined ? { model: agent.profile.model } : {}),
635
+ };
636
+ eventHandlers.forEach(h => h(startEvent));
637
+ // Run the loop — it reports its terminal status through the return value.
638
+ const { totalSteps, status: runStatus } = await this.runLoop(prompt, runId, ctx, abort, sessionId, userContentParts, agentOverride, runSaga, undefined, systemPrompt);
639
+ completed = true;
640
+ result = {
641
+ runId,
642
+ // RV-7: runLoop reports its own terminal status; the local `cancelled`
643
+ // flag (handle.cancel()) agrees whenever the run was cancelled. Never
644
+ // let a cancelled run be coerced into "succeeded".
645
+ status: cancelled || runStatus === "cancelled"
646
+ ? "cancelled"
647
+ : runStatus === "failed" ? "failed" : "succeeded",
648
+ totalSteps, // totalSteps from the run loop
649
+ };
650
+ // Emit agent.completed event
651
+ const completeEvent = {
652
+ type: "agent.completed",
653
+ timestamp: new Date().toISOString(),
654
+ runId,
655
+ status: result.status,
656
+ };
657
+ eventHandlers.forEach(h => h(completeEvent));
658
+ return result;
659
+ }
660
+ catch (err) {
661
+ completed = true;
662
+ result = {
663
+ runId,
664
+ status: "failed",
665
+ error: err instanceof Error ? err.message : String(err),
666
+ totalSteps: 0,
667
+ };
668
+ // Emit agent.error event
669
+ const errorEvent = {
670
+ type: "agent.error",
671
+ timestamp: new Date().toISOString(),
672
+ runId,
673
+ error: result.error ?? "Unknown error",
674
+ ...(err instanceof KernelError ? { code: err.code } : {}),
675
+ };
676
+ eventHandlers.forEach(h => h(errorEvent));
677
+ throw err;
678
+ }
679
+ finally {
680
+ this.stateMachine.runIdStack.pop();
681
+ this.runSessionStates.delete(runId);
682
+ }
683
+ })();
684
+ return {
685
+ runId,
686
+ completed: runPromise,
687
+ cancel() {
688
+ cancelled = true;
689
+ abortController.abort();
690
+ },
691
+ get isCancelled() { return cancelled; },
692
+ get isCompleted() { return completed; },
693
+ get isRunning() { return !completed && !cancelled; },
694
+ events() {
695
+ return (async function* () {
696
+ // Yield events as they occur
697
+ const queue = [];
698
+ const handler = (event) => queue.push(event);
699
+ eventHandlers.add(handler);
700
+ try {
701
+ while (!completed) {
702
+ if (queue.length > 0) {
703
+ yield queue.shift();
704
+ }
705
+ else {
706
+ await new Promise(r => setTimeout(r, 10));
707
+ }
708
+ }
709
+ // Yield remaining events
710
+ while (queue.length > 0) {
711
+ yield queue.shift();
712
+ }
713
+ }
714
+ finally {
715
+ eventHandlers.delete(handler);
716
+ }
717
+ })();
718
+ },
719
+ onEvent(handler) {
720
+ eventHandlers.add(handler);
721
+ return () => { eventHandlers.delete(handler); };
722
+ },
723
+ };
724
+ }
725
+ /**
726
+ * Start a new agent run and stream events as an async iterable.
727
+ * Returns the run ID and an async iterable of run events.
728
+ */
729
+ streamRun(prompt, ctx, sessionId, userContentParts, agentOverride) {
730
+ const handle = this.run(prompt, ctx, sessionId, userContentParts, agentOverride);
731
+ const eventBus = this.eventBus;
732
+ const events = eventBus ? streamRunEvents(eventBus, handle.runId, handle.completed) : (async function* () { })();
733
+ return {
734
+ runId: handle.runId,
735
+ events,
736
+ };
737
+ }
403
738
  getRunSession(runId) {
404
739
  return this.runSessionStates.get(runId) ?? this.sessionState;
405
740
  }
741
+ /** Resolve the context of the most recently started run, if one is active. */
742
+ activeRunContext() {
743
+ const runId = this.stateMachine.runIdStack.at(-1);
744
+ return runId ? this.runContexts.get(runId) : undefined;
745
+ }
406
746
  /** Run multiple sub-agents concurrently. Returns combined output. */
407
- async runAgentsParallel(tasks, ctx, sessionId) {
747
+ async runAgentsParallel(tasks, ctx, sessionId, parentRunId, signal) {
408
748
  if (!this.agentRegistry)
409
- throw new Error("No agent registry configured");
410
- return runSubAgentsParallel(this.subAgentDeps, tasks, ctx, sessionId);
749
+ throw new KernelError("internal_error", "No agent registry configured");
750
+ return runSubAgentsParallel(this.subAgentDeps, tasks, ctx, sessionId, parentRunId, signal);
411
751
  }
412
752
  /** Start a run with error-safe wrapper — returns `{ ok, value }` or `{ ok: false, error }`. */
413
753
  runSafe(prompt, ctx, sessionId, userContentParts) {
@@ -424,7 +764,12 @@ export class AgentKernel {
424
764
  }
425
765
  }
426
766
  /** Get the tool saga for inspecting completed tool calls and triggering rollbacks. */
427
- getSaga() {
767
+ getSaga(runId) {
768
+ if (runId) {
769
+ const runSaga = this.runSagas.get(runId);
770
+ if (runSaga)
771
+ return runSaga;
772
+ }
428
773
  return this.saga;
429
774
  }
430
775
  /** Get the circuit breaker for inspecting trip state and threshold. */
@@ -468,6 +813,13 @@ export class AgentKernel {
468
813
  if (normalized.compactionThreshold !== undefined) {
469
814
  this.compactionThreshold = normalized.compactionThreshold;
470
815
  }
816
+ if (normalized.maxRetries !== undefined || normalized.retryBackoffMs !== undefined || normalized.maxRetryBackoffMs !== undefined) {
817
+ this.circuitBreaker = new CircuitBreaker({
818
+ maxRetries: normalized.maxRetries ?? this.circuitBreaker.getOptions().maxRetries,
819
+ backoffMs: normalized.retryBackoffMs ?? this.circuitBreaker.getOptions().backoffMs,
820
+ maxBackoffMs: normalized.maxRetryBackoffMs ?? this.circuitBreaker.getOptions().maxBackoffMs,
821
+ });
822
+ }
471
823
  if (normalized.permissions?.globalPermissionRules) {
472
824
  this.permissionGate.setGlobalRules(normalized.permissions.globalPermissionRules);
473
825
  }
@@ -494,6 +846,96 @@ export class AgentKernel {
494
846
  /** Send a user input string to a waiting run (e.g., approval response). */
495
847
  sendInput(runId, text) {
496
848
  this.stateMachine.sendInput(runId, text);
849
+ this.queuePendingEntry(runId, text);
850
+ }
851
+ /** Queue an input into the runtime queue and (when a session is attached) start persisting it. */
852
+ queuePendingEntry(runId, text) {
853
+ const sessionId = this.runSessionIds.get(runId);
854
+ const list = this.runPendingEntries.get(runId) ?? [];
855
+ let persisted;
856
+ if (this.sessionStore && sessionId) {
857
+ persisted = this.persistPendingInput(sessionId, text);
858
+ }
859
+ list.push({ text, ...(persisted !== undefined ? { persisted } : {}) });
860
+ this.runPendingEntries.set(runId, list);
861
+ }
862
+ /** Serialize per-session writes so sequential admissions get sequential seqs (RV-21). */
863
+ persistPendingInput(sessionId, text) {
864
+ const prev = this.inputWriteChains.get(sessionId) ?? Promise.resolve();
865
+ const write = prev.catch(() => { }).then(async () => {
866
+ let next = this.nextAdmittedSeq.get(sessionId);
867
+ if (next === undefined) {
868
+ next = (await this.maxAdmittedSeq(sessionId)) + 1;
869
+ }
870
+ const admittedSeq = next;
871
+ this.nextAdmittedSeq.set(sessionId, admittedSeq + 1);
872
+ const msg = await this.sessionStore.addMessage(sessionId, "user", text, undefined, undefined, undefined, undefined, admittedSeq);
873
+ return { sessionId, messageId: msg.id, admittedSeq };
874
+ });
875
+ this.inputWriteChains.set(sessionId, write);
876
+ return write;
877
+ }
878
+ async maxAdmittedSeq(sessionId) {
879
+ if (typeof this.sessionStore?.listMessages !== "function")
880
+ return 0;
881
+ const messages = await this.sessionStore.listMessages(sessionId);
882
+ let max = 0;
883
+ for (const m of messages) {
884
+ if (typeof m.admittedSeq === "number" && m.admittedSeq > max)
885
+ max = m.admittedSeq;
886
+ }
887
+ return max;
888
+ }
889
+ /**
890
+ * Re-queue inputs admitted but not yet promoted (e.g. from a previous process
891
+ * that crashed mid-run) in admission order, exactly once (RV-21).
892
+ */
893
+ async hydratePendingInputs(runId, sessionId) {
894
+ if (!this.sessionStore || !sessionId)
895
+ return;
896
+ if (typeof this.sessionStore.listMessages !== "function")
897
+ return;
898
+ const messages = await this.sessionStore.listMessages(sessionId);
899
+ let max = 0;
900
+ // Seed the allocator from the max admittedSeq across ALL history so live
901
+ // inputs continue past persisted seqs even when nothing is pending.
902
+ for (const m of messages) {
903
+ if (typeof m.admittedSeq === "number" && m.admittedSeq > max)
904
+ max = m.admittedSeq;
905
+ }
906
+ const pending = messages
907
+ .filter((m) => typeof m.admittedSeq === "number" && (m.promotedSeq === undefined || m.promotedSeq < m.admittedSeq))
908
+ .sort((a, b) => (a.admittedSeq ?? 0) - (b.admittedSeq ?? 0));
909
+ const list = this.runPendingEntries.get(runId) ?? [];
910
+ for (const m of pending) {
911
+ this.stateMachine.sendInput(runId, m.content);
912
+ list.push({ text: m.content, persisted: Promise.resolve({ sessionId, messageId: m.id, admittedSeq: m.admittedSeq }) });
913
+ }
914
+ if (pending.length > 0 || list.length > 0) {
915
+ this.runPendingEntries.set(runId, list);
916
+ }
917
+ // Never regress the allocator under parallel restarts / in-flight writes.
918
+ this.nextAdmittedSeq.set(sessionId, Math.max(max + 1, this.nextAdmittedSeq.get(sessionId) ?? 0));
919
+ }
920
+ /** Mark the given drained inputs as promoted (best-effort, at-least-once on crash). */
921
+ async markInputsPromoted(runId, texts) {
922
+ const entries = this.runPendingEntries.get(runId);
923
+ if (!entries || entries.length === 0)
924
+ return;
925
+ const taken = entries.splice(0, texts.length);
926
+ if (this.sessionStore?.updateMessage) {
927
+ for (const entry of taken) {
928
+ if (!entry.persisted)
929
+ continue;
930
+ try {
931
+ const p = await entry.persisted;
932
+ await this.sessionStore.updateMessage(p.sessionId, p.messageId, { promotedSeq: p.admittedSeq });
933
+ }
934
+ catch {
935
+ // Best-effort: leave un-promoted so a later restart re-delivers at-least-once.
936
+ }
937
+ }
938
+ }
497
939
  }
498
940
  /** Abort all currently running sessions. */
499
941
  cancelCurrentRun() {
@@ -512,7 +954,7 @@ export class AgentKernel {
512
954
  this.cancelCurrentRun();
513
955
  return this.lifecycleManager.shutdown();
514
956
  }
515
- async runLoop(prompt, runId, ctx, runAbort, sessionId, userContentParts, agentOverride, runSaga) {
957
+ async runLoop(prompt, runId, ctx, runAbort, sessionId, userContentParts, agentOverride, runSaga, resume, systemPrompt) {
516
958
  const runModel = this.modelCaller.getActiveModel(runId);
517
959
  const runSessionState = this.getRunSession(runId);
518
960
  const currentAgent = agentOverride ?? this.currentAgent;
@@ -524,15 +966,24 @@ export class AgentKernel {
524
966
  judgeModel = this.modelCaller.resolveAgentModel(evaluator);
525
967
  }
526
968
  const runSagaInstance = runSaga ?? new ToolSaga();
527
- const prevAgent = this.stepExecutor.getCurrentAgent();
528
- this.stepExecutor.setCurrentAgent(currentAgent);
969
+ // Per-run context — the active agent, sub-agent depth/chain, saga and tool
970
+ // cache all live here so parallel runs never clobber each other's state.
971
+ const runContext = createRunContext(runId, currentAgent, runSagaInstance);
972
+ this.runContexts.set(runId, runContext);
973
+ // Point the run's recorder at this run's saga so that
974
+ // record()/registerCompensation()/rollbackStep() operate on the same instance.
975
+ this.runSagas.set(runId, runSagaInstance);
976
+ // Per-run circuit breaker — parallel runs must not share breaker state, so
977
+ // each run retries/trips independently. The instance breaker stays as the
978
+ // default config (exposed via getCircuitBreaker()) for new runs.
979
+ const runCircuitBreaker = new CircuitBreaker(this.circuitBreaker.getOptions());
529
980
  const orchestratorDeps = {
530
981
  modelCaller: this.modelCaller,
531
982
  permissionGate: this.permissionGate,
532
983
  stepExecutor: this.stepExecutor,
533
984
  saga: runSagaInstance,
534
985
  store: this.store,
535
- circuitBreaker: this.circuitBreaker,
986
+ circuitBreaker: runCircuitBreaker,
536
987
  stateMachine: this.stateMachine,
537
988
  ...(this.pluginManager ? { pluginManager: this.pluginManager } : {}),
538
989
  ...(this.systemContext ? { systemContext: this.systemContext } : {}),
@@ -543,35 +994,105 @@ export class AgentKernel {
543
994
  maxTokens: this.maxTokens,
544
995
  thinkingBudget: this.thinkingBudget,
545
996
  stepTimeout: this.stepTimeout,
546
- compactionThreshold: this.compactionThreshold,
997
+ ...(this.compactionThreshold !== undefined ? { compactionThreshold: this.compactionThreshold } : {}),
547
998
  ...(this.termination ? { termination: this.termination } : {}),
548
999
  ...(judgeModel ? { judgeModel } : {}),
549
1000
  ...(currentAgent ? { currentAgent } : {}),
550
1001
  addSessionMessage: (sid, role, content, extra) => this.addSessionMessage(sid, role, content, extra),
1002
+ beforeRun: async () => {
1003
+ const parentRunId = this.stateMachine.runIdStack.at(-2);
1004
+ if (parentRunId)
1005
+ return;
1006
+ await this.hydratePendingInputs(runId, sessionId);
1007
+ },
1008
+ onInputsDrained: (rid, texts) => this.markInputsPromoted(rid, texts),
551
1009
  };
552
1010
  try {
553
- await executeRunLoop(orchestratorDeps, {
1011
+ const result = await executeRunLoop(orchestratorDeps, {
554
1012
  prompt, runId, ctx, runAbort,
555
1013
  ...(sessionId !== undefined ? { sessionId } : {}),
556
1014
  ...(userContentParts !== undefined ? { userContentParts } : {}),
1015
+ ...(resume !== undefined ? { resume } : {}),
1016
+ ...(systemPrompt !== undefined ? { systemPrompt } : {}),
557
1017
  runModel,
558
1018
  ...(runSessionState !== undefined ? { runSessionState } : {}),
559
1019
  addSessionMessage: (sid, role, content, extra) => this.addSessionMessage(sid, role, content, extra),
560
1020
  emitEvent: (event, persist) => this.emitEvent(event, persist),
561
1021
  setState: (rid, state) => this.stateMachine.setState(rid, state),
562
- updateSessionOnComplete: (sid, rid, totalIn, totalOut, _steps, _status) => this.updateSessionOnComplete(sid, rid, totalIn, totalOut, _steps, _status),
563
- emitFail: (rid, c, reason, steps, sid, totalIn, totalOut, dur) => this.emitFail(rid, c, reason, steps, sid, totalIn, totalOut, dur),
1022
+ emitCompleted: (event, sid, rid, totalIn, totalOut, status) => this.emitCompleted(event, sid, rid, totalIn, totalOut, status),
1023
+ emitFail: (rid, c, reason, steps, sid, totalIn, totalOut, dur, cancelled) => this.emitFail(rid, c, reason, steps, sid, totalIn, totalOut, dur, cancelled),
564
1024
  });
1025
+ return result;
565
1026
  }
566
1027
  finally {
567
- this.stepExecutor.setCurrentAgent(prevAgent);
1028
+ // Drop this run's context and saga — parallel runs each own their own.
1029
+ this.runContexts.delete(runId);
1030
+ this.runSagas.delete(runId);
1031
+ this.runSessionIds.delete(runId);
1032
+ this.runPendingEntries.delete(runId);
568
1033
  }
569
1034
  }
570
- updateSessionOnComplete(sid, rid, totalIn, totalOut, _steps, _status) {
571
- return sessionUpdateComplete(this.sessionDeps, sid, rid, totalIn, totalOut);
1035
+ emitCompleted(event, sid, rid, totalIn, totalOut, status) {
1036
+ return sessionEmitCompleted(this.sessionDeps, event, sid, rid, totalIn, totalOut, status);
572
1037
  }
573
- emitFail(rid, c, reason, steps, sid, totalIn = 0, totalOut = 0, dur) {
574
- return sessionEmitFail(this.sessionDeps, rid, c, reason, steps, sid, totalIn, totalOut, dur);
1038
+ emitFail(rid, c, reason, steps, sid, totalIn = 0, totalOut = 0, dur, cancelled = false) {
1039
+ return sessionEmitFail(this.sessionDeps, rid, c, reason, steps, sid, totalIn, totalOut, dur, cancelled);
575
1040
  }
576
1041
  }
1042
+ /**
1043
+ * Stream the events of a single run from the event bus, filtered by run ID.
1044
+ * Subscribes eagerly so no event published before the first `next()` is missed.
1045
+ * Terminates when the run completes (or fails).
1046
+ */
1047
+ function streamRunEvents(eventBus, runId, completed) {
1048
+ const queue = [];
1049
+ let notify = null;
1050
+ let done = false;
1051
+ const wake = () => {
1052
+ const r = notify;
1053
+ notify = null;
1054
+ if (r)
1055
+ r();
1056
+ };
1057
+ const unsubscribe = eventBus.subscribeAll((event) => {
1058
+ if (event.aggregateId !== runId)
1059
+ return;
1060
+ queue.push(event);
1061
+ wake();
1062
+ });
1063
+ completed.then(wake, wake);
1064
+ return {
1065
+ [Symbol.asyncIterator]() {
1066
+ const self = this;
1067
+ return {
1068
+ async next() {
1069
+ while (!done) {
1070
+ if (queue.length > 0) {
1071
+ return { done: false, value: queue.shift() };
1072
+ }
1073
+ const stopped = await Promise.race([
1074
+ new Promise((resolve) => { notify = () => resolve(false); }),
1075
+ completed.then(() => true, () => true),
1076
+ ]);
1077
+ if (stopped)
1078
+ done = true;
1079
+ }
1080
+ unsubscribe();
1081
+ // Drain any events that arrived after the run completed
1082
+ if (queue.length > 0) {
1083
+ return { done: false, value: queue.shift() };
1084
+ }
1085
+ return { done: true, value: undefined };
1086
+ },
1087
+ return() {
1088
+ unsubscribe();
1089
+ return Promise.resolve({ done: true, value: undefined });
1090
+ },
1091
+ [Symbol.asyncIterator]() {
1092
+ return self;
1093
+ },
1094
+ };
1095
+ },
1096
+ };
1097
+ }
577
1098
  //# sourceMappingURL=kernel.js.map