@sschepis/oboto-agent 0.2.1 → 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 +276 -40
- 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
|
@@ -81,10 +81,21 @@ var ContextManager = class {
|
|
|
81
81
|
const text = typeof m.content === "string" ? m.content : m.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
82
82
|
return `${m.role}: ${text}`;
|
|
83
83
|
}).join("\n");
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
try {
|
|
85
|
+
const result = await this.localRuntime.execute(this.summarizeFn, {
|
|
86
|
+
conversation
|
|
87
|
+
});
|
|
88
|
+
return result.data.summary;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn(
|
|
91
|
+
"[ContextManager] Summarization failed, using truncation fallback:",
|
|
92
|
+
err instanceof Error ? err.message : err
|
|
93
|
+
);
|
|
94
|
+
return messages.slice(-3).map((m) => {
|
|
95
|
+
const text = typeof m.content === "string" ? m.content : "[complex]";
|
|
96
|
+
return `${m.role}: ${text.slice(-500)}`;
|
|
97
|
+
}).join("\n");
|
|
98
|
+
}
|
|
88
99
|
});
|
|
89
100
|
}
|
|
90
101
|
localRuntime;
|
|
@@ -1152,6 +1163,9 @@ var ObotoAgent = class {
|
|
|
1152
1163
|
usageTracker;
|
|
1153
1164
|
usageBridge;
|
|
1154
1165
|
routerEventBridge;
|
|
1166
|
+
heartbeatTimer = null;
|
|
1167
|
+
heartbeatStart = 0;
|
|
1168
|
+
currentPhase = "request";
|
|
1155
1169
|
constructor(config) {
|
|
1156
1170
|
this.config = config;
|
|
1157
1171
|
this.localProvider = config.localModel;
|
|
@@ -1402,6 +1416,75 @@ var ObotoAgent = class {
|
|
|
1402
1416
|
const { context } = await this.conversationRAG.retrieve(query);
|
|
1403
1417
|
return context || void 0;
|
|
1404
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
|
+
}
|
|
1405
1488
|
// ── Internal ───────────────────────────────────────────────────────
|
|
1406
1489
|
/**
|
|
1407
1490
|
* Record a message in the session, context manager, and optionally RAG index.
|
|
@@ -1427,6 +1510,7 @@ var ObotoAgent = class {
|
|
|
1427
1510
|
}
|
|
1428
1511
|
}
|
|
1429
1512
|
async executionLoop(userInput) {
|
|
1513
|
+
this.emitPhase("request", `Processing: ${userInput.substring(0, 80)}${userInput.length > 80 ? "\u2026" : ""}`);
|
|
1430
1514
|
this.bus.emit("user_input", { text: userInput });
|
|
1431
1515
|
const userMsg = {
|
|
1432
1516
|
role: MessageRole4.User,
|
|
@@ -1434,6 +1518,7 @@ var ObotoAgent = class {
|
|
|
1434
1518
|
};
|
|
1435
1519
|
await this.recordMessage(userMsg);
|
|
1436
1520
|
this.bus.emit("state_updated", { reason: "user_input" });
|
|
1521
|
+
this.emitPhase("planning", "Building context and preparing tools\u2026");
|
|
1437
1522
|
if (this.conversationRAG) {
|
|
1438
1523
|
try {
|
|
1439
1524
|
const { context } = await this.conversationRAG.retrieve(userInput);
|
|
@@ -1442,16 +1527,24 @@ var ObotoAgent = class {
|
|
|
1442
1527
|
role: "system",
|
|
1443
1528
|
content: context
|
|
1444
1529
|
});
|
|
1530
|
+
this.bus.emit("agent_thought", {
|
|
1531
|
+
text: "Retrieved relevant past context via RAG.",
|
|
1532
|
+
model: "system"
|
|
1533
|
+
});
|
|
1445
1534
|
}
|
|
1446
1535
|
} catch (err) {
|
|
1447
1536
|
console.warn("[ObotoAgent] RAG retrieval failed:", err instanceof Error ? err.message : err);
|
|
1448
1537
|
}
|
|
1449
1538
|
}
|
|
1539
|
+
this.emitPhase("precheck", "Checking if direct answer is possible\u2026");
|
|
1540
|
+
this.startHeartbeat("precheck");
|
|
1450
1541
|
const triageResult = await this.triage(userInput);
|
|
1542
|
+
this.stopHeartbeat();
|
|
1451
1543
|
this.bus.emit("triage_result", triageResult);
|
|
1452
1544
|
if (this.interrupted) return;
|
|
1453
1545
|
if (!triageResult.escalate && triageResult.directResponse) {
|
|
1454
1546
|
const response = triageResult.directResponse;
|
|
1547
|
+
this.emitPhase("complete", "Answered directly \u2014 no tools needed.");
|
|
1455
1548
|
this.bus.emit("agent_thought", { text: response, model: "local" });
|
|
1456
1549
|
const assistantMsg = {
|
|
1457
1550
|
role: MessageRole4.Assistant,
|
|
@@ -1463,15 +1556,17 @@ var ObotoAgent = class {
|
|
|
1463
1556
|
return;
|
|
1464
1557
|
}
|
|
1465
1558
|
if (triageResult.escalate) {
|
|
1559
|
+
this.emitPhase("thinking", `Escalating to remote model: ${triageResult.reasoning}`);
|
|
1466
1560
|
this.bus.emit("agent_thought", {
|
|
1467
1561
|
text: triageResult.reasoning,
|
|
1468
1562
|
model: "local",
|
|
1469
1563
|
escalating: true
|
|
1470
1564
|
});
|
|
1565
|
+
} else {
|
|
1566
|
+
this.emitPhase("thinking", "This requires tools and deeper reasoning \u2014 entering agent loop.");
|
|
1471
1567
|
}
|
|
1472
1568
|
const modelName = triageResult.escalate ? this.config.remoteModelName : this.config.localModelName;
|
|
1473
1569
|
const runtime = triageResult.escalate ? this.remoteRuntime : this.localRuntime;
|
|
1474
|
-
console.log("[ObotoAgent] Executing with model:", modelName, "| via lmscript AgentLoop");
|
|
1475
1570
|
if (this.onToken) {
|
|
1476
1571
|
await this.executeWithStreaming(runtime, modelName, userInput);
|
|
1477
1572
|
} else {
|
|
@@ -1504,6 +1599,8 @@ var ObotoAgent = class {
|
|
|
1504
1599
|
*/
|
|
1505
1600
|
async executeWithAgentLoop(runtime, modelName, userInput) {
|
|
1506
1601
|
const { z: z5 } = await import("zod");
|
|
1602
|
+
this.emitPhase("thinking", `Turn 1/${this.maxIterations}: Analyzing request\u2026`);
|
|
1603
|
+
this.startHeartbeat("thinking");
|
|
1507
1604
|
const agentFn = {
|
|
1508
1605
|
name: "agent-task",
|
|
1509
1606
|
model: modelName,
|
|
@@ -1526,44 +1623,79 @@ user: ${input}` : input;
|
|
|
1526
1623
|
temperature: 0.7,
|
|
1527
1624
|
maxRetries: 1
|
|
1528
1625
|
};
|
|
1626
|
+
let iterationTools = [];
|
|
1627
|
+
let totalToolCalls = 0;
|
|
1529
1628
|
const agentConfig = {
|
|
1530
1629
|
maxIterations: this.maxIterations,
|
|
1531
1630
|
onToolCall: (tc) => {
|
|
1532
1631
|
const command = typeof tc.arguments === "object" && tc.arguments !== null ? tc.arguments.command ?? tc.name : tc.name;
|
|
1533
1632
|
const kwargs = typeof tc.arguments === "object" && tc.arguments !== null ? tc.arguments.kwargs ?? {} : {};
|
|
1534
|
-
this.bus.emit("tool_execution_complete", {
|
|
1535
|
-
command,
|
|
1536
|
-
kwargs,
|
|
1537
|
-
result: typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result)
|
|
1538
|
-
});
|
|
1539
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++;
|
|
1540
1640
|
this.recordToolResult(String(command), kwargs, resultStr);
|
|
1541
1641
|
},
|
|
1542
1642
|
onIteration: (iteration, response) => {
|
|
1543
|
-
this.
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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");
|
|
1548
1664
|
if (this.interrupted) return false;
|
|
1549
1665
|
}
|
|
1550
1666
|
};
|
|
1551
1667
|
const agentLoop = new AgentLoop(runtime, agentConfig);
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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
|
+
}
|
|
1567
1699
|
}
|
|
1568
1700
|
/**
|
|
1569
1701
|
* Execute with streaming token emission.
|
|
@@ -1599,6 +1731,8 @@ user: ${input}` : input;
|
|
|
1599
1731
|
];
|
|
1600
1732
|
let totalToolCalls = 0;
|
|
1601
1733
|
const callHistory = [];
|
|
1734
|
+
const consecutiveDupes = /* @__PURE__ */ new Map();
|
|
1735
|
+
let doomLoopRedirected = false;
|
|
1602
1736
|
const turnStartTime = Date.now();
|
|
1603
1737
|
const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1604
1738
|
const syntheticCtx = {
|
|
@@ -1611,13 +1745,22 @@ user: ${input}` : input;
|
|
|
1611
1745
|
await this.middleware.runBeforeExecute(syntheticCtx);
|
|
1612
1746
|
try {
|
|
1613
1747
|
for (let iteration = 1; iteration <= this.maxIterations; iteration++) {
|
|
1614
|
-
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");
|
|
1615
1757
|
if (this.costTracker && this.budget) {
|
|
1616
1758
|
this.costTracker.checkBudget(this.budget);
|
|
1617
1759
|
}
|
|
1618
1760
|
await this.rateLimiter?.acquire();
|
|
1619
1761
|
const isLastIteration = iteration === this.maxIterations;
|
|
1620
1762
|
if (isLastIteration) {
|
|
1763
|
+
this.emitPhase("continuation", "Maximum iterations reached \u2014 synthesizing final response\u2026");
|
|
1621
1764
|
messages.push({
|
|
1622
1765
|
role: "user",
|
|
1623
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."
|
|
@@ -1633,13 +1776,15 @@ user: ${input}` : input;
|
|
|
1633
1776
|
try {
|
|
1634
1777
|
response = await this.streamAndAggregate(provider, params);
|
|
1635
1778
|
} catch (err) {
|
|
1779
|
+
this.stopHeartbeat();
|
|
1780
|
+
this.emitPhase("error", `LLM call failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1636
1781
|
await this.middleware.runError(
|
|
1637
1782
|
syntheticCtx,
|
|
1638
1783
|
err instanceof Error ? err : new Error(String(err))
|
|
1639
1784
|
);
|
|
1640
|
-
console.error("[ObotoAgent] LLM call failed:", err instanceof Error ? err.message : err);
|
|
1641
1785
|
throw err;
|
|
1642
1786
|
}
|
|
1787
|
+
this.stopHeartbeat();
|
|
1643
1788
|
const usage = response?.usage;
|
|
1644
1789
|
if (usage) {
|
|
1645
1790
|
const promptTokens = usage.prompt_tokens ?? 0;
|
|
@@ -1673,12 +1818,21 @@ user: ${input}` : input;
|
|
|
1673
1818
|
});
|
|
1674
1819
|
}
|
|
1675
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");
|
|
1676
1829
|
const assistantMsg = {
|
|
1677
1830
|
role: MessageRole4.Assistant,
|
|
1678
1831
|
blocks: [{ kind: "text", text: content }]
|
|
1679
1832
|
};
|
|
1680
1833
|
await this.recordMessage(assistantMsg);
|
|
1681
1834
|
this.bus.emit("state_updated", { reason: "assistant_response" });
|
|
1835
|
+
this.emitPhase("complete", "Response ready.");
|
|
1682
1836
|
this.bus.emit("turn_complete", {
|
|
1683
1837
|
model: modelName,
|
|
1684
1838
|
escalated: true,
|
|
@@ -1693,12 +1847,15 @@ user: ${input}` : input;
|
|
|
1693
1847
|
});
|
|
1694
1848
|
return;
|
|
1695
1849
|
}
|
|
1850
|
+
this.emitPhase("tools", `Executing ${toolCalls.length} tool(s)\u2026`);
|
|
1696
1851
|
messages.push({
|
|
1697
1852
|
role: "assistant",
|
|
1698
1853
|
content: content || null,
|
|
1699
1854
|
tool_calls: toolCalls
|
|
1700
1855
|
});
|
|
1701
|
-
|
|
1856
|
+
const roundTools = [];
|
|
1857
|
+
for (let ti = 0; ti < toolCalls.length; ti++) {
|
|
1858
|
+
const tc = toolCalls[ti];
|
|
1702
1859
|
if (this.interrupted) break;
|
|
1703
1860
|
let args;
|
|
1704
1861
|
try {
|
|
@@ -1717,6 +1874,7 @@ user: ${input}` : input;
|
|
|
1717
1874
|
tool_call_id: tc.id,
|
|
1718
1875
|
content: `Permission denied for tool "${command}": ${outcome.reason ?? "denied by policy"}`
|
|
1719
1876
|
});
|
|
1877
|
+
roundTools.push({ command, success: false });
|
|
1720
1878
|
totalToolCalls++;
|
|
1721
1879
|
continue;
|
|
1722
1880
|
}
|
|
@@ -1729,6 +1887,7 @@ user: ${input}` : input;
|
|
|
1729
1887
|
tool_call_id: tc.id,
|
|
1730
1888
|
content: `Tool "${command}" blocked by pre-use hook: ${hookResult.messages.join("; ")}`
|
|
1731
1889
|
});
|
|
1890
|
+
roundTools.push({ command, success: false });
|
|
1732
1891
|
totalToolCalls++;
|
|
1733
1892
|
continue;
|
|
1734
1893
|
}
|
|
@@ -1736,16 +1895,77 @@ user: ${input}` : input;
|
|
|
1736
1895
|
const callSig = JSON.stringify({ command, kwargs });
|
|
1737
1896
|
const dupeCount = callHistory.filter((s) => s === callSig).length;
|
|
1738
1897
|
callHistory.push(callSig);
|
|
1898
|
+
const prevConsec = consecutiveDupes.get(command) ?? 0;
|
|
1899
|
+
consecutiveDupes.set(command, prevConsec + 1);
|
|
1739
1900
|
if (dupeCount >= 2) {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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 });
|
|
1745
1963
|
totalToolCalls++;
|
|
1746
1964
|
continue;
|
|
1965
|
+
} else {
|
|
1966
|
+
consecutiveDupes.set(command, 0);
|
|
1747
1967
|
}
|
|
1748
|
-
this.bus.emit("tool_execution_start", { command, kwargs });
|
|
1968
|
+
this.bus.emit("tool_execution_start", { command, kwargs, index: ti, total: toolCalls.length });
|
|
1749
1969
|
let result;
|
|
1750
1970
|
let isError = false;
|
|
1751
1971
|
try {
|
|
@@ -1761,8 +1981,9 @@ user: ${input}` : input;
|
|
|
1761
1981
|
if (this.hookIntegration) {
|
|
1762
1982
|
this.hookIntegration.runPostToolUse(command, toolInputStr, truncated, isError);
|
|
1763
1983
|
}
|
|
1764
|
-
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 });
|
|
1765
1985
|
this.recordToolResult(command, kwargs, truncated);
|
|
1986
|
+
roundTools.push({ command, success: !isError });
|
|
1766
1987
|
totalToolCalls++;
|
|
1767
1988
|
messages.push({
|
|
1768
1989
|
role: "tool",
|
|
@@ -1770,13 +1991,24 @@ user: ${input}` : input;
|
|
|
1770
1991
|
content: truncated
|
|
1771
1992
|
});
|
|
1772
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
|
+
}
|
|
1773
2003
|
}
|
|
2004
|
+
this.emitPhase("continuation", "Maximum iterations reached \u2014 synthesizing response\u2026");
|
|
1774
2005
|
const fallbackMsg = {
|
|
1775
2006
|
role: MessageRole4.Assistant,
|
|
1776
2007
|
blocks: [{ kind: "text", text: "I reached the maximum number of iterations. Here is what I have so far." }]
|
|
1777
2008
|
};
|
|
1778
2009
|
await this.recordMessage(fallbackMsg);
|
|
1779
2010
|
this.bus.emit("state_updated", { reason: "max_iterations" });
|
|
2011
|
+
this.emitPhase("complete", "Response ready.");
|
|
1780
2012
|
this.bus.emit("turn_complete", {
|
|
1781
2013
|
model: modelName,
|
|
1782
2014
|
escalated: true,
|
|
@@ -1790,6 +2022,8 @@ user: ${input}` : input;
|
|
|
1790
2022
|
usage: totalUsage
|
|
1791
2023
|
});
|
|
1792
2024
|
} catch (err) {
|
|
2025
|
+
this.stopHeartbeat();
|
|
2026
|
+
this.emitPhase("error", `Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1793
2027
|
if (!(err instanceof Error && err.message.includes("LLM call failed"))) {
|
|
1794
2028
|
await this.middleware.runError(
|
|
1795
2029
|
syntheticCtx,
|
|
@@ -1797,6 +2031,8 @@ user: ${input}` : input;
|
|
|
1797
2031
|
);
|
|
1798
2032
|
}
|
|
1799
2033
|
throw err;
|
|
2034
|
+
} finally {
|
|
2035
|
+
this.stopHeartbeat();
|
|
1800
2036
|
}
|
|
1801
2037
|
}
|
|
1802
2038
|
/**
|