@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.
- package/README.md +194 -12
- package/dist/agent/agent-factory.d.ts +11 -0
- package/dist/agent/agent-factory.d.ts.map +1 -1
- package/dist/agent/agent-factory.js +11 -2
- package/dist/agent/agent-factory.js.map +1 -1
- package/dist/agent/agent-registry.d.ts +3 -0
- package/dist/agent/agent-registry.d.ts.map +1 -1
- package/dist/agent/agent-registry.js.map +1 -1
- package/dist/agent/create-agent-tool.d.ts +13 -13
- package/dist/agent/create-agent-tool.d.ts.map +1 -1
- package/dist/agent/delegate-batch-tool.d.ts.map +1 -1
- package/dist/agent/delegate-batch-tool.js +1 -1
- package/dist/agent/delegate-batch-tool.js.map +1 -1
- package/dist/agent/delegate-tool.js +1 -1
- package/dist/agent/delegate-tool.js.map +1 -1
- package/dist/agent/execution-engine.d.ts +1 -1
- package/dist/agent/execution-engine.d.ts.map +1 -1
- package/dist/agent/spawn-agent-tool.d.ts +10 -10
- package/dist/agent/spawn-agent-tool.d.ts.map +1 -1
- package/dist/event-bus/bridge.d.ts +2 -3
- package/dist/event-bus/bridge.d.ts.map +1 -1
- package/dist/event-bus/bridge.js +12 -5
- package/dist/event-bus/bridge.js.map +1 -1
- package/dist/event-bus/global-bus.d.ts +8 -0
- package/dist/event-bus/global-bus.d.ts.map +1 -1
- package/dist/event-bus/global-bus.js +48 -0
- package/dist/event-bus/global-bus.js.map +1 -1
- package/dist/event-bus/in-memory-bus.d.ts +14 -0
- package/dist/event-bus/in-memory-bus.d.ts.map +1 -1
- package/dist/event-bus/in-memory-bus.js +53 -0
- package/dist/event-bus/in-memory-bus.js.map +1 -1
- package/dist/event-bus/types.d.ts +17 -0
- package/dist/event-bus/types.d.ts.map +1 -1
- package/dist/fakes/fake-approval-store.d.ts +2 -2
- package/dist/fakes/fake-approval-store.d.ts.map +1 -1
- package/dist/fakes/fake-approval-store.js +28 -6
- package/dist/fakes/fake-approval-store.js.map +1 -1
- package/dist/fakes/fake-model.d.ts.map +1 -1
- package/dist/fakes/fake-model.js +2 -1
- package/dist/fakes/fake-model.js.map +1 -1
- package/dist/fakes/fake-store.d.ts +4 -1
- package/dist/fakes/fake-store.d.ts.map +1 -1
- package/dist/fakes/fake-store.js +33 -0
- package/dist/fakes/fake-store.js.map +1 -1
- package/dist/index.d.ts +232 -109
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +191 -66
- package/dist/index.js.map +1 -1
- package/dist/kernel/approval-handler.d.ts +1 -1
- package/dist/kernel/approval-handler.d.ts.map +1 -1
- package/dist/kernel/approval-handler.js +5 -1
- package/dist/kernel/approval-handler.js.map +1 -1
- package/dist/kernel/circuit-breaker.d.ts +19 -1
- package/dist/kernel/circuit-breaker.d.ts.map +1 -1
- package/dist/kernel/circuit-breaker.js +54 -10
- package/dist/kernel/circuit-breaker.js.map +1 -1
- package/dist/kernel/kernel-error.d.ts +3 -2
- package/dist/kernel/kernel-error.d.ts.map +1 -1
- package/dist/kernel/kernel-error.js +5 -6
- package/dist/kernel/kernel-error.js.map +1 -1
- package/dist/kernel/kernel-session.d.ts +13 -5
- package/dist/kernel/kernel-session.d.ts.map +1 -1
- package/dist/kernel/kernel-session.js +71 -13
- package/dist/kernel/kernel-session.js.map +1 -1
- package/dist/kernel/kernel-types.d.ts +82 -9
- package/dist/kernel/kernel-types.d.ts.map +1 -1
- package/dist/kernel/kernel-types.js.map +1 -1
- package/dist/kernel/kernel.d.ts +94 -20
- package/dist/kernel/kernel.d.ts.map +1 -1
- package/dist/kernel/kernel.js +591 -70
- package/dist/kernel/kernel.js.map +1 -1
- package/dist/kernel/model-caller.d.ts +30 -2
- package/dist/kernel/model-caller.d.ts.map +1 -1
- package/dist/kernel/model-caller.js +55 -20
- package/dist/kernel/model-caller.js.map +1 -1
- package/dist/kernel/permission-gate.d.ts +6 -4
- package/dist/kernel/permission-gate.d.ts.map +1 -1
- package/dist/kernel/permission-gate.js +16 -6
- package/dist/kernel/permission-gate.js.map +1 -1
- package/dist/kernel/run-context.d.ts +26 -0
- package/dist/kernel/run-context.d.ts.map +1 -0
- package/dist/kernel/run-context.js +13 -0
- package/dist/kernel/run-context.js.map +1 -0
- package/dist/kernel/run-loop.d.ts +35 -14
- package/dist/kernel/run-loop.d.ts.map +1 -1
- package/dist/kernel/run-loop.js +295 -87
- package/dist/kernel/run-loop.js.map +1 -1
- package/dist/kernel/self-correction.d.ts +2 -2
- package/dist/kernel/self-correction.d.ts.map +1 -1
- package/dist/kernel/self-correction.js +3 -2
- package/dist/kernel/self-correction.js.map +1 -1
- package/dist/kernel/state-machine.d.ts +2 -0
- package/dist/kernel/state-machine.d.ts.map +1 -1
- package/dist/kernel/state-machine.js +2 -0
- package/dist/kernel/state-machine.js.map +1 -1
- package/dist/kernel/step-executor.d.ts +11 -3
- package/dist/kernel/step-executor.d.ts.map +1 -1
- package/dist/kernel/step-executor.js +54 -24
- package/dist/kernel/step-executor.js.map +1 -1
- package/dist/kernel/sub-agent-runner.d.ts +10 -7
- package/dist/kernel/sub-agent-runner.d.ts.map +1 -1
- package/dist/kernel/sub-agent-runner.js +105 -31
- package/dist/kernel/sub-agent-runner.js.map +1 -1
- package/dist/kernel/tool-context-builder.d.ts +7 -2
- package/dist/kernel/tool-context-builder.d.ts.map +1 -1
- package/dist/kernel/tool-context-builder.js +23 -6
- package/dist/kernel/tool-context-builder.js.map +1 -1
- package/dist/kernel/tool-error-router.d.ts +1 -1
- package/dist/kernel/tool-error-router.d.ts.map +1 -1
- package/dist/kernel/tool-error-router.js +4 -3
- package/dist/kernel/tool-error-router.js.map +1 -1
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +3 -0
- package/dist/logger.js.map +1 -1
- package/dist/model.d.ts +3 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +2 -0
- package/dist/model.js.map +1 -1
- package/dist/plugin/manager.d.ts +3 -1
- package/dist/plugin/manager.d.ts.map +1 -1
- package/dist/plugin/manager.js +51 -3
- package/dist/plugin/manager.js.map +1 -1
- package/dist/plugin/plugin-file-loader.js +2 -2
- package/dist/plugin/plugin-file-loader.js.map +1 -1
- package/dist/plugin.d.ts +50 -4
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +14 -1
- package/dist/plugin.js.map +1 -1
- package/dist/session/null-store.d.ts +1 -0
- package/dist/session/null-store.d.ts.map +1 -1
- package/dist/session/null-store.js +7 -0
- package/dist/session/null-store.js.map +1 -1
- package/dist/session/run-coordinator.d.ts +9 -3
- package/dist/session/run-coordinator.d.ts.map +1 -1
- package/dist/session/run-coordinator.js +4 -0
- package/dist/session/run-coordinator.js.map +1 -1
- package/dist/skill/create-skill-tool.d.ts +5 -4
- package/dist/skill/create-skill-tool.d.ts.map +1 -1
- package/dist/skill/create-skill-tool.js +8 -5
- package/dist/skill/create-skill-tool.js.map +1 -1
- package/dist/skill/skill-def-registry.d.ts +6 -0
- package/dist/skill/skill-def-registry.d.ts.map +1 -1
- package/dist/skill/skill-def-registry.js +4 -0
- package/dist/skill/skill-def-registry.js.map +1 -1
- package/dist/system-context/sources/instructions-source.js +1 -1
- package/dist/system-context/sources/instructions-source.js.map +1 -1
- package/dist/tool/bridge.d.ts +7 -1
- package/dist/tool/bridge.d.ts.map +1 -1
- package/dist/tool/bridge.js +5 -0
- package/dist/tool/bridge.js.map +1 -1
- package/dist/tool/builtin-provider.d.ts +35 -0
- package/dist/tool/builtin-provider.d.ts.map +1 -0
- package/dist/tool/builtin-provider.js +75 -0
- package/dist/tool/builtin-provider.js.map +1 -0
- package/dist/tool/index.d.ts +21 -23
- package/dist/tool/index.d.ts.map +1 -1
- package/dist/tool/index.js +19 -18
- package/dist/tool/index.js.map +1 -1
- package/dist/tool/provider-helpers.d.ts.map +1 -1
- package/dist/tool/provider-helpers.js +3 -2
- package/dist/tool/provider-helpers.js.map +1 -1
- package/dist/tool/runtime.d.ts +23 -2
- package/dist/tool/runtime.d.ts.map +1 -1
- package/dist/tool/runtime.js +20 -16
- package/dist/tool/runtime.js.map +1 -1
- package/dist/tracer.d.ts +5 -0
- package/dist/tracer.d.ts.map +1 -1
- package/dist/tracer.js +4 -0
- package/dist/tracer.js.map +1 -1
- package/dist/workflow/agent-graph.js +3 -3
- package/dist/workflow/agent-graph.js.map +1 -1
- package/dist/workflow/workflow-helpers.js +2 -2
- package/dist/workflow/workflow-helpers.js.map +1 -1
- package/dist/workspace.d.ts +7 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +4 -0
- package/dist/workspace.js.map +1 -1
- package/package.json +18 -62
package/dist/kernel/kernel.js
CHANGED
|
@@ -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 "
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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,
|
|
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 "
|
|
17
|
-
export { KernelError } from "
|
|
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
|
|
84
|
+
throw new ValidationError("maxSteps must be >= 1");
|
|
68
85
|
}
|
|
69
86
|
if (normalized.maxTokens !== undefined && normalized.maxTokens < 1) {
|
|
70
|
-
throw new
|
|
87
|
+
throw new ValidationError("maxTokens must be >= 1");
|
|
71
88
|
}
|
|
72
89
|
if (normalized.stepTimeout !== undefined && normalized.stepTimeout < 1000) {
|
|
73
|
-
throw new
|
|
90
|
+
throw new ValidationError("stepTimeout must be >= 1000ms");
|
|
74
91
|
}
|
|
75
92
|
if (normalized.maxToolCallsPerStep !== undefined && normalized.maxToolCallsPerStep < 1) {
|
|
76
|
-
throw new
|
|
93
|
+
throw new ValidationError("maxToolCallsPerStep must be >= 1");
|
|
77
94
|
}
|
|
78
95
|
if (normalized.doomLoopThreshold !== undefined && normalized.doomLoopThreshold < 1) {
|
|
79
|
-
throw new
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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: {
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
368
|
+
const perm = this.permissionGate.checkTool(t.id, t.risk, undefined, target);
|
|
279
369
|
return perm.allowed || perm.needsApproval;
|
|
280
370
|
});
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
382
|
-
|
|
383
|
-
const systemPrompt = agent
|
|
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(
|
|
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
|
|
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
|
-
|
|
528
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
571
|
-
return
|
|
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
|