@vinhnt-sdk/core 0.1.2 → 0.1.4
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/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/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 +3 -2
- 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 +20 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- 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.map +1 -1
- package/dist/kernel/circuit-breaker.js +4 -1
- package/dist/kernel/circuit-breaker.js.map +1 -1
- package/dist/kernel/kernel-error.d.ts +1 -2
- package/dist/kernel/kernel-error.d.ts.map +1 -1
- package/dist/kernel/kernel-error.js +2 -3
- 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 +6 -8
- 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 +60 -16
- package/dist/kernel/kernel.d.ts.map +1 -1
- package/dist/kernel/kernel.js +428 -76
- package/dist/kernel/kernel.js.map +1 -1
- package/dist/kernel/model-caller.d.ts +28 -2
- package/dist/kernel/model-caller.d.ts.map +1 -1
- package/dist/kernel/model-caller.js +47 -12
- 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 +9 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +18 -4
- 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 +50 -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/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 +21 -11
package/dist/kernel/run-loop.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { buildJudgeMessages, parseJudgeVerdict } from "
|
|
1
|
+
import { getTextContent, COMPACTION_SUMMARY_PREFIX } from "@vinhnt-sdk/schema";
|
|
2
|
+
import { KernelError } from "@vinhnt-sdk/step-executor";
|
|
3
|
+
import { evaluateStopConditions, buildJudgeMessages, parseJudgeVerdict } from "@vinhnt-sdk/step-executor";
|
|
4
|
+
/** Index of the head `system` message — the first one. All system instructions
|
|
5
|
+
* (identity, agent systemPrompt, context baseline/reconciled updates) must live
|
|
6
|
+
* in that single message so providers never see mid-conversation `system`
|
|
7
|
+
* messages (RV-40). */
|
|
8
|
+
function headSystemIndex(messages) {
|
|
9
|
+
return messages.findIndex((m) => m.role === "system");
|
|
10
|
+
}
|
|
4
11
|
async function initializeSystemContext(systemContext, messages) {
|
|
5
12
|
const sc = await systemContext.initialize();
|
|
6
13
|
if (sc.baseline) {
|
|
14
|
+
const idx = headSystemIndex(messages);
|
|
15
|
+
if (idx >= 0) {
|
|
16
|
+
const updated = [...messages];
|
|
17
|
+
const head = updated[idx];
|
|
18
|
+
updated[idx] = { ...head, content: `${getTextContent(head.content)}\n\n${sc.baseline}` };
|
|
19
|
+
return { messages: updated, contextEpochActive: true, didChange: true };
|
|
20
|
+
}
|
|
7
21
|
return { messages: [{ role: "system", content: sc.baseline }, ...messages], contextEpochActive: true, didChange: true };
|
|
8
22
|
}
|
|
9
23
|
return { messages, contextEpochActive: true, didChange: false };
|
|
@@ -11,13 +25,25 @@ async function initializeSystemContext(systemContext, messages) {
|
|
|
11
25
|
async function reconcileSystemContext(systemContext, messages, step) {
|
|
12
26
|
const result = await systemContext.reconcile();
|
|
13
27
|
if (result.type === "updated" && result.update) {
|
|
28
|
+
// Merge the update into the head system message rather than appending a new
|
|
29
|
+
// mid-conversation `system` message that providers may reject or mis-handle.
|
|
30
|
+
const idx = headSystemIndex(messages);
|
|
31
|
+
if (idx >= 0) {
|
|
32
|
+
const updated = [...messages];
|
|
33
|
+
const head = updated[idx];
|
|
34
|
+
updated[idx] = { ...head, content: `${getTextContent(head.content)}\n\n${result.update}` };
|
|
35
|
+
return { messages: updated, contextEpochActive: true, didChange: true };
|
|
36
|
+
}
|
|
37
|
+
// No head system message (run without an agent) — keep the update rather than drop it.
|
|
14
38
|
return { messages: [...messages, { role: "system", content: result.update }], contextEpochActive: true, didChange: true };
|
|
15
39
|
}
|
|
16
40
|
if (result.type === "replaced" && step > 0) {
|
|
17
|
-
|
|
41
|
+
// Replaced: refresh the head system message content in place.
|
|
42
|
+
const idx = headSystemIndex(messages);
|
|
18
43
|
const updated = [...messages];
|
|
19
44
|
if (idx >= 0) {
|
|
20
|
-
updated[idx]
|
|
45
|
+
const head = updated[idx];
|
|
46
|
+
updated[idx] = { ...head, content: result.systemContext.baseline };
|
|
21
47
|
}
|
|
22
48
|
else {
|
|
23
49
|
updated.unshift({ role: "system", content: result.systemContext.baseline });
|
|
@@ -26,12 +52,12 @@ async function reconcileSystemContext(systemContext, messages, step) {
|
|
|
26
52
|
}
|
|
27
53
|
return { messages, contextEpochActive: true, didChange: false };
|
|
28
54
|
}
|
|
29
|
-
async function maybeCompact(messages, runModel, deps, signal, emitEvent) {
|
|
55
|
+
async function maybeCompact(messages, runModel, deps, signal, sessionId, emitEvent) {
|
|
30
56
|
if (!deps.compactor)
|
|
31
57
|
return { messages, didCompact: false };
|
|
32
58
|
let shouldCompact = true;
|
|
33
59
|
if (runModel.countTokens) {
|
|
34
|
-
const estimatedInput = messages.reduce((sum, m) => sum + runModel.countTokens(m.content), 0);
|
|
60
|
+
const estimatedInput = messages.reduce((sum, m) => sum + runModel.countTokens(getTextContent(m.content)), 0);
|
|
35
61
|
const contextWindow = runModel.contextLimit ?? deps.maxTokens * 4;
|
|
36
62
|
const ratio = deps.compactionThreshold ?? 0.75;
|
|
37
63
|
const threshold = Math.floor(contextWindow * ratio);
|
|
@@ -49,14 +75,46 @@ async function maybeCompact(messages, runModel, deps, signal, emitEvent) {
|
|
|
49
75
|
originalCount: compacted.summary.originalMessageCount,
|
|
50
76
|
compressedCount: compacted.summary.compressedMessageCount,
|
|
51
77
|
});
|
|
78
|
+
// RV-15 durable compaction: persist the summary as a durable marker so that
|
|
79
|
+
// a restart / resume rebuilds the compacted context instead of the raw
|
|
80
|
+
// transcript. Best-effort — a store failure must NOT roll back the run or
|
|
81
|
+
// the compaction (the in-memory compacted context still applies).
|
|
82
|
+
if (sessionId) {
|
|
83
|
+
const summaryText = compacted.summary.summary
|
|
84
|
+
? `${COMPACTION_SUMMARY_PREFIX}${compacted.summary.summary}`
|
|
85
|
+
: `${COMPACTION_SUMMARY_PREFIX}Compressed ${compacted.summary.originalMessageCount} → ${compacted.summary.compressedMessageCount} messages`;
|
|
86
|
+
try {
|
|
87
|
+
await deps.addSessionMessage(sessionId, "system", summaryText);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (typeof console !== "undefined") {
|
|
91
|
+
console.warn("[run-loop] Failed to persist compaction summary:", err instanceof Error ? err.message : String(err));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
52
95
|
return { messages: [...compacted.messages], didCompact: true };
|
|
53
96
|
}
|
|
54
97
|
return { messages, didCompact: false };
|
|
55
98
|
}
|
|
56
|
-
async function compactOnOverflow(messages, compactor, step, signal) {
|
|
99
|
+
async function compactOnOverflow(messages, compactor, step, signal, sessionId, addSessionMessage) {
|
|
57
100
|
if (step <= 0)
|
|
58
101
|
throw new Error("Cannot compact on step 0 — context overflow should not occur on first step");
|
|
59
102
|
const compacted = await compactor.compact(messages, signal);
|
|
103
|
+
// RV-15: overflow compaction is also durable — persist a summary marker so a
|
|
104
|
+
// restart rebuilds the compacted context. Best-effort, never rolls back.
|
|
105
|
+
if (compacted.summary.compressedMessageCount < compacted.summary.originalMessageCount && sessionId) {
|
|
106
|
+
const summaryText = compacted.summary.summary
|
|
107
|
+
? `${COMPACTION_SUMMARY_PREFIX}${compacted.summary.summary}`
|
|
108
|
+
: `${COMPACTION_SUMMARY_PREFIX}Compressed ${compacted.summary.originalMessageCount} → ${compacted.summary.compressedMessageCount} messages`;
|
|
109
|
+
try {
|
|
110
|
+
await addSessionMessage(sessionId, "system", summaryText);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (typeof console !== "undefined") {
|
|
114
|
+
console.warn("[run-loop] Failed to persist overflow compaction summary:", err instanceof Error ? err.message : String(err));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
60
118
|
return [...compacted.messages];
|
|
61
119
|
}
|
|
62
120
|
function isContextOverflowError(msg) {
|
|
@@ -72,15 +130,10 @@ async function processStep(deps, input) {
|
|
|
72
130
|
runAbort.signal.addEventListener("abort", onRunAbort, { once: true });
|
|
73
131
|
try {
|
|
74
132
|
if (runAbort.signal.aborted) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
lastStepToolOutcomes: [],
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
if (!deps.permissionGate.checkMaxSteps(input.step, deps.currentAgent)) {
|
|
83
|
-
throw new KernelError("max_steps_exceeded", `Agent max steps exceeded (${input.step})`);
|
|
133
|
+
// RV-7: an abort landing between the loop guard and the step start must
|
|
134
|
+
// not silently report `succeeded` — surface it so the run-loop catch can
|
|
135
|
+
// converge on the single `cancelled` terminal outcome.
|
|
136
|
+
throw new DOMException("Run cancelled", "AbortError");
|
|
84
137
|
}
|
|
85
138
|
if (!deps.permissionGate.checkMaxTokens(input.totalInputTokens, input.totalOutputTokens, deps.currentAgent)) {
|
|
86
139
|
throw new KernelError("max_tokens_exceeded", `Agent max tokens exceeded (${input.totalInputTokens + input.totalOutputTokens})`);
|
|
@@ -89,14 +142,26 @@ async function processStep(deps, input) {
|
|
|
89
142
|
if (deps.thinkingBudget > 0) {
|
|
90
143
|
await deps.modelCaller.doThinkingStep(messages, input.step, runId, ctx, stepTimeoutController.signal);
|
|
91
144
|
}
|
|
92
|
-
if (runModel.countTokens) {
|
|
93
|
-
input.totalInputTokens += messages.reduce((sum, m) => sum + runModel.countTokens(m.content), 0);
|
|
94
|
-
}
|
|
95
145
|
let response;
|
|
96
146
|
try {
|
|
97
|
-
response = await deps.circuitBreaker.call(() => deps.modelCaller.callModelStream(messages, input.step, runId, ctx, stepTimeoutController.signal, deps.currentAgent?.permissions?.maxTokens, input.disableTools));
|
|
147
|
+
response = await deps.circuitBreaker.call(() => deps.modelCaller.callModelStream(messages, input.step, runId, ctx, stepTimeoutController.signal, deps.currentAgent?.permissions?.maxTokens, input.disableTools), stepTimeoutController.signal);
|
|
98
148
|
}
|
|
99
149
|
catch (err) {
|
|
150
|
+
if (stepTimeoutController.signal.aborted && !runAbort.signal.aborted) {
|
|
151
|
+
// Step-level timeout: the step fails but the run continues to the next step.
|
|
152
|
+
return {
|
|
153
|
+
messages,
|
|
154
|
+
step: input.step,
|
|
155
|
+
runId,
|
|
156
|
+
totalInputTokens: input.totalInputTokens,
|
|
157
|
+
totalOutputTokens: input.totalOutputTokens,
|
|
158
|
+
finalOutput: input.finalOutput,
|
|
159
|
+
completed: false,
|
|
160
|
+
toolCallCount: 0,
|
|
161
|
+
lastStepToolOutcomes: [],
|
|
162
|
+
stepFailed: { reason: "timeout", error: `Model call timed out after ${deps.stepTimeout}ms` },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
100
165
|
const circuitErr = err;
|
|
101
166
|
if (circuitErr?.constructor?.name === "CircuitBreakerOpenError") {
|
|
102
167
|
throw new KernelError("model_unavailable", err.message, err);
|
|
@@ -104,15 +169,28 @@ async function processStep(deps, input) {
|
|
|
104
169
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
105
170
|
if (isContextOverflowError(errMsg) && deps.compactor && input.step > 0) {
|
|
106
171
|
const compactor = deps.compactor;
|
|
107
|
-
messages = await compactOnOverflow(messages, compactor, input.step, stepTimeoutController.signal);
|
|
172
|
+
messages = await compactOnOverflow(messages, compactor, input.step, stepTimeoutController.signal, sessionId, deps.addSessionMessage);
|
|
108
173
|
runSessionState?.resetMessages(messages);
|
|
109
|
-
response = await deps.circuitBreaker.call(() => deps.modelCaller.callModelStream(messages, input.step, runId, ctx, stepTimeoutController.signal, deps.currentAgent?.permissions?.maxTokens));
|
|
174
|
+
response = await deps.circuitBreaker.call(() => deps.modelCaller.callModelStream(messages, input.step, runId, ctx, stepTimeoutController.signal, deps.currentAgent?.permissions?.maxTokens), stepTimeoutController.signal);
|
|
110
175
|
}
|
|
111
176
|
else {
|
|
112
177
|
throw err;
|
|
113
178
|
}
|
|
114
179
|
}
|
|
115
|
-
|
|
180
|
+
// RV-42: single authoritative token accounting — prefer the provider's usage
|
|
181
|
+
// (now surfaced on ModelResponse), fall back to local countTokens.
|
|
182
|
+
const usageIn = response.usage?.promptTokens;
|
|
183
|
+
const usageOut = response.usage?.completionTokens;
|
|
184
|
+
if (usageIn !== undefined && usageIn > 0) {
|
|
185
|
+
input.totalInputTokens += usageIn;
|
|
186
|
+
}
|
|
187
|
+
else if (runModel.countTokens) {
|
|
188
|
+
input.totalInputTokens += messages.reduce((sum, m) => sum + runModel.countTokens(getTextContent(m.content)), 0);
|
|
189
|
+
}
|
|
190
|
+
if (usageOut !== undefined && usageOut > 0) {
|
|
191
|
+
input.totalOutputTokens += usageOut;
|
|
192
|
+
}
|
|
193
|
+
else if (runModel.countTokens) {
|
|
116
194
|
input.totalOutputTokens += runModel.countTokens(response.content);
|
|
117
195
|
}
|
|
118
196
|
const toolCalls = response.toolCalls ?? [];
|
|
@@ -149,6 +227,27 @@ async function processStep(deps, input) {
|
|
|
149
227
|
};
|
|
150
228
|
}
|
|
151
229
|
const { toolCallCount, selfCorrectTokens, toolResults } = await deps.stepExecutor.executeToolCalls(toolCalls.map((tc) => ({ toolId: tc.id, toolName: tc.name, args: tc.args })), messages, input.step, runId, ctx, stepTimeoutController, sessionId, runModel);
|
|
230
|
+
if (stepTimeoutController.signal.aborted && !runAbort.signal.aborted) {
|
|
231
|
+
// Step timed out during tool execution — surface an error for any tool call
|
|
232
|
+
// that never got a response so the conversation stays coherent for the model.
|
|
233
|
+
for (const tc of toolCalls) {
|
|
234
|
+
if (!messages.some((m) => m.role === "tool" && m.toolCallId === tc.id)) {
|
|
235
|
+
messages.push({ role: "tool", toolCallId: tc.id, content: `Error: Step timed out after ${deps.stepTimeout}ms` });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
messages,
|
|
240
|
+
step: input.step,
|
|
241
|
+
runId,
|
|
242
|
+
totalInputTokens: input.totalInputTokens,
|
|
243
|
+
totalOutputTokens: input.totalOutputTokens,
|
|
244
|
+
finalOutput: input.finalOutput,
|
|
245
|
+
completed: false,
|
|
246
|
+
toolCallCount,
|
|
247
|
+
lastStepToolOutcomes: toolResults,
|
|
248
|
+
stepFailed: { reason: "timeout", error: `Tool execution timed out after ${deps.stepTimeout}ms` },
|
|
249
|
+
};
|
|
250
|
+
}
|
|
152
251
|
// Accumulate self-correction tokens into run totals
|
|
153
252
|
input.totalInputTokens += selfCorrectTokens.input;
|
|
154
253
|
input.totalOutputTokens += selfCorrectTokens.output;
|
|
@@ -156,19 +255,23 @@ async function processStep(deps, input) {
|
|
|
156
255
|
runSessionState.step = input.step + 1;
|
|
157
256
|
runSessionState.toolCallCount += toolCallCount;
|
|
158
257
|
}
|
|
159
|
-
// Snapshot run state
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
258
|
+
// Snapshot run state — awaited so a write failure is observed before the
|
|
259
|
+
// step is reported complete, preventing silent state loss (RV-33).
|
|
260
|
+
try {
|
|
261
|
+
await deps.store.saveSnapshot(runId, {
|
|
262
|
+
step: input.step + 1,
|
|
263
|
+
model: runModel.model,
|
|
264
|
+
totalInputTokens: input.totalInputTokens,
|
|
265
|
+
totalOutputTokens: input.totalOutputTokens,
|
|
266
|
+
finalOutput: input.finalOutput,
|
|
267
|
+
sessionId,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
168
271
|
if (typeof console !== "undefined") {
|
|
169
272
|
console.warn("[run-loop] Failed to save snapshot:", err instanceof Error ? err.message : String(err));
|
|
170
273
|
}
|
|
171
|
-
}
|
|
274
|
+
}
|
|
172
275
|
return {
|
|
173
276
|
messages,
|
|
174
277
|
step: input.step,
|
|
@@ -190,7 +293,7 @@ async function processStep(deps, input) {
|
|
|
190
293
|
// Run loop
|
|
191
294
|
// ---------------------------------------------------------------------------
|
|
192
295
|
export async function runLoop(deps, input) {
|
|
193
|
-
const { prompt, runId, ctx, runAbort, sessionId, userContentParts, runModel, runSessionState, addSessionMessage, emitEvent, setState,
|
|
296
|
+
const { prompt, runId, ctx, runAbort, sessionId, userContentParts, runModel, runSessionState, addSessionMessage, emitEvent, setState, emitCompleted, emitFail } = input;
|
|
194
297
|
const startTime = Date.now();
|
|
195
298
|
let totalInputTokens = 0;
|
|
196
299
|
let totalOutputTokens = 0;
|
|
@@ -198,53 +301,123 @@ export async function runLoop(deps, input) {
|
|
|
198
301
|
let step = 0;
|
|
199
302
|
let finalOutput = "";
|
|
200
303
|
let contextEpochActive = false;
|
|
201
|
-
|
|
304
|
+
// Real system head (identity + agent systemPrompt) sent as a proper `system`
|
|
305
|
+
// message at the head of the conversation instead of being flattened into the
|
|
306
|
+
// user turn (RV-40).
|
|
307
|
+
const systemHead = input.systemPrompt
|
|
308
|
+
? [{ role: "system", content: input.systemPrompt }]
|
|
309
|
+
: [];
|
|
310
|
+
// Single max-steps authority: resolve the run's step budget from every
|
|
311
|
+
// applicable source (config default, termination policy, agent permission)
|
|
312
|
+
// once, then enforce it in exactly one place — the loop bound below.
|
|
313
|
+
const runMaxSteps = Math.min(deps.termination?.maxSteps ?? Number.POSITIVE_INFINITY, deps.currentAgent?.permissions?.maxSteps ?? Number.POSITIVE_INFINITY, deps.maxSteps);
|
|
202
314
|
const emitEvt = (type, data) => emitEvent({
|
|
203
315
|
id: crypto.randomUUID(), runId, type,
|
|
204
316
|
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
205
317
|
data,
|
|
206
318
|
});
|
|
319
|
+
const saveFinalSnapshot = async (status) => {
|
|
320
|
+
try {
|
|
321
|
+
await deps.store.saveSnapshot(runId, {
|
|
322
|
+
step: step + 1,
|
|
323
|
+
model: runModel.model,
|
|
324
|
+
totalInputTokens,
|
|
325
|
+
totalOutputTokens,
|
|
326
|
+
finalOutput,
|
|
327
|
+
sessionId,
|
|
328
|
+
status,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
if (typeof console !== "undefined") {
|
|
333
|
+
console.warn("[run-loop] Failed to save final snapshot:", err instanceof Error ? err.message : String(err));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
// RV-7: exactly ONE terminal cancelled outcome regardless of abort timing.
|
|
338
|
+
// Every cancellation path funnels through this single block (loop guard,
|
|
339
|
+
// step-start abort, in-flight AbortError, and terminal-emission guards), so a
|
|
340
|
+
// run cancelled at any moment reports `cancelled` exactly once — never a
|
|
341
|
+
// silent `succeeded` or a confusing `failed`.
|
|
342
|
+
const cancelRun = async (steps) => {
|
|
343
|
+
await emitFail(runId, ctx, "Run cancelled", steps, sessionId, totalInputTokens, totalOutputTokens, Date.now() - startTime, true);
|
|
344
|
+
await deps.saga.rollbackAll();
|
|
345
|
+
setState(runId, "cancelled");
|
|
346
|
+
if (runSessionState)
|
|
347
|
+
runSessionState.isRunning = false;
|
|
348
|
+
await saveFinalSnapshot("cancelled");
|
|
349
|
+
return { totalSteps: steps, status: "cancelled" };
|
|
350
|
+
};
|
|
207
351
|
try {
|
|
208
352
|
setState(runId, "running");
|
|
353
|
+
// Re-queue any persisted-but-un-promoted inputs before the first step drains (RV-21).
|
|
354
|
+
if (deps.beforeRun) {
|
|
355
|
+
await deps.beforeRun(runId);
|
|
356
|
+
}
|
|
209
357
|
const startedModel = runModel.model;
|
|
210
358
|
const startedAgentName = deps.currentAgent?.profile.name;
|
|
211
359
|
const startedAgentId = deps.currentAgent?.id;
|
|
212
360
|
// Skip run.started event when resuming from durable storage
|
|
213
361
|
if (!input.resume) {
|
|
362
|
+
// Audit + session record keep the full effective prompt (system head +
|
|
363
|
+
// user prompt); the messages sent to the model split them into a real
|
|
364
|
+
// `system` head + `user` turn (RV-40).
|
|
365
|
+
const effectivePrompt = [input.systemPrompt, prompt].filter(Boolean).join("\n\n");
|
|
214
366
|
await emitEvt("run.started", {
|
|
215
|
-
prompt,
|
|
367
|
+
prompt: effectivePrompt,
|
|
216
368
|
...(startedModel ? { model: startedModel } : {}),
|
|
217
369
|
...(startedAgentName ? { agentName: startedAgentName } : {}),
|
|
218
370
|
...(startedAgentId ? { agentId: startedAgentId } : {}),
|
|
219
371
|
});
|
|
220
372
|
await deps.pluginManager?.fireHook("onRunStarted", { runId, prompt });
|
|
221
373
|
const currentModel = runModel.model;
|
|
222
|
-
await addSessionMessage(sessionId, "user",
|
|
374
|
+
await addSessionMessage(sessionId, "user", effectivePrompt, { ...(currentModel ? { model: currentModel } : {}) });
|
|
223
375
|
messages = [
|
|
376
|
+
...systemHead,
|
|
224
377
|
userContentParts?.length
|
|
225
|
-
? { role: "user", content:
|
|
378
|
+
? { role: "user", content: userContentParts }
|
|
226
379
|
: { role: "user", content: prompt },
|
|
227
380
|
];
|
|
228
381
|
}
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
382
|
+
// On resume, rebuild from persisted history (never double-append it) and keep
|
|
383
|
+
// the current steering prompt as the latest user turn. On a continuation run
|
|
384
|
+
// the conversation history must come BEFORE the fresh prompt so the
|
|
385
|
+
// transcript stays chronologically ordered (the prompt was seeded above).
|
|
386
|
+
if (input.resume) {
|
|
387
|
+
if (runSessionState && runSessionState.messages.length > 0) {
|
|
388
|
+
messages = [...runSessionState.messages];
|
|
389
|
+
}
|
|
390
|
+
if (prompt)
|
|
391
|
+
messages.push({ role: "user", content: prompt });
|
|
392
|
+
// Persisted history predates the RV-40 system head — re-inject it so a
|
|
393
|
+
// resumed run still sends a real `system` message to the model.
|
|
394
|
+
if (systemHead.length > 0 && !messages.some((m) => m.role === "system")) {
|
|
395
|
+
messages = [...systemHead, ...messages];
|
|
396
|
+
}
|
|
232
397
|
}
|
|
233
|
-
if (runSessionState && runSessionState.messages.length > 0) {
|
|
234
|
-
messages
|
|
398
|
+
else if (runSessionState && runSessionState.messages.length > 0) {
|
|
399
|
+
messages = [
|
|
400
|
+
...systemHead,
|
|
401
|
+
...runSessionState.messages,
|
|
402
|
+
userContentParts?.length
|
|
403
|
+
? { role: "user", content: userContentParts }
|
|
404
|
+
: { role: "user", content: prompt },
|
|
405
|
+
];
|
|
235
406
|
}
|
|
236
|
-
|
|
237
|
-
|
|
407
|
+
// Continue from the restored step counter so max-steps accounting and any
|
|
408
|
+
// step-relative logic stay continuous across a resume.
|
|
409
|
+
const startedStep = runSessionState?.step ?? 0;
|
|
410
|
+
for (step = startedStep; step < runMaxSteps; step++) {
|
|
238
411
|
if (runAbort.signal.aborted) {
|
|
239
|
-
|
|
240
|
-
await deps.saga.rollbackAll();
|
|
241
|
-
setState(runId, "cancelled");
|
|
242
|
-
return;
|
|
412
|
+
return cancelRun(step);
|
|
243
413
|
}
|
|
244
414
|
const drainedInputs = deps.stateMachine.drainInputs(runId);
|
|
245
415
|
for (const text of drainedInputs) {
|
|
246
416
|
messages.push({ role: "user", content: text });
|
|
247
417
|
}
|
|
418
|
+
if (drainedInputs.length > 0 && deps.onInputsDrained) {
|
|
419
|
+
await deps.onInputsDrained(runId, drainedInputs);
|
|
420
|
+
}
|
|
248
421
|
if (deps.systemContext) {
|
|
249
422
|
if (!contextEpochActive) {
|
|
250
423
|
const result = await initializeSystemContext(deps.systemContext, messages);
|
|
@@ -262,7 +435,7 @@ export async function runLoop(deps, input) {
|
|
|
262
435
|
const compactResult = await maybeCompact(messages, {
|
|
263
436
|
...(runModel.countTokens ? { countTokens: runModel.countTokens } : {}),
|
|
264
437
|
...(runModel.contextLimit !== undefined ? { contextLimit: runModel.contextLimit } : {}),
|
|
265
|
-
}, deps, runAbort.signal, (type, data) => emitEvt(type, data));
|
|
438
|
+
}, deps, runAbort.signal, sessionId, (type, data) => emitEvt(type, data));
|
|
266
439
|
if (compactResult.didCompact) {
|
|
267
440
|
messages = compactResult.messages;
|
|
268
441
|
runSessionState?.resetMessages(compactResult.messages);
|
|
@@ -270,13 +443,13 @@ export async function runLoop(deps, input) {
|
|
|
270
443
|
contextEpochActive = false;
|
|
271
444
|
}
|
|
272
445
|
}
|
|
273
|
-
if (step >=
|
|
446
|
+
if (step >= runMaxSteps - 1) {
|
|
274
447
|
messages.push({
|
|
275
448
|
role: "system",
|
|
276
|
-
content: `[You have reached the maximum number of steps (${
|
|
449
|
+
content: `[You have reached the maximum number of steps (${runMaxSteps}). This is your final opportunity to respond. Do NOT call any tools. Provide a comprehensive summary and any final output.]`,
|
|
277
450
|
});
|
|
278
451
|
}
|
|
279
|
-
const onLastStep = step >=
|
|
452
|
+
const onLastStep = step >= runMaxSteps - 1;
|
|
280
453
|
const stepResult = await processStep(deps, {
|
|
281
454
|
messages, step, runId, ctx, runAbort,
|
|
282
455
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
@@ -289,8 +462,14 @@ export async function runLoop(deps, input) {
|
|
|
289
462
|
totalInputTokens = stepResult.totalInputTokens;
|
|
290
463
|
totalOutputTokens = stepResult.totalOutputTokens;
|
|
291
464
|
finalOutput = stepResult.finalOutput;
|
|
292
|
-
|
|
293
|
-
|
|
465
|
+
if (stepResult.stepFailed) {
|
|
466
|
+
await emitEvt("step.failed", { step, reason: stepResult.stepFailed.reason, ...(stepResult.stepFailed.error ? { error: stepResult.stepFailed.error } : {}) });
|
|
467
|
+
await deps.pluginManager?.fireHook("onStepFailed", { step, reason: stepResult.stepFailed.reason, ...(stepResult.stepFailed.error ? { error: stepResult.stepFailed.error } : {}) });
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
await emitEvt("step.completed", { step, toolCallCount: stepResult.toolCallCount });
|
|
471
|
+
await deps.pluginManager?.fireHook("onStepCompleted", { step, toolCallCount: stepResult.toolCallCount });
|
|
472
|
+
}
|
|
294
473
|
if (stepResult.completed) {
|
|
295
474
|
finalOutput = stepResult.finalOutput;
|
|
296
475
|
break;
|
|
@@ -305,11 +484,13 @@ export async function runLoop(deps, input) {
|
|
|
305
484
|
(budget.output !== undefined && totalOutputTokens > budget.output) ||
|
|
306
485
|
(budget.total !== undefined && total > budget.total);
|
|
307
486
|
if (over) {
|
|
487
|
+
if (runAbort.signal.aborted)
|
|
488
|
+
return cancelRun(step + 1);
|
|
308
489
|
const reason = `Token budget exceeded (${total} tokens)`;
|
|
309
490
|
await emitFail(runId, ctx, reason, step, sessionId, totalInputTokens, totalOutputTokens);
|
|
310
491
|
await deps.saga.rollbackAll();
|
|
311
492
|
setState(runId, "failed");
|
|
312
|
-
return;
|
|
493
|
+
return { totalSteps: step + 1, status: "failed" };
|
|
313
494
|
}
|
|
314
495
|
}
|
|
315
496
|
const stopCtx = {
|
|
@@ -349,13 +530,19 @@ export async function runLoop(deps, input) {
|
|
|
349
530
|
}
|
|
350
531
|
}
|
|
351
532
|
if (stopReason) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
533
|
+
if (runAbort.signal.aborted)
|
|
534
|
+
return cancelRun(step + 1);
|
|
535
|
+
await emitCompleted({
|
|
536
|
+
id: crypto.randomUUID(), runId, type: "run.completed",
|
|
537
|
+
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
538
|
+
data: {
|
|
539
|
+
status: "succeeded", output: finalOutput, totalSteps: step + 1,
|
|
540
|
+
durationMs: Date.now() - startTime,
|
|
541
|
+
stopCondition: stopReason,
|
|
542
|
+
...(totalInputTokens > 0 ? { inputTokens: totalInputTokens } : {}),
|
|
543
|
+
...(totalOutputTokens > 0 ? { outputTokens: totalOutputTokens } : {}),
|
|
544
|
+
},
|
|
545
|
+
}, sessionId, runId, totalInputTokens, totalOutputTokens, "succeeded");
|
|
359
546
|
await deps.pluginManager?.fireHook("onRunCompleted", { status: "succeeded", output: finalOutput, stopCondition: stopReason });
|
|
360
547
|
setState(runId, "completed");
|
|
361
548
|
if (runSessionState) {
|
|
@@ -363,20 +550,26 @@ export async function runLoop(deps, input) {
|
|
|
363
550
|
runSessionState.step = step + 1;
|
|
364
551
|
runSessionState.isRunning = false;
|
|
365
552
|
}
|
|
366
|
-
await
|
|
367
|
-
return;
|
|
553
|
+
await saveFinalSnapshot("succeeded");
|
|
554
|
+
return { totalSteps: step + 1, status: "succeeded" };
|
|
368
555
|
}
|
|
369
556
|
}
|
|
370
557
|
}
|
|
371
558
|
const durationMs = Date.now() - startTime;
|
|
372
|
-
if (step >=
|
|
559
|
+
if (step >= runMaxSteps) {
|
|
560
|
+
if (runAbort.signal.aborted)
|
|
561
|
+
return cancelRun(step + 1);
|
|
373
562
|
if (finalOutput) {
|
|
374
|
-
await
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
563
|
+
await emitCompleted({
|
|
564
|
+
id: crypto.randomUUID(), runId, type: "run.completed",
|
|
565
|
+
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
566
|
+
data: {
|
|
567
|
+
status: "succeeded", output: finalOutput, totalSteps: step + 1,
|
|
568
|
+
durationMs,
|
|
569
|
+
...(totalInputTokens > 0 ? { inputTokens: totalInputTokens } : {}),
|
|
570
|
+
...(totalOutputTokens > 0 ? { outputTokens: totalOutputTokens } : {}),
|
|
571
|
+
},
|
|
572
|
+
}, sessionId, runId, totalInputTokens, totalOutputTokens, "succeeded");
|
|
380
573
|
await deps.pluginManager?.fireHook("onRunCompleted", { status: "succeeded", output: finalOutput });
|
|
381
574
|
if (deps.sessionStore && deps.sessionTitleGenerator && sessionId) {
|
|
382
575
|
const session = await deps.sessionStore.getSession(sessionId).catch(() => null);
|
|
@@ -391,24 +584,30 @@ export async function runLoop(deps, input) {
|
|
|
391
584
|
}
|
|
392
585
|
}
|
|
393
586
|
}
|
|
394
|
-
if (sessionId) {
|
|
395
|
-
await updateSessionOnComplete(sessionId, runId, totalInputTokens, totalOutputTokens, step + 1, "succeeded");
|
|
396
|
-
}
|
|
397
587
|
setState(runId, "completed");
|
|
398
|
-
|
|
588
|
+
await saveFinalSnapshot("succeeded");
|
|
589
|
+
return { totalSteps: step + 1, status: "succeeded" };
|
|
399
590
|
}
|
|
400
|
-
|
|
591
|
+
if (runAbort.signal.aborted)
|
|
592
|
+
return cancelRun(step + 1);
|
|
593
|
+
await emitFail(runId, ctx, `Exceeded max steps (${runMaxSteps})`, step, sessionId, totalInputTokens, totalOutputTokens, durationMs);
|
|
401
594
|
await deps.saga.rollbackAll();
|
|
402
595
|
setState(runId, "failed");
|
|
403
|
-
return;
|
|
596
|
+
return { totalSteps: step, status: "failed" };
|
|
404
597
|
}
|
|
405
598
|
else {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
599
|
+
if (runAbort.signal.aborted)
|
|
600
|
+
return cancelRun(step + 1);
|
|
601
|
+
await emitCompleted({
|
|
602
|
+
id: crypto.randomUUID(), runId, type: "run.completed",
|
|
603
|
+
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
604
|
+
data: {
|
|
605
|
+
status: "succeeded", output: finalOutput, totalSteps: step + 1,
|
|
606
|
+
durationMs,
|
|
607
|
+
...(totalInputTokens > 0 ? { inputTokens: totalInputTokens } : {}),
|
|
608
|
+
...(totalOutputTokens > 0 ? { outputTokens: totalOutputTokens } : {}),
|
|
609
|
+
},
|
|
610
|
+
}, sessionId, runId, totalInputTokens, totalOutputTokens, "succeeded");
|
|
412
611
|
await deps.pluginManager?.fireHook("onRunCompleted", { status: "succeeded", output: finalOutput });
|
|
413
612
|
if (deps.sessionStore && deps.sessionTitleGenerator && sessionId) {
|
|
414
613
|
const session = await deps.sessionStore.getSession(sessionId).catch(() => null);
|
|
@@ -430,16 +629,25 @@ export async function runLoop(deps, input) {
|
|
|
430
629
|
runSessionState.step = step + 1;
|
|
431
630
|
runSessionState.isRunning = false;
|
|
432
631
|
}
|
|
433
|
-
await
|
|
632
|
+
await saveFinalSnapshot("succeeded");
|
|
633
|
+
return { totalSteps: step + 1, status: "succeeded" };
|
|
434
634
|
}
|
|
435
635
|
catch (err) {
|
|
636
|
+
// A cancellation that surfaced as an abort (e.g. the model call rejecting
|
|
637
|
+
// with AbortError, or the step-start guard) must converge on the single
|
|
638
|
+
// `cancelled` terminal outcome — never a confusing `failed`.
|
|
639
|
+
if (runAbort.signal.aborted) {
|
|
640
|
+
return cancelRun(step + 1);
|
|
641
|
+
}
|
|
436
642
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
437
643
|
const failDurationMs = Date.now() - startTime;
|
|
438
|
-
// emitFail already emits run.completed(failed)
|
|
644
|
+
// emitFail already emits run.completed(failed); roll back the per-run saga explicitly.
|
|
645
|
+
await deps.saga.rollbackAll();
|
|
439
646
|
await emitFail(runId, ctx, errorMsg, step, sessionId, totalInputTokens, totalOutputTokens, failDurationMs);
|
|
440
647
|
setState(runId, "failed");
|
|
441
648
|
if (runSessionState)
|
|
442
649
|
runSessionState.isRunning = false;
|
|
650
|
+
return { totalSteps: step + 1, status: "failed" };
|
|
443
651
|
}
|
|
444
652
|
finally {
|
|
445
653
|
deps.saga.clear();
|