@sschepis/oboto-agent 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -74,7 +74,33 @@ interface ObotoAgentConfig {
74
74
  /** as-agent Wasm runtime for slash commands and utilities */
75
75
  agentRuntime?: AgentRuntime;
76
76
  }
77
- type AgentEventType = "user_input" | "agent_thought" | "token" | "triage_result" | "tool_execution_start" | "tool_execution_complete" | "tool_round_complete" | "state_updated" | "interruption" | "error" | "cost_update" | "turn_complete" | "permission_denied" | "session_compacted" | "hook_denied" | "hook_message" | "router_event" | "slash_command";
77
+ type AgentEventType = "user_input" | "agent_thought" | "token" | "phase" | "triage_result" | "tool_execution_start" | "tool_execution_complete" | "tool_round_complete" | "state_updated" | "interruption" | "error" | "cost_update" | "turn_complete" | "permission_denied" | "session_compacted" | "hook_denied" | "hook_message" | "router_event" | "slash_command" | "doom_loop";
78
+ /** Phase identifiers matching the unified provider's phase system. */
79
+ type AgentPhase = "request" | "precheck" | "planning" | "thinking" | "tools" | "validation" | "memory" | "continuation" | "error" | "doom" | "cancel" | "complete";
80
+ /** Payload for phase events. */
81
+ interface PhaseEvent {
82
+ phase: AgentPhase;
83
+ message: string;
84
+ }
85
+ /** Payload for doom loop events. */
86
+ interface DoomLoopEvent {
87
+ reason: string;
88
+ command: string;
89
+ count: number;
90
+ redirected: boolean;
91
+ }
92
+ /** Payload for tool round completion with narrative. */
93
+ interface ToolRoundEvent {
94
+ iteration: number;
95
+ tools: Array<{
96
+ command: string;
97
+ success: boolean;
98
+ kwargs?: Record<string, unknown>;
99
+ durationMs?: number;
100
+ }>;
101
+ totalToolCalls: number;
102
+ narrative: string;
103
+ }
78
104
  interface AgentEvent<T = unknown> {
79
105
  type: AgentEventType;
80
106
  payload: T;
@@ -657,7 +683,7 @@ declare class ObotoAgent {
657
683
  private maxIterations;
658
684
  private config;
659
685
  private onToken?;
660
- private costTracker?;
686
+ private costTracker;
661
687
  private modelPricing?;
662
688
  private rateLimiter?;
663
689
  private middleware;
@@ -670,6 +696,7 @@ declare class ObotoAgent {
670
696
  private usageTracker;
671
697
  private usageBridge;
672
698
  private routerEventBridge;
699
+ private currentPhase;
673
700
  constructor(config: ObotoAgentConfig);
674
701
  /** Subscribe to agent events. Returns an unsubscribe function. */
675
702
  on(type: AgentEventType, handler: EventHandler): () => void;
@@ -731,6 +758,14 @@ declare class ObotoAgent {
731
758
  * Returns undefined if RAG is not configured.
732
759
  */
733
760
  retrieveContext(query: string): Promise<string | undefined>;
761
+ /** Emit a phase transition event with a human-readable message. */
762
+ private emitPhase;
763
+ /**
764
+ * Build a human-readable narrative for a batch of tool executions.
765
+ * Uses both command name and kwargs to produce accurate descriptions.
766
+ * E.g. "Just read file data, and edited files. Sending results back to AI for next steps…"
767
+ */
768
+ private buildToolRoundNarrative;
734
769
  /**
735
770
  * Record a message in the session, context manager, and optionally RAG index.
736
771
  * Centralizes message recording to ensure RAG indexing stays in sync.
@@ -1167,4 +1202,4 @@ type TriageInput = {
1167
1202
  */
1168
1203
  declare function createTriageFunction(modelName: string): LScriptFunction<TriageInput, typeof TriageSchema>;
1169
1204
 
1170
- export { AgentDynamicTools, type AgentEvent, AgentEventBus, type AgentEventType, type AgentPipelineConfig, type AgentToolTreeConfig, AgentUsageTracker, ContextManager, ConversationRAG, type ConversationRAGConfig, type DynamicToolEntry, type DynamicToolProvider, type ExecutionOutput, ExecutionSchema, HookIntegration, ObotoAgent, type ObotoAgentConfig, PermissionGuard, type PlanOutput, PlanSchema, type ProviderLike$1 as ProviderLike, type RAGRetrievalResult, RouterEventBridge, SessionCompactor, SlashCommandRegistry, type SummaryOutput, SummarySchema, type ToolExecutionEvent, type TriagePipelineOutput, TriagePipelineSchema, type TriageResult, TriageSchema, type UnifiedCostSummary, UsageBridge, asTokenUsageToLmscript, createAgentToolTree, createAnalyzeRespondPipeline, createEmptySession, createExecutionStep, createFullPipeline, createPlanStep, createRouterTool, createSummaryStep, createToolAuditMiddleware, createToolTimeoutMiddleware, createToolTimingMiddleware, createTriageFunction, createTriagePlanExecutePipeline, createTriageStep, estimateCostFromAsAgent, fromChat, isLLMRouter, lmscriptToAsTokenUsage, runAgentPipeline, sessionToHistory, toChat, toLmscriptProvider };
1205
+ export { AgentDynamicTools, type AgentEvent, AgentEventBus, type AgentEventType, type AgentPhase, type AgentPipelineConfig, type AgentToolTreeConfig, AgentUsageTracker, ContextManager, ConversationRAG, type ConversationRAGConfig, type DoomLoopEvent, type DynamicToolEntry, type DynamicToolProvider, type ExecutionOutput, ExecutionSchema, HookIntegration, ObotoAgent, type ObotoAgentConfig, PermissionGuard, type PhaseEvent, type PlanOutput, PlanSchema, type ProviderLike$1 as ProviderLike, type RAGRetrievalResult, RouterEventBridge, SessionCompactor, SlashCommandRegistry, type SummaryOutput, SummarySchema, type ToolExecutionEvent, type ToolRoundEvent, type TriagePipelineOutput, TriagePipelineSchema, type TriageResult, TriageSchema, type UnifiedCostSummary, UsageBridge, asTokenUsageToLmscript, createAgentToolTree, createAnalyzeRespondPipeline, createEmptySession, createExecutionStep, createFullPipeline, createPlanStep, createRouterTool, createSummaryStep, createToolAuditMiddleware, createToolTimeoutMiddleware, createToolTimingMiddleware, createTriageFunction, createTriagePlanExecutePipeline, createTriageStep, estimateCostFromAsAgent, fromChat, isLLMRouter, lmscriptToAsTokenUsage, runAgentPipeline, sessionToHistory, toChat, toLmscriptProvider };
package/dist/index.js CHANGED
@@ -1163,6 +1163,7 @@ var ObotoAgent = class {
1163
1163
  usageTracker;
1164
1164
  usageBridge;
1165
1165
  routerEventBridge;
1166
+ currentPhase = "request";
1166
1167
  constructor(config) {
1167
1168
  this.config = config;
1168
1169
  this.localProvider = config.localModel;
@@ -1175,10 +1176,8 @@ var ObotoAgent = class {
1175
1176
  this.middleware.use(hooks);
1176
1177
  }
1177
1178
  }
1178
- if (config.modelPricing) {
1179
- this.costTracker = new CostTracker();
1180
- this.modelPricing = config.modelPricing;
1181
- }
1179
+ this.costTracker = new CostTracker();
1180
+ this.modelPricing = config.modelPricing;
1182
1181
  const localCache = config.triageCacheTtlMs ? new ExecutionCache(new MemoryCacheBackend()) : void 0;
1183
1182
  this.localRuntime = new LScriptRuntime({
1184
1183
  provider: localLmscript,
@@ -1413,6 +1412,71 @@ var ObotoAgent = class {
1413
1412
  const { context } = await this.conversationRAG.retrieve(query);
1414
1413
  return context || void 0;
1415
1414
  }
1415
+ // ── Conversational helpers ──────────────────────────────────────────
1416
+ /** Emit a phase transition event with a human-readable message. */
1417
+ emitPhase(phase, message) {
1418
+ this.currentPhase = phase;
1419
+ this.bus.emit("phase", { phase, message });
1420
+ }
1421
+ /**
1422
+ * Build a human-readable narrative for a batch of tool executions.
1423
+ * Uses both command name and kwargs to produce accurate descriptions.
1424
+ * E.g. "Just read file data, and edited files. Sending results back to AI for next steps…"
1425
+ */
1426
+ buildToolRoundNarrative(tools) {
1427
+ if (tools.length === 0) return "No tools executed.";
1428
+ const verbs = /* @__PURE__ */ new Map();
1429
+ for (const t of tools) {
1430
+ const cmd = t.command.toLowerCase();
1431
+ const kw = t.kwargs || {};
1432
+ const kwStr = JSON.stringify(kw).toLowerCase();
1433
+ let verb;
1434
+ if (kwStr.includes('"cmd"') && (kwStr.includes("cat ") || kwStr.includes("head ") || kwStr.includes("tail "))) {
1435
+ verb = "read file data";
1436
+ } else if (kwStr.includes('"cmd"') && (kwStr.includes("ls ") || kwStr.includes("find ") || kwStr.includes("tree "))) {
1437
+ verb = "listed files";
1438
+ } else if (kwStr.includes('"cmd"') && (kwStr.includes("grep ") || kwStr.includes("rg ") || kwStr.includes("ag "))) {
1439
+ verb = "searched for information";
1440
+ } else if (cmd.includes("read") || cmd.includes("get_file") || cmd.includes("view")) {
1441
+ verb = "read file data";
1442
+ } else if (cmd.includes("write") || cmd.includes("edit") || cmd.includes("patch") || cmd.includes("update")) {
1443
+ verb = "edited files";
1444
+ } else if (cmd.includes("search") || cmd.includes("grep") || cmd.includes("find") || cmd.includes("glob")) {
1445
+ verb = "searched for information";
1446
+ } else if (cmd.includes("list") || cmd.includes("ls")) {
1447
+ verb = "listed items";
1448
+ } else if (cmd.includes("run") || cmd.includes("exec") || cmd.includes("bash") || cmd.includes("shell")) {
1449
+ verb = "ran a command";
1450
+ } else if (cmd.includes("browse") || cmd.includes("web") || cmd.includes("fetch")) {
1451
+ verb = "browsed the web";
1452
+ } else if (cmd.includes("surface")) {
1453
+ verb = cmd.includes("read") ? "read surface data" : "accessed surfaces";
1454
+ } else if (cmd.includes("help")) {
1455
+ verb = "checked available tools";
1456
+ } else {
1457
+ verb = `used ${t.command}`;
1458
+ }
1459
+ if (!verbs.has(verb)) verbs.set(verb, []);
1460
+ verbs.get(verb).push(t.command);
1461
+ }
1462
+ const parts = Array.from(verbs.keys());
1463
+ let narrative;
1464
+ if (parts.length === 1) {
1465
+ narrative = `Just ${parts[0]}.`;
1466
+ } else if (parts.length === 2) {
1467
+ narrative = `Just ${parts[0]}, and ${parts[1]}.`;
1468
+ } else {
1469
+ const last = parts.pop();
1470
+ narrative = `Just ${parts.join(", ")}, and ${last}.`;
1471
+ }
1472
+ const errors = tools.filter((t) => !t.success);
1473
+ if (errors.length > 0) {
1474
+ const errNames = errors.map((e) => e.command).join(", ");
1475
+ narrative += ` (${errors.length} error${errors.length > 1 ? "s" : ""}: ${errNames})`;
1476
+ }
1477
+ narrative += " Sending results back to AI for next steps\u2026";
1478
+ return narrative;
1479
+ }
1416
1480
  // ── Internal ───────────────────────────────────────────────────────
1417
1481
  /**
1418
1482
  * Record a message in the session, context manager, and optionally RAG index.
@@ -1438,6 +1502,7 @@ var ObotoAgent = class {
1438
1502
  }
1439
1503
  }
1440
1504
  async executionLoop(userInput) {
1505
+ this.emitPhase("request", `Processing: ${userInput.substring(0, 80)}${userInput.length > 80 ? "\u2026" : ""}`);
1441
1506
  this.bus.emit("user_input", { text: userInput });
1442
1507
  const userMsg = {
1443
1508
  role: MessageRole4.User,
@@ -1445,6 +1510,7 @@ var ObotoAgent = class {
1445
1510
  };
1446
1511
  await this.recordMessage(userMsg);
1447
1512
  this.bus.emit("state_updated", { reason: "user_input" });
1513
+ this.emitPhase("planning", "Building context and preparing tools\u2026");
1448
1514
  if (this.conversationRAG) {
1449
1515
  try {
1450
1516
  const { context } = await this.conversationRAG.retrieve(userInput);
@@ -1453,16 +1519,22 @@ var ObotoAgent = class {
1453
1519
  role: "system",
1454
1520
  content: context
1455
1521
  });
1522
+ this.bus.emit("agent_thought", {
1523
+ text: "Retrieved relevant past context via RAG.",
1524
+ model: "system"
1525
+ });
1456
1526
  }
1457
1527
  } catch (err) {
1458
1528
  console.warn("[ObotoAgent] RAG retrieval failed:", err instanceof Error ? err.message : err);
1459
1529
  }
1460
1530
  }
1531
+ this.emitPhase("precheck", "Checking if direct answer is possible\u2026");
1461
1532
  const triageResult = await this.triage(userInput);
1462
1533
  this.bus.emit("triage_result", triageResult);
1463
1534
  if (this.interrupted) return;
1464
1535
  if (!triageResult.escalate && triageResult.directResponse) {
1465
1536
  const response = triageResult.directResponse;
1537
+ this.emitPhase("complete", "Answered directly \u2014 no tools needed.");
1466
1538
  this.bus.emit("agent_thought", { text: response, model: "local" });
1467
1539
  const assistantMsg = {
1468
1540
  role: MessageRole4.Assistant,
@@ -1474,15 +1546,12 @@ var ObotoAgent = class {
1474
1546
  return;
1475
1547
  }
1476
1548
  if (triageResult.escalate) {
1477
- this.bus.emit("agent_thought", {
1478
- text: triageResult.reasoning,
1479
- model: "local",
1480
- escalating: true
1481
- });
1549
+ this.emitPhase("thinking", "Entering agent loop with remote model\u2026");
1550
+ } else {
1551
+ this.emitPhase("thinking", "This requires tools and deeper reasoning \u2014 entering agent loop.");
1482
1552
  }
1483
1553
  const modelName = triageResult.escalate ? this.config.remoteModelName : this.config.localModelName;
1484
1554
  const runtime = triageResult.escalate ? this.remoteRuntime : this.localRuntime;
1485
- console.log("[ObotoAgent] Executing with model:", modelName, "| via lmscript AgentLoop");
1486
1555
  if (this.onToken) {
1487
1556
  await this.executeWithStreaming(runtime, modelName, userInput);
1488
1557
  } else {
@@ -1515,6 +1584,7 @@ var ObotoAgent = class {
1515
1584
  */
1516
1585
  async executeWithAgentLoop(runtime, modelName, userInput) {
1517
1586
  const { z: z5 } = await import("zod");
1587
+ this.emitPhase("thinking", `Turn 1/${this.maxIterations}: Analyzing request\u2026`);
1518
1588
  const agentFn = {
1519
1589
  name: "agent-task",
1520
1590
  model: modelName,
@@ -1537,30 +1607,57 @@ user: ${input}` : input;
1537
1607
  temperature: 0.7,
1538
1608
  maxRetries: 1
1539
1609
  };
1610
+ let iterationTools = [];
1611
+ let totalToolCalls = 0;
1540
1612
  const agentConfig = {
1541
1613
  maxIterations: this.maxIterations,
1542
1614
  onToolCall: (tc) => {
1543
1615
  const command = typeof tc.arguments === "object" && tc.arguments !== null ? tc.arguments.command ?? tc.name : tc.name;
1544
1616
  const kwargs = typeof tc.arguments === "object" && tc.arguments !== null ? tc.arguments.kwargs ?? {} : {};
1545
- this.bus.emit("tool_execution_complete", {
1546
- command,
1547
- kwargs,
1548
- result: typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result)
1549
- });
1550
1617
  const resultStr = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
1618
+ const isError = resultStr.startsWith("Error:");
1619
+ this.emitPhase("tools", `Running tool: ${String(command)}`);
1620
+ this.bus.emit("tool_execution_start", { command, kwargs });
1621
+ this.bus.emit("tool_execution_complete", { command, kwargs, result: resultStr });
1622
+ iterationTools.push({ command: String(command), success: !isError, kwargs });
1623
+ totalToolCalls++;
1551
1624
  this.recordToolResult(String(command), kwargs, resultStr);
1552
1625
  },
1553
1626
  onIteration: (iteration, response) => {
1554
- this.bus.emit("agent_thought", {
1555
- text: response,
1556
- model: modelName,
1557
- iteration
1558
- });
1627
+ if (iterationTools.length > 0) {
1628
+ const narrative = this.buildToolRoundNarrative(iterationTools);
1629
+ const roundEvent = {
1630
+ iteration,
1631
+ tools: iterationTools,
1632
+ totalToolCalls,
1633
+ narrative
1634
+ };
1635
+ this.bus.emit("tool_round_complete", roundEvent);
1636
+ iterationTools = [];
1637
+ }
1638
+ if (response) {
1639
+ this.bus.emit("agent_thought", {
1640
+ text: response,
1641
+ model: modelName,
1642
+ iteration
1643
+ });
1644
+ }
1645
+ this.emitPhase("thinking", `Turn ${iteration + 1}/${this.maxIterations}: Analyzing results\u2026`);
1559
1646
  if (this.interrupted) return false;
1560
1647
  }
1561
1648
  };
1562
1649
  const agentLoop = new AgentLoop(runtime, agentConfig);
1563
1650
  const result = await agentLoop.run(agentFn, userInput);
1651
+ if (iterationTools.length > 0) {
1652
+ const narrative = this.buildToolRoundNarrative(iterationTools);
1653
+ this.bus.emit("tool_round_complete", {
1654
+ iteration: result.iterations,
1655
+ tools: iterationTools,
1656
+ totalToolCalls,
1657
+ narrative
1658
+ });
1659
+ }
1660
+ this.emitPhase("memory", "Recording interaction\u2026");
1564
1661
  const responseText = result.data.response;
1565
1662
  const assistantMsg = {
1566
1663
  role: MessageRole4.Assistant,
@@ -1568,6 +1665,7 @@ user: ${input}` : input;
1568
1665
  };
1569
1666
  await this.recordMessage(assistantMsg);
1570
1667
  this.bus.emit("state_updated", { reason: "assistant_response" });
1668
+ this.emitPhase("complete", "Response ready.");
1571
1669
  this.bus.emit("turn_complete", {
1572
1670
  model: modelName,
1573
1671
  escalated: true,
@@ -1610,6 +1708,8 @@ user: ${input}` : input;
1610
1708
  ];
1611
1709
  let totalToolCalls = 0;
1612
1710
  const callHistory = [];
1711
+ const consecutiveDupes = /* @__PURE__ */ new Map();
1712
+ let doomLoopRedirected = false;
1613
1713
  const turnStartTime = Date.now();
1614
1714
  const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1615
1715
  const syntheticCtx = {
@@ -1622,13 +1722,21 @@ user: ${input}` : input;
1622
1722
  await this.middleware.runBeforeExecute(syntheticCtx);
1623
1723
  try {
1624
1724
  for (let iteration = 1; iteration <= this.maxIterations; iteration++) {
1625
- if (this.interrupted) break;
1626
- if (this.costTracker && this.budget) {
1725
+ if (this.interrupted) {
1726
+ this.emitPhase("cancel", "Interrupted by user.");
1727
+ break;
1728
+ }
1729
+ this.emitPhase(
1730
+ "thinking",
1731
+ `Turn ${iteration}/${this.maxIterations}: ${iteration === 1 ? "Analyzing request\u2026" : "Analyzing results\u2026"}`
1732
+ );
1733
+ if (this.budget) {
1627
1734
  this.costTracker.checkBudget(this.budget);
1628
1735
  }
1629
1736
  await this.rateLimiter?.acquire();
1630
1737
  const isLastIteration = iteration === this.maxIterations;
1631
1738
  if (isLastIteration) {
1739
+ this.emitPhase("continuation", "Maximum iterations reached \u2014 synthesizing final response\u2026");
1632
1740
  messages.push({
1633
1741
  role: "user",
1634
1742
  content: "You have used all available tool iterations. Please provide your final response now based on what you have gathered so far. Do not call any more tools."
@@ -1644,11 +1752,11 @@ user: ${input}` : input;
1644
1752
  try {
1645
1753
  response = await this.streamAndAggregate(provider, params);
1646
1754
  } catch (err) {
1755
+ this.emitPhase("error", `LLM call failed: ${err instanceof Error ? err.message : String(err)}`);
1647
1756
  await this.middleware.runError(
1648
1757
  syntheticCtx,
1649
1758
  err instanceof Error ? err : new Error(String(err))
1650
1759
  );
1651
- console.error("[ObotoAgent] LLM call failed:", err instanceof Error ? err.message : err);
1652
1760
  throw err;
1653
1761
  }
1654
1762
  const usage = response?.usage;
@@ -1665,13 +1773,11 @@ user: ${input}` : input;
1665
1773
  completionTokens,
1666
1774
  totalTokens: usageTotal
1667
1775
  });
1668
- if (this.costTracker) {
1669
- this.bus.emit("cost_update", {
1670
- iteration,
1671
- totalTokens: this.costTracker.getTotalTokens(),
1672
- totalCost: this.costTracker.getTotalCost(this.modelPricing)
1673
- });
1674
- }
1776
+ this.bus.emit("cost_update", {
1777
+ iteration,
1778
+ totalTokens: this.costTracker.getTotalTokens(),
1779
+ totalCost: this.costTracker.getTotalCost(this.modelPricing)
1780
+ });
1675
1781
  }
1676
1782
  const choice = response?.choices?.[0];
1677
1783
  const content = choice?.message?.content ?? "";
@@ -1684,12 +1790,21 @@ user: ${input}` : input;
1684
1790
  });
1685
1791
  }
1686
1792
  if (!toolCalls || toolCalls.length === 0) {
1793
+ if (!content) {
1794
+ this.bus.emit("agent_thought", {
1795
+ text: `Empty response from AI \u2014 iteration ${iteration}`,
1796
+ model: "system"
1797
+ });
1798
+ if (iteration < this.maxIterations) continue;
1799
+ }
1800
+ this.emitPhase("memory", "Recording interaction\u2026");
1687
1801
  const assistantMsg = {
1688
1802
  role: MessageRole4.Assistant,
1689
1803
  blocks: [{ kind: "text", text: content }]
1690
1804
  };
1691
1805
  await this.recordMessage(assistantMsg);
1692
1806
  this.bus.emit("state_updated", { reason: "assistant_response" });
1807
+ this.emitPhase("complete", "Response ready.");
1693
1808
  this.bus.emit("turn_complete", {
1694
1809
  model: modelName,
1695
1810
  escalated: true,
@@ -1704,12 +1819,15 @@ user: ${input}` : input;
1704
1819
  });
1705
1820
  return;
1706
1821
  }
1822
+ this.emitPhase("tools", `Executing ${toolCalls.length} tool(s)\u2026`);
1707
1823
  messages.push({
1708
1824
  role: "assistant",
1709
1825
  content: content || null,
1710
1826
  tool_calls: toolCalls
1711
1827
  });
1712
- for (const tc of toolCalls) {
1828
+ const roundTools = [];
1829
+ for (let ti = 0; ti < toolCalls.length; ti++) {
1830
+ const tc = toolCalls[ti];
1713
1831
  if (this.interrupted) break;
1714
1832
  let args;
1715
1833
  try {
@@ -1728,6 +1846,7 @@ user: ${input}` : input;
1728
1846
  tool_call_id: tc.id,
1729
1847
  content: `Permission denied for tool "${command}": ${outcome.reason ?? "denied by policy"}`
1730
1848
  });
1849
+ roundTools.push({ command, success: false });
1731
1850
  totalToolCalls++;
1732
1851
  continue;
1733
1852
  }
@@ -1740,6 +1859,7 @@ user: ${input}` : input;
1740
1859
  tool_call_id: tc.id,
1741
1860
  content: `Tool "${command}" blocked by pre-use hook: ${hookResult.messages.join("; ")}`
1742
1861
  });
1862
+ roundTools.push({ command, success: false });
1743
1863
  totalToolCalls++;
1744
1864
  continue;
1745
1865
  }
@@ -1747,16 +1867,77 @@ user: ${input}` : input;
1747
1867
  const callSig = JSON.stringify({ command, kwargs });
1748
1868
  const dupeCount = callHistory.filter((s) => s === callSig).length;
1749
1869
  callHistory.push(callSig);
1870
+ const prevConsec = consecutiveDupes.get(command) ?? 0;
1871
+ consecutiveDupes.set(command, prevConsec + 1);
1750
1872
  if (dupeCount >= 2) {
1751
- messages.push({
1752
- role: "tool",
1753
- tool_call_id: tc.id,
1754
- content: `You already called "${command}" with these arguments ${dupeCount} time(s). Use the data you already have.`
1755
- });
1873
+ if (prevConsec >= 3 && !doomLoopRedirected) {
1874
+ doomLoopRedirected = true;
1875
+ this.emitPhase("doom", `Doom loop detected: repeated calls to "${command}"`);
1876
+ this.bus.emit("doom_loop", {
1877
+ reason: `Repeated calls to "${command}"`,
1878
+ command,
1879
+ count: prevConsec + 1,
1880
+ redirected: true
1881
+ });
1882
+ messages.push({
1883
+ role: "tool",
1884
+ tool_call_id: tc.id,
1885
+ content: `STOP: You have been calling "${command}" repeatedly with the same arguments ${prevConsec + 1} times. This is a doom loop. You MUST take a different approach. Summarize what you know so far and either try a completely different strategy or provide your best answer.`
1886
+ });
1887
+ } else if (doomLoopRedirected && prevConsec >= 5) {
1888
+ this.emitPhase("doom", `Persistent doom loop \u2014 terminating.`);
1889
+ this.bus.emit("doom_loop", {
1890
+ reason: `Persistent doom loop on "${command}"`,
1891
+ command,
1892
+ count: prevConsec + 1,
1893
+ redirected: false
1894
+ });
1895
+ roundTools.push({ command, success: false });
1896
+ totalToolCalls++;
1897
+ if (roundTools.length > 0) {
1898
+ const narrative = this.buildToolRoundNarrative(roundTools);
1899
+ this.bus.emit("tool_round_complete", {
1900
+ iteration,
1901
+ tools: roundTools,
1902
+ totalToolCalls,
1903
+ narrative
1904
+ });
1905
+ }
1906
+ this.emitPhase("continuation", "Synthesizing response after doom loop\u2026");
1907
+ const assistantMsg = {
1908
+ role: MessageRole4.Assistant,
1909
+ blocks: [{ kind: "text", text: content || "I encountered a repeating pattern and am unable to make further progress. Here is what I have so far." }]
1910
+ };
1911
+ await this.recordMessage(assistantMsg);
1912
+ this.bus.emit("state_updated", { reason: "doom_loop" });
1913
+ this.emitPhase("complete", "Response ready.");
1914
+ this.bus.emit("turn_complete", {
1915
+ model: modelName,
1916
+ escalated: true,
1917
+ iterations: iteration,
1918
+ toolCalls: totalToolCalls,
1919
+ usage: totalUsage
1920
+ });
1921
+ await this.middleware.runComplete(syntheticCtx, {
1922
+ data: content || "doom_loop_terminated",
1923
+ raw: content,
1924
+ usage: totalUsage
1925
+ });
1926
+ return;
1927
+ } else {
1928
+ messages.push({
1929
+ role: "tool",
1930
+ tool_call_id: tc.id,
1931
+ content: `You already called "${command}" with these arguments ${dupeCount} time(s). Use the data you already have.`
1932
+ });
1933
+ }
1934
+ roundTools.push({ command, success: false });
1756
1935
  totalToolCalls++;
1757
1936
  continue;
1937
+ } else {
1938
+ consecutiveDupes.set(command, 0);
1758
1939
  }
1759
- this.bus.emit("tool_execution_start", { command, kwargs });
1940
+ this.bus.emit("tool_execution_start", { command, kwargs, index: ti, total: toolCalls.length });
1760
1941
  let result;
1761
1942
  let isError = false;
1762
1943
  try {
@@ -1772,8 +1953,9 @@ user: ${input}` : input;
1772
1953
  if (this.hookIntegration) {
1773
1954
  this.hookIntegration.runPostToolUse(command, toolInputStr, truncated, isError);
1774
1955
  }
1775
- this.bus.emit("tool_execution_complete", { command, kwargs, result: truncated });
1956
+ this.bus.emit("tool_execution_complete", { command, kwargs, result: truncated, error: isError ? truncated : void 0 });
1776
1957
  this.recordToolResult(command, kwargs, truncated);
1958
+ roundTools.push({ command, success: !isError, kwargs });
1777
1959
  totalToolCalls++;
1778
1960
  messages.push({
1779
1961
  role: "tool",
@@ -1781,13 +1963,24 @@ user: ${input}` : input;
1781
1963
  content: truncated
1782
1964
  });
1783
1965
  }
1966
+ if (roundTools.length > 0) {
1967
+ const narrative = this.buildToolRoundNarrative(roundTools);
1968
+ this.bus.emit("tool_round_complete", {
1969
+ iteration,
1970
+ tools: roundTools,
1971
+ totalToolCalls,
1972
+ narrative
1973
+ });
1974
+ }
1784
1975
  }
1976
+ this.emitPhase("continuation", "Maximum iterations reached \u2014 synthesizing response\u2026");
1785
1977
  const fallbackMsg = {
1786
1978
  role: MessageRole4.Assistant,
1787
1979
  blocks: [{ kind: "text", text: "I reached the maximum number of iterations. Here is what I have so far." }]
1788
1980
  };
1789
1981
  await this.recordMessage(fallbackMsg);
1790
1982
  this.bus.emit("state_updated", { reason: "max_iterations" });
1983
+ this.emitPhase("complete", "Response ready.");
1791
1984
  this.bus.emit("turn_complete", {
1792
1985
  model: modelName,
1793
1986
  escalated: true,
@@ -1801,6 +1994,7 @@ user: ${input}` : input;
1801
1994
  usage: totalUsage
1802
1995
  });
1803
1996
  } catch (err) {
1997
+ this.emitPhase("error", `Error: ${err instanceof Error ? err.message : String(err)}`);
1804
1998
  if (!(err instanceof Error && err.message.includes("LLM call failed"))) {
1805
1999
  await this.middleware.runError(
1806
2000
  syntheticCtx,