@oni.bot/core 1.0.2 → 1.0.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/CHANGELOG.md +146 -146
- package/dist/agents/define-agent.d.ts.map +1 -1
- package/dist/agents/define-agent.js +7 -2
- package/dist/agents/define-agent.js.map +1 -1
- package/dist/checkpoint.d.ts.map +1 -1
- package/dist/checkpoint.js +7 -2
- package/dist/checkpoint.js.map +1 -1
- package/dist/checkpointers/postgres.d.ts.map +1 -1
- package/dist/checkpointers/postgres.js +43 -27
- package/dist/checkpointers/postgres.js.map +1 -1
- package/dist/circuit-breaker.d.ts +1 -0
- package/dist/circuit-breaker.d.ts.map +1 -1
- package/dist/circuit-breaker.js +13 -0
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +0 -1
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +0 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +12 -4
- package/dist/config/loader.js.map +1 -1
- package/dist/coordination/pubsub.d.ts +1 -0
- package/dist/coordination/pubsub.d.ts.map +1 -1
- package/dist/coordination/pubsub.js +31 -16
- package/dist/coordination/pubsub.js.map +1 -1
- package/dist/coordination/request-reply.d.ts +6 -0
- package/dist/coordination/request-reply.d.ts.map +1 -1
- package/dist/coordination/request-reply.js +56 -14
- package/dist/coordination/request-reply.js.map +1 -1
- package/dist/events/bus.d.ts +1 -0
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +16 -10
- package/dist/events/bus.js.map +1 -1
- package/dist/functional.d.ts.map +1 -1
- package/dist/functional.js +3 -0
- package/dist/functional.js.map +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +5 -2
- package/dist/graph.js.map +1 -1
- package/dist/guardrails/audit.d.ts +4 -1
- package/dist/guardrails/audit.d.ts.map +1 -1
- package/dist/guardrails/audit.js +18 -1
- package/dist/guardrails/audit.js.map +1 -1
- package/dist/harness/agent-loop.d.ts.map +1 -1
- package/dist/harness/agent-loop.js +471 -352
- package/dist/harness/agent-loop.js.map +1 -1
- package/dist/harness/context-compactor.d.ts +1 -0
- package/dist/harness/context-compactor.d.ts.map +1 -1
- package/dist/harness/context-compactor.js +43 -1
- package/dist/harness/context-compactor.js.map +1 -1
- package/dist/harness/harness.d.ts +6 -0
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +32 -5
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/hooks-engine.d.ts.map +1 -1
- package/dist/harness/hooks-engine.js +12 -10
- package/dist/harness/hooks-engine.js.map +1 -1
- package/dist/harness/index.d.ts +3 -1
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +2 -0
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/memory-loader.d.ts +150 -0
- package/dist/harness/memory-loader.d.ts.map +1 -0
- package/dist/harness/memory-loader.js +714 -0
- package/dist/harness/memory-loader.js.map +1 -0
- package/dist/harness/safety-gate.d.ts.map +1 -1
- package/dist/harness/safety-gate.js +47 -26
- package/dist/harness/safety-gate.js.map +1 -1
- package/dist/harness/skill-loader.d.ts +7 -0
- package/dist/harness/skill-loader.d.ts.map +1 -1
- package/dist/harness/skill-loader.js +24 -8
- package/dist/harness/skill-loader.js.map +1 -1
- package/dist/harness/todo-module.d.ts.map +1 -1
- package/dist/harness/todo-module.js +13 -6
- package/dist/harness/todo-module.js.map +1 -1
- package/dist/harness/types.d.ts +7 -0
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/validate-args.js +18 -3
- package/dist/harness/validate-args.js.map +1 -1
- package/dist/hitl/interrupt.d.ts +2 -2
- package/dist/hitl/interrupt.d.ts.map +1 -1
- package/dist/hitl/interrupt.js +6 -4
- package/dist/hitl/interrupt.js.map +1 -1
- package/dist/hitl/resume.d.ts +10 -0
- package/dist/hitl/resume.d.ts.map +1 -1
- package/dist/hitl/resume.js +31 -0
- package/dist/hitl/resume.js.map +1 -1
- package/dist/injected.d.ts.map +1 -1
- package/dist/injected.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +28 -8
- package/dist/inspect.js.map +1 -1
- package/dist/lsp/client.d.ts +2 -0
- package/dist/lsp/client.d.ts.map +1 -1
- package/dist/lsp/client.js +62 -17
- package/dist/lsp/client.js.map +1 -1
- package/dist/lsp/index.d.ts.map +1 -1
- package/dist/lsp/index.js.map +1 -1
- package/dist/mcp/client.d.ts +2 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +44 -13
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/convert.js +1 -1
- package/dist/mcp/convert.js.map +1 -1
- package/dist/mcp/transport.d.ts +2 -0
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +33 -8
- package/dist/mcp/transport.js.map +1 -1
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/index.js +7 -1
- package/dist/messages/index.js.map +1 -1
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +25 -15
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/google.d.ts.map +1 -1
- package/dist/models/google.js +23 -7
- package/dist/models/google.js.map +1 -1
- package/dist/models/ollama.d.ts.map +1 -1
- package/dist/models/ollama.js +11 -1
- package/dist/models/ollama.js.map +1 -1
- package/dist/models/openai.d.ts.map +1 -1
- package/dist/models/openai.js +15 -3
- package/dist/models/openai.js.map +1 -1
- package/dist/models/openrouter.d.ts.map +1 -1
- package/dist/models/openrouter.js +14 -3
- package/dist/models/openrouter.js.map +1 -1
- package/dist/prebuilt/react-agent.d.ts.map +1 -1
- package/dist/prebuilt/react-agent.js +1 -0
- package/dist/prebuilt/react-agent.js.map +1 -1
- package/dist/pregel.d.ts +9 -6
- package/dist/pregel.d.ts.map +1 -1
- package/dist/pregel.js +89 -39
- package/dist/pregel.js.map +1 -1
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +7 -6
- package/dist/retry.js.map +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +36 -9
- package/dist/store/index.js.map +1 -1
- package/dist/stream-events.d.ts.map +1 -1
- package/dist/stream-events.js +3 -9
- package/dist/stream-events.js.map +1 -1
- package/dist/swarm/graph.d.ts +15 -2
- package/dist/swarm/graph.d.ts.map +1 -1
- package/dist/swarm/graph.js +43 -14
- package/dist/swarm/graph.js.map +1 -1
- package/dist/swarm/index.d.ts +2 -1
- package/dist/swarm/index.d.ts.map +1 -1
- package/dist/swarm/index.js.map +1 -1
- package/dist/swarm/mailbox.d.ts.map +1 -1
- package/dist/swarm/mailbox.js +3 -1
- package/dist/swarm/mailbox.js.map +1 -1
- package/dist/swarm/mermaid.d.ts +2 -1
- package/dist/swarm/mermaid.d.ts.map +1 -1
- package/dist/swarm/mermaid.js +6 -3
- package/dist/swarm/mermaid.js.map +1 -1
- package/dist/swarm/pool.d.ts.map +1 -1
- package/dist/swarm/pool.js +18 -1
- package/dist/swarm/pool.js.map +1 -1
- package/dist/swarm/registry.d.ts.map +1 -1
- package/dist/swarm/registry.js +7 -0
- package/dist/swarm/registry.js.map +1 -1
- package/dist/swarm/scaling.d.ts +10 -1
- package/dist/swarm/scaling.d.ts.map +1 -1
- package/dist/swarm/scaling.js +85 -14
- package/dist/swarm/scaling.js.map +1 -1
- package/dist/swarm/snapshot.d.ts.map +1 -1
- package/dist/swarm/snapshot.js +10 -1
- package/dist/swarm/snapshot.js.map +1 -1
- package/dist/swarm/supervisor.js +20 -12
- package/dist/swarm/supervisor.js.map +1 -1
- package/dist/swarm/tracer.d.ts +3 -1
- package/dist/swarm/tracer.d.ts.map +1 -1
- package/dist/swarm/tracer.js +66 -15
- package/dist/swarm/tracer.js.map +1 -1
- package/dist/swarm/types.d.ts +1 -6
- package/dist/swarm/types.d.ts.map +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// ============================================================
|
|
5
5
|
import { generateId } from "./types.js";
|
|
6
6
|
import { validateToolArgs } from "./validate-args.js";
|
|
7
|
+
import { MemoryLoader } from "./memory-loader.js";
|
|
7
8
|
// ─── agentLoop ─────────────────────────────────────────────────────────────
|
|
8
9
|
export async function* agentLoop(prompt, config) {
|
|
9
10
|
const sessionId = generateId("ses");
|
|
@@ -13,6 +14,27 @@ export async function* agentLoop(prompt, config) {
|
|
|
13
14
|
? [...config.initialMessages]
|
|
14
15
|
: [];
|
|
15
16
|
let turn = 0;
|
|
17
|
+
// ── 0. Memory init ───────────────────────────────────────────────────────
|
|
18
|
+
const memoryLoader = config.memoryRoot
|
|
19
|
+
? MemoryLoader.fromRoot(config.memoryRoot, {
|
|
20
|
+
budgets: config.memoryBudgets,
|
|
21
|
+
debug: config.memoryDebug,
|
|
22
|
+
})
|
|
23
|
+
: null;
|
|
24
|
+
let memoryContext = "";
|
|
25
|
+
if (memoryLoader && !config.signal?.aborted) {
|
|
26
|
+
memoryLoader.wake();
|
|
27
|
+
memoryLoader.orient();
|
|
28
|
+
const t2 = memoryLoader.match(prompt);
|
|
29
|
+
if (config.memoryDebug && t2.dropped.length > 0) {
|
|
30
|
+
console.log(`[MemoryLoader] match() dropped ${t2.dropped.length} units over T2 budget`);
|
|
31
|
+
}
|
|
32
|
+
memoryContext = memoryLoader.buildSystemPrompt([0, 1, 2]);
|
|
33
|
+
}
|
|
34
|
+
const effectiveSystemPrompt = memoryContext
|
|
35
|
+
? [memoryContext, config.systemPrompt].filter(Boolean).join("\n\n")
|
|
36
|
+
: config.systemPrompt;
|
|
37
|
+
let sessionOutcome = "completed";
|
|
16
38
|
// ── 1. Session Init ──────────────────────────────────────────────────
|
|
17
39
|
if (config.hooksEngine) {
|
|
18
40
|
const hookResult = await config.hooksEngine.fire("SessionStart", {
|
|
@@ -25,6 +47,12 @@ export async function* agentLoop(prompt, config) {
|
|
|
25
47
|
messages.push({ role: "assistant", content: "Context loaded." });
|
|
26
48
|
}
|
|
27
49
|
}
|
|
50
|
+
// ── 1b. Build allTools (base tools + memory_query if memory is active) ──
|
|
51
|
+
// Note: config.tools is used in SessionStart hooks (intentional — memory_query
|
|
52
|
+
// is a runtime capability, not a base tool). allTools is used for llmTools/toolMap.
|
|
53
|
+
const allTools = memoryLoader
|
|
54
|
+
? [...config.tools, memoryLoader.getQueryTool()]
|
|
55
|
+
: config.tools;
|
|
28
56
|
// Push user prompt
|
|
29
57
|
messages.push({ role: "user", content: prompt });
|
|
30
58
|
yield makeMessage("system", sessionId, turn, {
|
|
@@ -32,423 +60,478 @@ export async function* agentLoop(prompt, config) {
|
|
|
32
60
|
content: `Session ${sessionId} started for agent "${config.agentName}"`,
|
|
33
61
|
});
|
|
34
62
|
// ── 2. Build LLMToolDef[] ────────────────────────────────────────────
|
|
35
|
-
const llmTools =
|
|
63
|
+
const llmTools = allTools.map((t) => ({
|
|
36
64
|
name: t.name,
|
|
37
65
|
description: t.description,
|
|
38
66
|
parameters: t.schema,
|
|
39
67
|
}));
|
|
40
68
|
// Tool lookup map
|
|
41
69
|
const toolMap = new Map();
|
|
42
|
-
for (const t of
|
|
70
|
+
for (const t of allTools) {
|
|
43
71
|
toolMap.set(t.name, t);
|
|
44
72
|
}
|
|
45
73
|
// ── 3. Main Loop ─────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
// ── 3b. Context Compaction ───────────────────────────────────────
|
|
55
|
-
if (config.compactor) {
|
|
56
|
-
const compactionCheck = config.compactor.checkCompaction(messages);
|
|
57
|
-
if (compactionCheck.needed) {
|
|
58
|
-
const beforeCount = messages.length;
|
|
59
|
-
yield makeMessage("system", sessionId, turn, {
|
|
60
|
-
subtype: "compact_start",
|
|
61
|
-
content: "Context compaction starting",
|
|
62
|
-
metadata: {
|
|
63
|
-
beforeCount,
|
|
64
|
-
estimatedTokens: compactionCheck.estimatedTokens,
|
|
65
|
-
threshold: compactionCheck.threshold,
|
|
66
|
-
maxTokens: compactionCheck.maxTokens,
|
|
67
|
-
percentUsed: compactionCheck.percentUsed,
|
|
68
|
-
},
|
|
74
|
+
try {
|
|
75
|
+
while (turn < maxTurns) {
|
|
76
|
+
// ── 3a. Check AbortSignal ────────────────────────────────────────
|
|
77
|
+
if (config.signal?.aborted) {
|
|
78
|
+
sessionOutcome = "interrupted";
|
|
79
|
+
yield makeMessage("error", sessionId, turn, {
|
|
80
|
+
content: "Agent loop aborted by signal",
|
|
69
81
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
const compacted = await config.compactor.compact(messages, { skipInitialCheck: true });
|
|
79
|
-
const estimatedTokensAfter = config.compactor.estimateTokens(compacted);
|
|
80
|
-
const percentUsedAfter = compactionCheck.maxTokens > 0
|
|
81
|
-
? estimatedTokensAfter / compactionCheck.maxTokens
|
|
82
|
-
: 0;
|
|
83
|
-
const afterCount = compacted.length;
|
|
84
|
-
const summarized = afterCount <= 2 && beforeCount > 2;
|
|
85
|
-
messages.length = 0;
|
|
86
|
-
messages.push(...compacted);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// ── 3b. Context Compaction ───────────────────────────────────────
|
|
85
|
+
if (config.compactor) {
|
|
86
|
+
const compactionCheck = config.compactor.checkCompaction(messages);
|
|
87
|
+
if (compactionCheck.needed) {
|
|
88
|
+
const beforeCount = messages.length;
|
|
87
89
|
yield makeMessage("system", sessionId, turn, {
|
|
88
|
-
subtype: "
|
|
89
|
-
content: "Context
|
|
90
|
+
subtype: "compact_start",
|
|
91
|
+
content: "Context compaction starting",
|
|
90
92
|
metadata: {
|
|
91
93
|
beforeCount,
|
|
92
|
-
|
|
93
|
-
summarized,
|
|
94
|
-
estimatedTokensBefore: compactionCheck.estimatedTokens,
|
|
95
|
-
estimatedTokensAfter,
|
|
94
|
+
estimatedTokens: compactionCheck.estimatedTokens,
|
|
96
95
|
threshold: compactionCheck.threshold,
|
|
97
96
|
maxTokens: compactionCheck.maxTokens,
|
|
98
|
-
|
|
99
|
-
percentUsedAfter,
|
|
97
|
+
percentUsed: compactionCheck.percentUsed,
|
|
100
98
|
},
|
|
101
99
|
});
|
|
100
|
+
try {
|
|
101
|
+
if (config.hooksEngine) {
|
|
102
|
+
await config.hooksEngine.fire("PreCompact", {
|
|
103
|
+
sessionId,
|
|
104
|
+
messageCount: beforeCount,
|
|
105
|
+
estimatedTokens: compactionCheck.estimatedTokens,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const compacted = await config.compactor.compact(messages, { skipInitialCheck: true });
|
|
109
|
+
const estimatedTokensAfter = config.compactor.estimateTokens(compacted);
|
|
110
|
+
const percentUsedAfter = compactionCheck.maxTokens > 0
|
|
111
|
+
? estimatedTokensAfter / compactionCheck.maxTokens
|
|
112
|
+
: 0;
|
|
113
|
+
const afterCount = compacted.length;
|
|
114
|
+
const summarized = afterCount <= 2 && beforeCount > 2;
|
|
115
|
+
messages.length = 0;
|
|
116
|
+
messages.push(...compacted);
|
|
117
|
+
yield makeMessage("system", sessionId, turn, {
|
|
118
|
+
subtype: "compact_boundary",
|
|
119
|
+
content: "Context compacted",
|
|
120
|
+
metadata: {
|
|
121
|
+
beforeCount,
|
|
122
|
+
afterCount,
|
|
123
|
+
summarized,
|
|
124
|
+
estimatedTokensBefore: compactionCheck.estimatedTokens,
|
|
125
|
+
estimatedTokensAfter,
|
|
126
|
+
threshold: compactionCheck.threshold,
|
|
127
|
+
maxTokens: compactionCheck.maxTokens,
|
|
128
|
+
percentUsedBefore: compactionCheck.percentUsed,
|
|
129
|
+
percentUsedAfter,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
if (config.hooksEngine) {
|
|
133
|
+
await config.hooksEngine.fire("PostCompact", {
|
|
134
|
+
sessionId,
|
|
135
|
+
beforeCount,
|
|
136
|
+
afterCount,
|
|
137
|
+
estimatedTokensAfter,
|
|
138
|
+
summarized,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
144
|
+
yield makeMessage("system", sessionId, turn, {
|
|
145
|
+
subtype: "compact_error",
|
|
146
|
+
content: `Context compaction failed: ${errorMsg}`,
|
|
147
|
+
metadata: {
|
|
148
|
+
beforeCount,
|
|
149
|
+
afterCount: beforeCount,
|
|
150
|
+
estimatedTokensBefore: compactionCheck.estimatedTokens,
|
|
151
|
+
threshold: compactionCheck.threshold,
|
|
152
|
+
maxTokens: compactionCheck.maxTokens,
|
|
153
|
+
percentUsedBefore: compactionCheck.percentUsed,
|
|
154
|
+
error: errorMsg,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ── 3c. Build system prompt ──────────────────────────────────────
|
|
161
|
+
let systemPrompt = effectiveSystemPrompt;
|
|
162
|
+
// Inject remaining turns so the model knows its budget
|
|
163
|
+
const remaining = maxTurns - turn;
|
|
164
|
+
systemPrompt += `\n\nYou have ${remaining} turns remaining. Each turn lets you call multiple tools. Do NOT stop early — use your tools and complete the task autonomously.`;
|
|
165
|
+
if (config.env) {
|
|
166
|
+
const envLines = [];
|
|
167
|
+
if (config.env.cwd)
|
|
168
|
+
envLines.push(`Working directory: ${config.env.cwd}`);
|
|
169
|
+
if (config.env.platform)
|
|
170
|
+
envLines.push(`Platform: ${config.env.platform}`);
|
|
171
|
+
if (config.env.date)
|
|
172
|
+
envLines.push(`Date: ${config.env.date}`);
|
|
173
|
+
if (config.env.gitBranch)
|
|
174
|
+
envLines.push(`Git branch: ${config.env.gitBranch}`);
|
|
175
|
+
if (config.env.gitStatus)
|
|
176
|
+
envLines.push(`Git status: ${config.env.gitStatus}`);
|
|
177
|
+
if (envLines.length > 0) {
|
|
178
|
+
systemPrompt += `\n\n<env>\n${envLines.join("\n")}\n</env>`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ── 3d. Skill injection ──────────────────────────────────────────
|
|
182
|
+
if (config.skillLoader) {
|
|
183
|
+
const pending = config.skillLoader.getPendingInjection();
|
|
184
|
+
if (pending) {
|
|
185
|
+
messages.push({ role: "user", content: pending });
|
|
186
|
+
messages.push({ role: "assistant", content: "Skill instructions loaded." });
|
|
187
|
+
config.skillLoader.clearPendingInjection();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// ── 3e. Yield step_start ─────────────────────────────────────────
|
|
191
|
+
const stepStartTime = Date.now();
|
|
192
|
+
yield makeMessage("step_start", sessionId, turn, {
|
|
193
|
+
metadata: { step: turn },
|
|
194
|
+
});
|
|
195
|
+
// ── 3f. Inference (with retry on transient errors) ───────────────
|
|
196
|
+
let response;
|
|
197
|
+
const maxRetries = 3;
|
|
198
|
+
let lastInferenceError = null;
|
|
199
|
+
let succeeded = false;
|
|
200
|
+
const inferenceTimeoutMs = config.inferenceTimeoutMs ?? 120_000;
|
|
201
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
202
|
+
try {
|
|
203
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Inference timeout after ${inferenceTimeoutMs}ms`)), inferenceTimeoutMs));
|
|
204
|
+
response = await Promise.race([
|
|
205
|
+
config.model.chat({
|
|
206
|
+
messages,
|
|
207
|
+
tools: llmTools.length > 0 ? llmTools : undefined,
|
|
208
|
+
systemPrompt,
|
|
209
|
+
maxTokens: config.maxTokens ?? 8192,
|
|
210
|
+
}),
|
|
211
|
+
timeoutPromise,
|
|
212
|
+
]);
|
|
213
|
+
succeeded = true;
|
|
214
|
+
break;
|
|
102
215
|
}
|
|
103
216
|
catch (err) {
|
|
104
|
-
|
|
217
|
+
lastInferenceError = err;
|
|
218
|
+
const isRetryable = isRetryableError(err);
|
|
219
|
+
if (!isRetryable || attempt >= maxRetries) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
// Check abort signal before retrying
|
|
223
|
+
if (config.signal?.aborted) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
// Yield inference_retry so the conductor can emit events and update the UI
|
|
227
|
+
const delayMs = getRetryDelay(err, attempt);
|
|
105
228
|
yield makeMessage("system", sessionId, turn, {
|
|
106
|
-
subtype: "
|
|
107
|
-
content: `
|
|
229
|
+
subtype: "inference_retry",
|
|
230
|
+
content: `Retrying inference (attempt ${attempt + 1}/${maxRetries}): ${err instanceof Error ? err.message : String(err)}`,
|
|
108
231
|
metadata: {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
maxTokens: compactionCheck.maxTokens,
|
|
114
|
-
percentUsedBefore: compactionCheck.percentUsed,
|
|
115
|
-
error: errorMsg,
|
|
232
|
+
attempt: attempt + 1,
|
|
233
|
+
maxRetries,
|
|
234
|
+
delayMs,
|
|
235
|
+
error: err instanceof Error ? err.message : String(err),
|
|
116
236
|
},
|
|
117
237
|
});
|
|
238
|
+
if (delayMs > 0) {
|
|
239
|
+
// Abort-aware delay: resolve early if signal fires
|
|
240
|
+
await new Promise((resolve) => {
|
|
241
|
+
if (config.signal) {
|
|
242
|
+
const onAbort = () => { clearTimeout(timer); resolve(); };
|
|
243
|
+
if (config.signal.aborted) {
|
|
244
|
+
resolve();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
config.signal.addEventListener("abort", onAbort, { once: true });
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
config.signal.removeEventListener("abort", onAbort);
|
|
250
|
+
resolve();
|
|
251
|
+
}, delayMs);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
setTimeout(resolve, delayMs);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
118
258
|
}
|
|
119
259
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
systemPrompt += `\n\nYou have ${remaining} turns remaining. Each turn lets you call multiple tools. Do NOT stop early — use your tools and complete the task autonomously.`;
|
|
126
|
-
if (config.env) {
|
|
127
|
-
const envLines = [];
|
|
128
|
-
if (config.env.cwd)
|
|
129
|
-
envLines.push(`Working directory: ${config.env.cwd}`);
|
|
130
|
-
if (config.env.platform)
|
|
131
|
-
envLines.push(`Platform: ${config.env.platform}`);
|
|
132
|
-
if (config.env.date)
|
|
133
|
-
envLines.push(`Date: ${config.env.date}`);
|
|
134
|
-
if (config.env.gitBranch)
|
|
135
|
-
envLines.push(`Git branch: ${config.env.gitBranch}`);
|
|
136
|
-
if (config.env.gitStatus)
|
|
137
|
-
envLines.push(`Git status: ${config.env.gitStatus}`);
|
|
138
|
-
if (envLines.length > 0) {
|
|
139
|
-
systemPrompt += `\n\n<env>\n${envLines.join("\n")}\n</env>`;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// ── 3d. Skill injection ──────────────────────────────────────────
|
|
143
|
-
if (config.skillLoader) {
|
|
144
|
-
const pending = config.skillLoader.getPendingInjection();
|
|
145
|
-
if (pending) {
|
|
146
|
-
messages.push({ role: "user", content: pending });
|
|
147
|
-
messages.push({ role: "assistant", content: "Skill instructions loaded." });
|
|
148
|
-
config.skillLoader.clearPendingInjection();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
// ── 3e. Yield step_start ─────────────────────────────────────────
|
|
152
|
-
const stepStartTime = Date.now();
|
|
153
|
-
yield makeMessage("step_start", sessionId, turn, {
|
|
154
|
-
metadata: { step: turn },
|
|
155
|
-
});
|
|
156
|
-
// ── 3f. Inference (with retry on transient errors) ───────────────
|
|
157
|
-
let response;
|
|
158
|
-
const maxRetries = 3;
|
|
159
|
-
let lastInferenceError = null;
|
|
160
|
-
let succeeded = false;
|
|
161
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
162
|
-
try {
|
|
163
|
-
response = await config.model.chat({
|
|
164
|
-
messages,
|
|
165
|
-
tools: llmTools.length > 0 ? llmTools : undefined,
|
|
166
|
-
systemPrompt,
|
|
167
|
-
maxTokens: config.maxTokens ?? 8192,
|
|
260
|
+
if (!succeeded) {
|
|
261
|
+
sessionOutcome = "error";
|
|
262
|
+
yield makeMessage("error", sessionId, turn, {
|
|
263
|
+
content: `Inference error: ${lastInferenceError instanceof Error ? lastInferenceError.message : String(lastInferenceError)}`,
|
|
264
|
+
metadata: { finalMessages: messages },
|
|
168
265
|
});
|
|
169
|
-
|
|
170
|
-
break;
|
|
266
|
+
return;
|
|
171
267
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
268
|
+
// ── 3g. Yield assistant message ──────────────────────────────────
|
|
269
|
+
yield makeMessage("assistant", sessionId, turn, {
|
|
270
|
+
content: response.content,
|
|
271
|
+
toolCalls: response.toolCalls,
|
|
272
|
+
metadata: {
|
|
273
|
+
usage: response.usage,
|
|
274
|
+
stopReason: response.stopReason,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
// ── 3g2. Yield step_finish with usage (for parent stats merge) ──
|
|
278
|
+
yield makeMessage("step_finish", sessionId, turn, {
|
|
279
|
+
metadata: {
|
|
280
|
+
stepDurationMs: Date.now() - stepStartTime,
|
|
281
|
+
usage: response.usage,
|
|
282
|
+
stopReason: response.stopReason,
|
|
283
|
+
toolCount: response.toolCalls?.length ?? 0,
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
// ── 3h. Stop condition ───────────────────────────────────────────
|
|
287
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
288
|
+
if (config.hooksEngine) {
|
|
289
|
+
const stopResult = await config.hooksEngine.fire("Stop", {
|
|
290
|
+
sessionId,
|
|
291
|
+
response: response.content,
|
|
292
|
+
});
|
|
293
|
+
if (stopResult?.decision === "block") {
|
|
294
|
+
// Inject feedback as user message, increment turn, continue
|
|
295
|
+
const feedback = stopResult.reason ?? "Please provide a more complete response.";
|
|
296
|
+
messages.push({
|
|
297
|
+
role: "assistant",
|
|
298
|
+
content: response.content,
|
|
299
|
+
});
|
|
300
|
+
messages.push({
|
|
301
|
+
role: "user",
|
|
302
|
+
content: feedback,
|
|
303
|
+
});
|
|
304
|
+
turn++;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
181
307
|
}
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
attempt: attempt + 1,
|
|
189
|
-
maxRetries,
|
|
190
|
-
delayMs,
|
|
191
|
-
error: err instanceof Error ? err.message : String(err),
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
if (delayMs > 0) {
|
|
195
|
-
// Abort-aware delay: resolve early if signal fires
|
|
196
|
-
await new Promise((resolve) => {
|
|
197
|
-
const timer = setTimeout(resolve, delayMs);
|
|
198
|
-
if (config.signal) {
|
|
199
|
-
const onAbort = () => { clearTimeout(timer); resolve(); };
|
|
200
|
-
if (config.signal.aborted) {
|
|
201
|
-
clearTimeout(timer);
|
|
202
|
-
resolve();
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
config.signal.addEventListener("abort", onAbort, { once: true });
|
|
206
|
-
}
|
|
308
|
+
// Fire SessionEnd and yield result
|
|
309
|
+
if (config.hooksEngine) {
|
|
310
|
+
await config.hooksEngine.fire("SessionEnd", {
|
|
311
|
+
sessionId,
|
|
312
|
+
reason: "completed",
|
|
313
|
+
turns: turn + 1,
|
|
207
314
|
});
|
|
208
315
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
metadata: { finalMessages: messages },
|
|
215
|
-
});
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
// ── 3g. Yield assistant message ──────────────────────────────────
|
|
219
|
-
yield makeMessage("assistant", sessionId, turn, {
|
|
220
|
-
content: response.content,
|
|
221
|
-
toolCalls: response.toolCalls,
|
|
222
|
-
metadata: {
|
|
223
|
-
usage: response.usage,
|
|
224
|
-
stopReason: response.stopReason,
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
// ── 3g2. Yield step_finish with usage (for parent stats merge) ──
|
|
228
|
-
yield makeMessage("step_finish", sessionId, turn, {
|
|
229
|
-
metadata: {
|
|
230
|
-
stepDurationMs: Date.now() - stepStartTime,
|
|
231
|
-
usage: response.usage,
|
|
232
|
-
stopReason: response.stopReason,
|
|
233
|
-
toolCount: response.toolCalls?.length ?? 0,
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
// ── 3h. Stop condition ───────────────────────────────────────────
|
|
237
|
-
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
238
|
-
if (config.hooksEngine) {
|
|
239
|
-
const stopResult = await config.hooksEngine.fire("Stop", {
|
|
240
|
-
sessionId,
|
|
241
|
-
response: response.content,
|
|
316
|
+
// Include the assistant's final message in history before emitting
|
|
317
|
+
messages.push({ role: "assistant", content: response.content });
|
|
318
|
+
yield makeMessage("result", sessionId, turn, {
|
|
319
|
+
content: response.content,
|
|
320
|
+
metadata: { totalTurns: turn + 1, finalMessages: messages },
|
|
242
321
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// ── 3i. Tool execution ───────────────────────────────────────────
|
|
325
|
+
const toolResults = [];
|
|
326
|
+
for (const toolCall of response.toolCalls) {
|
|
327
|
+
// Fire PreToolUse hook
|
|
328
|
+
if (config.hooksEngine) {
|
|
329
|
+
const preResult = await config.hooksEngine.fire("PreToolUse", {
|
|
330
|
+
sessionId,
|
|
331
|
+
toolName: toolCall.name,
|
|
332
|
+
input: toolCall.args,
|
|
249
333
|
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
334
|
+
if (preResult?.decision === "deny") {
|
|
335
|
+
toolResults.push({
|
|
336
|
+
toolCallId: toolCall.id,
|
|
337
|
+
toolName: toolCall.name,
|
|
338
|
+
content: `Tool use denied: ${preResult.reason ?? "blocked by hook"}`,
|
|
339
|
+
isError: true,
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
// Apply modifiedInput if hook returned one
|
|
344
|
+
if (preResult?.modifiedInput) {
|
|
345
|
+
Object.assign(toolCall.args, preResult.modifiedInput);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Safety gate check
|
|
349
|
+
if (config.safetyGate && config.safetyGate.requiresCheck(toolCall.name)) {
|
|
350
|
+
const safetyResult = await config.safetyGate.check({
|
|
351
|
+
id: toolCall.id,
|
|
352
|
+
name: toolCall.name,
|
|
353
|
+
args: toolCall.args,
|
|
253
354
|
});
|
|
254
|
-
|
|
255
|
-
|
|
355
|
+
if (!safetyResult.approved) {
|
|
356
|
+
toolResults.push({
|
|
357
|
+
toolCallId: toolCall.id,
|
|
358
|
+
toolName: toolCall.name,
|
|
359
|
+
content: `Tool blocked by safety gate: ${safetyResult.reason ?? "unsafe operation"}`,
|
|
360
|
+
isError: true,
|
|
361
|
+
});
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
256
364
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
await config.hooksEngine.fire("SessionEnd", {
|
|
261
|
-
sessionId,
|
|
262
|
-
reason: "completed",
|
|
263
|
-
turns: turn + 1,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
// Include the assistant's final message in history before emitting
|
|
267
|
-
messages.push({ role: "assistant", content: response.content });
|
|
268
|
-
yield makeMessage("result", sessionId, turn, {
|
|
269
|
-
content: response.content,
|
|
270
|
-
metadata: { totalTurns: turn + 1, finalMessages: messages },
|
|
271
|
-
});
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
// ── 3i. Tool execution ───────────────────────────────────────────
|
|
275
|
-
const toolResults = [];
|
|
276
|
-
for (const toolCall of response.toolCalls) {
|
|
277
|
-
// Fire PreToolUse hook
|
|
278
|
-
if (config.hooksEngine) {
|
|
279
|
-
const preResult = await config.hooksEngine.fire("PreToolUse", {
|
|
280
|
-
sessionId,
|
|
281
|
-
toolName: toolCall.name,
|
|
282
|
-
input: toolCall.args,
|
|
283
|
-
});
|
|
284
|
-
if (preResult?.decision === "deny") {
|
|
365
|
+
// Find and execute tool
|
|
366
|
+
const toolDef = toolMap.get(toolCall.name);
|
|
367
|
+
if (!toolDef) {
|
|
285
368
|
toolResults.push({
|
|
286
369
|
toolCallId: toolCall.id,
|
|
287
370
|
toolName: toolCall.name,
|
|
288
|
-
content: `
|
|
371
|
+
content: `Unknown tool: ${toolCall.name}`,
|
|
289
372
|
isError: true,
|
|
290
373
|
});
|
|
291
374
|
continue;
|
|
292
375
|
}
|
|
293
|
-
//
|
|
294
|
-
if (
|
|
295
|
-
|
|
376
|
+
// Validate tool arguments before execution
|
|
377
|
+
if (toolDef.schema) {
|
|
378
|
+
const validationError = validateToolArgs(toolCall.args, toolDef.schema, toolCall.name);
|
|
379
|
+
if (validationError) {
|
|
380
|
+
toolResults.push({
|
|
381
|
+
toolCallId: toolCall.id,
|
|
382
|
+
toolName: toolCall.name,
|
|
383
|
+
content: `${validationError}. Please retry with correct arguments.`,
|
|
384
|
+
isError: true,
|
|
385
|
+
});
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
296
388
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
389
|
+
// Yield tool_start so the conductor can emit tool.call events
|
|
390
|
+
yield makeMessage("tool_start", sessionId, turn, {
|
|
391
|
+
metadata: {
|
|
392
|
+
toolName: toolCall.name,
|
|
393
|
+
toolArgs: toolCall.args,
|
|
394
|
+
toolCallId: toolCall.id,
|
|
395
|
+
},
|
|
304
396
|
});
|
|
305
|
-
|
|
397
|
+
const metadataUpdates = [];
|
|
398
|
+
const toolCtx = {
|
|
399
|
+
config: {},
|
|
400
|
+
store: null,
|
|
401
|
+
state: {},
|
|
402
|
+
emit: () => { },
|
|
403
|
+
sessionId,
|
|
404
|
+
threadId,
|
|
405
|
+
agentName: config.agentName,
|
|
406
|
+
turn,
|
|
407
|
+
signal: config.signal,
|
|
408
|
+
metadata: (update) => {
|
|
409
|
+
metadataUpdates.push(update);
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
try {
|
|
413
|
+
const startTime = Date.now();
|
|
414
|
+
const rawResult = await toolDef.execute(toolCall.args, toolCtx);
|
|
415
|
+
const durationMs = Date.now() - startTime;
|
|
416
|
+
const content = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult);
|
|
417
|
+
// Push the successful result first — before the hook — so a hook
|
|
418
|
+
// throw cannot retroactively mark this tool call as failed.
|
|
306
419
|
toolResults.push({
|
|
307
420
|
toolCallId: toolCall.id,
|
|
308
421
|
toolName: toolCall.name,
|
|
309
|
-
content
|
|
310
|
-
|
|
422
|
+
content,
|
|
423
|
+
durationMs,
|
|
311
424
|
});
|
|
312
|
-
|
|
425
|
+
// Yield collected metadata updates from ctx.metadata() calls
|
|
426
|
+
for (const mu of metadataUpdates) {
|
|
427
|
+
yield makeMessage("tool_metadata", sessionId, turn, {
|
|
428
|
+
metadata: {
|
|
429
|
+
toolCallId: toolCall.id,
|
|
430
|
+
toolName: toolCall.name,
|
|
431
|
+
title: mu.title,
|
|
432
|
+
data: mu.metadata,
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// Fire PostToolUse — in its own try/catch so a hook error does not
|
|
437
|
+
// corrupt the conversation history by triggering PostToolUseFailure
|
|
438
|
+
// for a tool that actually succeeded.
|
|
439
|
+
if (config.hooksEngine) {
|
|
440
|
+
try {
|
|
441
|
+
await config.hooksEngine.fire("PostToolUse", {
|
|
442
|
+
sessionId,
|
|
443
|
+
toolName: toolCall.name,
|
|
444
|
+
input: toolCall.args,
|
|
445
|
+
output: rawResult,
|
|
446
|
+
durationMs,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
// Hook errors are non-fatal — the tool result is already recorded.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
313
453
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
454
|
+
catch (err) {
|
|
455
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
456
|
+
// Fire PostToolUseFailure — in its own try/catch so a hook error does not
|
|
457
|
+
// drop the error result from toolResults or corrupt conversation history.
|
|
458
|
+
if (config.hooksEngine) {
|
|
459
|
+
try {
|
|
460
|
+
await config.hooksEngine.fire("PostToolUseFailure", {
|
|
461
|
+
sessionId,
|
|
462
|
+
toolName: toolCall.name,
|
|
463
|
+
input: toolCall.args,
|
|
464
|
+
error: err instanceof Error ? err : errorMsg,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// Hook errors are non-fatal — the tool error result is still recorded.
|
|
469
|
+
}
|
|
470
|
+
}
|
|
330
471
|
toolResults.push({
|
|
331
472
|
toolCallId: toolCall.id,
|
|
332
473
|
toolName: toolCall.name,
|
|
333
|
-
content:
|
|
474
|
+
content: memoryLoader
|
|
475
|
+
? `Tool error: ${errorMsg}\n\nMemory query may help — call memory_query with a specific topic if this failure suggests a knowledge gap.`
|
|
476
|
+
: `Tool error: ${errorMsg}`,
|
|
334
477
|
isError: true,
|
|
335
478
|
});
|
|
336
|
-
continue;
|
|
337
479
|
}
|
|
338
480
|
}
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
toolCallId: toolCall.id,
|
|
345
|
-
},
|
|
481
|
+
// ── 3j. Update messages ──────────────────────────────────────────
|
|
482
|
+
messages.push({
|
|
483
|
+
role: "assistant",
|
|
484
|
+
content: response.content,
|
|
485
|
+
toolCalls: response.toolCalls,
|
|
346
486
|
});
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
sessionId,
|
|
354
|
-
threadId,
|
|
355
|
-
agentName: config.agentName,
|
|
356
|
-
turn,
|
|
357
|
-
signal: config.signal,
|
|
358
|
-
metadata: (update) => {
|
|
359
|
-
metadataUpdates.push(update);
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
|
-
try {
|
|
363
|
-
const startTime = Date.now();
|
|
364
|
-
const rawResult = await toolDef.execute(toolCall.args, toolCtx);
|
|
365
|
-
const durationMs = Date.now() - startTime;
|
|
366
|
-
const content = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult);
|
|
367
|
-
// Fire PostToolUse
|
|
368
|
-
if (config.hooksEngine) {
|
|
369
|
-
await config.hooksEngine.fire("PostToolUse", {
|
|
370
|
-
sessionId,
|
|
371
|
-
toolName: toolCall.name,
|
|
372
|
-
input: toolCall.args,
|
|
373
|
-
output: rawResult,
|
|
374
|
-
durationMs,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
toolResults.push({
|
|
378
|
-
toolCallId: toolCall.id,
|
|
379
|
-
toolName: toolCall.name,
|
|
380
|
-
content,
|
|
381
|
-
durationMs,
|
|
487
|
+
for (const tr of toolResults) {
|
|
488
|
+
messages.push({
|
|
489
|
+
role: "tool",
|
|
490
|
+
content: tr.content,
|
|
491
|
+
toolCallId: tr.toolCallId,
|
|
492
|
+
name: tr.toolName,
|
|
382
493
|
});
|
|
383
|
-
// Yield collected metadata updates from ctx.metadata() calls
|
|
384
|
-
for (const mu of metadataUpdates) {
|
|
385
|
-
yield makeMessage("tool_metadata", sessionId, turn, {
|
|
386
|
-
metadata: {
|
|
387
|
-
toolCallId: toolCall.id,
|
|
388
|
-
toolName: toolCall.name,
|
|
389
|
-
title: mu.title,
|
|
390
|
-
data: mu.metadata,
|
|
391
|
-
},
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
494
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
495
|
+
// ── 3j. TODO reminder ────────────────────────────────────────────
|
|
496
|
+
if (config.todoModule) {
|
|
497
|
+
const todoState = config.todoModule.getState();
|
|
498
|
+
if (todoState.todos.length > 0) {
|
|
499
|
+
const reminder = config.todoModule.toContextString();
|
|
500
|
+
messages.push({ role: "user", content: `<system-reminder>\n${reminder}\n</system-reminder>` });
|
|
501
|
+
messages.push({ role: "assistant", content: "Noted." });
|
|
502
|
+
yield makeMessage("system", sessionId, turn, {
|
|
503
|
+
subtype: "todo_reminder",
|
|
504
|
+
content: reminder,
|
|
404
505
|
});
|
|
405
506
|
}
|
|
406
|
-
toolResults.push({
|
|
407
|
-
toolCallId: toolCall.id,
|
|
408
|
-
toolName: toolCall.name,
|
|
409
|
-
content: `Tool error: ${errorMsg}`,
|
|
410
|
-
isError: true,
|
|
411
|
-
});
|
|
412
507
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
role: "assistant",
|
|
417
|
-
content: response.content,
|
|
418
|
-
toolCalls: response.toolCalls,
|
|
419
|
-
});
|
|
420
|
-
for (const tr of toolResults) {
|
|
421
|
-
messages.push({
|
|
422
|
-
role: "tool",
|
|
423
|
-
content: tr.content,
|
|
424
|
-
toolCallId: tr.toolCallId,
|
|
425
|
-
name: tr.toolName,
|
|
508
|
+
// ── 3k. Yield tool_result, increment turn ───────────────────────
|
|
509
|
+
yield makeMessage("tool_result", sessionId, turn, {
|
|
510
|
+
toolResults,
|
|
426
511
|
});
|
|
512
|
+
turn++;
|
|
427
513
|
}
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
messages.push({ role: "user", content: `<system-reminder>\n${reminder}\n</system-reminder>` });
|
|
434
|
-
messages.push({ role: "assistant", content: "Noted." });
|
|
435
|
-
yield makeMessage("system", sessionId, turn, {
|
|
436
|
-
subtype: "todo_reminder",
|
|
437
|
-
content: reminder,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
// ── 3k. Yield tool_result, increment turn ───────────────────────
|
|
442
|
-
yield makeMessage("tool_result", sessionId, turn, {
|
|
443
|
-
toolResults,
|
|
514
|
+
// Post-loop: maxTurns exhausted
|
|
515
|
+
sessionOutcome = "budget-exceeded";
|
|
516
|
+
yield makeMessage("error", sessionId, turn, {
|
|
517
|
+
content: `Agent loop exceeded maxTurns (${maxTurns})`,
|
|
518
|
+
metadata: { finalMessages: messages },
|
|
444
519
|
});
|
|
445
|
-
turn++;
|
|
446
520
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
521
|
+
catch (err) {
|
|
522
|
+
sessionOutcome = "error";
|
|
523
|
+
yield makeMessage("error", sessionId, turn, {
|
|
524
|
+
content: `Agent loop error: ${err instanceof Error ? err.message : String(err)}`,
|
|
525
|
+
metadata: { finalMessages: messages },
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
if (memoryLoader) {
|
|
530
|
+
const log = buildEpisodicLog(sessionId, prompt, turn, sessionOutcome, config.compactor);
|
|
531
|
+
memoryLoader.persistEpisodic(sessionId, log);
|
|
532
|
+
memoryLoader.resetSession();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
452
535
|
}
|
|
453
536
|
// ─── wrapWithAgentLoop ─────────────────────────────────────────────────────
|
|
454
537
|
export function wrapWithAgentLoop(config) {
|
|
@@ -521,4 +604,40 @@ function getRetryDelay(err, attempt) {
|
|
|
521
604
|
}
|
|
522
605
|
return Math.min(1000 * Math.pow(2, attempt), 10_000);
|
|
523
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* buildEpisodicLog — Assemble session log for persistEpisodic().
|
|
609
|
+
*
|
|
610
|
+
* When compactor.getLastSummary() is unavailable (not yet implemented),
|
|
611
|
+
* ## What Happened falls back to the raw task description.
|
|
612
|
+
*/
|
|
613
|
+
function buildEpisodicLog(sessionId, taskDescription, turnCount, outcome, compactor) {
|
|
614
|
+
const c = compactor;
|
|
615
|
+
const summary = c?.getLastSummary?.() ?? null;
|
|
616
|
+
const openThreads = c?.getOpenThreads?.() ?? "none";
|
|
617
|
+
const outcomeNote = outcome === "budget-exceeded"
|
|
618
|
+
? "Session ended at turn limit. Task may be incomplete — check Open Threads before assuming prior work is done."
|
|
619
|
+
: "";
|
|
620
|
+
return [
|
|
621
|
+
`---`,
|
|
622
|
+
`type: episodic`,
|
|
623
|
+
`session: ${sessionId}`,
|
|
624
|
+
`outcome: ${outcome}`,
|
|
625
|
+
`turns: ${turnCount}`,
|
|
626
|
+
`created: ${new Date().toISOString()}`,
|
|
627
|
+
`---`,
|
|
628
|
+
``,
|
|
629
|
+
`## What Happened`,
|
|
630
|
+
summary ?? taskDescription,
|
|
631
|
+
``,
|
|
632
|
+
`## Turns`,
|
|
633
|
+
String(turnCount),
|
|
634
|
+
``,
|
|
635
|
+
`## Outcome`,
|
|
636
|
+
outcome,
|
|
637
|
+
...(outcomeNote ? [``, outcomeNote] : []),
|
|
638
|
+
``,
|
|
639
|
+
`## Open Threads`,
|
|
640
|
+
openThreads,
|
|
641
|
+
].join("\n");
|
|
642
|
+
}
|
|
524
643
|
//# sourceMappingURL=agent-loop.js.map
|