@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 +38 -3
- package/dist/index.js +232 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
1179
|
-
|
|
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.
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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)
|
|
1626
|
-
|
|
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
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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,
|