@sschepis/oboto-agent 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +47 -2
- package/dist/index.js +261 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -74,7 +74,38 @@ 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" | "heartbeat";
|
|
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 heartbeat events. */
|
|
86
|
+
interface HeartbeatEvent {
|
|
87
|
+
phase: AgentPhase;
|
|
88
|
+
elapsedMs: number;
|
|
89
|
+
message: string;
|
|
90
|
+
}
|
|
91
|
+
/** Payload for doom loop events. */
|
|
92
|
+
interface DoomLoopEvent {
|
|
93
|
+
reason: string;
|
|
94
|
+
command: string;
|
|
95
|
+
count: number;
|
|
96
|
+
redirected: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** Payload for tool round completion with narrative. */
|
|
99
|
+
interface ToolRoundEvent {
|
|
100
|
+
iteration: number;
|
|
101
|
+
tools: Array<{
|
|
102
|
+
command: string;
|
|
103
|
+
success: boolean;
|
|
104
|
+
durationMs?: number;
|
|
105
|
+
}>;
|
|
106
|
+
totalToolCalls: number;
|
|
107
|
+
narrative: string;
|
|
108
|
+
}
|
|
78
109
|
interface AgentEvent<T = unknown> {
|
|
79
110
|
type: AgentEventType;
|
|
80
111
|
payload: T;
|
|
@@ -670,6 +701,9 @@ declare class ObotoAgent {
|
|
|
670
701
|
private usageTracker;
|
|
671
702
|
private usageBridge;
|
|
672
703
|
private routerEventBridge;
|
|
704
|
+
private heartbeatTimer;
|
|
705
|
+
private heartbeatStart;
|
|
706
|
+
private currentPhase;
|
|
673
707
|
constructor(config: ObotoAgentConfig);
|
|
674
708
|
/** Subscribe to agent events. Returns an unsubscribe function. */
|
|
675
709
|
on(type: AgentEventType, handler: EventHandler): () => void;
|
|
@@ -731,6 +765,17 @@ declare class ObotoAgent {
|
|
|
731
765
|
* Returns undefined if RAG is not configured.
|
|
732
766
|
*/
|
|
733
767
|
retrieveContext(query: string): Promise<string | undefined>;
|
|
768
|
+
/** Emit a phase transition event with a human-readable message. */
|
|
769
|
+
private emitPhase;
|
|
770
|
+
/** Start a heartbeat that re-emits the current phase every intervalMs. */
|
|
771
|
+
private startHeartbeat;
|
|
772
|
+
/** Stop the heartbeat timer. */
|
|
773
|
+
private stopHeartbeat;
|
|
774
|
+
/**
|
|
775
|
+
* Build a human-readable narrative for a batch of tool executions.
|
|
776
|
+
* E.g. "Just read file data, and ran a command. Sending results back to AI for next steps…"
|
|
777
|
+
*/
|
|
778
|
+
private buildToolRoundNarrative;
|
|
734
779
|
/**
|
|
735
780
|
* Record a message in the session, context manager, and optionally RAG index.
|
|
736
781
|
* Centralizes message recording to ensure RAG indexing stays in sync.
|
|
@@ -1167,4 +1212,4 @@ type TriageInput = {
|
|
|
1167
1212
|
*/
|
|
1168
1213
|
declare function createTriageFunction(modelName: string): LScriptFunction<TriageInput, typeof TriageSchema>;
|
|
1169
1214
|
|
|
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 };
|
|
1215
|
+
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, type HeartbeatEvent, 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,9 @@ var ObotoAgent = class {
|
|
|
1163
1163
|
usageTracker;
|
|
1164
1164
|
usageBridge;
|
|
1165
1165
|
routerEventBridge;
|
|
1166
|
+
heartbeatTimer = null;
|
|
1167
|
+
heartbeatStart = 0;
|
|
1168
|
+
currentPhase = "request";
|
|
1166
1169
|
constructor(config) {
|
|
1167
1170
|
this.config = config;
|
|
1168
1171
|
this.localProvider = config.localModel;
|
|
@@ -1413,6 +1416,75 @@ var ObotoAgent = class {
|
|
|
1413
1416
|
const { context } = await this.conversationRAG.retrieve(query);
|
|
1414
1417
|
return context || void 0;
|
|
1415
1418
|
}
|
|
1419
|
+
// ── Conversational helpers ──────────────────────────────────────────
|
|
1420
|
+
/** Emit a phase transition event with a human-readable message. */
|
|
1421
|
+
emitPhase(phase, message) {
|
|
1422
|
+
this.currentPhase = phase;
|
|
1423
|
+
this.bus.emit("phase", { phase, message });
|
|
1424
|
+
}
|
|
1425
|
+
/** Start a heartbeat that re-emits the current phase every intervalMs. */
|
|
1426
|
+
startHeartbeat(phase, intervalMs = 3e3) {
|
|
1427
|
+
this.stopHeartbeat();
|
|
1428
|
+
this.heartbeatStart = Date.now();
|
|
1429
|
+
this.currentPhase = phase;
|
|
1430
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1431
|
+
const elapsed = Date.now() - this.heartbeatStart;
|
|
1432
|
+
const secs = Math.round(elapsed / 1e3);
|
|
1433
|
+
const message = phase === "thinking" ? `Waiting for AI response\u2026 (${secs}s)` : `${phase}\u2026 (${secs}s)`;
|
|
1434
|
+
this.bus.emit("heartbeat", { phase: this.currentPhase, elapsedMs: elapsed, message });
|
|
1435
|
+
}, intervalMs);
|
|
1436
|
+
}
|
|
1437
|
+
/** Stop the heartbeat timer. */
|
|
1438
|
+
stopHeartbeat() {
|
|
1439
|
+
if (this.heartbeatTimer) {
|
|
1440
|
+
clearInterval(this.heartbeatTimer);
|
|
1441
|
+
this.heartbeatTimer = null;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Build a human-readable narrative for a batch of tool executions.
|
|
1446
|
+
* E.g. "Just read file data, and ran a command. Sending results back to AI for next steps…"
|
|
1447
|
+
*/
|
|
1448
|
+
buildToolRoundNarrative(tools) {
|
|
1449
|
+
if (tools.length === 0) return "No tools executed.";
|
|
1450
|
+
const verbs = /* @__PURE__ */ new Map();
|
|
1451
|
+
for (const t of tools) {
|
|
1452
|
+
const cmd = t.command.toLowerCase();
|
|
1453
|
+
let verb;
|
|
1454
|
+
if (cmd.includes("read") || cmd.includes("cat") || cmd.includes("get")) {
|
|
1455
|
+
verb = "read file data";
|
|
1456
|
+
} else if (cmd.includes("write") || cmd.includes("edit") || cmd.includes("patch")) {
|
|
1457
|
+
verb = "edited files";
|
|
1458
|
+
} else if (cmd.includes("search") || cmd.includes("grep") || cmd.includes("find") || cmd.includes("glob")) {
|
|
1459
|
+
verb = "searched for information";
|
|
1460
|
+
} else if (cmd.includes("run") || cmd.includes("exec") || cmd.includes("bash") || cmd.includes("shell")) {
|
|
1461
|
+
verb = "ran a command";
|
|
1462
|
+
} else if (cmd.includes("browse") || cmd.includes("web") || cmd.includes("fetch")) {
|
|
1463
|
+
verb = "browsed the web";
|
|
1464
|
+
} else {
|
|
1465
|
+
verb = `used ${t.command}`;
|
|
1466
|
+
}
|
|
1467
|
+
if (!verbs.has(verb)) verbs.set(verb, []);
|
|
1468
|
+
verbs.get(verb).push(t.command);
|
|
1469
|
+
}
|
|
1470
|
+
const parts = Array.from(verbs.keys());
|
|
1471
|
+
let narrative;
|
|
1472
|
+
if (parts.length === 1) {
|
|
1473
|
+
narrative = `Just ${parts[0]}.`;
|
|
1474
|
+
} else if (parts.length === 2) {
|
|
1475
|
+
narrative = `Just ${parts[0]}, and ${parts[1]}.`;
|
|
1476
|
+
} else {
|
|
1477
|
+
const last = parts.pop();
|
|
1478
|
+
narrative = `Just ${parts.join(", ")}, and ${last}.`;
|
|
1479
|
+
}
|
|
1480
|
+
const errors = tools.filter((t) => !t.success);
|
|
1481
|
+
if (errors.length > 0) {
|
|
1482
|
+
const errNames = errors.map((e) => e.command).join(", ");
|
|
1483
|
+
narrative += ` (${errors.length} error${errors.length > 1 ? "s" : ""}: ${errNames})`;
|
|
1484
|
+
}
|
|
1485
|
+
narrative += " Sending results back to AI for next steps\u2026";
|
|
1486
|
+
return narrative;
|
|
1487
|
+
}
|
|
1416
1488
|
// ── Internal ───────────────────────────────────────────────────────
|
|
1417
1489
|
/**
|
|
1418
1490
|
* Record a message in the session, context manager, and optionally RAG index.
|
|
@@ -1438,6 +1510,7 @@ var ObotoAgent = class {
|
|
|
1438
1510
|
}
|
|
1439
1511
|
}
|
|
1440
1512
|
async executionLoop(userInput) {
|
|
1513
|
+
this.emitPhase("request", `Processing: ${userInput.substring(0, 80)}${userInput.length > 80 ? "\u2026" : ""}`);
|
|
1441
1514
|
this.bus.emit("user_input", { text: userInput });
|
|
1442
1515
|
const userMsg = {
|
|
1443
1516
|
role: MessageRole4.User,
|
|
@@ -1445,6 +1518,7 @@ var ObotoAgent = class {
|
|
|
1445
1518
|
};
|
|
1446
1519
|
await this.recordMessage(userMsg);
|
|
1447
1520
|
this.bus.emit("state_updated", { reason: "user_input" });
|
|
1521
|
+
this.emitPhase("planning", "Building context and preparing tools\u2026");
|
|
1448
1522
|
if (this.conversationRAG) {
|
|
1449
1523
|
try {
|
|
1450
1524
|
const { context } = await this.conversationRAG.retrieve(userInput);
|
|
@@ -1453,16 +1527,24 @@ var ObotoAgent = class {
|
|
|
1453
1527
|
role: "system",
|
|
1454
1528
|
content: context
|
|
1455
1529
|
});
|
|
1530
|
+
this.bus.emit("agent_thought", {
|
|
1531
|
+
text: "Retrieved relevant past context via RAG.",
|
|
1532
|
+
model: "system"
|
|
1533
|
+
});
|
|
1456
1534
|
}
|
|
1457
1535
|
} catch (err) {
|
|
1458
1536
|
console.warn("[ObotoAgent] RAG retrieval failed:", err instanceof Error ? err.message : err);
|
|
1459
1537
|
}
|
|
1460
1538
|
}
|
|
1539
|
+
this.emitPhase("precheck", "Checking if direct answer is possible\u2026");
|
|
1540
|
+
this.startHeartbeat("precheck");
|
|
1461
1541
|
const triageResult = await this.triage(userInput);
|
|
1542
|
+
this.stopHeartbeat();
|
|
1462
1543
|
this.bus.emit("triage_result", triageResult);
|
|
1463
1544
|
if (this.interrupted) return;
|
|
1464
1545
|
if (!triageResult.escalate && triageResult.directResponse) {
|
|
1465
1546
|
const response = triageResult.directResponse;
|
|
1547
|
+
this.emitPhase("complete", "Answered directly \u2014 no tools needed.");
|
|
1466
1548
|
this.bus.emit("agent_thought", { text: response, model: "local" });
|
|
1467
1549
|
const assistantMsg = {
|
|
1468
1550
|
role: MessageRole4.Assistant,
|
|
@@ -1474,15 +1556,17 @@ var ObotoAgent = class {
|
|
|
1474
1556
|
return;
|
|
1475
1557
|
}
|
|
1476
1558
|
if (triageResult.escalate) {
|
|
1559
|
+
this.emitPhase("thinking", `Escalating to remote model: ${triageResult.reasoning}`);
|
|
1477
1560
|
this.bus.emit("agent_thought", {
|
|
1478
1561
|
text: triageResult.reasoning,
|
|
1479
1562
|
model: "local",
|
|
1480
1563
|
escalating: true
|
|
1481
1564
|
});
|
|
1565
|
+
} else {
|
|
1566
|
+
this.emitPhase("thinking", "This requires tools and deeper reasoning \u2014 entering agent loop.");
|
|
1482
1567
|
}
|
|
1483
1568
|
const modelName = triageResult.escalate ? this.config.remoteModelName : this.config.localModelName;
|
|
1484
1569
|
const runtime = triageResult.escalate ? this.remoteRuntime : this.localRuntime;
|
|
1485
|
-
console.log("[ObotoAgent] Executing with model:", modelName, "| via lmscript AgentLoop");
|
|
1486
1570
|
if (this.onToken) {
|
|
1487
1571
|
await this.executeWithStreaming(runtime, modelName, userInput);
|
|
1488
1572
|
} else {
|
|
@@ -1515,6 +1599,8 @@ var ObotoAgent = class {
|
|
|
1515
1599
|
*/
|
|
1516
1600
|
async executeWithAgentLoop(runtime, modelName, userInput) {
|
|
1517
1601
|
const { z: z5 } = await import("zod");
|
|
1602
|
+
this.emitPhase("thinking", `Turn 1/${this.maxIterations}: Analyzing request\u2026`);
|
|
1603
|
+
this.startHeartbeat("thinking");
|
|
1518
1604
|
const agentFn = {
|
|
1519
1605
|
name: "agent-task",
|
|
1520
1606
|
model: modelName,
|
|
@@ -1537,44 +1623,79 @@ user: ${input}` : input;
|
|
|
1537
1623
|
temperature: 0.7,
|
|
1538
1624
|
maxRetries: 1
|
|
1539
1625
|
};
|
|
1626
|
+
let iterationTools = [];
|
|
1627
|
+
let totalToolCalls = 0;
|
|
1540
1628
|
const agentConfig = {
|
|
1541
1629
|
maxIterations: this.maxIterations,
|
|
1542
1630
|
onToolCall: (tc) => {
|
|
1543
1631
|
const command = typeof tc.arguments === "object" && tc.arguments !== null ? tc.arguments.command ?? tc.name : tc.name;
|
|
1544
1632
|
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
1633
|
const resultStr = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
|
|
1634
|
+
const isError = resultStr.startsWith("Error:");
|
|
1635
|
+
this.emitPhase("tools", `Running tool: ${String(command)}`);
|
|
1636
|
+
this.bus.emit("tool_execution_start", { command, kwargs });
|
|
1637
|
+
this.bus.emit("tool_execution_complete", { command, kwargs, result: resultStr });
|
|
1638
|
+
iterationTools.push({ command: String(command), success: !isError });
|
|
1639
|
+
totalToolCalls++;
|
|
1551
1640
|
this.recordToolResult(String(command), kwargs, resultStr);
|
|
1552
1641
|
},
|
|
1553
1642
|
onIteration: (iteration, response) => {
|
|
1554
|
-
this.
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1643
|
+
this.stopHeartbeat();
|
|
1644
|
+
if (iterationTools.length > 0) {
|
|
1645
|
+
const narrative = this.buildToolRoundNarrative(iterationTools);
|
|
1646
|
+
const roundEvent = {
|
|
1647
|
+
iteration,
|
|
1648
|
+
tools: iterationTools,
|
|
1649
|
+
totalToolCalls,
|
|
1650
|
+
narrative
|
|
1651
|
+
};
|
|
1652
|
+
this.bus.emit("tool_round_complete", roundEvent);
|
|
1653
|
+
iterationTools = [];
|
|
1654
|
+
}
|
|
1655
|
+
if (response) {
|
|
1656
|
+
this.bus.emit("agent_thought", {
|
|
1657
|
+
text: response,
|
|
1658
|
+
model: modelName,
|
|
1659
|
+
iteration
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
this.emitPhase("thinking", `Turn ${iteration + 1}/${this.maxIterations}: Analyzing results\u2026`);
|
|
1663
|
+
this.startHeartbeat("thinking");
|
|
1559
1664
|
if (this.interrupted) return false;
|
|
1560
1665
|
}
|
|
1561
1666
|
};
|
|
1562
1667
|
const agentLoop = new AgentLoop(runtime, agentConfig);
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1668
|
+
try {
|
|
1669
|
+
const result = await agentLoop.run(agentFn, userInput);
|
|
1670
|
+
this.stopHeartbeat();
|
|
1671
|
+
if (iterationTools.length > 0) {
|
|
1672
|
+
const narrative = this.buildToolRoundNarrative(iterationTools);
|
|
1673
|
+
this.bus.emit("tool_round_complete", {
|
|
1674
|
+
iteration: result.iterations,
|
|
1675
|
+
tools: iterationTools,
|
|
1676
|
+
totalToolCalls,
|
|
1677
|
+
narrative
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
this.emitPhase("memory", "Recording interaction\u2026");
|
|
1681
|
+
const responseText = result.data.response;
|
|
1682
|
+
const assistantMsg = {
|
|
1683
|
+
role: MessageRole4.Assistant,
|
|
1684
|
+
blocks: [{ kind: "text", text: responseText }]
|
|
1685
|
+
};
|
|
1686
|
+
await this.recordMessage(assistantMsg);
|
|
1687
|
+
this.bus.emit("state_updated", { reason: "assistant_response" });
|
|
1688
|
+
this.emitPhase("complete", "Response ready.");
|
|
1689
|
+
this.bus.emit("turn_complete", {
|
|
1690
|
+
model: modelName,
|
|
1691
|
+
escalated: true,
|
|
1692
|
+
iterations: result.iterations,
|
|
1693
|
+
toolCalls: result.toolCalls.length,
|
|
1694
|
+
usage: result.usage
|
|
1695
|
+
});
|
|
1696
|
+
} finally {
|
|
1697
|
+
this.stopHeartbeat();
|
|
1698
|
+
}
|
|
1578
1699
|
}
|
|
1579
1700
|
/**
|
|
1580
1701
|
* Execute with streaming token emission.
|
|
@@ -1610,6 +1731,8 @@ user: ${input}` : input;
|
|
|
1610
1731
|
];
|
|
1611
1732
|
let totalToolCalls = 0;
|
|
1612
1733
|
const callHistory = [];
|
|
1734
|
+
const consecutiveDupes = /* @__PURE__ */ new Map();
|
|
1735
|
+
let doomLoopRedirected = false;
|
|
1613
1736
|
const turnStartTime = Date.now();
|
|
1614
1737
|
const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1615
1738
|
const syntheticCtx = {
|
|
@@ -1622,13 +1745,22 @@ user: ${input}` : input;
|
|
|
1622
1745
|
await this.middleware.runBeforeExecute(syntheticCtx);
|
|
1623
1746
|
try {
|
|
1624
1747
|
for (let iteration = 1; iteration <= this.maxIterations; iteration++) {
|
|
1625
|
-
if (this.interrupted)
|
|
1748
|
+
if (this.interrupted) {
|
|
1749
|
+
this.emitPhase("cancel", "Interrupted by user.");
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
this.emitPhase(
|
|
1753
|
+
"thinking",
|
|
1754
|
+
`Turn ${iteration}/${this.maxIterations}: ${iteration === 1 ? "Analyzing request\u2026" : "Analyzing results\u2026"}`
|
|
1755
|
+
);
|
|
1756
|
+
this.startHeartbeat("thinking");
|
|
1626
1757
|
if (this.costTracker && this.budget) {
|
|
1627
1758
|
this.costTracker.checkBudget(this.budget);
|
|
1628
1759
|
}
|
|
1629
1760
|
await this.rateLimiter?.acquire();
|
|
1630
1761
|
const isLastIteration = iteration === this.maxIterations;
|
|
1631
1762
|
if (isLastIteration) {
|
|
1763
|
+
this.emitPhase("continuation", "Maximum iterations reached \u2014 synthesizing final response\u2026");
|
|
1632
1764
|
messages.push({
|
|
1633
1765
|
role: "user",
|
|
1634
1766
|
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,13 +1776,15 @@ user: ${input}` : input;
|
|
|
1644
1776
|
try {
|
|
1645
1777
|
response = await this.streamAndAggregate(provider, params);
|
|
1646
1778
|
} catch (err) {
|
|
1779
|
+
this.stopHeartbeat();
|
|
1780
|
+
this.emitPhase("error", `LLM call failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1647
1781
|
await this.middleware.runError(
|
|
1648
1782
|
syntheticCtx,
|
|
1649
1783
|
err instanceof Error ? err : new Error(String(err))
|
|
1650
1784
|
);
|
|
1651
|
-
console.error("[ObotoAgent] LLM call failed:", err instanceof Error ? err.message : err);
|
|
1652
1785
|
throw err;
|
|
1653
1786
|
}
|
|
1787
|
+
this.stopHeartbeat();
|
|
1654
1788
|
const usage = response?.usage;
|
|
1655
1789
|
if (usage) {
|
|
1656
1790
|
const promptTokens = usage.prompt_tokens ?? 0;
|
|
@@ -1684,12 +1818,21 @@ user: ${input}` : input;
|
|
|
1684
1818
|
});
|
|
1685
1819
|
}
|
|
1686
1820
|
if (!toolCalls || toolCalls.length === 0) {
|
|
1821
|
+
if (!content) {
|
|
1822
|
+
this.bus.emit("agent_thought", {
|
|
1823
|
+
text: `Empty response from AI \u2014 iteration ${iteration}`,
|
|
1824
|
+
model: "system"
|
|
1825
|
+
});
|
|
1826
|
+
if (iteration < this.maxIterations) continue;
|
|
1827
|
+
}
|
|
1828
|
+
this.emitPhase("memory", "Recording interaction\u2026");
|
|
1687
1829
|
const assistantMsg = {
|
|
1688
1830
|
role: MessageRole4.Assistant,
|
|
1689
1831
|
blocks: [{ kind: "text", text: content }]
|
|
1690
1832
|
};
|
|
1691
1833
|
await this.recordMessage(assistantMsg);
|
|
1692
1834
|
this.bus.emit("state_updated", { reason: "assistant_response" });
|
|
1835
|
+
this.emitPhase("complete", "Response ready.");
|
|
1693
1836
|
this.bus.emit("turn_complete", {
|
|
1694
1837
|
model: modelName,
|
|
1695
1838
|
escalated: true,
|
|
@@ -1704,12 +1847,15 @@ user: ${input}` : input;
|
|
|
1704
1847
|
});
|
|
1705
1848
|
return;
|
|
1706
1849
|
}
|
|
1850
|
+
this.emitPhase("tools", `Executing ${toolCalls.length} tool(s)\u2026`);
|
|
1707
1851
|
messages.push({
|
|
1708
1852
|
role: "assistant",
|
|
1709
1853
|
content: content || null,
|
|
1710
1854
|
tool_calls: toolCalls
|
|
1711
1855
|
});
|
|
1712
|
-
|
|
1856
|
+
const roundTools = [];
|
|
1857
|
+
for (let ti = 0; ti < toolCalls.length; ti++) {
|
|
1858
|
+
const tc = toolCalls[ti];
|
|
1713
1859
|
if (this.interrupted) break;
|
|
1714
1860
|
let args;
|
|
1715
1861
|
try {
|
|
@@ -1728,6 +1874,7 @@ user: ${input}` : input;
|
|
|
1728
1874
|
tool_call_id: tc.id,
|
|
1729
1875
|
content: `Permission denied for tool "${command}": ${outcome.reason ?? "denied by policy"}`
|
|
1730
1876
|
});
|
|
1877
|
+
roundTools.push({ command, success: false });
|
|
1731
1878
|
totalToolCalls++;
|
|
1732
1879
|
continue;
|
|
1733
1880
|
}
|
|
@@ -1740,6 +1887,7 @@ user: ${input}` : input;
|
|
|
1740
1887
|
tool_call_id: tc.id,
|
|
1741
1888
|
content: `Tool "${command}" blocked by pre-use hook: ${hookResult.messages.join("; ")}`
|
|
1742
1889
|
});
|
|
1890
|
+
roundTools.push({ command, success: false });
|
|
1743
1891
|
totalToolCalls++;
|
|
1744
1892
|
continue;
|
|
1745
1893
|
}
|
|
@@ -1747,16 +1895,77 @@ user: ${input}` : input;
|
|
|
1747
1895
|
const callSig = JSON.stringify({ command, kwargs });
|
|
1748
1896
|
const dupeCount = callHistory.filter((s) => s === callSig).length;
|
|
1749
1897
|
callHistory.push(callSig);
|
|
1898
|
+
const prevConsec = consecutiveDupes.get(command) ?? 0;
|
|
1899
|
+
consecutiveDupes.set(command, prevConsec + 1);
|
|
1750
1900
|
if (dupeCount >= 2) {
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1901
|
+
if (prevConsec >= 3 && !doomLoopRedirected) {
|
|
1902
|
+
doomLoopRedirected = true;
|
|
1903
|
+
this.emitPhase("doom", `Doom loop detected: repeated calls to "${command}"`);
|
|
1904
|
+
this.bus.emit("doom_loop", {
|
|
1905
|
+
reason: `Repeated calls to "${command}"`,
|
|
1906
|
+
command,
|
|
1907
|
+
count: prevConsec + 1,
|
|
1908
|
+
redirected: true
|
|
1909
|
+
});
|
|
1910
|
+
messages.push({
|
|
1911
|
+
role: "tool",
|
|
1912
|
+
tool_call_id: tc.id,
|
|
1913
|
+
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.`
|
|
1914
|
+
});
|
|
1915
|
+
} else if (doomLoopRedirected && prevConsec >= 5) {
|
|
1916
|
+
this.emitPhase("doom", `Persistent doom loop \u2014 terminating.`);
|
|
1917
|
+
this.bus.emit("doom_loop", {
|
|
1918
|
+
reason: `Persistent doom loop on "${command}"`,
|
|
1919
|
+
command,
|
|
1920
|
+
count: prevConsec + 1,
|
|
1921
|
+
redirected: false
|
|
1922
|
+
});
|
|
1923
|
+
roundTools.push({ command, success: false });
|
|
1924
|
+
totalToolCalls++;
|
|
1925
|
+
if (roundTools.length > 0) {
|
|
1926
|
+
const narrative = this.buildToolRoundNarrative(roundTools);
|
|
1927
|
+
this.bus.emit("tool_round_complete", {
|
|
1928
|
+
iteration,
|
|
1929
|
+
tools: roundTools,
|
|
1930
|
+
totalToolCalls,
|
|
1931
|
+
narrative
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
this.emitPhase("continuation", "Synthesizing response after doom loop\u2026");
|
|
1935
|
+
const assistantMsg = {
|
|
1936
|
+
role: MessageRole4.Assistant,
|
|
1937
|
+
blocks: [{ kind: "text", text: content || "I encountered a repeating pattern and am unable to make further progress. Here is what I have so far." }]
|
|
1938
|
+
};
|
|
1939
|
+
await this.recordMessage(assistantMsg);
|
|
1940
|
+
this.bus.emit("state_updated", { reason: "doom_loop" });
|
|
1941
|
+
this.emitPhase("complete", "Response ready.");
|
|
1942
|
+
this.bus.emit("turn_complete", {
|
|
1943
|
+
model: modelName,
|
|
1944
|
+
escalated: true,
|
|
1945
|
+
iterations: iteration,
|
|
1946
|
+
toolCalls: totalToolCalls,
|
|
1947
|
+
usage: totalUsage
|
|
1948
|
+
});
|
|
1949
|
+
await this.middleware.runComplete(syntheticCtx, {
|
|
1950
|
+
data: content || "doom_loop_terminated",
|
|
1951
|
+
raw: content,
|
|
1952
|
+
usage: totalUsage
|
|
1953
|
+
});
|
|
1954
|
+
return;
|
|
1955
|
+
} else {
|
|
1956
|
+
messages.push({
|
|
1957
|
+
role: "tool",
|
|
1958
|
+
tool_call_id: tc.id,
|
|
1959
|
+
content: `You already called "${command}" with these arguments ${dupeCount} time(s). Use the data you already have.`
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
roundTools.push({ command, success: false });
|
|
1756
1963
|
totalToolCalls++;
|
|
1757
1964
|
continue;
|
|
1965
|
+
} else {
|
|
1966
|
+
consecutiveDupes.set(command, 0);
|
|
1758
1967
|
}
|
|
1759
|
-
this.bus.emit("tool_execution_start", { command, kwargs });
|
|
1968
|
+
this.bus.emit("tool_execution_start", { command, kwargs, index: ti, total: toolCalls.length });
|
|
1760
1969
|
let result;
|
|
1761
1970
|
let isError = false;
|
|
1762
1971
|
try {
|
|
@@ -1772,8 +1981,9 @@ user: ${input}` : input;
|
|
|
1772
1981
|
if (this.hookIntegration) {
|
|
1773
1982
|
this.hookIntegration.runPostToolUse(command, toolInputStr, truncated, isError);
|
|
1774
1983
|
}
|
|
1775
|
-
this.bus.emit("tool_execution_complete", { command, kwargs, result: truncated });
|
|
1984
|
+
this.bus.emit("tool_execution_complete", { command, kwargs, result: truncated, error: isError ? truncated : void 0 });
|
|
1776
1985
|
this.recordToolResult(command, kwargs, truncated);
|
|
1986
|
+
roundTools.push({ command, success: !isError });
|
|
1777
1987
|
totalToolCalls++;
|
|
1778
1988
|
messages.push({
|
|
1779
1989
|
role: "tool",
|
|
@@ -1781,13 +1991,24 @@ user: ${input}` : input;
|
|
|
1781
1991
|
content: truncated
|
|
1782
1992
|
});
|
|
1783
1993
|
}
|
|
1994
|
+
if (roundTools.length > 0) {
|
|
1995
|
+
const narrative = this.buildToolRoundNarrative(roundTools);
|
|
1996
|
+
this.bus.emit("tool_round_complete", {
|
|
1997
|
+
iteration,
|
|
1998
|
+
tools: roundTools,
|
|
1999
|
+
totalToolCalls,
|
|
2000
|
+
narrative
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
1784
2003
|
}
|
|
2004
|
+
this.emitPhase("continuation", "Maximum iterations reached \u2014 synthesizing response\u2026");
|
|
1785
2005
|
const fallbackMsg = {
|
|
1786
2006
|
role: MessageRole4.Assistant,
|
|
1787
2007
|
blocks: [{ kind: "text", text: "I reached the maximum number of iterations. Here is what I have so far." }]
|
|
1788
2008
|
};
|
|
1789
2009
|
await this.recordMessage(fallbackMsg);
|
|
1790
2010
|
this.bus.emit("state_updated", { reason: "max_iterations" });
|
|
2011
|
+
this.emitPhase("complete", "Response ready.");
|
|
1791
2012
|
this.bus.emit("turn_complete", {
|
|
1792
2013
|
model: modelName,
|
|
1793
2014
|
escalated: true,
|
|
@@ -1801,6 +2022,8 @@ user: ${input}` : input;
|
|
|
1801
2022
|
usage: totalUsage
|
|
1802
2023
|
});
|
|
1803
2024
|
} catch (err) {
|
|
2025
|
+
this.stopHeartbeat();
|
|
2026
|
+
this.emitPhase("error", `Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1804
2027
|
if (!(err instanceof Error && err.message.includes("LLM call failed"))) {
|
|
1805
2028
|
await this.middleware.runError(
|
|
1806
2029
|
syntheticCtx,
|
|
@@ -1808,6 +2031,8 @@ user: ${input}` : input;
|
|
|
1808
2031
|
);
|
|
1809
2032
|
}
|
|
1810
2033
|
throw err;
|
|
2034
|
+
} finally {
|
|
2035
|
+
this.stopHeartbeat();
|
|
1811
2036
|
}
|
|
1812
2037
|
}
|
|
1813
2038
|
/**
|