@lloyal-labs/lloyal-agents 1.5.6 → 1.7.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 (55) hide show
  1. package/dist/Agent.d.ts +34 -1
  2. package/dist/Agent.d.ts.map +1 -1
  3. package/dist/Agent.js +65 -1
  4. package/dist/Agent.js.map +1 -1
  5. package/dist/AgentPolicy.d.ts +51 -8
  6. package/dist/AgentPolicy.d.ts.map +1 -1
  7. package/dist/AgentPolicy.js +105 -63
  8. package/dist/AgentPolicy.js.map +1 -1
  9. package/dist/Tool.d.ts +5 -7
  10. package/dist/Tool.d.ts.map +1 -1
  11. package/dist/Tool.js +5 -7
  12. package/dist/Tool.js.map +1 -1
  13. package/dist/agent-pool.d.ts +9 -3
  14. package/dist/agent-pool.d.ts.map +1 -1
  15. package/dist/agent-pool.js +446 -407
  16. package/dist/agent-pool.js.map +1 -1
  17. package/dist/combinators.d.ts +29 -0
  18. package/dist/combinators.d.ts.map +1 -0
  19. package/dist/combinators.js +37 -0
  20. package/dist/combinators.js.map +1 -0
  21. package/dist/create-agent-pool.d.ts +78 -0
  22. package/dist/create-agent-pool.d.ts.map +1 -0
  23. package/dist/create-agent-pool.js +60 -0
  24. package/dist/create-agent-pool.js.map +1 -0
  25. package/dist/index.d.ts +6 -5
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +8 -8
  28. package/dist/index.js.map +1 -1
  29. package/dist/source.d.ts.map +1 -1
  30. package/dist/source.js.map +1 -1
  31. package/dist/trace-types.d.ts +4 -2
  32. package/dist/trace-types.d.ts.map +1 -1
  33. package/dist/trace-writer.d.ts +4 -1
  34. package/dist/trace-writer.d.ts.map +1 -1
  35. package/dist/trace-writer.js +6 -2
  36. package/dist/trace-writer.js.map +1 -1
  37. package/dist/types.d.ts +9 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/use-agent.d.ts +92 -0
  40. package/dist/use-agent.d.ts.map +1 -0
  41. package/dist/use-agent.js +131 -0
  42. package/dist/use-agent.js.map +1 -0
  43. package/package.json +2 -2
  44. package/dist/generate.d.ts +0 -77
  45. package/dist/generate.d.ts.map +0 -1
  46. package/dist/generate.js +0 -166
  47. package/dist/generate.js.map +0 -1
  48. package/dist/run-agents.d.ts +0 -39
  49. package/dist/run-agents.d.ts.map +0 -1
  50. package/dist/run-agents.js +0 -46
  51. package/dist/run-agents.js.map +0 -1
  52. package/dist/spawn-agents.d.ts +0 -104
  53. package/dist/spawn-agents.d.ts.map +0 -1
  54. package/dist/spawn-agents.js +0 -255
  55. package/dist/spawn-agents.js.map +0 -1
@@ -7,12 +7,17 @@ const sdk_1 = require("@lloyal-labs/sdk");
7
7
  const context_1 = require("./context");
8
8
  const sdk_2 = require("@lloyal-labs/sdk");
9
9
  const trace_scope_1 = require("./trace-scope");
10
- const generate_1 = require("./generate");
11
10
  const Agent_1 = require("./Agent");
12
11
  const AgentPolicy_1 = require("./AgentPolicy");
