@oni.bot/core 1.0.3 → 1.1.0

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 (146) hide show
  1. package/dist/checkpointers/postgres.d.ts.map +1 -1
  2. package/dist/checkpointers/postgres.js +2 -1
  3. package/dist/checkpointers/postgres.js.map +1 -1
  4. package/dist/cli/inspect.d.ts.map +1 -1
  5. package/dist/cli/inspect.js +4 -2
  6. package/dist/cli/inspect.js.map +1 -1
  7. package/dist/coordination/request-reply.d.ts +11 -2
  8. package/dist/coordination/request-reply.d.ts.map +1 -1
  9. package/dist/coordination/request-reply.js.map +1 -1
  10. package/dist/events/bus.d.ts.map +1 -1
  11. package/dist/events/bus.js +1 -0
  12. package/dist/events/bus.js.map +1 -1
  13. package/dist/graph.d.ts +11 -1
  14. package/dist/graph.d.ts.map +1 -1
  15. package/dist/graph.js +4 -2
  16. package/dist/graph.js.map +1 -1
  17. package/dist/harness/agent-loop.d.ts +1 -7
  18. package/dist/harness/agent-loop.d.ts.map +1 -1
  19. package/dist/harness/agent-loop.js +2 -642
  20. package/dist/harness/agent-loop.js.map +1 -1
  21. package/dist/harness/loop/hooks.d.ts +7 -0
  22. package/dist/harness/loop/hooks.d.ts.map +1 -0
  23. package/dist/harness/loop/hooks.js +46 -0
  24. package/dist/harness/loop/hooks.js.map +1 -0
  25. package/dist/harness/loop/index.d.ts +8 -0
  26. package/dist/harness/loop/index.d.ts.map +1 -0
  27. package/dist/harness/loop/index.js +257 -0
  28. package/dist/harness/loop/index.js.map +1 -0
  29. package/dist/harness/loop/inference.d.ts +19 -0
  30. package/dist/harness/loop/inference.d.ts.map +1 -0
  31. package/dist/harness/loop/inference.js +121 -0
  32. package/dist/harness/loop/inference.js.map +1 -0
  33. package/dist/harness/loop/memory.d.ts +22 -0
  34. package/dist/harness/loop/memory.d.ts.map +1 -0
  35. package/dist/harness/loop/memory.js +73 -0
  36. package/dist/harness/loop/memory.js.map +1 -0
  37. package/dist/harness/loop/safety.d.ts +8 -0
  38. package/dist/harness/loop/safety.d.ts.map +1 -0
  39. package/dist/harness/loop/safety.js +21 -0
  40. package/dist/harness/loop/safety.js.map +1 -0
  41. package/dist/harness/loop/tools.d.ts +24 -0
  42. package/dist/harness/loop/tools.d.ts.map +1 -0
  43. package/dist/harness/loop/tools.js +184 -0
  44. package/dist/harness/loop/tools.js.map +1 -0
  45. package/dist/harness/loop/types.d.ts +7 -0
  46. package/dist/harness/loop/types.d.ts.map +1 -0
  47. package/dist/harness/loop/types.js +9 -0
  48. package/dist/harness/loop/types.js.map +1 -0
  49. package/dist/harness/memory/fs-compat.d.ts +3 -0
  50. package/dist/harness/memory/fs-compat.d.ts.map +1 -0
  51. package/dist/harness/memory/fs-compat.js +26 -0
  52. package/dist/harness/memory/fs-compat.js.map +1 -0
  53. package/dist/harness/memory/index.d.ts +105 -0
  54. package/dist/harness/memory/index.d.ts.map +1 -0
  55. package/dist/harness/memory/index.js +491 -0
  56. package/dist/harness/memory/index.js.map +1 -0
  57. package/dist/harness/memory/prompter.d.ts +7 -0
  58. package/dist/harness/memory/prompter.d.ts.map +1 -0
  59. package/dist/harness/memory/prompter.js +24 -0
  60. package/dist/harness/memory/prompter.js.map +1 -0
  61. package/dist/harness/memory/ranker.d.ts +15 -0
  62. package/dist/harness/memory/ranker.d.ts.map +1 -0
  63. package/dist/harness/memory/ranker.js +72 -0
  64. package/dist/harness/memory/ranker.js.map +1 -0
  65. package/dist/harness/memory/scanner.d.ts +26 -0
  66. package/dist/harness/memory/scanner.d.ts.map +1 -0
  67. package/dist/harness/memory/scanner.js +187 -0
  68. package/dist/harness/memory/scanner.js.map +1 -0
  69. package/dist/harness/memory/types.d.ts +50 -0
  70. package/dist/harness/memory/types.d.ts.map +1 -0
  71. package/dist/harness/memory/types.js +7 -0
  72. package/dist/harness/memory/types.js.map +1 -0
  73. package/dist/harness/memory-loader.d.ts +2 -149
  74. package/dist/harness/memory-loader.d.ts.map +1 -1
  75. package/dist/harness/memory-loader.js +1 -713
  76. package/dist/harness/memory-loader.js.map +1 -1
  77. package/dist/hitl/interrupt.d.ts.map +1 -1
  78. package/dist/hitl/interrupt.js +2 -1
  79. package/dist/hitl/interrupt.js.map +1 -1
  80. package/dist/prebuilt/react-agent.d.ts.map +1 -1
  81. package/dist/prebuilt/react-agent.js +6 -2
  82. package/dist/prebuilt/react-agent.js.map +1 -1
  83. package/dist/pregel/checkpointing.d.ts +12 -0
  84. package/dist/pregel/checkpointing.d.ts.map +1 -0
  85. package/dist/pregel/checkpointing.js +60 -0
  86. package/dist/pregel/checkpointing.js.map +1 -0
  87. package/dist/pregel/execution.d.ts +7 -0
  88. package/dist/pregel/execution.d.ts.map +1 -0
  89. package/dist/pregel/execution.js +178 -0
  90. package/dist/pregel/execution.js.map +1 -0
  91. package/dist/pregel/index.d.ts +61 -0
  92. package/dist/pregel/index.d.ts.map +1 -0
  93. package/dist/pregel/index.js +154 -0
  94. package/dist/pregel/index.js.map +1 -0
  95. package/dist/pregel/interrupts.d.ts +3 -0
  96. package/dist/pregel/interrupts.d.ts.map +1 -0
  97. package/dist/pregel/interrupts.js +7 -0
  98. package/dist/pregel/interrupts.js.map +1 -0
  99. package/dist/pregel/state-helpers.d.ts +12 -0
  100. package/dist/pregel/state-helpers.d.ts.map +1 -0
  101. package/dist/pregel/state-helpers.js +71 -0
  102. package/dist/pregel/state-helpers.js.map +1 -0
  103. package/dist/pregel/streaming.d.ts +5 -0
  104. package/dist/pregel/streaming.d.ts.map +1 -0
  105. package/dist/pregel/streaming.js +462 -0
  106. package/dist/pregel/streaming.js.map +1 -0
  107. package/dist/pregel/types.d.ts +48 -0
  108. package/dist/pregel/types.d.ts.map +1 -0
  109. package/dist/pregel/types.js +5 -0
  110. package/dist/pregel/types.js.map +1 -0
  111. package/dist/pregel.d.ts +1 -66
  112. package/dist/pregel.d.ts.map +1 -1
  113. package/dist/pregel.js +2 -854
  114. package/dist/pregel.js.map +1 -1
  115. package/dist/swarm/agent-node.d.ts +11 -0
  116. package/dist/swarm/agent-node.d.ts.map +1 -0
  117. package/dist/swarm/agent-node.js +156 -0
  118. package/dist/swarm/agent-node.js.map +1 -0
  119. package/dist/swarm/compile-ext.d.ts +5 -0
  120. package/dist/swarm/compile-ext.d.ts.map +1 -0
  121. package/dist/swarm/compile-ext.js +126 -0
  122. package/dist/swarm/compile-ext.js.map +1 -0
  123. package/dist/swarm/config.d.ts +147 -0
  124. package/dist/swarm/config.d.ts.map +1 -0
  125. package/dist/swarm/config.js +17 -0
  126. package/dist/swarm/config.js.map +1 -0
  127. package/dist/swarm/factories.d.ts +37 -0
  128. package/dist/swarm/factories.d.ts.map +1 -0
  129. package/dist/swarm/factories.js +703 -0
  130. package/dist/swarm/factories.js.map +1 -0
  131. package/dist/swarm/graph.d.ts +14 -147
  132. package/dist/swarm/graph.d.ts.map +1 -1
  133. package/dist/swarm/graph.js +30 -917
  134. package/dist/swarm/graph.js.map +1 -1
  135. package/dist/swarm/pool.js.map +1 -1
  136. package/dist/swarm/supervisor.js.map +1 -1
  137. package/dist/testing/index.d.ts +2 -2
  138. package/dist/testing/index.d.ts.map +1 -1
  139. package/dist/testing/index.js +3 -2
  140. package/dist/testing/index.js.map +1 -1
  141. package/dist/tools/define.d.ts +2 -1
  142. package/dist/tools/define.d.ts.map +1 -1
  143. package/dist/tools/define.js +3 -1
  144. package/dist/tools/define.js.map +1 -1
  145. package/dist/tools/types.d.ts.map +1 -1
  146. package/package.json +1 -1
