@oni.bot/core 1.0.1 → 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.
Files changed (214) hide show
  1. package/CHANGELOG.md +146 -126
  2. package/SECURITY.md +2 -2
  3. package/dist/agents/context.d.ts +0 -2
  4. package/dist/agents/context.d.ts.map +1 -1
  5. package/dist/agents/context.js +1 -3
  6. package/dist/agents/context.js.map +1 -1
  7. package/dist/agents/define-agent.d.ts.map +1 -1
  8. package/dist/agents/define-agent.js +99 -41
  9. package/dist/agents/define-agent.js.map +1 -1
  10. package/dist/agents/functional-agent.d.ts.map +1 -1
  11. package/dist/agents/functional-agent.js +0 -7
  12. package/dist/agents/functional-agent.js.map +1 -1
  13. package/dist/agents/types.d.ts +0 -2
  14. package/dist/agents/types.d.ts.map +1 -1
  15. package/dist/checkpoint.d.ts.map +1 -1
  16. package/dist/checkpoint.js +7 -2
  17. package/dist/checkpoint.js.map +1 -1
  18. package/dist/checkpointers/postgres.d.ts.map +1 -1
  19. package/dist/checkpointers/postgres.js +47 -31
  20. package/dist/checkpointers/postgres.js.map +1 -1
  21. package/dist/checkpointers/sqlite.js +4 -4
  22. package/dist/checkpointers/sqlite.js.map +1 -1
  23. package/dist/circuit-breaker.d.ts +1 -0
  24. package/dist/circuit-breaker.d.ts.map +1 -1
  25. package/dist/circuit-breaker.js +13 -0
  26. package/dist/circuit-breaker.js.map +1 -1
  27. package/dist/cli/dev.d.ts.map +1 -1
  28. package/dist/cli/dev.js +0 -1
  29. package/dist/cli/dev.js.map +1 -1
  30. package/dist/cli/router.js +1 -1
  31. package/dist/cli/run.d.ts.map +1 -1
  32. package/dist/cli/run.js +0 -1
  33. package/dist/cli/run.js.map +1 -1
  34. package/dist/config/loader.d.ts +1 -1
  35. package/dist/config/loader.d.ts.map +1 -1
  36. package/dist/config/loader.js +35 -5
  37. package/dist/config/loader.js.map +1 -1
  38. package/dist/context.d.ts +20 -0
  39. package/dist/context.d.ts.map +1 -1
  40. package/dist/context.js.map +1 -1
  41. package/dist/coordination/pubsub.d.ts +1 -0
  42. package/dist/coordination/pubsub.d.ts.map +1 -1
  43. package/dist/coordination/pubsub.js +31 -16
  44. package/dist/coordination/pubsub.js.map +1 -1
  45. package/dist/coordination/request-reply.d.ts +7 -0
  46. package/dist/coordination/request-reply.d.ts.map +1 -1
  47. package/dist/coordination/request-reply.js +65 -11
  48. package/dist/coordination/request-reply.js.map +1 -1
  49. package/dist/events/bus.d.ts +1 -0
  50. package/dist/events/bus.d.ts.map +1 -1
  51. package/dist/events/bus.js +16 -10
  52. package/dist/events/bus.js.map +1 -1
  53. package/dist/functional.d.ts.map +1 -1
  54. package/dist/functional.js +3 -0
  55. package/dist/functional.js.map +1 -1
  56. package/dist/graph.d.ts.map +1 -1
  57. package/dist/graph.js +21 -3
  58. package/dist/graph.js.map +1 -1
  59. package/dist/guardrails/audit.d.ts +4 -1
  60. package/dist/guardrails/audit.d.ts.map +1 -1
  61. package/dist/guardrails/audit.js +18 -1
  62. package/dist/guardrails/audit.js.map +1 -1
  63. package/dist/harness/agent-loop.d.ts.map +1 -1
  64. package/dist/harness/agent-loop.js +471 -352
  65. package/dist/harness/agent-loop.js.map +1 -1
  66. package/dist/harness/context-compactor.d.ts +1 -0
  67. package/dist/harness/context-compactor.d.ts.map +1 -1
  68. package/dist/harness/context-compactor.js +43 -1
  69. package/dist/harness/context-compactor.js.map +1 -1
  70. package/dist/harness/harness.d.ts +6 -0
  71. package/dist/harness/harness.d.ts.map +1 -1
  72. package/dist/harness/harness.js +32 -5
  73. package/dist/harness/harness.js.map +1 -1
  74. package/dist/harness/hooks-engine.d.ts.map +1 -1
  75. package/dist/harness/hooks-engine.js +12 -10
  76. package/dist/harness/hooks-engine.js.map +1 -1
  77. package/dist/harness/index.d.ts +3 -1
  78. package/dist/harness/index.d.ts.map +1 -1
  79. package/dist/harness/index.js +2 -0
  80. package/dist/harness/index.js.map +1 -1
  81. package/dist/harness/memory-loader.d.ts +150 -0
  82. package/dist/harness/memory-loader.d.ts.map +1 -0
  83. package/dist/harness/memory-loader.js +714 -0
  84. package/dist/harness/memory-loader.js.map +1 -0
  85. package/dist/harness/safety-gate.d.ts.map +1 -1
  86. package/dist/harness/safety-gate.js +47 -26
  87. package/dist/harness/safety-gate.js.map +1 -1
  88. package/dist/harness/skill-loader.d.ts +7 -0
  89. package/dist/harness/skill-loader.d.ts.map +1 -1
  90. package/dist/harness/skill-loader.js +24 -8
  91. package/dist/harness/skill-loader.js.map +1 -1
  92. package/dist/harness/todo-module.d.ts.map +1 -1
  93. package/dist/harness/todo-module.js +13 -6
  94. package/dist/harness/todo-module.js.map +1 -1
  95. package/dist/harness/types.d.ts +7 -0
  96. package/dist/harness/types.d.ts.map +1 -1
  97. package/dist/harness/types.js.map +1 -1
  98. package/dist/harness/validate-args.js +18 -3
  99. package/dist/harness/validate-args.js.map +1 -1
  100. package/dist/hitl/interrupt.d.ts +2 -2
  101. package/dist/hitl/interrupt.d.ts.map +1 -1
  102. package/dist/hitl/interrupt.js +6 -4
  103. package/dist/hitl/interrupt.js.map +1 -1
  104. package/dist/hitl/resume.d.ts +10 -0
  105. package/dist/hitl/resume.d.ts.map +1 -1
  106. package/dist/hitl/resume.js +31 -0
  107. package/dist/hitl/resume.js.map +1 -1
  108. package/dist/index.js +1 -1
  109. package/dist/injected.d.ts.map +1 -1
  110. package/dist/injected.js.map +1 -1
  111. package/dist/inspect.d.ts.map +1 -1
  112. package/dist/inspect.js +28 -8
  113. package/dist/inspect.js.map +1 -1
  114. package/dist/lsp/client.d.ts +2 -0
  115. package/dist/lsp/client.d.ts.map +1 -1
  116. package/dist/lsp/client.js +62 -17
  117. package/dist/lsp/client.js.map +1 -1
  118. package/dist/lsp/index.d.ts.map +1 -1
  119. package/dist/lsp/index.js.map +1 -1
  120. package/dist/mcp/client.d.ts +2 -0
  121. package/dist/mcp/client.d.ts.map +1 -1
  122. package/dist/mcp/client.js +45 -14
  123. package/dist/mcp/client.js.map +1 -1
  124. package/dist/mcp/convert.js +1 -1
  125. package/dist/mcp/convert.js.map +1 -1
  126. package/dist/mcp/transport.d.ts +2 -0
  127. package/dist/mcp/transport.d.ts.map +1 -1
  128. package/dist/mcp/transport.js +33 -8
  129. package/dist/mcp/transport.js.map +1 -1
  130. package/dist/messages/index.d.ts.map +1 -1
  131. package/dist/messages/index.js +7 -1
  132. package/dist/messages/index.js.map +1 -1
  133. package/dist/models/anthropic.d.ts.map +1 -1
  134. package/dist/models/anthropic.js +25 -15
  135. package/dist/models/anthropic.js.map +1 -1
  136. package/dist/models/google.d.ts.map +1 -1
  137. package/dist/models/google.js +23 -7
  138. package/dist/models/google.js.map +1 -1
  139. package/dist/models/ollama.d.ts.map +1 -1
  140. package/dist/models/ollama.js +11 -1
  141. package/dist/models/ollama.js.map +1 -1
  142. package/dist/models/openai.d.ts.map +1 -1
  143. package/dist/models/openai.js +15 -3
  144. package/dist/models/openai.js.map +1 -1
  145. package/dist/models/openrouter.d.ts.map +1 -1
  146. package/dist/models/openrouter.js +14 -3
  147. package/dist/models/openrouter.js.map +1 -1
  148. package/dist/prebuilt/react-agent.d.ts.map +1 -1
  149. package/dist/prebuilt/react-agent.js +1 -0
  150. package/dist/prebuilt/react-agent.js.map +1 -1
  151. package/dist/pregel.d.ts +11 -6
  152. package/dist/pregel.d.ts.map +1 -1
  153. package/dist/pregel.js +473 -349
  154. package/dist/pregel.js.map +1 -1
  155. package/dist/retry.d.ts.map +1 -1
  156. package/dist/retry.js +7 -6
  157. package/dist/retry.js.map +1 -1
  158. package/dist/store/index.d.ts +1 -1
  159. package/dist/store/index.d.ts.map +1 -1
  160. package/dist/store/index.js +63 -13
  161. package/dist/store/index.js.map +1 -1
  162. package/dist/stream-events.d.ts.map +1 -1
  163. package/dist/stream-events.js +3 -9
  164. package/dist/stream-events.js.map +1 -1
  165. package/dist/streaming.d.ts +5 -2
  166. package/dist/streaming.d.ts.map +1 -1
  167. package/dist/streaming.js +9 -8
  168. package/dist/streaming.js.map +1 -1
  169. package/dist/swarm/graph.d.ts +16 -2
  170. package/dist/swarm/graph.d.ts.map +1 -1
  171. package/dist/swarm/graph.js +204 -53
  172. package/dist/swarm/graph.js.map +1 -1
  173. package/dist/swarm/index.d.ts +2 -1
  174. package/dist/swarm/index.d.ts.map +1 -1
  175. package/dist/swarm/index.js.map +1 -1
  176. package/dist/swarm/mailbox.d.ts.map +1 -1
  177. package/dist/swarm/mailbox.js +3 -1
  178. package/dist/swarm/mailbox.js.map +1 -1
  179. package/dist/swarm/mermaid.d.ts +2 -1
  180. package/dist/swarm/mermaid.d.ts.map +1 -1
  181. package/dist/swarm/mermaid.js +6 -3
  182. package/dist/swarm/mermaid.js.map +1 -1
  183. package/dist/swarm/pool.d.ts.map +1 -1
  184. package/dist/swarm/pool.js +30 -5
  185. package/dist/swarm/pool.js.map +1 -1
  186. package/dist/swarm/registry.d.ts.map +1 -1
  187. package/dist/swarm/registry.js +7 -0
  188. package/dist/swarm/registry.js.map +1 -1
  189. package/dist/swarm/scaling.d.ts +10 -1
  190. package/dist/swarm/scaling.d.ts.map +1 -1
  191. package/dist/swarm/scaling.js +85 -14
  192. package/dist/swarm/scaling.js.map +1 -1
  193. package/dist/swarm/snapshot.d.ts.map +1 -1
  194. package/dist/swarm/snapshot.js +10 -1
  195. package/dist/swarm/snapshot.js.map +1 -1
  196. package/dist/swarm/supervisor.js +20 -12
  197. package/dist/swarm/supervisor.js.map +1 -1
  198. package/dist/swarm/tracer.d.ts +3 -1
  199. package/dist/swarm/tracer.d.ts.map +1 -1
  200. package/dist/swarm/tracer.js +66 -15
  201. package/dist/swarm/tracer.js.map +1 -1
  202. package/dist/swarm/types.d.ts +1 -6
  203. package/dist/swarm/types.d.ts.map +1 -1
  204. package/dist/testing/index.d.ts +2 -2
  205. package/dist/testing/index.d.ts.map +1 -1
  206. package/dist/testing/index.js.map +1 -1
  207. package/dist/tools/define.d.ts.map +1 -1
  208. package/dist/tools/define.js +1 -0
  209. package/dist/tools/define.js.map +1 -1
  210. package/dist/tools/types.d.ts +2 -0
  211. package/dist/tools/types.d.ts.map +1 -1
  212. package/dist/types.d.ts +3 -1
  213. package/dist/types.d.ts.map +1 -1
  214. package/package.json +7 -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 = config.tools.map((t) => ({
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 config.tools) {
70
+ for (const t of allTools) {
43
71
  toolMap.set(t.name, t);
44
72
  }
45
73
  // ── 3. Main Loop ─────────────────────────────────────────────────────
46
- while (turn < maxTurns) {
47
- // ── 3a. Check AbortSignal ────────────────────────────────────────
48
- if (config.signal?.aborted) {
49
- yield makeMessage("error", sessionId, turn, {
50
- content: "Agent loop aborted by signal",
51
- });
52
- return;
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
- try {
71
- if (config.hooksEngine) {
72
- await config.hooksEngine.fire("PreCompact", {
73
- sessionId,
74
- messageCount: beforeCount,
75
- estimatedTokens: compactionCheck.estimatedTokens,
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: "compact_boundary",
89
- content: "Context compacted",
90
+ subtype: "compact_start",
91
+ content: "Context compaction starting",
90
92
  metadata: {
91
93
  beforeCount,
92
- afterCount,
93
- summarized,
94
- estimatedTokensBefore: compactionCheck.estimatedTokens,
95
- estimatedTokensAfter,
94
+ estimatedTokens: compactionCheck.estimatedTokens,
96
95
  threshold: compactionCheck.threshold,
97
96
  maxTokens: compactionCheck.maxTokens,
98
- percentUsedBefore: compactionCheck.percentUsed,
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
- const errorMsg = err instanceof Error ? err.message : String(err);
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: "compact_error",
107
- content: `Context compaction failed: ${errorMsg}`,
229
+ subtype: "inference_retry",
230
+ content: `Retrying inference (attempt ${attempt + 1}/${maxRetries}): ${err instanceof Error ? err.message : String(err)}`,
108
231
  metadata: {
109
- beforeCount,
110
- afterCount: beforeCount,
111
- estimatedTokensBefore: compactionCheck.estimatedTokens,
112
- threshold: compactionCheck.threshold,
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
- // ── 3c. Build system prompt ──────────────────────────────────────
122
- let systemPrompt = config.systemPrompt;
123
- // Inject remaining turns so the model knows its budget
124
- const remaining = maxTurns - turn;
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
- succeeded = true;
170
- break;
266
+ return;
171
267
  }
172
- catch (err) {
173
- lastInferenceError = err;
174
- const isRetryable = isRetryableError(err);
175
- if (!isRetryable || attempt >= maxRetries) {
176
- break;
177
- }
178
- // Check abort signal before retrying
179
- if (config.signal?.aborted) {
180
- break;
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
- // Yield inference_retry so the conductor can emit events and update the UI
183
- const delayMs = getRetryDelay(err, attempt);
184
- yield makeMessage("system", sessionId, turn, {
185
- subtype: "inference_retry",
186
- content: `Retrying inference (attempt ${attempt + 1}/${maxRetries}): ${err instanceof Error ? err.message : String(err)}`,
187
- metadata: {
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
- if (!succeeded) {
212
- yield makeMessage("error", sessionId, turn, {
213
- content: `Inference error: ${lastInferenceError instanceof Error ? lastInferenceError.message : String(lastInferenceError)}`,
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
- if (stopResult?.decision === "block") {
244
- // Inject feedback as user message, increment turn, continue
245
- const feedback = stopResult.reason ?? "Please provide a more complete response.";
246
- messages.push({
247
- role: "assistant",
248
- content: response.content,
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
- messages.push({
251
- role: "user",
252
- content: feedback,
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
- turn++;
255
- continue;
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
- // Fire SessionEnd and yield result
259
- if (config.hooksEngine) {
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: `Tool use denied: ${preResult.reason ?? "blocked by hook"}`,
371
+ content: `Unknown tool: ${toolCall.name}`,
289
372
  isError: true,
290
373
  });
291
374
  continue;
292
375
  }
293
- // Apply modifiedInput if hook returned one
294
- if (preResult?.modifiedInput) {
295
- Object.assign(toolCall.args, preResult.modifiedInput);
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
- // Safety gate check
299
- if (config.safetyGate && config.safetyGate.requiresCheck(toolCall.name)) {
300
- const safetyResult = await config.safetyGate.check({
301
- id: toolCall.id,
302
- name: toolCall.name,
303
- args: toolCall.args,
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
- if (!safetyResult.approved) {
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: `Tool blocked by safety gate: ${safetyResult.reason ?? "unsafe operation"}`,
310
- isError: true,
422
+ content,
423
+ durationMs,
311
424
  });
312
- continue;
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
- // Find and execute tool
316
- const toolDef = toolMap.get(toolCall.name);
317
- if (!toolDef) {
318
- toolResults.push({
319
- toolCallId: toolCall.id,
320
- toolName: toolCall.name,
321
- content: `Unknown tool: ${toolCall.name}`,
322
- isError: true,
323
- });
324
- continue;
325
- }
326
- // Validate tool arguments before execution
327
- if (toolDef.schema) {
328
- const validationError = validateToolArgs(toolCall.args, toolDef.schema, toolCall.name);
329
- if (validationError) {
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: `${validationError}. Please retry with correct arguments.`,
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
- // Yield tool_start so the conductor can emit tool.call events
340
- yield makeMessage("tool_start", sessionId, turn, {
341
- metadata: {
342
- toolName: toolCall.name,
343
- toolArgs: toolCall.args,
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 metadataUpdates = [];
348
- const toolCtx = {
349
- config: {},
350
- store: null,
351
- state: {},
352
- emit: () => { },
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
- catch (err) {
396
- const errorMsg = err instanceof Error ? err.message : String(err);
397
- // Fire PostToolUseFailure
398
- if (config.hooksEngine) {
399
- await config.hooksEngine.fire("PostToolUseFailure", {
400
- sessionId,
401
- toolName: toolCall.name,
402
- input: toolCall.args,
403
- error: err instanceof Error ? err : errorMsg,
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
- // ── 3j. Update messages ──────────────────────────────────────────
415
- messages.push({
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
- // ── 3j. TODO reminder ────────────────────────────────────────────
429
- if (config.todoModule) {
430
- const todoState = config.todoModule.getState();
431
- if (todoState.todos.length > 0) {
432
- const reminder = config.todoModule.toContextString();
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
- // ── 4. maxTurns exceeded ─────────────────────────────────────────────
448
- yield makeMessage("error", sessionId, turn, {
449
- content: `Agent loop exceeded maxTurns (${maxTurns})`,
450
- metadata: { finalMessages: messages },
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