13
12
  /**
14
13
  * Immutable KV budget snapshot for one tick of the agent loop
15
14
  *
15
+ * Frozen at phase boundaries (PRODUCE, SETTLE, DISPATCH) so that all
16
+ * decisions within a phase are evaluated against the same baseline.
17
+ * Without this, items processed earlier in a loop would see different
18
+ * pressure than items processed later — making reject/nudge/kill
19
+ * decisions order-dependent and nondeterministic.
20
+ *
16
21
  * Created from `SessionContext._storeKvPressure()` which returns
17
22
  * `{ nCtx, cellsUsed, remaining }` where `remaining = nCtx - cellsUsed`.
18
23
  * `cellsUsed` tracks unique KV cells per branch — incremented on
@@ -93,6 +98,119 @@ class ContextPressure {
93
98
  }
94
99
  }
95
100
  exports.ContextPressure = ContextPressure;
101
+ /**
102
+ * Inline recovery for a single killed agent (trailing stop).
103
+ *
104
+ * Prefills the extraction prompt into the agent's own branch, sets eager
105
+ * report grammar, generates to stop token, parses JSON, reports result,
106
+ * and prunes the branch — all before the tick loop continues. The freed
107
+ * KV lets remaining agents keep researching.
108
+ *
109
+ * Returns true if the agent reported findings.
110
+ */
111
+ function* recoverInline(agent, policy, ctx, store, tw, parentTraceId, events) {
112
+ const recovery = policy.onRecovery?.(agent);
113
+ if (!recovery || recovery.type === 'skip') {
114
+ if (!agent.branch.disposed)
115
+ agent.branch.pruneSync();
116
+ return false;
117
+ }
118
+ // Build the nudge prompt — a minimal turn injection that triggers
119
+ // report behavior. The agent's KV already contains the full
120
+ // conversation context; the prompt is just a nudge.
121
+ const { prompt } = ctx.formatChatSync(JSON.stringify([
122
+ { role: 'system', content: recovery.prompt.system },
123
+ { role: 'user', content: recovery.prompt.user },
124
+ ]), { enableThinking: false });
125
+ const sep = ctx.getTurnSeparator();
126
+ const delta = ctx.tokenizeSync(prompt, false);
127
+ const tokens = [...sep, ...delta];
128
+ // Eager report grammar — forces { result: string } output
129
+ const reportGrammar = yield* (0, effection_1.call)(() => ctx.jsonSchemaToGrammar(JSON.stringify({
130
+ type: 'object',
131
+ properties: { result: { type: 'string' } },
132
+ required: ['result'],
133
+ })));
134
+ // Recovery runs in its own scope — if prefill or decode fails
135
+ // (KV exhaustion), the scope tears down cleanly.
136
+ let reported = false;
137
+ try {
138
+ yield* (0, effection_1.scoped)(function* () {
139
+ yield* (0, effection_1.call)(() => store.prefill([[agent.branch, tokens]]));
140
+ agent.branch.setGrammar(reportGrammar);
141
+ tw.write({
142
+ traceId: tw.nextId(), parentTraceId, ts: performance.now(),
143
+ type: 'branch:prefill', branchHandle: agent.id,
144
+ tokenCount: tokens.length, role: 'recovery',
145
+ });
146
+ // Single-agent produce/commit loop
147
+ let output = '';
148
+ let tokenCount = 0;
149
+ for (;;) {
150
+ const { token, text, isStop } = agent.branch.produceSync();
151
+ if (isStop)
152
+ break;
153
+ output += text;
154
+ tokenCount++;
155
+ yield* (0, effection_1.call)(() => store.commit([[agent.branch, token]]));
156
+ yield* events.send({ type: 'agent:produce', agentId: agent.id, text, tokenCount });
157
+ }
158
+ // Parse + report
159
+ const parsed = JSON.parse(output);
160
+ if (parsed?.result) {
161
+ agent.reportResult(parsed.result, 'scratchpad');
162
+ yield* events.send({ type: 'agent:report', agentId: agent.id, result: agent.result });
163
+ reported = true;
164
+ }
165
+ });
166
+ }
167
+ catch { /* prefill overflow, decode failure, or malformed JSON — non-fatal */ }
168
+ // Always prune after scope exits (success or failure)
169
+ if (!agent.branch.disposed)
170
+ agent.branch.pruneSync();
171
+ // Emit tick so TUI updates pressure percentage after prune
172
+ const postPressure = new ContextPressure(ctx);
173
+ yield* events.send({ type: 'agent:tick', cellsUsed: postPressure.cellsUsed, nCtx: postPressure.nCtx });
174
+ return reported;
175
+ }
176
+ // ── PRODUCE action handlers ─────────────────────────────────────
177
+ // Each handler encapsulates state transitions, events, and trace for one
178
+ // policy action outcome. The PRODUCE switch dispatches to these.
179
+ function* handleFreeTextReport(a, content, events) {
180
+ a.reportResult(content, 'free_text');
181
+ a.transition('idle');
182
+ yield* events.send({ type: 'agent:report', agentId: a.id, result: a.result });
183
+ yield* events.send({ type: 'agent:done', agentId: a.id });
184
+ }
185
+ function* handleIdleDrop(a, reason, events, tw, parentTraceId) {
186
+ a.transition('idle');
187
+ if (reason !== 'free_text_stop') {
188
+ tw.write({ traceId: tw.nextId(), parentTraceId, ts: performance.now(),
189
+ type: 'pool:agentDrop', agentId: a.id,
190
+ reason: reason === 'max_turns' ? 'maxTurns' : 'pressure_softcut' });
191
+ }
192
+ yield* events.send({ type: 'agent:done', agentId: a.id });
193
+ }
194
+ function* handleNudge(a, message, tc, ctx, tools) {
195
+ const callId = tc?.id || `call_${a.toolCallCount}`;
196
+ const nudgeResult = { error: message };
197
+ a.incrementTurns();
198
+ a.transition('awaiting_tool');
199
+ const prefillTokens = (0, sdk_2.buildToolResultDelta)(ctx, JSON.stringify(nudgeResult), callId);
200
+ const probe = tools?.get(tc?.name || '')?.probe(nudgeResult) ?? undefined;
201
+ a.resetTurn();
202
+ return { agentId: a.id, prefillTokens, toolName: tc?.name || '', callId, args: tc?.arguments || '', probe };
203
+ }
204
+ function* handleReport(a, result, tc, terminalTool, pruneOnReport, events) {
205
+ a.reportResult(result, 'report_tool');
206
+ a.transition('idle');
207
+ a.incrementToolCalls();
208
+ yield* events.send({ type: 'agent:tool_call', agentId: a.id, tool: terminalTool, args: tc.arguments });
209
+ yield* events.send({ type: 'agent:report', agentId: a.id, result: a.result });
210
+ yield* events.send({ type: 'agent:done', agentId: a.id });
211
+ if (pruneOnReport && !a.branch.disposed)
212
+ a.branch.pruneSync();
213
+ }
96
214
  /**
97
215
  * Fork an agent from a parent branch with its own system prompt and task.
98
216
  *
@@ -197,13 +315,13 @@ function useAgentPool(opts) {
197
315
  return (0, effection_1.resource)(function* (provide) {
198
316
  const ctx = yield* context_1.Ctx.expect();
199
317
  const store = yield* context_1.Store.expect();
200
- const events = yield* context_1.Events.expect();
318
+ const poolChannel = (0, effection_1.createChannel)();
201
319
  // Bridge for onProgress callbacks — Signal is correct here (external callback).
202
- // A spawned forwarder drains the bridge into the Channel with proper scope context.
320
+ // A spawned forwarder drains the bridge into the poolChannel with proper scope context.
203
321
  const progressBridge = (0, effection_1.createSignal)();
204
322
  yield* (0, effection_1.spawn)(function* () {
205
323
  for (const ev of yield* (0, effection_1.each)(progressBridge)) {
206
- yield* events.send(ev);
324
+ yield* poolChannel.send(ev);
207
325
  yield* effection_1.each.next();
208
326
  }
209
327
  });
@@ -296,11 +414,6 @@ function useAgentPool(opts) {
296
414
  taskSuffixTokens: prefillSetup.map(([, t]) => t.length),
297
415
  pressure: { remaining: initPressure.remaining, softLimit: initPressure.softLimit, headroom: initPressure.headroom },
298
416
  });
299
- // Emit spawn events and activate agents
300
- for (const a of agents) {
301
- a.transition('active');
302
- yield* events.send({ type: 'agent:spawn', agentId: a.id, parentAgentId: a.parentId });
303
- }
304
417
  // ── Lazy grammar setup ───────────────────────────────────
305
418
  const applyLazyGrammar = (a) => {
306
419
  if (a.fmt.grammar && a.fmt.grammarLazy && a.fmt.grammarTriggers.length > 0) {
@@ -318,434 +431,360 @@ function useAgentPool(opts) {
318
431
  };
319
432
  for (const a of agents)
320
433
  applyLazyGrammar(a);
321
- // ── Tool dispatch coordination ───────────────────────────
322
- // Tool results land in settledBuffer during DISPATCH, drained by SETTLE
323
- // in the next tick. DISPATCH awaits each tool to completion via
324
- // scoped() + call() — no concurrent llama_decode possible.
325
- const settledBuffer = [];
326
- const dispatchedProbes = new Map();
327
434
  const agentById = new Map(agents.map(a => [a.id, a]));
328
- let steps = 0;
329
- let totalToolCalls = 0;
330
- const counters = {
331
- warmPrefillCalls: 0,
332
- warmPrefillBranches: 0,
333
- };
334
- // ── Four-phase tick loop ─────────────────────────────────
335
- for (;;) {
336
- // -- Phase 1: PRODUCE -- sample from active agents, collect tool calls
337
- const pressure = new ContextPressure(ctx, pressureOpts);
338
- if (trace && (pressure.critical || pressure.headroom < 0)) {
339
- try {
340
- process.stderr.write(`[PRODUCE] ${pressure.critical ? 'CRITICAL' : 'SOFT_LIMIT'} remaining=${pressure.remaining} headroom=${pressure.headroom} cellsUsed=${pressure.cellsUsed} nCtx=${pressure.nCtx}\n`);
341
- }
342
- catch { }
343
- }
344
- const entries = [];
345
- const toolCalls = [];
346
- for (const a of agents) {
347
- if (a.status !== 'active')
348
- continue;
349
- const policyExit = policy.shouldExit?.(a, pressure);
350
- if (policyExit ?? pressure.critical) {
351
- a.transition('idle');
352
- const exitReason = pressure.critical ? 'pressure_critical'
353
- : policyExit ? 'policy_exit'
354
- : 'pressure_critical';
355
- tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
356
- type: 'pool:agentDrop', agentId: a.id, reason: exitReason });
357
- yield* events.send({ type: 'agent:done', agentId: a.id });
358
- continue;
435
+ // Subscribe BEFORE spawning tick loop — no events missed
436
+ const subscription = yield* poolChannel;
437
+ // Spawn tick loop — runs concurrently with Subscription consumption.
438
+ // scoped() creates an error boundary: if llama_decode fails (KV exhaustion),
439
+ // the scope tears down and the channel closes with whatever results exist.
440
+ yield* (0, effection_1.spawn)(function* () {
441
+ let steps = 0;
442
+ let totalToolCalls = 0;
443
+ const counters = { warmPrefillCalls: 0, warmPrefillBranches: 0 };
444
+ try {
445
+ // Emit spawn events and activate agents
446
+ for (const a of agents) {
447
+ a.transition('active');
448
+ yield* poolChannel.send({ type: 'agent:spawn', agentId: a.id, parentAgentId: a.parentId });
359
449
  }
360
- const { token, text, isStop } = a.branch.produceSync();
361
- if (isStop) {
362
- const parsed = ctx.parseChatOutput(a.rawOutput, a.fmt.format, {
363
- reasoningFormat: a.fmt.reasoningFormat,
364
- generationPrompt: a.fmt.generationPrompt,
365
- parser: a.fmt.parser,
366
- });
367
- tw.write({
368
- traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
369
- type: 'agent:turn', agentId: a.id, turn: a.turns,
370
- rawOutput: a.rawOutput,
371
- parsedContent: parsed.content || null,
372
- parsedToolCalls: parsed.toolCalls.map(tc => ({ name: tc.name, arguments: tc.arguments })),
373
- });
374
- // Policy decides what to do with the parsed output
375
- const action = policy.onProduced(a, parsed, pressure, policyConfig);
376
- switch (action.type) {
377
- case 'free_text_report':
378
- a.reportResult(action.content, 'free_text');
379
- a.transition('idle');
380
- yield* events.send({ type: 'agent:report', agentId: a.id, result: a.result });
381
- yield* events.send({ type: 'agent:done', agentId: a.id });
450
+ // ── Phase operations (close over pool scope) ────────────
451
+ /** SETTLE: prefill tool results that fit, defer oversized items for next tick */
452
+ function* settle(items) {
453
+ const settlePressure = new ContextPressure(ctx, pressureOpts);
454
+ let headroom = settlePressure.headroom;
455
+ if (trace) {
456
+ const desc = items.map(s => `${s.toolName}:${s.prefillTokens.length}`).join(', ');
457
+ try {
458
+ process.stderr.write(`[SETTLE] remaining=${settlePressure.remaining} headroom=${headroom} cellsUsed=${settlePressure.cellsUsed} nCtx=${settlePressure.nCtx} items=[${desc}]\n`);
459
+ }
460
+ catch { }
461
+ }
462
+ const prefillPairs = [];
463
+ const settledAgents = [];
464
+ const deferred = [];
465
+ for (const item of items) {
466
+ const a = agentById.get(item.agentId);
467
+ if (!a || a.status === 'idle')
382
468
  continue;
383
- case 'idle':
384
- a.transition('idle');
385
- if (action.reason !== 'free_text_stop') {
386
- tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
387
- type: 'pool:agentDrop', agentId: a.id,
388
- reason: action.reason === 'max_turns' ? 'maxTurns' : 'pressure_softcut' });
469
+ if (item.prefillTokens.length > headroom) {
470
+ if (trace) {
471
+ try {
472
+ process.stderr.write(`[SETTLE] DEFER ${item.toolName}:${item.prefillTokens.length} > headroom=${headroom}\n`);
473
+ }
474
+ catch { }
389
475
  }
390
- yield* events.send({ type: 'agent:done', agentId: a.id });
391
- continue;
392
- case 'nudge': {
393
- const tc = parsed.toolCalls[0];
394
- const callId = tc?.id || `call_${a.toolCallCount}`;
395
- const nudgeMsg = JSON.stringify({ error: action.message });
396
- a.incrementTurns();
397
- a.transition('awaiting_tool');
398
- const prefillTokens = (0, sdk_2.buildToolResultDelta)(ctx, nudgeMsg, callId);
399
- settledBuffer.push({ agentId: a.id, prefillTokens, toolName: tc?.name || '', callId });
400
- a.resetTurn();
401
- tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
402
- type: 'pool:agentNudge', agentId: a.id, reason: 'pressure_softcut' });
476
+ deferred.push(item);
403
477
  continue;
404
478
  }