@@ -1,643 +1,3 @@
1
- // ============================================================
2
- // @oni.bot/core/harness AgentLoop
3
- // Think → Act → Observe async generator driving each agent node
4
- // ============================================================
5
- import { generateId } from "./types.js";
6
- import { validateToolArgs } from "./validate-args.js";
7
- import { MemoryLoader } from "./memory-loader.js";
8
- // ─── agentLoop ─────────────────────────────────────────────────────────────
9
- export async function* agentLoop(prompt, config) {
10
- const sessionId = generateId("ses");
11
- const threadId = config.threadId ?? generateId("thr");
12
- const maxTurns = config.maxTurns ?? 10;
13
- const messages = config.initialMessages
14
- ? [...config.initialMessages]
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";
38
- // ── 1. Session Init ──────────────────────────────────────────────────
39
- if (config.hooksEngine) {
40
- const hookResult = await config.hooksEngine.fire("SessionStart", {
41
- sessionId,
42
- agentName: config.agentName,
43
- tools: config.tools.map((t) => t.name),
44
- });
45
- if (hookResult?.additionalContext) {
46
- messages.push({ role: "user", content: hookResult.additionalContext });
47
- messages.push({ role: "assistant", content: "Context loaded." });
48
- }
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;
56
- // Push user prompt
57
- messages.push({ role: "user", content: prompt });
58
- yield makeMessage("system", sessionId, turn, {
59
- subtype: "init",
60
- content: `Session ${sessionId} started for agent "${config.agentName}"`,
61
- });
62
- // ── 2. Build LLMToolDef[] ────────────────────────────────────────────
63
- const llmTools = allTools.map((t) => ({
64
- name: t.name,
65
- description: t.description,
66
- parameters: t.schema,
67
- }));
68
- // Tool lookup map
69
- const toolMap = new Map();
70
- for (const t of allTools) {
71
- toolMap.set(t.name, t);
72
- }
73
- // ── 3. Main Loop ─────────────────────────────────────────────────────
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",
81
- });
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;
89
- yield makeMessage("system", sessionId, turn, {
90
- subtype: "compact_start",
91
- content: "Context compaction starting",
92
- metadata: {
93
- beforeCount,
94
- estimatedTokens: compactionCheck.estimatedTokens,
95
- threshold: compactionCheck.threshold,
96
- maxTokens: compactionCheck.maxTokens,
97
- percentUsed: compactionCheck.percentUsed,
98
- },
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;
215
- }
216
- catch (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);
228
- yield makeMessage("system", sessionId, turn, {
229
- subtype: "inference_retry",
230
- content: `Retrying inference (attempt ${attempt + 1}/${maxRetries}): ${err instanceof Error ? err.message : String(err)}`,
231
- metadata: {
232
- attempt: attempt + 1,
233
- maxRetries,
234
- delayMs,
235
- error: err instanceof Error ? err.message : String(err),
236
- },
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
- }
258
- }
259
- }
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 },
265
- });
266
- return;
267
- }
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
- }
307
- }
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,
314
- });
315
- }
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 },
321
- });
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,
333
- });
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,
354
- });
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
- }
364
- }
365
- // Find and execute tool
366
- const toolDef = toolMap.get(toolCall.name);
367
- if (!toolDef) {
368
- toolResults.push({
369
- toolCallId: toolCall.id,
370
- toolName: toolCall.name,
371
- content: `Unknown tool: ${toolCall.name}`,
372
- isError: true,
373
- });
374
- continue;
375
- }
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
- }
388
- }
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
- },
396
- });
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.
419
- toolResults.push({
420
- toolCallId: toolCall.id,
421
- toolName: toolCall.name,
422
- content,
423
- durationMs,
424
- });
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
- }
453
- }
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
- }
471
- toolResults.push({
472
- toolCallId: toolCall.id,
473
- toolName: toolCall.name,
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}`,
477
- isError: true,
478
- });
479
- }
480
- }
481
- // ── 3j. Update messages ──────────────────────────────────────────
482
- messages.push({
483
- role: "assistant",
484
- content: response.content,
485
- toolCalls: response.toolCalls,
486
- });
487
- for (const tr of toolResults) {
488
- messages.push({
489
- role: "tool",
490
- content: tr.content,
491
- toolCallId: tr.toolCallId,
492
- name: tr.toolName,
493
- });
494
- }
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,
505
- });
506
- }
507
- }
508
- // ── 3k. Yield tool_result, increment turn ───────────────────────
509
- yield makeMessage("tool_result", sessionId, turn, {
510
- toolResults,
511
- });
512
- turn++;
513
- }
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 },
519
- });
520
- }
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
- }
535
- }
536
- // ─── wrapWithAgentLoop ─────────────────────────────────────────────────────
537
- export function wrapWithAgentLoop(config) {
538
- return async (state) => {
539
- const prompt = state.task ??
540
- state.context ??
541
- "";
542
- let finalResult = "";
543
- for await (const msg of agentLoop(prompt, config)) {
544
- if (msg.type === "result") {
545
- finalResult = msg.content ?? "";
546
- }
547
- }
548
- return {
549
- agentResults: {
550
- ...(state.agentResults ?? {}),
551
- [config.agentName]: finalResult,
552
- },
553
- };
554
- };
555
- }
556
- // ─── Helpers ───────────────────────────────────────────────────────────────
557
- function makeMessage(type, sessionId, turn, overrides = {}) {
558
- return {
559
- type,
560
- sessionId,
561
- turn,
562
- timestamp: Date.now(),
563
- ...overrides,
564
- };
565
- }
566
- /** Check if an error is retryable (rate limits, transient server errors). */
567
- function isRetryableError(err) {
568
- if (err && typeof err === "object" && "isRetryable" in err) {
569
- return !!err.isRetryable;
570
- }
571
- if (err && typeof err === "object") {
572
- const status = err.status;
573
- if (status === 429 || status === 503 || status === 502 || status === 500) {
574
- return true;
575
- }
576
- }
577
- if (err instanceof Error) {
578
- const msg = err.message.toLowerCase();
579
- if (msg.includes("rate limit") || msg.includes("429") || msg.includes("503") || msg.includes("overloaded")) {
580
- return true;
581
- }
582
- }
583
- return false;
584
- }
585
- /** Get retry delay with exponential backoff. */
586
- function getRetryDelay(err, attempt) {
587
- if (err && typeof err === "object") {
588
- // Respect retryAfterMs if provided by the model adapter
589
- if ("retryAfterMs" in err) {
590
- const after = err.retryAfterMs;
591
- if (typeof after === "number" && after > 0)
592
- return after;
593
- }
594
- // Check headers for retry-after-ms (common in HTTP error responses)
595
- const headers = err.headers;
596
- if (headers) {
597
- const headerMs = headers["retry-after-ms"];
598
- if (headerMs) {
599
- const parsed = parseInt(headerMs, 10);
600
- if (!isNaN(parsed) && parsed > 0)
601
- return parsed;
602
- }
603
- }
604
- }
605
- return Math.min(1000 * Math.pow(2, attempt), 10_000);
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
- }
1
+ // Re-export shim — implementation has moved to src/harness/loop/
2
+ export { agentLoop, wrapWithAgentLoop } from "./loop/index.js";
643
3
  //# sourceMappingURL=agent-loop.js.map