@lloyal-labs/lloyal-agents 1.5.8 → 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 (51) 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 +25 -6
  6. package/dist/AgentPolicy.d.ts.map +1 -1
  7. package/dist/AgentPolicy.js +40 -31
  8. package/dist/AgentPolicy.js.map +1 -1
  9. package/dist/agent-pool.d.ts +3 -3
  10. package/dist/agent-pool.d.ts.map +1 -1
  11. package/dist/agent-pool.js +343 -320
  12. package/dist/agent-pool.js.map +1 -1
  13. package/dist/combinators.d.ts +29 -0
  14. package/dist/combinators.d.ts.map +1 -0
  15. package/dist/combinators.js +37 -0
  16. package/dist/combinators.js.map +1 -0
  17. package/dist/create-agent-pool.d.ts +78 -0
  18. package/dist/create-agent-pool.d.ts.map +1 -0
  19. package/dist/create-agent-pool.js +60 -0
  20. package/dist/create-agent-pool.js.map +1 -0
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +8 -8
  24. package/dist/index.js.map +1 -1
  25. package/dist/source.d.ts.map +1 -1
  26. package/dist/source.js.map +1 -1
  27. package/dist/trace-types.d.ts +2 -1
  28. package/dist/trace-types.d.ts.map +1 -1
  29. package/dist/trace-writer.d.ts +4 -1
  30. package/dist/trace-writer.d.ts.map +1 -1
  31. package/dist/trace-writer.js +6 -2
  32. package/dist/trace-writer.js.map +1 -1
  33. package/dist/types.d.ts +9 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/use-agent.d.ts +92 -0
  36. package/dist/use-agent.d.ts.map +1 -0
  37. package/dist/use-agent.js +131 -0
  38. package/dist/use-agent.js.map +1 -0
  39. package/package.json +2 -2
  40. package/dist/generate.d.ts +0 -77
  41. package/dist/generate.d.ts.map +0 -1
  42. package/dist/generate.js +0 -166
  43. package/dist/generate.js.map +0 -1
  44. package/dist/run-agents.d.ts +0 -39
  45. package/dist/run-agents.d.ts.map +0 -1
  46. package/dist/run-agents.js +0 -46
  47. package/dist/run-agents.js.map +0 -1
  48. package/dist/spawn-agents.d.ts +0 -104
  49. package/dist/spawn-agents.d.ts.map +0 -1
  50. package/dist/spawn-agents.js +0 -255
  51. package/dist/spawn-agents.js.map +0 -1