405
- case 'report':
406
- a.reportResult(action.result, 'report_tool');
407
- a.transition('idle');
408
- a.incrementToolCalls();
409
- totalToolCalls++;
410
- yield* events.send({ type: 'agent:tool_call', agentId: a.id, tool: terminalTool, args: parsed.toolCalls[0].arguments });
411
- yield* events.send({ type: 'agent:report', agentId: a.id, result: a.result });
412
- yield* events.send({ type: 'agent:done', agentId: a.id });
413
- if (pruneOnReport && !a.branch.disposed) {
414
- a.branch.pruneSync();
415
- }
416
- continue;
417
- case 'tool_call':
418
- a.transition('awaiting_tool');
419
- toolCalls.push({ agent: a, tc: action.tc });
420
- a.resetTurn();
421
- continue;
422
- }
423
- }
424
- entries.push([a.branch, token]);
425
- if (trace) {
426
- const entropy = a.branch.modelEntropy();
427
- const surprisal = a.branch.modelSurprisal(token);
428
- a.accumulateTokenWithTrace(text, entropy, surprisal);
429
- yield* events.send({
430
- type: 'agent:produce', agentId: a.id, text, tokenCount: a.tokenCount,
431
- entropy, surprisal,
432
- });
433
- }
434
- else {
435
- a.accumulateToken(text);
436
- yield* events.send({ type: 'agent:produce', agentId: a.id, text, tokenCount: a.tokenCount });
437
- }
438
- }
439
- // -- Phase 2: COMMIT -- batch-decode produced tokens
440
- if (entries.length > 0) {
441
- yield* (0, effection_1.call)(() => store.commit(entries));
442
- steps++;
443
- const commitPressure = new ContextPressure(ctx, pressureOpts);
444
- yield* events.send({ type: 'agent:tick', cellsUsed: commitPressure.cellsUsed, nCtx: commitPressure.nCtx });
445
- }
446
- // -- Phase 3: SETTLE -- drain settled tool buffer, batch prefill
447
- const settled = settledBuffer.splice(0);
448
- if (settled.length > 0) {
449
- // Fresh snapshot — Phase 2 commits may have advanced positions
450
- const settlePressure = new ContextPressure(ctx, pressureOpts);
451
- let headroom = settlePressure.headroom;
452
- if (trace) {
453
- const items = settled.map(s => `${s.toolName}:${s.prefillTokens.length}`).join(', ');
454
- try {
455
- process.stderr.write(`[SETTLE] remaining=${settlePressure.remaining} headroom=${headroom} cellsUsed=${settlePressure.cellsUsed} nCtx=${settlePressure.nCtx} items=[${items}]\n`);
479
+ prefillPairs.push([a.branch, item.prefillTokens]);
480
+ settledAgents.push(a);
481
+ headroom -= item.prefillTokens.length;
482
+ const postSettle = new ContextPressure(ctx, pressureOpts);
483
+ a.recordToolResult({
484
+ name: item.toolName, args: item.args,
485
+ resultTokenCount: item.prefillTokens.length,
486
+ contextAfterPercent: postSettle.percentAvailable,
487
+ timestamp: performance.now(),
488
+ });
489
+ tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
490
+ type: 'branch:prefill', branchHandle: a.id,
491
+ tokenCount: item.prefillTokens.length, role: 'toolResult' });
456
492
  }
457
- catch { }
458
- }
459
- const prefillPairs = [];
460
- const settledAgents = [];
461
- for (const item of settled) {
462
- const a = agentById.get(item.agentId);
463
- if (!a || a.status === 'idle')
464
- continue;
465
- if (item.prefillTokens.length > headroom) {
493
+ if (prefillPairs.length > 0) {
466
494
  if (trace) {
495
+ const total = prefillPairs.reduce((s, [, t]) => s + t.length, 0);
467
496
  try {
468
- process.stderr.write(`[SETTLE] REJECT ${item.toolName}:${item.prefillTokens.length} > headroom=${headroom}\n`);
497
+ process.stderr.write(`[SETTLE] PREFILL ${prefillPairs.length} branches, ${total} tokens, headroom_after=${headroom}\n`);
469
498
  }
470
499
  catch { }
471
500
  }
472
- const settleAction = policy.onSettleReject(a, item.prefillTokens.length, settlePressure, policyConfig);
473
- if (settleAction.type === 'nudge') {
474
- const nudgeMsg = JSON.stringify({ error: settleAction.message });
475
- const nudgeTokens = (0, sdk_2.buildToolResultDelta)(ctx, nudgeMsg, item.callId);
476
- if (nudgeTokens.length <= headroom) {
477
- prefillPairs.push([a.branch, nudgeTokens]);
478
- settledAgents.push(a);
479
- headroom -= nudgeTokens.length;
501
+ yield* (0, effection_1.call)(() => store.prefill(prefillPairs));
502
+ counters.warmPrefillCalls++;
503
+ counters.warmPrefillBranches += prefillPairs.length;
504
+ // Probe prefill from DISPATCH
505
+ const probePairs = [];
506
+ for (const a of settledAgents) {
507
+ const probe = items.find(s => s.agentId === a.id)?.probe;
508
+ if (probe) {
509
+ const probeTokens = ctx.tokenizeSync(probe, false);
510
+ probePairs.push([a.branch, probeTokens]);
480
511
  tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
481
- type: 'pool:agentNudge', agentId: a.id, reason: 'pressure_settle_reject' });
482
- continue;
512
+ type: 'branch:prefill', branchHandle: a.id,
513
+ tokenCount: probeTokens.length, role: 'probe', probeText: probe });
483
514
  }
484
515
  }
485
- // Nudge failed (tokens don't fit) or policy said kill
486
- tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
487
- type: 'pool:agentDrop', agentId: a.id, reason: 'pressure_settle_reject' });
488
- a.transition('idle');
489
- yield* events.send({ type: 'agent:done', agentId: a.id });
490
- continue;
516
+ if (probePairs.length > 0) {
517
+ yield* (0, effection_1.call)(() => store.prefill(probePairs));
518
+ }
519
+ for (const a of settledAgents) {
520
+ a.transition('active');
521
+ a.resetTurn();
522
+ applyLazyGrammar(a);
523
+ }
491
524
  }
492
- prefillPairs.push([a.branch, item.prefillTokens]);
493
- settledAgents.push(a);
494
- headroom -= item.prefillTokens.length;
495
- // Record tool history for policy decisions
496
- const postSettle = new ContextPressure(ctx, pressureOpts);
497
- a.recordToolResult({
498
- name: item.toolName,
499
- args: item.callId,
500
- resultTokenCount: item.prefillTokens.length,
501
- contextAfterPercent: postSettle.percentAvailable,
502
- timestamp: performance.now(),
503
- });
504
- tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
505
- type: 'branch:prefill', branchHandle: a.id,
506
- tokenCount: item.prefillTokens.length, role: 'toolResult' });
525
+ return deferred;
507
526
  }