@@ -115,6 +115,9 @@ function* recoverInline(agent, policy, ctx, store, tw, parentTraceId, events) {
115
115
  agent.branch.pruneSync();
116
116
  return false;
117
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.
118
121
  const { prompt } = ctx.formatChatSync(JSON.stringify([
119
122
  { role: 'system', content: recovery.prompt.system },
120
123
  { role: 'user', content: recovery.prompt.user },
@@ -122,23 +125,14 @@ function* recoverInline(agent, policy, ctx, store, tw, parentTraceId, events) {
122
125
  const sep = ctx.getTurnSeparator();
123
126
  const delta = ctx.tokenizeSync(prompt, false);
124
127
  const tokens = [...sep, ...delta];
125
- // Check if extraction prompt fits
126
- const pressure = new ContextPressure(ctx);
127
- if (pressure.remaining < tokens.length) {
128
- if (!agent.branch.disposed)
129
- agent.branch.pruneSync();
130
- return false;
131
- }
132
- // Eager report grammar
128
+ // Eager report grammar forces { result: string } output
133
129
  const reportGrammar = yield* (0, effection_1.call)(() => ctx.jsonSchemaToGrammar(JSON.stringify({
134
130
  type: 'object',
135
131
  properties: { result: { type: 'string' } },
136
132
  required: ['result'],
137
133
  })));
138
- // Recovery runs in its own scope — if decode fails (KV exhaustion),
139
- // the scope tears down cleanly without propagating to the pool.
140
- // Mirrors the old prepare()-based recovery which used try/catch around
141
- // a Resource with its own ensure().
134
+ // Recovery runs in its own scope — if prefill or decode fails
135
+ // (KV exhaustion), the scope tears down cleanly.
142
136
  let reported = false;
143
137
  try {
144
138
  yield* (0, effection_1.scoped)(function* () {
@@ -149,7 +143,6 @@ function* recoverInline(agent, policy, ctx, store, tw, parentTraceId, events) {
149
143
  type: 'branch:prefill', branchHandle: agent.id,
150
144
  tokenCount: tokens.length, role: 'recovery',
151
145
  });
152
- yield* events.send({ type: 'agent:spawn', agentId: agent.id, parentAgentId: agent.parentId });
153
146
  // Single-agent produce/commit loop
154
147
  let output = '';
155
148
  let tokenCount = 0;
@@ -171,8 +164,8 @@ function* recoverInline(agent, policy, ctx, store, tw, parentTraceId, events) {
171
164
  }
172
165
  });
173
166
  }
174
- catch { /* decode failure or malformed JSON — non-fatal, prune below */ }
175
- // Always prune after scope exits (success or decode failure)
167
+ catch { /* prefill overflow, decode failure, or malformed JSON — non-fatal */ }
168
+ // Always prune after scope exits (success or failure)
176
169
  if (!agent.branch.disposed)
177
170
  agent.branch.pruneSync();
178
171
  // Emit tick so TUI updates pressure percentage after prune
@@ -206,7 +199,7 @@ function* handleNudge(a, message, tc, ctx, tools) {
206
199
  const prefillTokens = (0, sdk_2.buildToolResultDelta)(ctx, JSON.stringify(nudgeResult), callId);
207
200
  const probe = tools?.get(tc?.name || '')?.probe(nudgeResult) ?? undefined;
208
201
  a.resetTurn();
209
- return { agentId: a.id, prefillTokens, toolName: tc?.name || '', callId, probe };
202
+ return { agentId: a.id, prefillTokens, toolName: tc?.name || '', callId, args: tc?.arguments || '', probe };
210
203
  }
211
204
  function* handleReport(a, result, tc, terminalTool, pruneOnReport, events) {
212
205
  a.reportResult(result, 'report_tool');
@@ -322,13 +315,13 @@ function useAgentPool(opts) {
322
315
  return (0, effection_1.resource)(function* (provide) {
323
316
  const ctx = yield* context_1.Ctx.expect();
324
317
  const store = yield* context_1.Store.expect();
325
- const events = yield* context_1.Events.expect();
318
+ const poolChannel = (0, effection_1.createChannel)();
326
319
  // Bridge for onProgress callbacks — Signal is correct here (external callback).
327
- // 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.
328
321
  const progressBridge = (0, effection_1.createSignal)();
329
322
  yield* (0, effection_1.spawn)(function* () {
330
323
  for (const ev of yield* (0, effection_1.each)(progressBridge)) {
331
- yield* events.send(ev);
324
+ yield* poolChannel.send(ev);
332
325
  yield* effection_1.each.next();
333
326
  }
334
327
  });
@@ -421,11 +414,6 @@ function useAgentPool(opts) {
421
414
  taskSuffixTokens: prefillSetup.map(([, t]) => t.length),
422
415
  pressure: { remaining: initPressure.remaining, softLimit: initPressure.softLimit, headroom: initPressure.headroom },
423
416
  });
424
- // Emit spawn events and activate agents
425
- for (const a of agents) {
426
- a.transition('active');
427
- yield* events.send({ type: 'agent:spawn', agentId: a.id, parentAgentId: a.parentId });
428
- }
429
417
  // ── Lazy grammar setup ───────────────────────────────────
430
418
  const applyLazyGrammar = (a) => {
431
419
  if (a.fmt.grammar && a.fmt.grammarLazy && a.fmt.grammarTriggers.length > 0) {
@@ -444,324 +432,359 @@ function useAgentPool(opts) {
444
432
  for (const a of agents)
445
433
  applyLazyGrammar(a);
446
434
  const agentById = new Map(agents.map(a => [a.id, a]));
447
- let steps = 0;
448
- let totalToolCalls = 0;
449
- const counters = { warmPrefillCalls: 0, warmPrefillBranches: 0 };
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`);
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 });
459
449
  }
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')
468
- continue;
469
- if (item.prefillTokens.length > headroom) {
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;
470
455
  if (trace) {
456
+ const desc = items.map(s => `${s.toolName}:${s.prefillTokens.length}`).join(', ');
471
457
  try {
472
- process.stderr.write(`[SETTLE] DEFER ${item.toolName}:${item.prefillTokens.length} > headroom=${headroom}\n`);
458
+ process.stderr.write(`[SETTLE] remaining=${settlePressure.remaining} headroom=${headroom} cellsUsed=${settlePressure.cellsUsed} nCtx=${settlePressure.nCtx} items=[${desc}]\n`);
473
459
  }
474
460
  catch { }
475
461
  }
476
- deferred.push(item);
477
- continue;
478
- }
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.callId,
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' });
492
- }
493
- if (prefillPairs.length > 0) {
494
- if (trace) {
495
- const total = prefillPairs.reduce((s, [, t]) => s + t.length, 0);
496
- try {
497
- process.stderr.write(`[SETTLE] PREFILL ${prefillPairs.length} branches, ${total} tokens, headroom_after=${headroom}\n`);
498
- }
499
- catch { }
500
- }
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]);
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')
468
+ continue;
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 { }
475
+ }
476
+ deferred.push(item);
477
+ continue;
478
+ }
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
+ });
511
489
  tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
512
490
  type: 'branch:prefill', branchHandle: a.id,
513
- tokenCount: probeTokens.length, role: 'probe', probeText: probe });
491
+ tokenCount: item.prefillTokens.length, role: 'toolResult' });
514
492
  }
515
- }
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
- }
524
- }
525
- return deferred;
526
- }
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;
532
- try {
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* events.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 toolContext = {
556
- agentId: agent.id, branch: agent.branch,
557
- onProgress: (p) => {
558
- progressBridge.send({ type: 'agent:tool_progress', agentId: agent.id, tool: tc.name, filled: p.filled, total: p.total });
559
- },
560
- scorer: opts.scorer, explore,
561
- pressurePercentAvailable: dispatchPressure.percentAvailable,
562
- };
563
- try {
564
- yield* context_1.TraceParent.set(dispatchTraceId);
565
- yield* context_1.CallingAgent.set(agent);
566
- const result = yield* (0, effection_1.scoped)(function* () {
567
- return yield* (0, effection_1.call)(() => tool ? tool.execute(toolArgs, toolContext) : Promise.resolve({ error: `Unknown tool: ${tc.name}` }));
568
- });
569
- const postToolPressure = new ContextPressure(ctx, pressureOpts);
570
- const contextAvailablePercent = postToolPressure.percentAvailable;
571
- if (result && typeof result === 'object' && !Array.isArray(result)) {
572
- result._contextAvailablePercent = contextAvailablePercent;
573
- const resultObj = result;
574
- if (Array.isArray(resultObj.results)) {
575
- agent.addNestedResults(resultObj.results.filter((f) => typeof f === 'string'));
493
+ if (prefillPairs.length > 0) {
494
+ if (trace) {
495
+ const total = prefillPairs.reduce((s, [, t]) => s + t.length, 0);
496
+ try {
497
+ process.stderr.write(`[SETTLE] PREFILL ${prefillPairs.length} branches, ${total} tokens, headroom_after=${headroom}\n`);
498
+ }
499
+ catch { }
500
+ }
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]);
511
+ tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
512
+ type: 'branch:prefill', branchHandle: a.id,
513
+ tokenCount: probeTokens.length, role: 'probe', probeText: probe });
514
+ }
576
515
  }
577
- if (Array.isArray(resultObj.nestedResults)) {
578
- agent.addNestedResults(resultObj.nestedResults.filter((f) => typeof f === 'string'));
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);
579
523
  }
580
524
  }
581
- const resultStr = JSON.stringify(result);
582
- yield* events.send({ type: 'agent:tool_result', agentId: agent.id, tool: tc.name, result: resultStr, contextAvailablePercent });
583
- const prefillTokens = (0, sdk_2.buildToolResultDelta)(ctx, resultStr, callId);
584
- const probe = tool?.probe(result) ?? undefined;
585
- results.push({ agentId: agent.id, prefillTokens, toolName: tc.name, callId, probe });
586
- tw.write({ traceId: tw.nextId(), parentTraceId: dispatchTraceId, ts: performance.now(),
587
- type: 'tool:result', agentId: agent.id, tool: tc.name,
588
- result, prefillTokenCount: prefillTokens.length,
589
- durationMs: performance.now() - toolT0 });
525
+ return deferred;
590
526
  }
591
- catch (err) {
592
- agent.transition('idle');
593
- agent.reportResult(`Tool error: ${err.message}`, 'tool_error');
594
- tw.write({ traceId: tw.nextId(), parentTraceId: dispatchTraceId, ts: performance.now(),
595
- type: 'tool:error', agentId: agent.id, tool: tc.name,
596
- error: err.message });
597
- }
598
- }
599
- return results;
600
- }
601
- // ── Four-phase tick loop ─────────────────────────────────
602
- let pendingSettled = [];
603
- // ── Four-phase tick loop ─────────────────────────────────
604
- let recoveryAttempted = false;
605
- for (;;) {
606
- // -- Phase 1: PRODUCE -- sample from active agents, collect tool calls
607
- policy.resetTick?.();
608
- const pressure = new ContextPressure(ctx, pressureOpts);
609
- if (trace && (pressure.critical || pressure.headroom < 0)) {
610
- try {
611
- process.stderr.write(`[PRODUCE] ${pressure.critical ? 'CRITICAL' : 'SOFT_LIMIT'} remaining=${pressure.remaining} headroom=${pressure.headroom} cellsUsed=${pressure.cellsUsed} nCtx=${pressure.nCtx}\n`);
612
- }
613
- catch { }
614
- }
615
- const entries = [];
616
- const toolCalls = [];
617
- const nudges = [];
618
- for (const a of agents) {
619
- if (a.status !== 'active')
620
- continue;
621
- const policyExit = policy.shouldExit?.(a, pressure);
622
- if (policyExit ?? pressure.critical) {
623
- a.transition('idle');
624
- const exitReason = pressure.critical ? 'pressure_critical'
625
- : policyExit ? 'policy_exit'
626
- : 'pressure_critical';
627
- tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
628
- type: 'pool:agentDrop', agentId: a.id, reason: exitReason });
629
- yield* events.send({ type: 'agent:done', agentId: a.id });
630
- // Trailing stop: extract findings inline, free KV for remaining agents
631
- yield* recoverInline(a, policy, ctx, store, tw, poolScope.traceId, events);
632
- continue;
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;
532
+ try {
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 });
601
+ }
602
+ }
603
+ return results;
633
604
  }
634
- const { token, text, isStop } = a.branch.produceSync();
635
- if (isStop) {
636
- const parsed = ctx.parseChatOutput(a.rawOutput, a.fmt.format, {
637
- reasoningFormat: a.fmt.reasoningFormat,
638
- generationPrompt: a.fmt.generationPrompt,
639
- parser: a.fmt.parser,
640
- });
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, events);
653
- continue;
654
- case 'idle':
655
- yield* handleIdleDrop(a, action.reason, events, tw, poolScope.traceId);
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 { }
618
+ }
619
+ const entries = [];
620
+ const toolCalls = [];
621
+ const nudges = [];
622
+ for (const a of agents) {
623
+ if (a.status !== 'active')
656
624
  continue;
657
- case 'nudge':
658
- nudges.push(yield* handleNudge(a, action.message, parsed.toolCalls[0], ctx, tools));
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';
659
631
  tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
660
- type: 'pool:agentNudge', agentId: a.id, reason: 'pressure_softcut' });
661
- continue;
662
- case 'report':
663
- yield* handleReport(a, action.result, parsed.toolCalls[0], terminalTool, pruneOnReport, events);
664
- totalToolCalls++;
665
- continue;
666
- case 'tool_call':
667
- a.transition('awaiting_tool');
668
- toolCalls.push({ agent: a, tc: action.tc });
669
- a.resetTurn();
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);
670
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
+ }
671
689
  }
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
- yield* events.send({
679
- type: 'agent:produce', agentId: a.id, text, tokenCount: a.tokenCount,
680
- entropy, surprisal,
681
- });
682
- }
683
- else {
684
- a.accumulateToken(text);
685
- yield* events.send({ type: 'agent:produce', agentId: a.id, text, tokenCount: a.tokenCount });
686
- }
687
- }
688
- // -- Phase 2: COMMIT -- batch-decode produced tokens
689
- if (entries.length > 0) {
690
- yield* (0, effection_1.call)(() => store.commit(entries));
691
- steps++;
692
- const commitPressure = new ContextPressure(ctx, pressureOpts);
693
- yield* events.send({ type: 'agent:tick', cellsUsed: commitPressure.cellsUsed, nCtx: commitPressure.nCtx });
694
- }
695
- // -- Phase 3: SETTLE (settle what fits, defer what doesn't)
696
- const toSettle = [...pendingSettled, ...nudges];
697
- const deferred = toSettle.length > 0 ? yield* settle(toSettle) : [];
698
- // Stall-breaker: if items are deferred and no active agents remain,
699
- // sacrifice an awaiting_tool agent to free KV. Without this, agents
700
- // with oversized results stay awaiting_tool indefinitely — PRODUCE
701
- // skips them, headroom never recovers, the pool loops forever.
702
- if (deferred.length > 0 && !agents.some(a => a.status === 'active')) {
703
- const victim = agents.find(a => a.status === 'awaiting_tool' && !a.branch.disposed);
704
- if (victim) {
705
- victim.transition('idle');
706
- tw.write({ traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
707
- type: 'pool:agentDrop', agentId: victim.id, reason: 'pressure_settle_reject' });
708
- yield* events.send({ type: 'agent:done', agentId: victim.id });
709
- yield* recoverInline(victim, policy, ctx, store, tw, poolScope.traceId, events);
710
- }
711
- }
712
- // -- Phase 4: DISPATCH
713
- const dispatched = yield* dispatch(toolCalls);
714
- // Deferred + new dispatch results → next tick's SETTLE
715
- pendingSettled = [...deferred, ...dispatched];
716
- // -- Termination + recovery
717
- if (agents.every(a => a.status === 'idle' || a.status === 'disposed')) {
718
- if (!recoveryAttempted) {
719
- recoveryAttempted = true;
720
- // Recover any idle agents that weren't handled by inline recovery
721
- // (e.g., killed by max_turns, time budget, or free_text_stop)
722
- for (const a of agents) {
723
- if (a.status === 'idle' && !a.result && !a.branch.disposed) {
724
- yield* recoverInline(a, policy, ctx, store, tw, poolScope.traceId, events);
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 });
696
+ }
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);
712
+ }
713
+ }
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
+ }
728
+ }
725
729
  }
730
+ break;
726
731
  }
727
732
  }
728
- break;
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);
729
768
  }
730
- }
731
- // ── Provide resultsuspends, branches stay alive ───────
732
- // Branch cleanup is handled by each branch's ensure() from setupAgent —
733
- // when this resource's scope exits, all ensure() callbacks fire.
734
- tw.write({
735
- traceId: tw.nextId(), parentTraceId: poolScope.traceId, ts: performance.now(),
736
- type: 'pool:close',
737
- agents: agents.map(a => ({
738
- agentId: a.id, tokenCount: a.tokenCount,
739
- toolCallCount: a.toolCallCount, result: a.result,
740
- ppl: a.branch.disposed ? 0 : a.branch.perplexity,
741
- })),
742
- totalTokens: agents.reduce((s, a) => s + a.tokenCount, 0),
743
- steps, durationMs: performance.now() - poolT0,
744
- });
745
- poolScope.close();
746
- const result = {
747
- agents: agents.map(a => ({
748
- agentId: a.id,
749
- parentAgentId: a.parentId,
750
- branch: a.branch,
751
- result: a.result,
752
- toolCallCount: a.toolCallCount,
753
- tokenCount: a.tokenCount,
754
- ppl: a.branch.disposed ? 0 : a.branch.perplexity,
755
- samplingPpl: a.branch.disposed ? 0 : a.branch.samplingPerplexity,
756
- trace: trace ? a.traceBuffer : undefined,
757
- nestedResults: [...a.nestedResults],
758
- })),
759
- totalTokens: agents.reduce((s, a) => s + a.tokenCount, 0),
760
- totalToolCalls,
761
- steps,
762
- counters,
763
- };
764
- 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);
765
788
  });
766
789
  }
767
790
  //# sourceMappingURL=agent-pool.js.map