508
- if (prefillPairs.length > 0) {
509
- if (trace) {
510
- const totalPrefill = prefillPairs.reduce((s, [, t]) => s + t.length, 0);
527
+ /** DISPATCH: execute tool calls sequentially, return settled items for next tick */
528
+ function* dispatch(calls) {
529
+ const results = [];
530
+ for (const { agent, tc } of calls) {
531
+ let toolArgs;
511
532
  try {
512
- process.stderr.write(`[SETTLE] PREFILL ${prefillPairs.length} branches, ${totalPrefill} tokens, headroom_after=${headroom}\n`);
533
+ toolArgs = JSON.parse(tc.arguments);
534
+ }
535
+ catch {
536
+ toolArgs = {};
537
+ }
538
+ const callId = tc.id || `call_${agent.toolCallCount}`;
539
+ agent.incrementToolCalls();
540
+ totalToolCalls++;
541
+ agent.incrementTurns();
542
+ yield* poolChannel.send({ type: 'agent:tool_call', agentId: agent.id, tool: tc.name, args: tc.arguments });
543
+ const tool = tools.get(tc.name);
544
+ const dispatchPressure = new ContextPressure(ctx, pressureOpts);
545
+ const explore = policy.shouldExplore?.(agent, dispatchPressure) ?? true;
546
+ const dispatchTraceId = tw.nextId();
547
+ const toolT0 = performance.now();
548
+ tw.write({
549
+ traceId: dispatchTraceId, parentTraceId: poolScope.traceId, ts: toolT0,
550
+ type: 'tool:dispatch', agentId: agent.id, tool: tc.name,
551
+ toolIndex: toolIndexMap.get(tc.name) ?? -1, toolkitSize,
552
+ args: toolArgs, callId,
553
+ explore, percentAvailable: dispatchPressure.percentAvailable,
554
+ });
555
+ const peerHistory = agents
556
+ .filter(a => a.id !== agent.id)
557
+ .flatMap(a => a.toolHistory);
558
+ const toolContext = {
559
+ agentId: agent.id, branch: agent.branch,
560
+ onProgress: (p) => {
561
+ progressBridge.send({ type: 'agent:tool_progress', agentId: agent.id, tool: tc.name, filled: p.filled, total: p.total });
562
+ },
563
+ scorer: opts.scorer, explore,
564
+ pressurePercentAvailable: dispatchPressure.percentAvailable,
565
+ peerHistory,
566
+ };
567
+ try {
568
+ yield* context_1.TraceParent.set(dispatchTraceId);
569
+ yield* context_1.CallingAgent.set(agent);
570
+ const result = yield* (0, effection_1.scoped)(function* () {
571
+ return yield* (0, effection_1.call)(() => tool ? tool.execute(toolArgs, toolContext) : Promise.resolve({ error: `Unknown tool: ${tc.name}` }));
572
+ });
573
+ const postToolPressure = new ContextPressure(ctx, pressureOpts);
574
+ const contextAvailablePercent = postToolPressure.percentAvailable;
575
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
576
+ result._contextAvailablePercent = contextAvailablePercent;
577
+ const resultObj = result;
578
+ if (Array.isArray(resultObj.results)) {
579
+ agent.addNestedResults(resultObj.results.filter((f) => typeof f === 'string'));
580
+ }
581
+ if (Array.isArray(resultObj.nestedResults)) {
582
+ agent.addNestedResults(resultObj.nestedResults.filter((f) => typeof f === 'string'));
583
+ }
584
+ }
585
+ const resultStr = JSON.stringify(result);
586
+ yield* poolChannel.send({ type: 'agent:tool_result', agentId: agent.id, tool: tc.name, result: resultStr, contextAvailablePercent });
587
+ const prefillTokens = (0, sdk_2.buildToolResultDelta)(ctx, resultStr, callId);
588
+ const probe = tool?.probe(result) ?? undefined;
589
+ results.push({ agentId: agent.id, prefillTokens, toolName: tc.name, callId, args: tc.arguments, probe });
590
+ tw.write({ traceId: tw.nextId(), parentTraceId: dispatchTraceId, ts: performance.now(),
591
+ type: 'tool:result', agentId: agent.id, tool: tc.name,
592
+ result, prefillTokenCount: prefillTokens.length,
593
+ durationMs: performance.now() - toolT0 });
594
+ }
595
+ catch (err) {
596
+ agent.transition('idle');
597
+ agent.reportResult(`Tool error: ${err.message}`, 'tool_error');
598
+ tw.write({ traceId: tw.nextId(), parentTraceId: dispatchTraceId, ts: performance.now(),
599
+ type: 'tool:error', agentId: agent.id, tool: tc.name,
600
+ error: err.message });
513
601
  }
514
- catch { }
515
602
  }
516
- yield* (0, effection_1.call)(() => store.prefill(prefillPairs));
517
- counters.warmPrefillCalls++;
518
- counters.warmPrefillBranches += prefillPairs.length;
519
- // Prefill per-tool reasoning probes for agents that just got real
520
- // tool results. Each tool can optionally return a probe string via
521
- // its `probe` getter — prefilled after the tool result to nudge the
522
- // model into prose reasoning before the next tool call.
523
- const probePairs = [];
524
- for (const a of settledAgents) {
525
- const probe = dispatchedProbes.get(a.id);
526
- if (probe)
527
- probePairs.push([a.branch, ctx.tokenizeSync(probe, false)]);
603
+ return results;
604
+ }
605
+ // ── Four-phase tick loop ─────────────────────────────────
606
+ let pendingSettled = [];
607
+ // ── Four-phase tick loop ─────────────────────────────────
608
+ let recoveryAttempted = false;
609
+ for (;;) {
610
+ // -- Phase 1: PRODUCE -- sample from active agents, collect tool calls
611
+ policy.resetTick?.();
612
+ const pressure = new ContextPressure(ctx, pressureOpts);
613
+ if (trace && (pressure.critical || pressure.headroom < 0)) {
614
+ try {
615
+ process.stderr.write(`[PRODUCE] ${pressure.critical ? 'CRITICAL' : 'SOFT_LIMIT'} remaining=${pressure.remaining} headroom=${pressure.headroom} cellsUsed=${pressure.cellsUsed} nCtx=${pressure.nCtx}\n`);
616
+ }
617
+ catch { }
528
618
  }
529
- if (probePairs.length > 0) {
530
- yield* (0, effection_1.call)(() => store.prefill(probePairs));
619
+ const entries = [];
620
+ const toolCalls = [];
621
+ const nudges = [];
622
+ for (const a of agents) {
623
+ if (a.status !== 'active')
624
+ continue;
625
+ const policyExit = policy.shouldExit?.(a, pressure);
626
+ if (policyExit ?? pressure.critical) {
627
+ a.transition('idle');
628
+ const exitReason = pressure.critical ? 'pressure_critical'
629
+ : policyExit ? 'policy_exit'
630
+ : 'pressure_critical';
631
+ tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
632
+ type: 'pool:agentDrop', agentId: a.id, reason: exitReason });
633
+ yield* poolChannel.send({ type: 'agent:done', agentId: a.id });
634
+ // Trailing stop: extract findings inline, free KV for remaining agents
635
+ yield* recoverInline(a, policy, ctx, store, tw, poolScope.traceId, poolChannel);
636
+ continue;
637
+ }
638
+ const { token, text, isStop } = a.branch.produceSync();
639
+ if (isStop) {
640
+ const parsed = a.finalize(ctx);
641
+ tw.write({
642
+ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
643
+ type: 'agent:turn', agentId: a.id, turn: a.turns,
644
+ rawOutput: a.rawOutput,
645
+ parsedContent: parsed.content || null,
646
+ parsedToolCalls: parsed.toolCalls.map(tc => ({ name: tc.name, arguments: tc.arguments })),
647
+ });
648
+ // Policy decides what to do with the parsed output
649
+ const action = policy.onProduced(a, parsed, pressure, policyConfig);
650
+ switch (action.type) {
651
+ case 'free_text_report':
652
+ yield* handleFreeTextReport(a, action.content, poolChannel);
653
+ continue;
654
+ case 'idle':
655
+ yield* handleIdleDrop(a, action.reason, poolChannel, tw, poolScope.traceId);
656
+ continue;
657
+ case 'nudge':
658
+ nudges.push(yield* handleNudge(a, action.message, parsed.toolCalls[0], ctx, tools));
659
+ tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
660
+ type: 'pool:agentNudge', agentId: a.id, reason: 'nudge', message: action.message });
661
+ continue;
662
+ case 'report':
663
+ yield* handleReport(a, action.result, parsed.toolCalls[0], terminalTool, pruneOnReport, poolChannel);
664
+ totalToolCalls++;
665
+ continue;
666
+ case 'tool_call':
667
+ a.transition('awaiting_tool');
668
+ toolCalls.push({ agent: a, tc: action.tc });
669
+ a.resetTurn();
670
+ continue;
671
+ }
672
+ }
673
+ entries.push([a.branch, token]);
674
+ if (trace) {
675
+ const entropy = a.branch.modelEntropy();
676
+ const surprisal = a.branch.modelSurprisal(token);
677
+ a.accumulateTokenWithTrace(text, entropy, surprisal);
678
+ a.observe(ctx);
679
+ yield* poolChannel.send({
680
+ type: 'agent:produce', agentId: a.id, text, tokenCount: a.tokenCount,
681
+ entropy, surprisal,
682
+ });
683
+ }
684
+ else {
685
+ a.accumulateToken(text);
686
+ a.observe(ctx);
687
+ yield* poolChannel.send({ type: 'agent:produce', agentId: a.id, text, tokenCount: a.tokenCount });
688
+ }
531
689
  }
532
- dispatchedProbes.clear();
533
- // Only NOW transition state + reset grammar
534
- for (const a of settledAgents) {
535
- a.transition('active');
536
- a.resetTurn();
537
- applyLazyGrammar(a);
690
+ // -- Phase 2: COMMIT -- batch-decode produced tokens
691
+ if (entries.length > 0) {
692
+ yield* (0, effection_1.call)(() => store.commit(entries));
693
+ steps++;
694
+ const commitPressure = new ContextPressure(ctx, pressureOpts);
695
+ yield* poolChannel.send({ type: 'agent:tick', cellsUsed: commitPressure.cellsUsed, nCtx: commitPressure.nCtx });
538
696
  }
539
- }
540
- }
541
- // -- Phase 4: DISPATCH -- execute collected tool calls sequentially
542
- // scoped() creates an error boundary inner pool errors are caught
543
- // here instead of crashing the outer pool. call() yields the Operation
544
- // directly, ensuring exclusive llama_context access (no concurrent
545
- // AsyncWorkers). See docs/agents/concurrency.md.
546
- for (const { agent, tc } of toolCalls) {
547
- let toolArgs;
548
- try {
549
- toolArgs = JSON.parse(tc.arguments);
550
- }
551
- catch {
552
- toolArgs = {};
553
- }
554
- const callId = tc.id || `call_${agent.toolCallCount}`;
555
- agent.incrementToolCalls();
556
- totalToolCalls++;
557
- agent.incrementTurns();
558
- yield* events.send({ type: 'agent:tool_call', agentId: agent.id, tool: tc.name, args: tc.arguments });
559
- const tool = tools.get(tc.name);
560
- // Fresh pressure snapshot — SETTLE may have consumed significant KV
561
- // since the PRODUCE-phase snapshot at tick-top. On 16K context, a
562
- // single SETTLE pass can drain 12-18% of capacity (3 agents' tool
563
- // results). Using stale PRODUCE pressure here would keep agents in
564
- // explore mode past the threshold.
565
- const dispatchPressure = new ContextPressure(ctx, pressureOpts);
566
- const explore = policy.shouldExplore?.(agent, dispatchPressure) ?? true;
567
- const dispatchTraceId = tw.nextId();
568
- const toolT0 = performance.now();
569
- tw.write({
570
- traceId: dispatchTraceId, parentTraceId: poolScope.traceId, ts: toolT0,
571
- type: 'tool:dispatch', agentId: agent.id, tool: tc.name,
572
- toolIndex: toolIndexMap.get(tc.name) ?? -1, toolkitSize,
573
- args: toolArgs, callId,
574
- explore, percentAvailable: dispatchPressure.percentAvailable,
575
- });
576
- const toolContext = {
577
- agentId: agent.id,
578
- branch: agent.branch,
579
- onProgress: (p) => {
580
- progressBridge.send({ type: 'agent:tool_progress', agentId: agent.id, tool: tc.name, filled: p.filled, total: p.total });
581
- },
582
- scorer: opts.scorer,
583
- explore,
584
- pressurePercentAvailable: dispatchPressure.percentAvailable,
585
- };
586
- try {
587
- // Set TraceParent + CallingAgent so inner pools inherit lineage
588
- yield* context_1.TraceParent.set(dispatchTraceId);
589
- yield* context_1.CallingAgent.set(agent);
590
- const result = yield* (0, effection_1.scoped)(function* () {
591
- return yield* (0, effection_1.call)(() => tool ? tool.execute(toolArgs, toolContext) : Promise.resolve({ error: `Unknown tool: ${tc.name}` }));
592
- });
593
- // Inject context availability into tool result so agent can make pressure-aware decisions
594
- const postToolPressure = new ContextPressure(ctx, pressureOpts);
595
- const contextAvailablePercent = postToolPressure.percentAvailable;
596
- if (result && typeof result === 'object' && !Array.isArray(result)) {
597
- result._contextAvailablePercent = contextAvailablePercent;
598
- // Collect nested results from recursive tool returns
599
- const resultObj = result;
600
- if (Array.isArray(resultObj.results)) {
601
- agent.addNestedResults(resultObj.results.filter((f) => typeof f === 'string'));
602
- }
603
- if (Array.isArray(resultObj.nestedResults)) {
604
- agent.addNestedResults(resultObj.nestedResults.filter((f) => typeof f === 'string'));
697
+ // -- Phase 3: SETTLE (settle what fits, defer what doesn't)
698
+ const toSettle = [...pendingSettled, ...nudges];
699
+ const deferred = toSettle.length > 0 ? yield* settle(toSettle) : [];
700
+ // Stall-breaker: if items are deferred and no active agents remain,
701
+ // sacrifice an awaiting_tool agent to free KV. Without this, agents
702
+ // with oversized results stay awaiting_tool indefinitely — PRODUCE
703
+ // skips them, headroom never recovers, the pool loops forever.
704
+ if (deferred.length > 0 && !agents.some(a => a.status === 'active')) {
705
+ const victim = agents.find(a => a.status === 'awaiting_tool' && !a.branch.disposed);
706
+ if (victim) {
707
+ victim.transition('idle');
708
+ tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
709
+ type: 'pool:agentDrop', agentId: victim.id, reason: 'pressure_settle_reject' });
710
+ yield* poolChannel.send({ type: 'agent:done', agentId: victim.id });
711
+ yield* recoverInline(victim, policy, ctx, store, tw, poolScope.traceId, poolChannel);
605
712
  }
606
713
  }
607
- const resultStr = JSON.stringify(result);
608
- yield* events.send({ type: 'agent:tool_result', agentId: agent.id, tool: tc.name, result: resultStr, contextAvailablePercent });
609
- const prefillTokens = (0, sdk_2.buildToolResultDelta)(ctx, resultStr, callId);
610
- settledBuffer.push({ agentId: agent.id, prefillTokens, toolName: tc.name, callId });
611
- const probe = tool?.probe;
612
- if (probe)
613
- dispatchedProbes.set(agent.id, probe);
614
- tw.write({
615
- traceId: tw.nextId(), parentTraceId: dispatchTraceId, ts: performance.now(),
616
- type: 'tool:result', agentId: agent.id, tool: tc.name,
617
- result, prefillTokenCount: prefillTokens.length,
618
- durationMs: performance.now() - toolT0,
619
- });
620
- }
621
- catch (err) {
622
- agent.transition('idle');
623
- agent.reportResult(`Tool error: ${err.message}`, 'tool_error');
624
- tw.write({
625
- traceId: tw.nextId(), parentTraceId: dispatchTraceId, ts: performance.now(),
626
- type: 'tool:error', agentId: agent.id, tool: tc.name,
627
- error: err.message,
628
- });
629
- }
630
- }
631
- // -- Termination
632
- if (agents.every(a => a.status === 'idle' || a.status === 'disposed'))
633
- break;
634
- }
635
- // ── Idle processing: scratchpad recovery ─────────────────
636
- // Policy decides per-agent whether to extract findings from killed agents.
637
- // The pool owns the grammar and fork/generate/parse mechanics.
638
- // Free KV from agents that already reported — gives room for extraction.
639
- for (const a of agents) {
640
- if (a.result && !a.branch.disposed) {
641
- a.branch.pruneSync();
642
- }
643
- }
644
- // Check if any agent needs recovery before setting up grammar
645
- const needsRecovery = agents.some(a => a.status === 'idle' && !a.result && !a.branch.disposed &&
646
- policy.onRecovery?.(a)?.type === 'extract');
647
- if (needsRecovery) {
648
- const reportSchema = {
649
- type: 'object',
650
- properties: { result: { type: 'string' } },
651
- required: ['result'],
652
- };
653
- const reportGrammar = yield* (0, effection_1.call)(() => ctx.jsonSchemaToGrammar(JSON.stringify(reportSchema)));
654
- // Cache formatted prompts per unique prompt object
655
- const promptCache = new Map();
656
- for (const a of agents) {
657
- if (a.status !== 'idle' || a.result || a.branch.disposed)
658
- continue;
659
- const recovery = policy.onRecovery?.(a);
660
- if (!recovery || recovery.type === 'skip') {
661
- if (!a.branch.disposed)
662
- a.branch.pruneSync();
663
- continue;
664
- }
665
- // Format extraction prompt (cache by system+user key)
666
- const cacheKey = recovery.prompt.system + '\0' + recovery.prompt.user;
667
- let extractionPromptStr = promptCache.get(cacheKey);
668
- if (!extractionPromptStr) {
669
- const reportMessages = [
670
- { role: 'system', content: recovery.prompt.system },
671
- { role: 'user', content: recovery.prompt.user },
672
- ];
673
- const { prompt } = ctx.formatChatSync(JSON.stringify(reportMessages), { enableThinking: false });
674
- extractionPromptStr = prompt;
675
- promptCache.set(cacheKey, prompt);
676
- }
677
- try {
678
- yield* events.send({ type: 'agent:spawn', agentId: a.id, parentAgentId: a.parentId });
679
- const branch = yield* (0, generate_1.prepare)({
680
- prompt: extractionPromptStr,
681
- grammar: reportGrammar,
682
- parent: a.branch,
683
- });
684
- try {
685
- let output = '';
686
- let tokenCount = 0;
687
- yield* (0, effection_1.call)(async () => {
688
- for await (const { text } of branch) {
689
- output += text;
690
- tokenCount++;
714
+ // -- Phase 4: DISPATCH
715
+ const dispatched = yield* dispatch(toolCalls);
716
+ // Deferred + new dispatch results → next tick's SETTLE
717
+ pendingSettled = [...deferred, ...dispatched];
718
+ // -- Termination + recovery
719
+ if (agents.every(a => a.status === 'idle' || a.status === 'disposed')) {
720
+ if (!recoveryAttempted) {
721
+ recoveryAttempted = true;
722
+ // Recover any idle agents that weren't handled by inline recovery
723
+ // (e.g., killed by max_turns, time budget, or free_text_stop)
724
+ for (const a of agents) {
725
+ if (a.status === 'idle' && !a.result && !a.branch.disposed) {
726
+ yield* recoverInline(a, policy, ctx, store, tw, poolScope.traceId, poolChannel);
727
+ }
691
728
  }
692
- });
693
- const tickPressure = new ContextPressure(ctx, pressureOpts);
694
- yield* events.send({
695
- type: 'agent:tick', cellsUsed: tickPressure.cellsUsed, nCtx: tickPressure.nCtx,
696
- });
697
- const parsed = JSON.parse(output);
698
- if (parsed?.result) {
699
- a.reportResult(parsed.result, 'scratchpad');
700
- yield* events.send({ type: 'agent:report', agentId: a.id, result: a.result });
701
729
  }
730
+ break;
702
731
  }
703
- finally {
704
- if (!branch.disposed)
705
- branch.pruneSync();
706
- }
707
- }
708
- catch {
709
- /* extraction failure non-fatal */
710
732
  }
711
- if (!a.branch.disposed)
712
- a.branch.pruneSync();
733
+ // ── Close channel with result — consumers get AgentPoolResult as close value ───────
734
+ // Branch cleanup is handled by each branch's ensure() from setupAgent —
735
+ // when this resource's scope exits, all ensure() callbacks fire.
736
+ tw.write({
737
+ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
738
+ type: 'pool:close',
739
+ agents: agents.map(a => ({
740
+ agentId: a.id, tokenCount: a.tokenCount,
741
+ toolCallCount: a.toolCallCount, result: a.result,
742
+ ppl: a.branch.disposed ? 0 : a.branch.perplexity,
743
+ })),
744
+ totalTokens: agents.reduce((s, a) => s + a.tokenCount, 0),
745
+ steps, durationMs: performance.now() - poolT0,
746
+ });
747
+ poolScope.close();
748
+ const result = {
749
+ agents: agents.map(a => ({
750
+ agentId: a.id,
751
+ parentAgentId: a.parentId,
752
+ branch: a.branch,
753
+ agent: a,
754
+ result: a.result,
755
+ toolCallCount: a.toolCallCount,
756
+ tokenCount: a.tokenCount,
757
+ ppl: a.branch.disposed ? 0 : a.branch.perplexity,
758
+ samplingPpl: a.branch.disposed ? 0 : a.branch.samplingPerplexity,
759
+ trace: trace ? a.traceBuffer : undefined,
760
+ nestedResults: [...a.nestedResults],
761
+ })),
762
+ totalTokens: agents.reduce((s, a) => s + a.tokenCount, 0),
763
+ totalToolCalls,
764
+ steps,
765
+ counters,
766
+ };
767
+ yield* poolChannel.close(result);
713
768
  }
714
- }
715
- // ── Provide resultsuspends, branches stay alive ───────
716
- // Branch cleanup is handled by each branch's ensure() from setupAgent —
717
- // when this resource's scope exits, all ensure() callbacks fire.
718
- tw.write({
719
- traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
720
- type: 'pool:close',
721
- agents: agents.map(a => ({
722
- agentId: a.id, tokenCount: a.tokenCount,
723
- toolCallCount: a.toolCallCount, result: a.result,
724
- ppl: a.branch.disposed ? 0 : a.branch.perplexity,
725
- })),
726
- totalTokens: agents.reduce((s, a) => s + a.tokenCount, 0),
727
- steps, durationMs: performance.now() - poolT0,
728
- });
729
- poolScope.close();
730
- const result = {
731
- agents: agents.map(a => ({
732
- agentId: a.id,
733
- parentAgentId: a.parentId,
734
- branch: a.branch,
735
- result: a.result,
736
- toolCallCount: a.toolCallCount,
737
- tokenCount: a.tokenCount,
738
- ppl: a.branch.disposed ? 0 : a.branch.perplexity,
739
- samplingPpl: a.branch.disposed ? 0 : a.branch.samplingPerplexity,
740
- trace: trace ? a.traceBuffer : undefined,
741
- nestedResults: [...a.nestedResults],
742
- })),
743
- totalTokens: agents.reduce((s, a) => s + a.tokenCount, 0),
744
- totalToolCalls,
745
- steps,
746
- counters,
747
- };
748
- yield* provide(result);
769
+ catch {
770
+ // KV exhaustion or other decode failure close with partial results
771
+ poolScope.close();
772
+ const partial = {
773
+ agents: agents.map(a => ({
774
+ agentId: a.id, parentAgentId: a.parentId, branch: a.branch, agent: a,
775
+ result: a.result, toolCallCount: a.toolCallCount, tokenCount: a.tokenCount,
776
+ ppl: a.branch.disposed ? 0 : a.branch.perplexity,
777
+ samplingPpl: a.branch.disposed ? 0 : a.branch.samplingPerplexity,
778
+ trace: trace ? a.traceBuffer : undefined,
779
+ nestedResults: [...a.nestedResults],
780
+ })),
781
+ totalTokens: agents.reduce((s, a) => s + a.tokenCount, 0),
782
+ totalToolCalls, steps, counters,
783
+ };
784
+ yield* poolChannel.close(partial);
785
+ }
786
+ }); // end spawn — tick loop
787
+ yield* provide(subscription);
749
788
  });
750
789
  }
751
790
  //# sourceMappingURL=agent-pool.js.map