@sschepis/oboto-agent 0.1.5 → 0.2.2
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 +999 -45
- package/dist/index.js +1719 -178
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// src/oboto-agent.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
LScriptRuntime,
|
|
4
|
+
MiddlewareManager,
|
|
5
|
+
ExecutionCache,
|
|
6
|
+
MemoryCacheBackend,
|
|
7
|
+
CostTracker,
|
|
8
|
+
RateLimiter,
|
|
9
|
+
AgentLoop
|
|
10
|
+
} from "@sschepis/lmscript";
|
|
3
11
|
import { aggregateStream } from "@sschepis/llm-wrapper";
|
|
4
|
-
import {
|
|
5
|
-
import { MessageRole as MessageRole2 } from "@sschepis/as-agent";
|
|
12
|
+
import { MessageRole as MessageRole4 } from "@sschepis/as-agent";
|
|
6
13
|
|
|
7
14
|
// src/event-bus.ts
|
|
8
15
|
var AgentEventBus = class {
|
|
@@ -74,10 +81,21 @@ var ContextManager = class {
|
|
|
74
81
|
const text = typeof m.content === "string" ? m.content : m.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
75
82
|
return `${m.role}: ${text}`;
|
|
76
83
|
}).join("\n");
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
}
|
|
81
99
|
});
|
|
82
100
|
}
|
|
83
101
|
localRuntime;
|
|
@@ -110,7 +128,7 @@ import { z as z2 } from "zod";
|
|
|
110
128
|
var TriageSchema = z2.object({
|
|
111
129
|
escalate: z2.boolean().describe("True if the request needs a powerful model, false if answerable directly"),
|
|
112
130
|
reasoning: z2.string().describe("Brief explanation of the triage decision"),
|
|
113
|
-
directResponse: z2.string().
|
|
131
|
+
directResponse: z2.string().nullish().transform((v) => v ?? void 0).describe("Direct answer if the request can be handled without escalation")
|
|
114
132
|
});
|
|
115
133
|
var TRIAGE_SYSTEM = `You are a fast triage classifier for an AI agent system.
|
|
116
134
|
Your job is to decide whether a user's request can be answered directly (simple queries,
|
|
@@ -118,10 +136,13 @@ casual chat, short lookups) or needs to be escalated to a more powerful model
|
|
|
118
136
|
(complex reasoning, multi-step tool usage, code generation, analysis).
|
|
119
137
|
|
|
120
138
|
Rules:
|
|
121
|
-
-
|
|
139
|
+
- Only respond directly for truly trivial exchanges: greetings, thanks, or very simple factual questions.
|
|
140
|
+
- When responding directly, use a natural, warm, conversational tone. Do NOT list capabilities or describe what tools you have access to.
|
|
141
|
+
- If the user's message could benefit from tool usage or detailed reasoning, always escalate.
|
|
122
142
|
- If the request needs tool calls, code analysis, or multi-step reasoning: escalate.
|
|
123
143
|
- If unsure, escalate. It's better to over-escalate than to give a poor direct answer.
|
|
124
144
|
- Keep directResponse under 200 words when answering directly.
|
|
145
|
+
- Pay attention to the recent context \u2014 if the user is continuing a conversation, your response should reflect that context.
|
|
125
146
|
|
|
126
147
|
Respond with JSON matching the schema.`;
|
|
127
148
|
function createTriageFunction(modelName) {
|
|
@@ -163,32 +184,38 @@ function createRouterTool(router, root) {
|
|
|
163
184
|
}
|
|
164
185
|
|
|
165
186
|
// src/adapters/llm-wrapper.ts
|
|
187
|
+
function convertMessages(messages) {
|
|
188
|
+
return messages.map((m) => ({
|
|
189
|
+
role: m.role,
|
|
190
|
+
content: typeof m.content === "string" ? m.content : m.content.filter((b) => b.type === "text").map((b) => b.text).join("\n")
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
function convertTools(tools) {
|
|
194
|
+
if (!tools || tools.length === 0) return void 0;
|
|
195
|
+
return tools.map((t) => ({
|
|
196
|
+
type: "function",
|
|
197
|
+
function: {
|
|
198
|
+
name: t.name,
|
|
199
|
+
description: t.description,
|
|
200
|
+
parameters: t.parameters
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
function buildParams(request, tools) {
|
|
205
|
+
return {
|
|
206
|
+
model: request.model,
|
|
207
|
+
messages: convertMessages(request.messages),
|
|
208
|
+
temperature: request.temperature,
|
|
209
|
+
...tools ? { tools } : {},
|
|
210
|
+
...request.jsonMode ? { response_format: { type: "json_object" } } : {}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
166
213
|
function toLmscriptProvider(provider, name) {
|
|
167
214
|
return {
|
|
168
|
-
name: name ?? provider.providerName,
|
|
215
|
+
name: name ?? provider.providerName ?? "llm-wrapper",
|
|
169
216
|
async chat(request) {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
content: typeof m.content === "string" ? m.content : m.content.filter((b) => b.type === "text").map((b) => b.text).join("\n")
|
|
173
|
-
}));
|
|
174
|
-
let tools;
|
|
175
|
-
if (request.tools && request.tools.length > 0) {
|
|
176
|
-
tools = request.tools.map((t) => ({
|
|
177
|
-
type: "function",
|
|
178
|
-
function: {
|
|
179
|
-
name: t.name,
|
|
180
|
-
description: t.description,
|
|
181
|
-
parameters: t.parameters
|
|
182
|
-
}
|
|
183
|
-
}));
|
|
184
|
-
}
|
|
185
|
-
const params = {
|
|
186
|
-
model: request.model,
|
|
187
|
-
messages,
|
|
188
|
-
temperature: request.temperature,
|
|
189
|
-
...tools ? { tools } : {},
|
|
190
|
-
...request.jsonMode ? { response_format: { type: "json_object" } } : {}
|
|
191
|
-
};
|
|
217
|
+
const tools = convertTools(request.tools);
|
|
218
|
+
const params = buildParams(request, tools);
|
|
192
219
|
const response = await provider.chat(params);
|
|
193
220
|
const choice = response.choices[0];
|
|
194
221
|
let toolCalls;
|
|
@@ -208,6 +235,16 @@ function toLmscriptProvider(provider, name) {
|
|
|
208
235
|
} : void 0,
|
|
209
236
|
toolCalls
|
|
210
237
|
};
|
|
238
|
+
},
|
|
239
|
+
async *chatStream(request) {
|
|
240
|
+
const tools = convertTools(request.tools);
|
|
241
|
+
const params = buildParams(request, tools);
|
|
242
|
+
for await (const chunk of provider.stream({ ...params, stream: true })) {
|
|
243
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
244
|
+
if (delta?.content) {
|
|
245
|
+
yield delta.content;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
211
248
|
}
|
|
212
249
|
};
|
|
213
250
|
}
|
|
@@ -259,10 +296,848 @@ function createEmptySession() {
|
|
|
259
296
|
return { version: 1, messages: [] };
|
|
260
297
|
}
|
|
261
298
|
|
|
299
|
+
// src/adapters/rag-integration.ts
|
|
300
|
+
import {
|
|
301
|
+
RAGPipeline,
|
|
302
|
+
MemoryVectorStore
|
|
303
|
+
} from "@sschepis/lmscript";
|
|
304
|
+
import { MessageRole as MessageRole2 } from "@sschepis/as-agent";
|
|
305
|
+
var ConversationRAG = class {
|
|
306
|
+
vectorStore;
|
|
307
|
+
embeddingProvider;
|
|
308
|
+
ragPipeline;
|
|
309
|
+
config;
|
|
310
|
+
indexedMessageCount = 0;
|
|
311
|
+
runtime;
|
|
312
|
+
constructor(runtime, config) {
|
|
313
|
+
this.runtime = runtime;
|
|
314
|
+
this.vectorStore = config.vectorStore ?? new MemoryVectorStore();
|
|
315
|
+
this.embeddingProvider = config.embeddingProvider;
|
|
316
|
+
this.config = {
|
|
317
|
+
embeddingProvider: config.embeddingProvider,
|
|
318
|
+
vectorStore: this.vectorStore,
|
|
319
|
+
topK: config.topK ?? 5,
|
|
320
|
+
minScore: config.minScore ?? 0.3,
|
|
321
|
+
embeddingModel: config.embeddingModel ?? "",
|
|
322
|
+
autoIndex: config.autoIndex ?? true,
|
|
323
|
+
indexToolResults: config.indexToolResults ?? true,
|
|
324
|
+
maxChunkSize: config.maxChunkSize ?? 2e3,
|
|
325
|
+
formatContext: config.formatContext ?? defaultConversationContextFormatter
|
|
326
|
+
};
|
|
327
|
+
this.ragPipeline = new RAGPipeline(runtime, {
|
|
328
|
+
embeddingProvider: this.embeddingProvider,
|
|
329
|
+
vectorStore: this.vectorStore,
|
|
330
|
+
topK: this.config.topK,
|
|
331
|
+
minScore: this.config.minScore,
|
|
332
|
+
embeddingModel: this.config.embeddingModel,
|
|
333
|
+
formatContext: this.config.formatContext
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// ── Indexing ──────────────────────────────────────────────────────
|
|
337
|
+
/**
|
|
338
|
+
* Index an entire as-agent Session into the vector store.
|
|
339
|
+
* Each message becomes one or more chunks (split by maxChunkSize).
|
|
340
|
+
*/
|
|
341
|
+
async indexSession(session) {
|
|
342
|
+
const documents = [];
|
|
343
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
344
|
+
const msg = session.messages[i];
|
|
345
|
+
const chunks = this.messageToChunks(msg, i);
|
|
346
|
+
documents.push(...chunks);
|
|
347
|
+
}
|
|
348
|
+
if (documents.length > 0) {
|
|
349
|
+
await this.ragPipeline.ingest(documents);
|
|
350
|
+
this.indexedMessageCount += session.messages.length;
|
|
351
|
+
}
|
|
352
|
+
return documents.length;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Index a single conversation message.
|
|
356
|
+
* Call this after adding a message to the session for real-time indexing.
|
|
357
|
+
*/
|
|
358
|
+
async indexMessage(msg, messageIndex) {
|
|
359
|
+
const idx = messageIndex ?? this.indexedMessageCount;
|
|
360
|
+
const chunks = this.messageToChunks(msg, idx);
|
|
361
|
+
if (chunks.length > 0) {
|
|
362
|
+
await this.ragPipeline.ingest(chunks);
|
|
363
|
+
this.indexedMessageCount++;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Index a tool execution result for later retrieval.
|
|
368
|
+
*/
|
|
369
|
+
async indexToolResult(command, kwargs, result) {
|
|
370
|
+
if (!this.config.indexToolResults) return;
|
|
371
|
+
const content = `Tool: ${command}
|
|
372
|
+
Args: ${JSON.stringify(kwargs)}
|
|
373
|
+
Result: ${result}`;
|
|
374
|
+
const chunks = this.splitChunks(content, `tool:${command}:${Date.now()}`);
|
|
375
|
+
if (chunks.length > 0) {
|
|
376
|
+
const documents = chunks.map((chunk, i) => ({
|
|
377
|
+
id: chunk.id,
|
|
378
|
+
content: chunk.content,
|
|
379
|
+
metadata: {
|
|
380
|
+
type: "tool_result",
|
|
381
|
+
command,
|
|
382
|
+
kwargs,
|
|
383
|
+
chunkIndex: i,
|
|
384
|
+
timestamp: Date.now()
|
|
385
|
+
}
|
|
386
|
+
}));
|
|
387
|
+
await this.ragPipeline.ingest(documents);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// ── Retrieval ─────────────────────────────────────────────────────
|
|
391
|
+
/**
|
|
392
|
+
* Retrieve relevant past context for a query.
|
|
393
|
+
* Returns formatted context string and raw results.
|
|
394
|
+
*/
|
|
395
|
+
async retrieve(query) {
|
|
396
|
+
const [queryVector] = await this.embeddingProvider.embed(
|
|
397
|
+
[query],
|
|
398
|
+
this.config.embeddingModel || void 0
|
|
399
|
+
);
|
|
400
|
+
const results = await this.vectorStore.search(queryVector, this.config.topK);
|
|
401
|
+
const filtered = results.filter((r) => r.score >= this.config.minScore);
|
|
402
|
+
const context = this.config.formatContext(filtered);
|
|
403
|
+
const totalDocuments = await this.vectorStore.count();
|
|
404
|
+
return { context, results: filtered, totalDocuments };
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Execute an lmscript function with RAG-augmented context from the
|
|
408
|
+
* conversation history.
|
|
409
|
+
*
|
|
410
|
+
* This is the primary integration point — it uses lmscript's RAGPipeline
|
|
411
|
+
* to inject relevant past conversation into the function's system prompt.
|
|
412
|
+
*/
|
|
413
|
+
async executeWithContext(fn, input, queryText) {
|
|
414
|
+
const ragResult = await this.ragPipeline.query(fn, input, queryText);
|
|
415
|
+
return {
|
|
416
|
+
result: ragResult.result,
|
|
417
|
+
retrievedDocuments: ragResult.retrievedDocuments,
|
|
418
|
+
context: ragResult.context
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
// ── Utility ───────────────────────────────────────────────────────
|
|
422
|
+
/** Get the number of indexed messages. */
|
|
423
|
+
get messageCount() {
|
|
424
|
+
return this.indexedMessageCount;
|
|
425
|
+
}
|
|
426
|
+
/** Get the total number of document chunks in the vector store. */
|
|
427
|
+
async documentCount() {
|
|
428
|
+
return this.vectorStore.count();
|
|
429
|
+
}
|
|
430
|
+
/** Clear the vector store and reset counters. */
|
|
431
|
+
async clear() {
|
|
432
|
+
await this.vectorStore.clear();
|
|
433
|
+
this.indexedMessageCount = 0;
|
|
434
|
+
}
|
|
435
|
+
/** Get the underlying vector store (for advanced usage). */
|
|
436
|
+
getVectorStore() {
|
|
437
|
+
return this.vectorStore;
|
|
438
|
+
}
|
|
439
|
+
/** Get the underlying RAG pipeline (for advanced usage). */
|
|
440
|
+
getRagPipeline() {
|
|
441
|
+
return this.ragPipeline;
|
|
442
|
+
}
|
|
443
|
+
// ── Private ───────────────────────────────────────────────────────
|
|
444
|
+
messageToChunks(msg, messageIndex) {
|
|
445
|
+
const roleLabel = messageRoleToLabel(msg.role);
|
|
446
|
+
const text = blocksToText2(msg.blocks);
|
|
447
|
+
if (!text.trim()) return [];
|
|
448
|
+
const prefixed = `[${roleLabel}]: ${text}`;
|
|
449
|
+
const baseId = `msg:${messageIndex}`;
|
|
450
|
+
return this.splitChunks(prefixed, baseId).map((chunk, i) => ({
|
|
451
|
+
id: chunk.id,
|
|
452
|
+
content: chunk.content,
|
|
453
|
+
metadata: {
|
|
454
|
+
type: "conversation",
|
|
455
|
+
role: roleLabel,
|
|
456
|
+
messageIndex,
|
|
457
|
+
chunkIndex: i,
|
|
458
|
+
timestamp: Date.now()
|
|
459
|
+
}
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
splitChunks(text, baseId) {
|
|
463
|
+
const maxSize = this.config.maxChunkSize;
|
|
464
|
+
if (text.length <= maxSize) {
|
|
465
|
+
return [{ id: baseId, content: text }];
|
|
466
|
+
}
|
|
467
|
+
const chunks = [];
|
|
468
|
+
let remaining = text;
|
|
469
|
+
let chunkIdx = 0;
|
|
470
|
+
while (remaining.length > 0) {
|
|
471
|
+
let splitAt = maxSize;
|
|
472
|
+
if (remaining.length > maxSize) {
|
|
473
|
+
const paraIdx = remaining.lastIndexOf("\n\n", maxSize);
|
|
474
|
+
if (paraIdx > maxSize * 0.3) {
|
|
475
|
+
splitAt = paraIdx + 2;
|
|
476
|
+
} else {
|
|
477
|
+
const sentIdx = remaining.lastIndexOf(". ", maxSize);
|
|
478
|
+
if (sentIdx > maxSize * 0.3) {
|
|
479
|
+
splitAt = sentIdx + 2;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
splitAt = remaining.length;
|
|
484
|
+
}
|
|
485
|
+
chunks.push({
|
|
486
|
+
id: `${baseId}:${chunkIdx}`,
|
|
487
|
+
content: remaining.slice(0, splitAt).trim()
|
|
488
|
+
});
|
|
489
|
+
remaining = remaining.slice(splitAt);
|
|
490
|
+
chunkIdx++;
|
|
491
|
+
}
|
|
492
|
+
return chunks;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
function messageRoleToLabel(role) {
|
|
496
|
+
switch (role) {
|
|
497
|
+
case MessageRole2.System:
|
|
498
|
+
return "system";
|
|
499
|
+
case MessageRole2.User:
|
|
500
|
+
return "user";
|
|
501
|
+
case MessageRole2.Assistant:
|
|
502
|
+
return "assistant";
|
|
503
|
+
case MessageRole2.Tool:
|
|
504
|
+
return "tool";
|
|
505
|
+
default:
|
|
506
|
+
return "unknown";
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function blocksToText2(blocks) {
|
|
510
|
+
return blocks.map((b) => {
|
|
511
|
+
switch (b.kind) {
|
|
512
|
+
case "text":
|
|
513
|
+
return b.text;
|
|
514
|
+
case "tool_use":
|
|
515
|
+
return `[Tool: ${b.name}(${b.input})]`;
|
|
516
|
+
case "tool_result":
|
|
517
|
+
return b.isError ? `[Error: ${b.toolName}: ${b.output}]` : `[Result: ${b.toolName}: ${b.output}]`;
|
|
518
|
+
default:
|
|
519
|
+
return "";
|
|
520
|
+
}
|
|
521
|
+
}).join("\n");
|
|
522
|
+
}
|
|
523
|
+
function defaultConversationContextFormatter(results) {
|
|
524
|
+
if (results.length === 0) return "";
|
|
525
|
+
return [
|
|
526
|
+
"## Relevant Past Context",
|
|
527
|
+
"",
|
|
528
|
+
...results.map((r, i) => {
|
|
529
|
+
const meta = r.document.metadata;
|
|
530
|
+
const type = meta?.type ?? "unknown";
|
|
531
|
+
const score = r.score.toFixed(3);
|
|
532
|
+
return `[${i + 1}] (${type}, score: ${score})
|
|
533
|
+
${r.document.content}`;
|
|
534
|
+
})
|
|
535
|
+
].join("\n");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/adapters/as-agent-features.ts
|
|
539
|
+
import { MessageRole as MessageRole3 } from "@sschepis/as-agent";
|
|
540
|
+
var PermissionGuard = class {
|
|
541
|
+
constructor(policy, prompter, bus) {
|
|
542
|
+
this.policy = policy;
|
|
543
|
+
this.prompter = prompter;
|
|
544
|
+
this.bus = bus;
|
|
545
|
+
}
|
|
546
|
+
policy;
|
|
547
|
+
prompter;
|
|
548
|
+
bus;
|
|
549
|
+
/**
|
|
550
|
+
* Check whether a tool call is authorized.
|
|
551
|
+
* Emits `permission_denied` on the event bus if denied.
|
|
552
|
+
*/
|
|
553
|
+
checkPermission(toolName, toolInput) {
|
|
554
|
+
const outcome = this.policy.authorize(toolName, toolInput, this.prompter);
|
|
555
|
+
if (outcome.kind === "deny") {
|
|
556
|
+
this.bus?.emit("permission_denied", {
|
|
557
|
+
toolName,
|
|
558
|
+
toolInput,
|
|
559
|
+
reason: outcome.reason ?? "denied by policy",
|
|
560
|
+
activeMode: this.policy.activeMode,
|
|
561
|
+
requiredMode: this.policy.requiredModeFor(toolName)
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return outcome;
|
|
565
|
+
}
|
|
566
|
+
/** Get the current active permission mode. */
|
|
567
|
+
get activeMode() {
|
|
568
|
+
return this.policy.activeMode;
|
|
569
|
+
}
|
|
570
|
+
/** Get the required permission mode for a specific tool. */
|
|
571
|
+
requiredModeFor(toolName) {
|
|
572
|
+
return this.policy.requiredModeFor(toolName);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
var SessionCompactor = class {
|
|
576
|
+
config;
|
|
577
|
+
bus;
|
|
578
|
+
compactionCount = 0;
|
|
579
|
+
totalRemovedMessages = 0;
|
|
580
|
+
constructor(bus, config) {
|
|
581
|
+
this.bus = bus;
|
|
582
|
+
this.config = config;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Check if the session needs compaction and perform it if so.
|
|
586
|
+
* Uses a simple heuristic: ~4 chars per token for estimation.
|
|
587
|
+
*
|
|
588
|
+
* Since the actual compaction logic lives in the Wasm runtime
|
|
589
|
+
* (ConversationRuntime.compact()), this method provides a JS-side
|
|
590
|
+
* implementation that creates a summary of older messages and
|
|
591
|
+
* preserves recent ones.
|
|
592
|
+
*
|
|
593
|
+
* Returns null if compaction is not needed.
|
|
594
|
+
*/
|
|
595
|
+
compactIfNeeded(session, estimatedTokens) {
|
|
596
|
+
const tokenEstimate = estimatedTokens ?? this.estimateTokens(session);
|
|
597
|
+
if (tokenEstimate <= this.config.maxEstimatedTokens) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
return this.compact(session);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Force compaction of the session regardless of token count.
|
|
604
|
+
*/
|
|
605
|
+
compact(session) {
|
|
606
|
+
const preserve = this.config.preserveRecentMessages;
|
|
607
|
+
const totalMessages = session.messages.length;
|
|
608
|
+
if (totalMessages <= preserve) {
|
|
609
|
+
return {
|
|
610
|
+
summary: "",
|
|
611
|
+
formattedSummary: "",
|
|
612
|
+
compactedSession: session,
|
|
613
|
+
removedMessageCount: 0
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
const toSummarize = session.messages.slice(0, totalMessages - preserve);
|
|
617
|
+
const toPreserve = session.messages.slice(totalMessages - preserve);
|
|
618
|
+
const summaryParts = [];
|
|
619
|
+
for (const msg of toSummarize) {
|
|
620
|
+
const role = roleToString(msg.role);
|
|
621
|
+
const text = blocksToText3(msg.blocks);
|
|
622
|
+
if (text.trim()) {
|
|
623
|
+
summaryParts.push(`[${role}]: ${text}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const summary = summaryParts.join("\n\n");
|
|
627
|
+
const formattedSummary = `[Session Compaction Summary \u2014 ${toSummarize.length} messages summarized]
|
|
628
|
+
|
|
629
|
+
${summary}`;
|
|
630
|
+
const summaryMessage = {
|
|
631
|
+
role: MessageRole3.System,
|
|
632
|
+
blocks: [{
|
|
633
|
+
kind: "text",
|
|
634
|
+
text: `[Previous conversation summary \u2014 ${toSummarize.length} messages compacted]
|
|
635
|
+
|
|
636
|
+
${summary.length > 4e3 ? summary.slice(0, 4e3) + "\n\n[... summary truncated]" : summary}`
|
|
637
|
+
}]
|
|
638
|
+
};
|
|
639
|
+
const compactedSession = {
|
|
640
|
+
version: session.version,
|
|
641
|
+
messages: [summaryMessage, ...toPreserve]
|
|
642
|
+
};
|
|
643
|
+
const result = {
|
|
644
|
+
summary,
|
|
645
|
+
formattedSummary,
|
|
646
|
+
compactedSession,
|
|
647
|
+
removedMessageCount: toSummarize.length
|
|
648
|
+
};
|
|
649
|
+
this.compactionCount++;
|
|
650
|
+
this.totalRemovedMessages += toSummarize.length;
|
|
651
|
+
this.bus?.emit("session_compacted", {
|
|
652
|
+
removedMessageCount: toSummarize.length,
|
|
653
|
+
preservedMessageCount: toPreserve.length + 1,
|
|
654
|
+
// +1 for summary msg
|
|
655
|
+
estimatedTokensBefore: this.estimateTokens(session),
|
|
656
|
+
estimatedTokensAfter: this.estimateTokens(compactedSession),
|
|
657
|
+
compactionIndex: this.compactionCount
|
|
658
|
+
});
|
|
659
|
+
return result;
|
|
660
|
+
}
|
|
661
|
+
/** Get compaction statistics. */
|
|
662
|
+
get stats() {
|
|
663
|
+
return {
|
|
664
|
+
compactionCount: this.compactionCount,
|
|
665
|
+
totalRemovedMessages: this.totalRemovedMessages
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/** Update the compaction config at runtime. */
|
|
669
|
+
updateConfig(config) {
|
|
670
|
+
if (config.preserveRecentMessages !== void 0) {
|
|
671
|
+
this.config.preserveRecentMessages = config.preserveRecentMessages;
|
|
672
|
+
}
|
|
673
|
+
if (config.maxEstimatedTokens !== void 0) {
|
|
674
|
+
this.config.maxEstimatedTokens = config.maxEstimatedTokens;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/** Estimate token count for a session (~4 chars per token). */
|
|
678
|
+
estimateTokens(session) {
|
|
679
|
+
let charCount = 0;
|
|
680
|
+
for (const msg of session.messages) {
|
|
681
|
+
for (const block of msg.blocks) {
|
|
682
|
+
if (block.kind === "text") {
|
|
683
|
+
charCount += block.text.length;
|
|
684
|
+
} else if (block.kind === "tool_use") {
|
|
685
|
+
charCount += block.name.length + block.input.length;
|
|
686
|
+
} else if (block.kind === "tool_result") {
|
|
687
|
+
charCount += block.output.length;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return Math.ceil(charCount / 4);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
var HookIntegration = class {
|
|
695
|
+
constructor(runner, bus) {
|
|
696
|
+
this.runner = runner;
|
|
697
|
+
this.bus = bus;
|
|
698
|
+
}
|
|
699
|
+
runner;
|
|
700
|
+
bus;
|
|
701
|
+
/**
|
|
702
|
+
* Run pre-tool-use hooks. If any hook denies the call,
|
|
703
|
+
* the tool execution should be skipped.
|
|
704
|
+
*/
|
|
705
|
+
runPreToolUse(toolName, toolInput) {
|
|
706
|
+
const result = this.runner.runPreToolUse(toolName, toolInput);
|
|
707
|
+
if (result.denied) {
|
|
708
|
+
this.bus?.emit("hook_denied", {
|
|
709
|
+
phase: "pre",
|
|
710
|
+
toolName,
|
|
711
|
+
toolInput,
|
|
712
|
+
messages: result.messages
|
|
713
|
+
});
|
|
714
|
+
} else if (result.messages.length > 0) {
|
|
715
|
+
this.bus?.emit("hook_message", {
|
|
716
|
+
phase: "pre",
|
|
717
|
+
toolName,
|
|
718
|
+
messages: result.messages
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Run post-tool-use hooks. These can log, transform, or audit
|
|
725
|
+
* tool results but cannot retroactively deny execution.
|
|
726
|
+
*/
|
|
727
|
+
runPostToolUse(toolName, toolInput, toolOutput, isError) {
|
|
728
|
+
const result = this.runner.runPostToolUse(toolName, toolInput, toolOutput, isError);
|
|
729
|
+
if (result.messages.length > 0) {
|
|
730
|
+
this.bus?.emit("hook_message", {
|
|
731
|
+
phase: "post",
|
|
732
|
+
toolName,
|
|
733
|
+
messages: result.messages
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
var SlashCommandRegistry = class {
|
|
740
|
+
customCommands = /* @__PURE__ */ new Map();
|
|
741
|
+
wasmRuntime;
|
|
742
|
+
constructor(wasmRuntime) {
|
|
743
|
+
this.wasmRuntime = wasmRuntime;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Get all slash command specs (built-in from Wasm + custom).
|
|
747
|
+
*/
|
|
748
|
+
getCommandSpecs() {
|
|
749
|
+
const builtIn = this.wasmRuntime ? this.wasmRuntime.slashCommandSpecs() : [];
|
|
750
|
+
const custom = Array.from(this.customCommands.values()).map((c) => c.spec);
|
|
751
|
+
return [...builtIn, ...custom];
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Get the full help text for all commands.
|
|
755
|
+
* Uses the Wasm runtime's formatted help if available.
|
|
756
|
+
*/
|
|
757
|
+
getHelpText() {
|
|
758
|
+
const parts = [];
|
|
759
|
+
if (this.wasmRuntime) {
|
|
760
|
+
parts.push(this.wasmRuntime.renderSlashCommandHelp());
|
|
761
|
+
}
|
|
762
|
+
if (this.customCommands.size > 0) {
|
|
763
|
+
parts.push("\n## Custom Commands\n");
|
|
764
|
+
for (const [name, { spec }] of this.customCommands) {
|
|
765
|
+
const hint = spec.argumentHint ? ` ${spec.argumentHint}` : "";
|
|
766
|
+
parts.push(`/${name}${hint} \u2014 ${spec.summary}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return parts.join("\n");
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Register a custom slash command.
|
|
773
|
+
*/
|
|
774
|
+
registerCommand(spec, handler) {
|
|
775
|
+
this.customCommands.set(spec.name, { spec, handler });
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Unregister a custom slash command.
|
|
779
|
+
*/
|
|
780
|
+
unregisterCommand(name) {
|
|
781
|
+
return this.customCommands.delete(name);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Parse a user input string to check if it's a slash command.
|
|
785
|
+
* Returns the command name and arguments, or null if not a command.
|
|
786
|
+
*/
|
|
787
|
+
parseCommand(input) {
|
|
788
|
+
const trimmed = input.trim();
|
|
789
|
+
if (!trimmed.startsWith("/")) return null;
|
|
790
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
791
|
+
const name = spaceIdx === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIdx);
|
|
792
|
+
const args = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1).trim();
|
|
793
|
+
return { name, args };
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Execute a custom slash command by name.
|
|
797
|
+
* Returns the command output, or null if the command is not found
|
|
798
|
+
* in the custom registry (it may be a built-in Wasm command).
|
|
799
|
+
*/
|
|
800
|
+
async executeCustomCommand(name, args) {
|
|
801
|
+
const entry = this.customCommands.get(name);
|
|
802
|
+
if (!entry) return null;
|
|
803
|
+
return entry.handler(args);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Check if a command name exists (either built-in or custom).
|
|
807
|
+
*/
|
|
808
|
+
hasCommand(name) {
|
|
809
|
+
if (this.customCommands.has(name)) return true;
|
|
810
|
+
const specs = this.getCommandSpecs();
|
|
811
|
+
return specs.some((s) => s.name === name);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Get only commands that support resume (useful for session restoration).
|
|
815
|
+
*/
|
|
816
|
+
getResumeSupportedCommands() {
|
|
817
|
+
const builtIn = this.wasmRuntime ? this.wasmRuntime.resumeSupportedSlashCommands() : [];
|
|
818
|
+
const custom = Array.from(this.customCommands.values()).filter((c) => c.spec.resumeSupported).map((c) => c.spec);
|
|
819
|
+
return [...builtIn, ...custom];
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
var AgentUsageTracker = class {
|
|
823
|
+
turnUsages = [];
|
|
824
|
+
currentTurn = {
|
|
825
|
+
inputTokens: 0,
|
|
826
|
+
outputTokens: 0,
|
|
827
|
+
cacheCreationInputTokens: 0,
|
|
828
|
+
cacheReadInputTokens: 0
|
|
829
|
+
};
|
|
830
|
+
record(usage) {
|
|
831
|
+
this.currentTurn = {
|
|
832
|
+
inputTokens: this.currentTurn.inputTokens + usage.inputTokens,
|
|
833
|
+
outputTokens: this.currentTurn.outputTokens + usage.outputTokens,
|
|
834
|
+
cacheCreationInputTokens: this.currentTurn.cacheCreationInputTokens + usage.cacheCreationInputTokens,
|
|
835
|
+
cacheReadInputTokens: this.currentTurn.cacheReadInputTokens + usage.cacheReadInputTokens
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
currentTurnUsage() {
|
|
839
|
+
return { ...this.currentTurn };
|
|
840
|
+
}
|
|
841
|
+
cumulativeUsage() {
|
|
842
|
+
const cumulative = {
|
|
843
|
+
inputTokens: 0,
|
|
844
|
+
outputTokens: 0,
|
|
845
|
+
cacheCreationInputTokens: 0,
|
|
846
|
+
cacheReadInputTokens: 0
|
|
847
|
+
};
|
|
848
|
+
for (const usage of this.turnUsages) {
|
|
849
|
+
cumulative.inputTokens += usage.inputTokens;
|
|
850
|
+
cumulative.outputTokens += usage.outputTokens;
|
|
851
|
+
cumulative.cacheCreationInputTokens += usage.cacheCreationInputTokens;
|
|
852
|
+
cumulative.cacheReadInputTokens += usage.cacheReadInputTokens;
|
|
853
|
+
}
|
|
854
|
+
cumulative.inputTokens += this.currentTurn.inputTokens;
|
|
855
|
+
cumulative.outputTokens += this.currentTurn.outputTokens;
|
|
856
|
+
cumulative.cacheCreationInputTokens += this.currentTurn.cacheCreationInputTokens;
|
|
857
|
+
cumulative.cacheReadInputTokens += this.currentTurn.cacheReadInputTokens;
|
|
858
|
+
return cumulative;
|
|
859
|
+
}
|
|
860
|
+
turns() {
|
|
861
|
+
return this.turnUsages.length + (this.hasTurnActivity() ? 1 : 0);
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Finalize the current turn and start a new one.
|
|
865
|
+
* Call this at the end of each agent turn.
|
|
866
|
+
*/
|
|
867
|
+
endTurn() {
|
|
868
|
+
if (this.hasTurnActivity()) {
|
|
869
|
+
this.turnUsages.push({ ...this.currentTurn });
|
|
870
|
+
this.currentTurn = {
|
|
871
|
+
inputTokens: 0,
|
|
872
|
+
outputTokens: 0,
|
|
873
|
+
cacheCreationInputTokens: 0,
|
|
874
|
+
cacheReadInputTokens: 0
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/** Reset all usage data. */
|
|
879
|
+
reset() {
|
|
880
|
+
this.turnUsages = [];
|
|
881
|
+
this.currentTurn = {
|
|
882
|
+
inputTokens: 0,
|
|
883
|
+
outputTokens: 0,
|
|
884
|
+
cacheCreationInputTokens: 0,
|
|
885
|
+
cacheReadInputTokens: 0
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
hasTurnActivity() {
|
|
889
|
+
return this.currentTurn.inputTokens > 0 || this.currentTurn.outputTokens > 0;
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
function roleToString(role) {
|
|
893
|
+
switch (role) {
|
|
894
|
+
case MessageRole3.System:
|
|
895
|
+
return "system";
|
|
896
|
+
case MessageRole3.User:
|
|
897
|
+
return "user";
|
|
898
|
+
case MessageRole3.Assistant:
|
|
899
|
+
return "assistant";
|
|
900
|
+
case MessageRole3.Tool:
|
|
901
|
+
return "tool";
|
|
902
|
+
default:
|
|
903
|
+
return "unknown";
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function blocksToText3(blocks) {
|
|
907
|
+
return blocks.map((b) => {
|
|
908
|
+
switch (b.kind) {
|
|
909
|
+
case "text":
|
|
910
|
+
return b.text;
|
|
911
|
+
case "tool_use":
|
|
912
|
+
return `[Tool: ${b.name}(${b.input})]`;
|
|
913
|
+
case "tool_result":
|
|
914
|
+
return b.isError ? `[Error: ${b.toolName}: ${b.output}]` : `[Result: ${b.toolName}: ${b.output}]`;
|
|
915
|
+
default:
|
|
916
|
+
return "";
|
|
917
|
+
}
|
|
918
|
+
}).join("\n");
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/adapters/router-events.ts
|
|
922
|
+
var ROUTER_EVENTS = [
|
|
923
|
+
"route",
|
|
924
|
+
"fallback",
|
|
925
|
+
"circuit:open",
|
|
926
|
+
"circuit:close",
|
|
927
|
+
"circuit:half-open",
|
|
928
|
+
"request:complete",
|
|
929
|
+
"request:error"
|
|
930
|
+
];
|
|
931
|
+
var RouterEventBridge = class {
|
|
932
|
+
bus;
|
|
933
|
+
attachedRouters = /* @__PURE__ */ new Map();
|
|
934
|
+
constructor(bus) {
|
|
935
|
+
this.bus = bus;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Attach an LLMRouter and forward all its events to the agent bus.
|
|
939
|
+
*
|
|
940
|
+
* @param router - The LLMRouter to monitor
|
|
941
|
+
* @param label - A label to identify this router in events (e.g. "local", "remote")
|
|
942
|
+
*/
|
|
943
|
+
attach(router, label) {
|
|
944
|
+
this.detach(label);
|
|
945
|
+
const detachFns = [];
|
|
946
|
+
for (const eventName of ROUTER_EVENTS) {
|
|
947
|
+
const handler = (data) => {
|
|
948
|
+
this.bus.emit("router_event", {
|
|
949
|
+
routerLabel: label,
|
|
950
|
+
eventName,
|
|
951
|
+
data,
|
|
952
|
+
timestamp: Date.now()
|
|
953
|
+
});
|
|
954
|
+
};
|
|
955
|
+
router.events.on(eventName, handler);
|
|
956
|
+
detachFns.push(() => {
|
|
957
|
+
router.events.off(eventName, handler);
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
this.attachedRouters.set(label, { router, detachFns });
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Detach a previously attached router.
|
|
964
|
+
*/
|
|
965
|
+
detach(label) {
|
|
966
|
+
const entry = this.attachedRouters.get(label);
|
|
967
|
+
if (entry) {
|
|
968
|
+
for (const fn of entry.detachFns) {
|
|
969
|
+
fn();
|
|
970
|
+
}
|
|
971
|
+
this.attachedRouters.delete(label);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Detach all attached routers.
|
|
976
|
+
*/
|
|
977
|
+
detachAll() {
|
|
978
|
+
for (const label of this.attachedRouters.keys()) {
|
|
979
|
+
this.detach(label);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Get a health snapshot from all attached routers.
|
|
984
|
+
* Returns a map of router label -> endpoint name -> HealthState.
|
|
985
|
+
*/
|
|
986
|
+
getHealthSnapshot() {
|
|
987
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
988
|
+
for (const [label, { router }] of this.attachedRouters) {
|
|
989
|
+
snapshot.set(label, router.getHealthState());
|
|
990
|
+
}
|
|
991
|
+
return snapshot;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Check if any attached router has an endpoint in "open" (tripped) circuit state.
|
|
995
|
+
*/
|
|
996
|
+
hasTrippedCircuits() {
|
|
997
|
+
for (const [, { router }] of this.attachedRouters) {
|
|
998
|
+
for (const [, health] of router.getHealthState()) {
|
|
999
|
+
if (health.status === "open") return true;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Get the labels of all attached routers.
|
|
1006
|
+
*/
|
|
1007
|
+
get labels() {
|
|
1008
|
+
return Array.from(this.attachedRouters.keys());
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
function isLLMRouter(provider) {
|
|
1012
|
+
return typeof provider === "object" && provider !== null && "events" in provider && "getHealthState" in provider;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/adapters/usage-bridge.ts
|
|
1016
|
+
function asTokenUsageToLmscript(usage) {
|
|
1017
|
+
const promptTokens = usage.inputTokens;
|
|
1018
|
+
const completionTokens = usage.outputTokens;
|
|
1019
|
+
const totalTokens = usage.inputTokens + usage.outputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
|
|
1020
|
+
return { promptTokens, completionTokens, totalTokens };
|
|
1021
|
+
}
|
|
1022
|
+
function lmscriptToAsTokenUsage(usage) {
|
|
1023
|
+
return {
|
|
1024
|
+
inputTokens: usage.promptTokens,
|
|
1025
|
+
outputTokens: usage.completionTokens,
|
|
1026
|
+
cacheCreationInputTokens: 0,
|
|
1027
|
+
cacheReadInputTokens: 0
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function estimateCostFromAsAgent(usage, pricing) {
|
|
1031
|
+
const inputCostUsd = usage.inputTokens / 1e6 * pricing.inputCostPerMillion;
|
|
1032
|
+
const outputCostUsd = usage.outputTokens / 1e6 * pricing.outputCostPerMillion;
|
|
1033
|
+
const cacheCreationCostUsd = usage.cacheCreationInputTokens / 1e6 * pricing.cacheCreationCostPerMillion;
|
|
1034
|
+
const cacheReadCostUsd = usage.cacheReadInputTokens / 1e6 * pricing.cacheReadCostPerMillion;
|
|
1035
|
+
const totalCostUsd = inputCostUsd + outputCostUsd + cacheCreationCostUsd + cacheReadCostUsd;
|
|
1036
|
+
return {
|
|
1037
|
+
inputCostUsd,
|
|
1038
|
+
outputCostUsd,
|
|
1039
|
+
cacheCreationCostUsd,
|
|
1040
|
+
cacheReadCostUsd,
|
|
1041
|
+
totalCostUsd
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
var UsageBridge = class {
|
|
1045
|
+
constructor(asTracker, lmTracker) {
|
|
1046
|
+
this.asTracker = asTracker;
|
|
1047
|
+
this.lmTracker = lmTracker;
|
|
1048
|
+
}
|
|
1049
|
+
asTracker;
|
|
1050
|
+
lmTracker;
|
|
1051
|
+
/**
|
|
1052
|
+
* Record usage from an lmscript-format source (e.g. from the streaming path
|
|
1053
|
+
* or AgentLoop result).
|
|
1054
|
+
*
|
|
1055
|
+
* Converts to as-agent format and records in both trackers.
|
|
1056
|
+
*/
|
|
1057
|
+
recordFromLmscript(functionName, usage) {
|
|
1058
|
+
this.lmTracker?.trackUsage(functionName, usage);
|
|
1059
|
+
this.asTracker.record(lmscriptToAsTokenUsage(usage));
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Record usage from an as-agent-format source (e.g. from ConversationRuntime
|
|
1063
|
+
* or the Wasm runtime).
|
|
1064
|
+
*
|
|
1065
|
+
* Converts to lmscript format and records in both trackers.
|
|
1066
|
+
*/
|
|
1067
|
+
recordFromAsAgent(usage, functionName) {
|
|
1068
|
+
this.asTracker.record(usage);
|
|
1069
|
+
if (this.lmTracker) {
|
|
1070
|
+
this.lmTracker.trackUsage(
|
|
1071
|
+
functionName ?? "as-agent",
|
|
1072
|
+
asTokenUsageToLmscript(usage)
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* End the current turn in the as-agent tracker.
|
|
1078
|
+
*/
|
|
1079
|
+
endTurn() {
|
|
1080
|
+
this.asTracker.endTurn();
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Get unified cost summary combining both tracking systems.
|
|
1084
|
+
*/
|
|
1085
|
+
getCostSummary(lmPricing, asPricing) {
|
|
1086
|
+
const asUsage = this.asTracker.cumulativeUsage();
|
|
1087
|
+
const asTurns = this.asTracker.turns();
|
|
1088
|
+
let asCostEstimate;
|
|
1089
|
+
if (asPricing) {
|
|
1090
|
+
asCostEstimate = estimateCostFromAsAgent(asUsage, asPricing);
|
|
1091
|
+
}
|
|
1092
|
+
let lmTotalTokens;
|
|
1093
|
+
let lmTotalCost;
|
|
1094
|
+
let lmByFunction;
|
|
1095
|
+
if (this.lmTracker) {
|
|
1096
|
+
lmTotalTokens = this.lmTracker.getTotalTokens();
|
|
1097
|
+
lmTotalCost = this.lmTracker.getTotalCost(lmPricing);
|
|
1098
|
+
const usageMap = this.lmTracker.getUsageByFunction();
|
|
1099
|
+
lmByFunction = {};
|
|
1100
|
+
for (const [fnName, entry] of usageMap) {
|
|
1101
|
+
lmByFunction[fnName] = entry;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return {
|
|
1105
|
+
// as-agent view
|
|
1106
|
+
asAgent: {
|
|
1107
|
+
usage: asUsage,
|
|
1108
|
+
turns: asTurns,
|
|
1109
|
+
costEstimate: asCostEstimate
|
|
1110
|
+
},
|
|
1111
|
+
// lmscript view
|
|
1112
|
+
lmscript: this.lmTracker ? {
|
|
1113
|
+
totalTokens: lmTotalTokens,
|
|
1114
|
+
totalCost: lmTotalCost,
|
|
1115
|
+
byFunction: lmByFunction
|
|
1116
|
+
} : void 0
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Reset both tracking systems.
|
|
1121
|
+
*/
|
|
1122
|
+
reset() {
|
|
1123
|
+
this.asTracker.reset();
|
|
1124
|
+
this.lmTracker?.reset();
|
|
1125
|
+
}
|
|
1126
|
+
/** Get the as-agent usage tracker. */
|
|
1127
|
+
getAsTracker() {
|
|
1128
|
+
return this.asTracker;
|
|
1129
|
+
}
|
|
1130
|
+
/** Get the lmscript cost tracker (if available). */
|
|
1131
|
+
getLmTracker() {
|
|
1132
|
+
return this.lmTracker;
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
|
|
262
1136
|
// src/oboto-agent.ts
|
|
263
|
-
var ObotoAgent = class
|
|
1137
|
+
var ObotoAgent = class {
|
|
264
1138
|
bus = new AgentEventBus();
|
|
265
1139
|
localRuntime;
|
|
1140
|
+
remoteRuntime;
|
|
266
1141
|
localProvider;
|
|
267
1142
|
remoteProvider;
|
|
268
1143
|
contextManager;
|
|
@@ -275,12 +1150,54 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
275
1150
|
maxIterations;
|
|
276
1151
|
config;
|
|
277
1152
|
onToken;
|
|
1153
|
+
costTracker;
|
|
1154
|
+
modelPricing;
|
|
1155
|
+
rateLimiter;
|
|
1156
|
+
middleware;
|
|
1157
|
+
budget;
|
|
1158
|
+
conversationRAG;
|
|
1159
|
+
permissionGuard;
|
|
1160
|
+
sessionCompactor;
|
|
1161
|
+
hookIntegration;
|
|
1162
|
+
slashCommands;
|
|
1163
|
+
usageTracker;
|
|
1164
|
+
usageBridge;
|
|
1165
|
+
routerEventBridge;
|
|
278
1166
|
constructor(config) {
|
|
279
1167
|
this.config = config;
|
|
280
1168
|
this.localProvider = config.localModel;
|
|
281
1169
|
this.remoteProvider = config.remoteModel;
|
|
282
1170
|
const localLmscript = toLmscriptProvider(config.localModel, "local");
|
|
283
|
-
|
|
1171
|
+
const remoteLmscript = toLmscriptProvider(config.remoteModel, "remote");
|
|
1172
|
+
this.middleware = new MiddlewareManager();
|
|
1173
|
+
if (config.middleware) {
|
|
1174
|
+
for (const hooks of config.middleware) {
|
|
1175
|
+
this.middleware.use(hooks);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (config.modelPricing) {
|
|
1179
|
+
this.costTracker = new CostTracker();
|
|
1180
|
+
this.modelPricing = config.modelPricing;
|
|
1181
|
+
}
|
|
1182
|
+
const localCache = config.triageCacheTtlMs ? new ExecutionCache(new MemoryCacheBackend()) : void 0;
|
|
1183
|
+
this.localRuntime = new LScriptRuntime({
|
|
1184
|
+
provider: localLmscript,
|
|
1185
|
+
defaultTemperature: 0.1,
|
|
1186
|
+
cache: localCache,
|
|
1187
|
+
costTracker: this.costTracker
|
|
1188
|
+
});
|
|
1189
|
+
const remoteCache = config.cacheTtlMs ? new ExecutionCache(new MemoryCacheBackend()) : void 0;
|
|
1190
|
+
const rateLimiter = config.rateLimit ? new RateLimiter(config.rateLimit) : void 0;
|
|
1191
|
+
this.rateLimiter = rateLimiter;
|
|
1192
|
+
this.budget = config.budget;
|
|
1193
|
+
this.remoteRuntime = new LScriptRuntime({
|
|
1194
|
+
provider: remoteLmscript,
|
|
1195
|
+
middleware: this.middleware,
|
|
1196
|
+
cache: remoteCache,
|
|
1197
|
+
costTracker: this.costTracker,
|
|
1198
|
+
budget: config.budget,
|
|
1199
|
+
rateLimiter
|
|
1200
|
+
});
|
|
284
1201
|
this.session = config.session ?? createEmptySession();
|
|
285
1202
|
this.systemPrompt = config.systemPrompt ?? "You are a helpful AI assistant with access to tools.";
|
|
286
1203
|
this.maxIterations = config.maxIterations ?? 10;
|
|
@@ -290,12 +1207,55 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
290
1207
|
config.localModelName,
|
|
291
1208
|
config.maxContextTokens ?? 8192
|
|
292
1209
|
);
|
|
1210
|
+
if (config.toolMiddleware) {
|
|
1211
|
+
for (const mw of config.toolMiddleware) {
|
|
1212
|
+
config.router.use(mw);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
293
1215
|
this.routerTool = createRouterTool(config.router);
|
|
294
1216
|
this.triageFn = createTriageFunction(config.localModelName);
|
|
1217
|
+
if (config.embeddingProvider) {
|
|
1218
|
+
this.conversationRAG = new ConversationRAG(this.remoteRuntime, {
|
|
1219
|
+
embeddingProvider: config.embeddingProvider,
|
|
1220
|
+
vectorStore: config.vectorStore,
|
|
1221
|
+
topK: config.ragTopK,
|
|
1222
|
+
minScore: config.ragMinScore,
|
|
1223
|
+
embeddingModel: config.ragEmbeddingModel,
|
|
1224
|
+
autoIndex: config.ragAutoIndex,
|
|
1225
|
+
indexToolResults: config.ragIndexToolResults,
|
|
1226
|
+
formatContext: config.ragFormatContext
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
if (config.permissionPolicy) {
|
|
1230
|
+
this.permissionGuard = new PermissionGuard(
|
|
1231
|
+
config.permissionPolicy,
|
|
1232
|
+
config.permissionPrompter ?? null,
|
|
1233
|
+
this.bus
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (config.compactionConfig) {
|
|
1237
|
+
this.sessionCompactor = new SessionCompactor(this.bus, config.compactionConfig);
|
|
1238
|
+
}
|
|
1239
|
+
if (config.hookRunner) {
|
|
1240
|
+
this.hookIntegration = new HookIntegration(config.hookRunner, this.bus);
|
|
1241
|
+
}
|
|
1242
|
+
this.slashCommands = new SlashCommandRegistry(config.agentRuntime);
|
|
1243
|
+
this.usageTracker = new AgentUsageTracker();
|
|
1244
|
+
this.usageBridge = new UsageBridge(this.usageTracker, this.costTracker);
|
|
1245
|
+
this.routerEventBridge = new RouterEventBridge(this.bus);
|
|
1246
|
+
if (isLLMRouter(config.localModel)) {
|
|
1247
|
+
this.routerEventBridge.attach(config.localModel, "local");
|
|
1248
|
+
}
|
|
1249
|
+
if (isLLMRouter(config.remoteModel)) {
|
|
1250
|
+
this.routerEventBridge.attach(config.remoteModel, "remote");
|
|
1251
|
+
}
|
|
295
1252
|
this.contextManager.push({
|
|
296
1253
|
role: "system",
|
|
297
1254
|
content: this.systemPrompt
|
|
298
1255
|
});
|
|
1256
|
+
if (this.session.messages.length > 0) {
|
|
1257
|
+
this.contextManager.pushAll(sessionToHistory(this.session));
|
|
1258
|
+
}
|
|
299
1259
|
}
|
|
300
1260
|
// ── Public API ─────────────────────────────────────────────────────
|
|
301
1261
|
/** Subscribe to agent events. Returns an unsubscribe function. */
|
|
@@ -312,10 +1272,29 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
312
1272
|
this.interrupt(text);
|
|
313
1273
|
return;
|
|
314
1274
|
}
|
|
1275
|
+
const parsedCmd = this.slashCommands.parseCommand(text);
|
|
1276
|
+
if (parsedCmd) {
|
|
1277
|
+
const result = await this.slashCommands.executeCustomCommand(parsedCmd.name, parsedCmd.args);
|
|
1278
|
+
if (result !== null) {
|
|
1279
|
+
this.bus.emit("slash_command", {
|
|
1280
|
+
command: parsedCmd.name,
|
|
1281
|
+
args: parsedCmd.args,
|
|
1282
|
+
result
|
|
1283
|
+
});
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
315
1287
|
this.isProcessing = true;
|
|
316
1288
|
this.interrupted = false;
|
|
317
1289
|
try {
|
|
318
1290
|
await this.executionLoop(text);
|
|
1291
|
+
this.usageBridge.endTurn();
|
|
1292
|
+
if (this.sessionCompactor) {
|
|
1293
|
+
const result = this.sessionCompactor.compactIfNeeded(this.session);
|
|
1294
|
+
if (result && result.removedMessageCount > 0) {
|
|
1295
|
+
this.session = result.compactedSession;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
319
1298
|
} catch (err) {
|
|
320
1299
|
this.bus.emit("error", {
|
|
321
1300
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -329,16 +1308,15 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
329
1308
|
* Interrupt the current execution loop.
|
|
330
1309
|
* Optionally inject new directives into the context.
|
|
331
1310
|
*/
|
|
332
|
-
interrupt(newDirectives) {
|
|
1311
|
+
async interrupt(newDirectives) {
|
|
333
1312
|
this.interrupted = true;
|
|
334
1313
|
this.bus.emit("interruption", { newDirectives });
|
|
335
1314
|
if (newDirectives) {
|
|
336
1315
|
const msg = {
|
|
337
|
-
role:
|
|
1316
|
+
role: MessageRole4.User,
|
|
338
1317
|
blocks: [{ kind: "text", text: `[INTERRUPTION] ${newDirectives}` }]
|
|
339
1318
|
};
|
|
340
|
-
this.
|
|
341
|
-
this.contextManager.push(toChat(msg));
|
|
1319
|
+
await this.recordMessage(msg);
|
|
342
1320
|
this.bus.emit("state_updated", { reason: "interruption" });
|
|
343
1321
|
}
|
|
344
1322
|
}
|
|
@@ -350,20 +1328,136 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
350
1328
|
get processing() {
|
|
351
1329
|
return this.isProcessing;
|
|
352
1330
|
}
|
|
353
|
-
/**
|
|
1331
|
+
/** Get cost tracking summary (if cost tracking is enabled). */
|
|
1332
|
+
getCostSummary() {
|
|
1333
|
+
if (!this.costTracker) return void 0;
|
|
1334
|
+
const totalTokens = this.costTracker.getTotalTokens();
|
|
1335
|
+
const totalCost = this.costTracker.getTotalCost(this.modelPricing);
|
|
1336
|
+
const usageMap = this.costTracker.getUsageByFunction();
|
|
1337
|
+
const byFunction = {};
|
|
1338
|
+
for (const [fnName, entry] of usageMap) {
|
|
1339
|
+
byFunction[fnName] = entry;
|
|
1340
|
+
}
|
|
1341
|
+
return { totalCost, totalTokens, byFunction };
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Get unified cost summary combining both as-agent and lmscript tracking.
|
|
1345
|
+
* Uses the UsageBridge to provide a single view of all token/cost data.
|
|
1346
|
+
*/
|
|
1347
|
+
getUnifiedCostSummary(asPricing) {
|
|
1348
|
+
return this.usageBridge.getCostSummary(this.modelPricing, asPricing);
|
|
1349
|
+
}
|
|
1350
|
+
/** Get the usage bridge for direct access to unified tracking. */
|
|
1351
|
+
getUsageBridge() {
|
|
1352
|
+
return this.usageBridge;
|
|
1353
|
+
}
|
|
1354
|
+
/** Remove all event listeners and detach router event subscriptions. */
|
|
354
1355
|
removeAllListeners() {
|
|
355
1356
|
this.bus.removeAllListeners();
|
|
1357
|
+
this.routerEventBridge.detachAll();
|
|
1358
|
+
}
|
|
1359
|
+
/** Sync session and repopulate context manager (for reuse across turns). */
|
|
1360
|
+
async syncSession(session) {
|
|
1361
|
+
this.session = session;
|
|
1362
|
+
this.contextManager.clear();
|
|
1363
|
+
await this.contextManager.push({ role: "system", content: this.systemPrompt });
|
|
1364
|
+
if (session.messages.length > 0) {
|
|
1365
|
+
await this.contextManager.pushAll(sessionToHistory(session));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/** Update the streaming token callback between turns. */
|
|
1369
|
+
setOnToken(callback) {
|
|
1370
|
+
this.onToken = callback;
|
|
1371
|
+
}
|
|
1372
|
+
/** Get the ConversationRAG instance (if RAG is enabled). */
|
|
1373
|
+
getConversationRAG() {
|
|
1374
|
+
return this.conversationRAG;
|
|
1375
|
+
}
|
|
1376
|
+
/** Get the slash command registry. */
|
|
1377
|
+
getSlashCommands() {
|
|
1378
|
+
return this.slashCommands;
|
|
1379
|
+
}
|
|
1380
|
+
/** Get the as-agent usage tracker. */
|
|
1381
|
+
getUsageTracker() {
|
|
1382
|
+
return this.usageTracker;
|
|
1383
|
+
}
|
|
1384
|
+
/** Get the permission guard (if permissions are configured). */
|
|
1385
|
+
getPermissionGuard() {
|
|
1386
|
+
return this.permissionGuard;
|
|
1387
|
+
}
|
|
1388
|
+
/** Get the session compactor (if compaction is configured). */
|
|
1389
|
+
getSessionCompactor() {
|
|
1390
|
+
return this.sessionCompactor;
|
|
1391
|
+
}
|
|
1392
|
+
/** Get the router event bridge for observability into LLMRouter health. */
|
|
1393
|
+
getRouterEventBridge() {
|
|
1394
|
+
return this.routerEventBridge;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Manually compact the session. Returns null if compaction is not configured.
|
|
1398
|
+
*/
|
|
1399
|
+
compactSession() {
|
|
1400
|
+
if (!this.sessionCompactor) return null;
|
|
1401
|
+
const result = this.sessionCompactor.compact(this.session);
|
|
1402
|
+
if (result.removedMessageCount > 0) {
|
|
1403
|
+
this.session = result.compactedSession;
|
|
1404
|
+
}
|
|
1405
|
+
return result;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Retrieve relevant past context for a query via the RAG pipeline.
|
|
1409
|
+
* Returns undefined if RAG is not configured.
|
|
1410
|
+
*/
|
|
1411
|
+
async retrieveContext(query) {
|
|
1412
|
+
if (!this.conversationRAG) return void 0;
|
|
1413
|
+
const { context } = await this.conversationRAG.retrieve(query);
|
|
1414
|
+
return context || void 0;
|
|
356
1415
|
}
|
|
357
1416
|
// ── Internal ───────────────────────────────────────────────────────
|
|
1417
|
+
/**
|
|
1418
|
+
* Record a message in the session, context manager, and optionally RAG index.
|
|
1419
|
+
* Centralizes message recording to ensure RAG indexing stays in sync.
|
|
1420
|
+
*/
|
|
1421
|
+
async recordMessage(msg) {
|
|
1422
|
+
this.session.messages.push(msg);
|
|
1423
|
+
await this.contextManager.push(toChat(msg));
|
|
1424
|
+
if (this.conversationRAG) {
|
|
1425
|
+
this.conversationRAG.indexMessage(msg, this.session.messages.length - 1).catch((err) => {
|
|
1426
|
+
console.warn("[ObotoAgent] RAG indexing failed:", err instanceof Error ? err.message : err);
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Record a tool execution result in the RAG index.
|
|
1432
|
+
*/
|
|
1433
|
+
recordToolResult(command, kwargs, result) {
|
|
1434
|
+
if (this.conversationRAG) {
|
|
1435
|
+
this.conversationRAG.indexToolResult(command, kwargs, result).catch((err) => {
|
|
1436
|
+
console.warn("[ObotoAgent] RAG tool indexing failed:", err instanceof Error ? err.message : err);
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
358
1440
|
async executionLoop(userInput) {
|
|
359
1441
|
this.bus.emit("user_input", { text: userInput });
|
|
360
1442
|
const userMsg = {
|
|
361
|
-
role:
|
|
1443
|
+
role: MessageRole4.User,
|
|
362
1444
|
blocks: [{ kind: "text", text: userInput }]
|
|
363
1445
|
};
|
|
364
|
-
this.
|
|
365
|
-
await this.contextManager.push(toChat(userMsg));
|
|
1446
|
+
await this.recordMessage(userMsg);
|
|
366
1447
|
this.bus.emit("state_updated", { reason: "user_input" });
|
|
1448
|
+
if (this.conversationRAG) {
|
|
1449
|
+
try {
|
|
1450
|
+
const { context } = await this.conversationRAG.retrieve(userInput);
|
|
1451
|
+
if (context) {
|
|
1452
|
+
await this.contextManager.push({
|
|
1453
|
+
role: "system",
|
|
1454
|
+
content: context
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
} catch (err) {
|
|
1458
|
+
console.warn("[ObotoAgent] RAG retrieval failed:", err instanceof Error ? err.message : err);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
367
1461
|
const triageResult = await this.triage(userInput);
|
|
368
1462
|
this.bus.emit("triage_result", triageResult);
|
|
369
1463
|
if (this.interrupted) return;
|
|
@@ -371,17 +1465,14 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
371
1465
|
const response = triageResult.directResponse;
|
|
372
1466
|
this.bus.emit("agent_thought", { text: response, model: "local" });
|
|
373
1467
|
const assistantMsg = {
|
|
374
|
-
role:
|
|
1468
|
+
role: MessageRole4.Assistant,
|
|
375
1469
|
blocks: [{ kind: "text", text: response }]
|
|
376
1470
|
};
|
|
377
|
-
this.
|
|
378
|
-
await this.contextManager.push(toChat(assistantMsg));
|
|
1471
|
+
await this.recordMessage(assistantMsg);
|
|
379
1472
|
this.bus.emit("state_updated", { reason: "assistant_response" });
|
|
380
1473
|
this.bus.emit("turn_complete", { model: "local", escalated: false });
|
|
381
1474
|
return;
|
|
382
1475
|
}
|
|
383
|
-
const provider = triageResult.escalate ? this.remoteProvider : this.localProvider;
|
|
384
|
-
const modelName = triageResult.escalate ? this.config.remoteModelName : this.config.localModelName;
|
|
385
1476
|
if (triageResult.escalate) {
|
|
386
1477
|
this.bus.emit("agent_thought", {
|
|
387
1478
|
text: triageResult.reasoning,
|
|
@@ -389,8 +1480,14 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
389
1480
|
escalating: true
|
|
390
1481
|
});
|
|
391
1482
|
}
|
|
392
|
-
|
|
393
|
-
|
|
1483
|
+
const modelName = triageResult.escalate ? this.config.remoteModelName : this.config.localModelName;
|
|
1484
|
+
const runtime = triageResult.escalate ? this.remoteRuntime : this.localRuntime;
|
|
1485
|
+
console.log("[ObotoAgent] Executing with model:", modelName, "| via lmscript AgentLoop");
|
|
1486
|
+
if (this.onToken) {
|
|
1487
|
+
await this.executeWithStreaming(runtime, modelName, userInput);
|
|
1488
|
+
} else {
|
|
1489
|
+
await this.executeWithAgentLoop(runtime, modelName, userInput);
|
|
1490
|
+
}
|
|
394
1491
|
}
|
|
395
1492
|
async triage(userInput) {
|
|
396
1493
|
const recentMessages = this.contextManager.getMessages().slice(-5);
|
|
@@ -405,16 +1502,95 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
405
1502
|
});
|
|
406
1503
|
return result.data;
|
|
407
1504
|
}
|
|
408
|
-
/** Maximum characters per tool result before truncation. */
|
|
409
|
-
static MAX_TOOL_RESULT_CHARS = 8e3;
|
|
410
|
-
/** Maximum times the same tool+args can repeat before forcing a text response. */
|
|
411
|
-
static MAX_DUPLICATE_CALLS = 2;
|
|
412
1505
|
/**
|
|
413
|
-
* Execute
|
|
414
|
-
*
|
|
415
|
-
*
|
|
1506
|
+
* Execute using lmscript's AgentLoop for iterative tool calling.
|
|
1507
|
+
* This replaces the old custom tool loop with lmscript's battle-tested implementation.
|
|
1508
|
+
*
|
|
1509
|
+
* Benefits:
|
|
1510
|
+
* - Schema validation on final output
|
|
1511
|
+
* - Budget checking via CostTracker
|
|
1512
|
+
* - Rate limiting via RateLimiter
|
|
1513
|
+
* - Middleware lifecycle hooks
|
|
1514
|
+
* - Automatic retry with backoff
|
|
416
1515
|
*/
|
|
417
|
-
async
|
|
1516
|
+
async executeWithAgentLoop(runtime, modelName, userInput) {
|
|
1517
|
+
const { z: z5 } = await import("zod");
|
|
1518
|
+
const agentFn = {
|
|
1519
|
+
name: "agent-task",
|
|
1520
|
+
model: modelName,
|
|
1521
|
+
system: this.systemPrompt,
|
|
1522
|
+
prompt: (input) => {
|
|
1523
|
+
const contextMessages = this.contextManager.getMessages();
|
|
1524
|
+
const contextStr = contextMessages.filter((m) => m.role !== "system").map((m) => {
|
|
1525
|
+
const text = typeof m.content === "string" ? m.content : "[complex content]";
|
|
1526
|
+
return `${m.role}: ${text}`;
|
|
1527
|
+
}).join("\n");
|
|
1528
|
+
return contextStr ? `${contextStr}
|
|
1529
|
+
|
|
1530
|
+
user: ${input}` : input;
|
|
1531
|
+
},
|
|
1532
|
+
schema: z5.object({
|
|
1533
|
+
response: z5.string().describe("The assistant's response to the user"),
|
|
1534
|
+
reasoning: z5.string().optional().describe("Internal reasoning about the approach taken")
|
|
1535
|
+
}),
|
|
1536
|
+
tools: [this.routerTool],
|
|
1537
|
+
temperature: 0.7,
|
|
1538
|
+
maxRetries: 1
|
|
1539
|
+
};
|
|
1540
|
+
const agentConfig = {
|
|
1541
|
+
maxIterations: this.maxIterations,
|
|
1542
|
+
onToolCall: (tc) => {
|
|
1543
|
+
const command = typeof tc.arguments === "object" && tc.arguments !== null ? tc.arguments.command ?? tc.name : tc.name;
|
|
1544
|
+
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
|
+
const resultStr = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
|
|
1551
|
+
this.recordToolResult(String(command), kwargs, resultStr);
|
|
1552
|
+
},
|
|
1553
|
+
onIteration: (iteration, response) => {
|
|
1554
|
+
this.bus.emit("agent_thought", {
|
|
1555
|
+
text: response,
|
|
1556
|
+
model: modelName,
|
|
1557
|
+
iteration
|
|
1558
|
+
});
|
|
1559
|
+
if (this.interrupted) return false;
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
const agentLoop = new AgentLoop(runtime, agentConfig);
|
|
1563
|
+
const result = await agentLoop.run(agentFn, userInput);
|
|
1564
|
+
const responseText = result.data.response;
|
|
1565
|
+
const assistantMsg = {
|
|
1566
|
+
role: MessageRole4.Assistant,
|
|
1567
|
+
blocks: [{ kind: "text", text: responseText }]
|
|
1568
|
+
};
|
|
1569
|
+
await this.recordMessage(assistantMsg);
|
|
1570
|
+
this.bus.emit("state_updated", { reason: "assistant_response" });
|
|
1571
|
+
this.bus.emit("turn_complete", {
|
|
1572
|
+
model: modelName,
|
|
1573
|
+
escalated: true,
|
|
1574
|
+
iterations: result.iterations,
|
|
1575
|
+
toolCalls: result.toolCalls.length,
|
|
1576
|
+
usage: result.usage
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Execute with streaming token emission.
|
|
1581
|
+
* Uses the raw llm-wrapper provider for streaming, combined with
|
|
1582
|
+
* manual tool calling (since streaming + structured agent loops are complex).
|
|
1583
|
+
*
|
|
1584
|
+
* This preserves real-time token delivery while still leveraging
|
|
1585
|
+
* the lmscript infrastructure:
|
|
1586
|
+
* - Rate limiting (acquire/reportTokens per call)
|
|
1587
|
+
* - Cost tracking (trackUsage per call)
|
|
1588
|
+
* - Budget checking (checkBudget before each call)
|
|
1589
|
+
* - Middleware lifecycle hooks (onBeforeExecute/onComplete per turn)
|
|
1590
|
+
*/
|
|
1591
|
+
async executeWithStreaming(_runtime, modelName, _userInput) {
|
|
1592
|
+
const { zodToJsonSchema } = await import("zod-to-json-schema");
|
|
1593
|
+
const provider = modelName === this.config.remoteModelName ? this.remoteProvider : this.localProvider;
|
|
418
1594
|
const contextMessages = this.contextManager.getMessages();
|
|
419
1595
|
const messages = contextMessages.map((m) => ({
|
|
420
1596
|
role: m.role,
|
|
@@ -434,141 +1610,205 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
434
1610
|
];
|
|
435
1611
|
let totalToolCalls = 0;
|
|
436
1612
|
const callHistory = [];
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
};
|
|
453
|
-
let response;
|
|
454
|
-
try {
|
|
455
|
-
if (useStreaming) {
|
|
456
|
-
response = await this.streamAndAggregate(provider, params);
|
|
457
|
-
} else {
|
|
458
|
-
response = await provider.chat(params);
|
|
1613
|
+
const turnStartTime = Date.now();
|
|
1614
|
+
const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1615
|
+
const syntheticCtx = {
|
|
1616
|
+
fn: { name: "streaming-turn", model: modelName, system: this.systemPrompt, prompt: () => "", schema: {} },
|
|
1617
|
+
input: _userInput,
|
|
1618
|
+
messages,
|
|
1619
|
+
attempt: 1,
|
|
1620
|
+
startTime: turnStartTime
|
|
1621
|
+
};
|
|
1622
|
+
await this.middleware.runBeforeExecute(syntheticCtx);
|
|
1623
|
+
try {
|
|
1624
|
+
for (let iteration = 1; iteration <= this.maxIterations; iteration++) {
|
|
1625
|
+
if (this.interrupted) break;
|
|
1626
|
+
if (this.costTracker && this.budget) {
|
|
1627
|
+
this.costTracker.checkBudget(this.budget);
|
|
459
1628
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
} else if (!content && (!toolCalls || toolCalls.length === 0)) {
|
|
470
|
-
console.warn("[ObotoAgent] Empty response \u2014 no content, no tool_calls. finish_reason:", choice.finish_reason);
|
|
471
|
-
console.warn("[ObotoAgent] Messages sent:", messages.length, "| Model:", modelName);
|
|
472
|
-
console.warn("[ObotoAgent] Tool schema:", JSON.stringify(tools[0]?.function?.parameters).substring(0, 300));
|
|
473
|
-
}
|
|
474
|
-
if (content) {
|
|
475
|
-
this.bus.emit("agent_thought", {
|
|
476
|
-
text: content,
|
|
1629
|
+
await this.rateLimiter?.acquire();
|
|
1630
|
+
const isLastIteration = iteration === this.maxIterations;
|
|
1631
|
+
if (isLastIteration) {
|
|
1632
|
+
messages.push({
|
|
1633
|
+
role: "user",
|
|
1634
|
+
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."
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
const params = {
|
|
477
1638
|
model: modelName,
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (!toolCalls || toolCalls.length === 0) {
|
|
482
|
-
const assistantMsg = {
|
|
483
|
-
role: MessageRole2.Assistant,
|
|
484
|
-
blocks: [{ kind: "text", text: content }]
|
|
1639
|
+
messages: [...messages],
|
|
1640
|
+
temperature: 0.7,
|
|
1641
|
+
...isLastIteration ? {} : { tools, tool_choice: "auto" }
|
|
485
1642
|
};
|
|
486
|
-
|
|
487
|
-
await this.contextManager.push(toChat(assistantMsg));
|
|
488
|
-
this.bus.emit("state_updated", { reason: "assistant_response" });
|
|
489
|
-
this.bus.emit("turn_complete", {
|
|
490
|
-
model: modelName,
|
|
491
|
-
escalated: true,
|
|
492
|
-
iterations: iteration,
|
|
493
|
-
toolCalls: totalToolCalls
|
|
494
|
-
});
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
messages.push({
|
|
498
|
-
role: "assistant",
|
|
499
|
-
content: content || null,
|
|
500
|
-
tool_calls: toolCalls
|
|
501
|
-
});
|
|
502
|
-
const toolResults = [];
|
|
503
|
-
for (const tc of toolCalls) {
|
|
504
|
-
if (this.interrupted) break;
|
|
505
|
-
let args;
|
|
1643
|
+
let response;
|
|
506
1644
|
try {
|
|
507
|
-
|
|
508
|
-
} catch {
|
|
509
|
-
|
|
1645
|
+
response = await this.streamAndAggregate(provider, params);
|
|
1646
|
+
} catch (err) {
|
|
1647
|
+
await this.middleware.runError(
|
|
1648
|
+
syntheticCtx,
|
|
1649
|
+
err instanceof Error ? err : new Error(String(err))
|
|
1650
|
+
);
|
|
1651
|
+
console.error("[ObotoAgent] LLM call failed:", err instanceof Error ? err.message : err);
|
|
1652
|
+
throw err;
|
|
510
1653
|
}
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
1654
|
+
const usage = response?.usage;
|
|
1655
|
+
if (usage) {
|
|
1656
|
+
const promptTokens = usage.prompt_tokens ?? 0;
|
|
1657
|
+
const completionTokens = usage.completion_tokens ?? 0;
|
|
1658
|
+
const usageTotal = usage.total_tokens ?? promptTokens + completionTokens;
|
|
1659
|
+
totalUsage.promptTokens += promptTokens;
|
|
1660
|
+
totalUsage.completionTokens += completionTokens;
|
|
1661
|
+
totalUsage.totalTokens += usageTotal;
|
|
1662
|
+
this.rateLimiter?.reportTokens(usageTotal);
|
|
1663
|
+
this.usageBridge.recordFromLmscript(modelName, {
|
|
1664
|
+
promptTokens,
|
|
1665
|
+
completionTokens,
|
|
1666
|
+
totalTokens: usageTotal
|
|
521
1667
|
});
|
|
522
|
-
this.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
const choice = response?.choices?.[0];
|
|
1677
|
+
const content = choice?.message?.content ?? "";
|
|
1678
|
+
const toolCalls = choice?.message?.tool_calls;
|
|
1679
|
+
if (content) {
|
|
1680
|
+
this.bus.emit("agent_thought", {
|
|
1681
|
+
text: content,
|
|
1682
|
+
model: modelName,
|
|
1683
|
+
iteration
|
|
526
1684
|
});
|
|
527
|
-
toolResults.push({ command, success: false });
|
|
528
|
-
totalToolCalls++;
|
|
529
|
-
continue;
|
|
530
1685
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1686
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
1687
|
+
const assistantMsg = {
|
|
1688
|
+
role: MessageRole4.Assistant,
|
|
1689
|
+
blocks: [{ kind: "text", text: content }]
|
|
1690
|
+
};
|
|
1691
|
+
await this.recordMessage(assistantMsg);
|
|
1692
|
+
this.bus.emit("state_updated", { reason: "assistant_response" });
|
|
1693
|
+
this.bus.emit("turn_complete", {
|
|
1694
|
+
model: modelName,
|
|
1695
|
+
escalated: true,
|
|
1696
|
+
iterations: iteration,
|
|
1697
|
+
toolCalls: totalToolCalls,
|
|
1698
|
+
usage: totalUsage
|
|
1699
|
+
});
|
|
1700
|
+
await this.middleware.runComplete(syntheticCtx, {
|
|
1701
|
+
data: content,
|
|
1702
|
+
raw: content,
|
|
1703
|
+
usage: totalUsage
|
|
1704
|
+
});
|
|
1705
|
+
return;
|
|
539
1706
|
}
|
|
540
|
-
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
541
|
-
const truncated = resultStr.length > _ObotoAgent.MAX_TOOL_RESULT_CHARS ? resultStr.slice(0, _ObotoAgent.MAX_TOOL_RESULT_CHARS) + `
|
|
542
|
-
|
|
543
|
-
[... truncated ${resultStr.length - _ObotoAgent.MAX_TOOL_RESULT_CHARS} characters. Use the data above to proceed.]` : resultStr;
|
|
544
|
-
this.bus.emit("tool_execution_complete", { command, kwargs, result: truncated });
|
|
545
|
-
toolResults.push({ command, success });
|
|
546
|
-
totalToolCalls++;
|
|
547
1707
|
messages.push({
|
|
548
|
-
role: "
|
|
549
|
-
|
|
550
|
-
|
|
1708
|
+
role: "assistant",
|
|
1709
|
+
content: content || null,
|
|
1710
|
+
tool_calls: toolCalls
|
|
551
1711
|
});
|
|
1712
|
+
for (const tc of toolCalls) {
|
|
1713
|
+
if (this.interrupted) break;
|
|
1714
|
+
let args;
|
|
1715
|
+
try {
|
|
1716
|
+
args = JSON.parse(tc.function.arguments);
|
|
1717
|
+
} catch {
|
|
1718
|
+
args = {};
|
|
1719
|
+
}
|
|
1720
|
+
const command = args.command ?? tc.function.name;
|
|
1721
|
+
const kwargs = args.kwargs ?? {};
|
|
1722
|
+
const toolInputStr = JSON.stringify({ command, kwargs });
|
|
1723
|
+
if (this.permissionGuard) {
|
|
1724
|
+
const outcome = this.permissionGuard.checkPermission(command, toolInputStr);
|
|
1725
|
+
if (outcome.kind === "deny") {
|
|
1726
|
+
messages.push({
|
|
1727
|
+
role: "tool",
|
|
1728
|
+
tool_call_id: tc.id,
|
|
1729
|
+
content: `Permission denied for tool "${command}": ${outcome.reason ?? "denied by policy"}`
|
|
1730
|
+
});
|
|
1731
|
+
totalToolCalls++;
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (this.hookIntegration) {
|
|
1736
|
+
const hookResult = this.hookIntegration.runPreToolUse(command, toolInputStr);
|
|
1737
|
+
if (hookResult.denied) {
|
|
1738
|
+
messages.push({
|
|
1739
|
+
role: "tool",
|
|
1740
|
+
tool_call_id: tc.id,
|
|
1741
|
+
content: `Tool "${command}" blocked by pre-use hook: ${hookResult.messages.join("; ")}`
|
|
1742
|
+
});
|
|
1743
|
+
totalToolCalls++;
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
const callSig = JSON.stringify({ command, kwargs });
|
|
1748
|
+
const dupeCount = callHistory.filter((s) => s === callSig).length;
|
|
1749
|
+
callHistory.push(callSig);
|
|
1750
|
+
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
|
+
});
|
|
1756
|
+
totalToolCalls++;
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
this.bus.emit("tool_execution_start", { command, kwargs });
|
|
1760
|
+
let result;
|
|
1761
|
+
let isError = false;
|
|
1762
|
+
try {
|
|
1763
|
+
result = await tool.execute(args);
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
result = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1766
|
+
isError = true;
|
|
1767
|
+
}
|
|
1768
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
1769
|
+
const truncated = resultStr.length > 8e3 ? resultStr.slice(0, 8e3) + `
|
|
1770
|
+
|
|
1771
|
+
[... truncated ${resultStr.length - 8e3} characters.]` : resultStr;
|
|
1772
|
+
if (this.hookIntegration) {
|
|
1773
|
+
this.hookIntegration.runPostToolUse(command, toolInputStr, truncated, isError);
|
|
1774
|
+
}
|
|
1775
|
+
this.bus.emit("tool_execution_complete", { command, kwargs, result: truncated });
|
|
1776
|
+
this.recordToolResult(command, kwargs, truncated);
|
|
1777
|
+
totalToolCalls++;
|
|
1778
|
+
messages.push({
|
|
1779
|
+
role: "tool",
|
|
1780
|
+
tool_call_id: tc.id,
|
|
1781
|
+
content: truncated
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
552
1784
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
1785
|
+
const fallbackMsg = {
|
|
1786
|
+
role: MessageRole4.Assistant,
|
|
1787
|
+
blocks: [{ kind: "text", text: "I reached the maximum number of iterations. Here is what I have so far." }]
|
|
1788
|
+
};
|
|
1789
|
+
await this.recordMessage(fallbackMsg);
|
|
1790
|
+
this.bus.emit("state_updated", { reason: "max_iterations" });
|
|
1791
|
+
this.bus.emit("turn_complete", {
|
|
1792
|
+
model: modelName,
|
|
1793
|
+
escalated: true,
|
|
1794
|
+
iterations: this.maxIterations,
|
|
1795
|
+
toolCalls: totalToolCalls,
|
|
1796
|
+
usage: totalUsage
|
|
1797
|
+
});
|
|
1798
|
+
await this.middleware.runComplete(syntheticCtx, {
|
|
1799
|
+
data: "max_iterations_reached",
|
|
1800
|
+
raw: "",
|
|
1801
|
+
usage: totalUsage
|
|
557
1802
|
});
|
|
1803
|
+
} catch (err) {
|
|
1804
|
+
if (!(err instanceof Error && err.message.includes("LLM call failed"))) {
|
|
1805
|
+
await this.middleware.runError(
|
|
1806
|
+
syntheticCtx,
|
|
1807
|
+
err instanceof Error ? err : new Error(String(err))
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
throw err;
|
|
558
1811
|
}
|
|
559
|
-
const fallbackMsg = {
|
|
560
|
-
role: MessageRole2.Assistant,
|
|
561
|
-
blocks: [{ kind: "text", text: "I reached the maximum number of iterations. Here is what I have so far." }]
|
|
562
|
-
};
|
|
563
|
-
this.session.messages.push(fallbackMsg);
|
|
564
|
-
await this.contextManager.push(toChat(fallbackMsg));
|
|
565
|
-
this.bus.emit("state_updated", { reason: "max_iterations" });
|
|
566
|
-
this.bus.emit("turn_complete", {
|
|
567
|
-
model: modelName,
|
|
568
|
-
escalated: true,
|
|
569
|
-
iterations: this.maxIterations,
|
|
570
|
-
toolCalls: totalToolCalls
|
|
571
|
-
});
|
|
572
1812
|
}
|
|
573
1813
|
/**
|
|
574
1814
|
* Stream an LLM call, emitting tokens in real-time, then aggregate into
|
|
@@ -591,15 +1831,316 @@ var ObotoAgent = class _ObotoAgent {
|
|
|
591
1831
|
return aggregateStream(replay());
|
|
592
1832
|
}
|
|
593
1833
|
};
|
|
1834
|
+
|
|
1835
|
+
// src/adapters/tool-extensions.ts
|
|
1836
|
+
import {
|
|
1837
|
+
DynamicBranchNode,
|
|
1838
|
+
LeafNode,
|
|
1839
|
+
TreeBuilder,
|
|
1840
|
+
createMemoryModule
|
|
1841
|
+
} from "@sschepis/swiss-army-tool";
|
|
1842
|
+
var AgentDynamicTools = class extends DynamicBranchNode {
|
|
1843
|
+
provider;
|
|
1844
|
+
constructor(config) {
|
|
1845
|
+
super({
|
|
1846
|
+
name: config.name,
|
|
1847
|
+
description: config.description,
|
|
1848
|
+
ttlMs: config.ttlMs ?? 6e4
|
|
1849
|
+
});
|
|
1850
|
+
this.provider = config.provider;
|
|
1851
|
+
}
|
|
1852
|
+
async refresh() {
|
|
1853
|
+
const entries = await this.provider.discover();
|
|
1854
|
+
for (const entry of entries) {
|
|
1855
|
+
this.addChild(
|
|
1856
|
+
new LeafNode({
|
|
1857
|
+
name: entry.name,
|
|
1858
|
+
description: entry.description,
|
|
1859
|
+
requiredArgs: entry.requiredArgs,
|
|
1860
|
+
optionalArgs: entry.optionalArgs,
|
|
1861
|
+
handler: entry.handler
|
|
1862
|
+
}),
|
|
1863
|
+
{ overwrite: true }
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
/** Manually register a tool entry without waiting for refresh. */
|
|
1868
|
+
registerTool(entry) {
|
|
1869
|
+
this.addChild(
|
|
1870
|
+
new LeafNode({
|
|
1871
|
+
name: entry.name,
|
|
1872
|
+
description: entry.description,
|
|
1873
|
+
requiredArgs: entry.requiredArgs,
|
|
1874
|
+
optionalArgs: entry.optionalArgs,
|
|
1875
|
+
handler: entry.handler
|
|
1876
|
+
}),
|
|
1877
|
+
{ overwrite: true }
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
/** Remove a dynamically registered tool by name. */
|
|
1881
|
+
unregisterTool(name) {
|
|
1882
|
+
return this.removeChild(name);
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
function createToolTimingMiddleware(bus) {
|
|
1886
|
+
return async (ctx, next) => {
|
|
1887
|
+
const startTime = Date.now();
|
|
1888
|
+
bus.emit("tool_execution_start", {
|
|
1889
|
+
command: ctx.command,
|
|
1890
|
+
kwargs: ctx.kwargs,
|
|
1891
|
+
resolvedPath: ctx.resolvedPath
|
|
1892
|
+
});
|
|
1893
|
+
try {
|
|
1894
|
+
const result = await next();
|
|
1895
|
+
const durationMs = Date.now() - startTime;
|
|
1896
|
+
bus.emit("tool_execution_complete", {
|
|
1897
|
+
command: ctx.command,
|
|
1898
|
+
kwargs: ctx.kwargs,
|
|
1899
|
+
result: result.length > 500 ? result.slice(0, 500) + "..." : result,
|
|
1900
|
+
durationMs
|
|
1901
|
+
});
|
|
1902
|
+
return result;
|
|
1903
|
+
} catch (err) {
|
|
1904
|
+
const durationMs = Date.now() - startTime;
|
|
1905
|
+
bus.emit("error", {
|
|
1906
|
+
message: `Tool execution failed: ${ctx.command}`,
|
|
1907
|
+
error: err,
|
|
1908
|
+
durationMs
|
|
1909
|
+
});
|
|
1910
|
+
throw err;
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
function createToolTimeoutMiddleware(maxMs) {
|
|
1915
|
+
return async (_ctx, next) => {
|
|
1916
|
+
const result = await Promise.race([
|
|
1917
|
+
next(),
|
|
1918
|
+
new Promise(
|
|
1919
|
+
(_, reject) => setTimeout(
|
|
1920
|
+
() => reject(new Error(`Tool execution timed out after ${maxMs}ms`)),
|
|
1921
|
+
maxMs
|
|
1922
|
+
)
|
|
1923
|
+
)
|
|
1924
|
+
]);
|
|
1925
|
+
return result;
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function createToolAuditMiddleware(session) {
|
|
1929
|
+
let callIndex = 0;
|
|
1930
|
+
return async (ctx, next) => {
|
|
1931
|
+
const idx = ++callIndex;
|
|
1932
|
+
const startTime = Date.now();
|
|
1933
|
+
const result = await next();
|
|
1934
|
+
const durationMs = Date.now() - startTime;
|
|
1935
|
+
const entry = JSON.stringify({
|
|
1936
|
+
command: ctx.command,
|
|
1937
|
+
kwargs: ctx.kwargs,
|
|
1938
|
+
resultLength: result.length,
|
|
1939
|
+
durationMs,
|
|
1940
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1941
|
+
});
|
|
1942
|
+
session.kvStore.set(`_audit:${idx}`, entry);
|
|
1943
|
+
return result;
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function createAgentToolTree(config) {
|
|
1947
|
+
const builder = TreeBuilder.create("root", "Agent command tree with integrated tools");
|
|
1948
|
+
if (config.includeMemory !== false) {
|
|
1949
|
+
const memoryBranch = createMemoryModule(config.session);
|
|
1950
|
+
builder.addBranch(memoryBranch);
|
|
1951
|
+
}
|
|
1952
|
+
const root = builder.build();
|
|
1953
|
+
if (config.dynamicProviders) {
|
|
1954
|
+
for (const dp of config.dynamicProviders) {
|
|
1955
|
+
const dynamicBranch = new AgentDynamicTools({
|
|
1956
|
+
name: dp.name,
|
|
1957
|
+
description: dp.description,
|
|
1958
|
+
provider: dp.provider,
|
|
1959
|
+
ttlMs: dp.ttlMs
|
|
1960
|
+
});
|
|
1961
|
+
root.addChild(dynamicBranch);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
if (config.staticBranches) {
|
|
1965
|
+
for (const branch of config.staticBranches) {
|
|
1966
|
+
root.addChild(branch);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return root;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/adapters/pipeline-workflows.ts
|
|
1973
|
+
import { z as z4 } from "zod";
|
|
1974
|
+
import {
|
|
1975
|
+
Pipeline
|
|
1976
|
+
} from "@sschepis/lmscript";
|
|
1977
|
+
var TriagePipelineSchema = z4.object({
|
|
1978
|
+
intent: z4.string().describe("The classified intent of the user input"),
|
|
1979
|
+
complexity: z4.enum(["simple", "moderate", "complex"]).describe("Estimated task complexity"),
|
|
1980
|
+
requiresTools: z4.boolean().describe("Whether tools are needed"),
|
|
1981
|
+
suggestedApproach: z4.string().describe("Brief approach suggestion"),
|
|
1982
|
+
escalate: z4.boolean().describe("Whether to escalate to the remote model")
|
|
1983
|
+
});
|
|
1984
|
+
var PlanSchema = z4.object({
|
|
1985
|
+
steps: z4.array(z4.object({
|
|
1986
|
+
description: z4.string(),
|
|
1987
|
+
toolRequired: z4.string().optional(),
|
|
1988
|
+
expectedOutput: z4.string().optional()
|
|
1989
|
+
})).describe("Ordered list of steps to accomplish the task"),
|
|
1990
|
+
estimatedComplexity: z4.enum(["low", "medium", "high"]),
|
|
1991
|
+
reasoning: z4.string().describe("Why this plan was chosen")
|
|
1992
|
+
});
|
|
1993
|
+
var ExecutionSchema = z4.object({
|
|
1994
|
+
response: z4.string().describe("The response or result of executing the plan"),
|
|
1995
|
+
stepsCompleted: z4.number().describe("Number of plan steps completed"),
|
|
1996
|
+
toolsUsed: z4.array(z4.string()).optional(),
|
|
1997
|
+
confidence: z4.enum(["low", "medium", "high"])
|
|
1998
|
+
});
|
|
1999
|
+
var SummarySchema2 = z4.object({
|
|
2000
|
+
summary: z4.string().describe("Concise summary of what was accomplished"),
|
|
2001
|
+
keyPoints: z4.array(z4.string()).describe("Key points from the execution"),
|
|
2002
|
+
followUpSuggestions: z4.array(z4.string()).optional()
|
|
2003
|
+
});
|
|
2004
|
+
function createTriageStep(modelName, systemPrompt) {
|
|
2005
|
+
return {
|
|
2006
|
+
name: "pipeline-triage",
|
|
2007
|
+
model: modelName,
|
|
2008
|
+
system: systemPrompt ?? "You are a task classifier. Analyze the input and determine its intent, complexity, and whether it requires tools or escalation.",
|
|
2009
|
+
prompt: (input) => `Classify this request:
|
|
2010
|
+
|
|
2011
|
+
${input}`,
|
|
2012
|
+
schema: TriagePipelineSchema,
|
|
2013
|
+
temperature: 0.1,
|
|
2014
|
+
maxRetries: 1
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
function createPlanStep(modelName, systemPrompt) {
|
|
2018
|
+
return {
|
|
2019
|
+
name: "pipeline-plan",
|
|
2020
|
+
model: modelName,
|
|
2021
|
+
system: systemPrompt ?? "You are a task planner. Given the triage analysis, create a step-by-step plan to accomplish the task.",
|
|
2022
|
+
prompt: (triage) => `Create an execution plan based on this analysis:
|
|
2023
|
+
Intent: ${triage.intent}
|
|
2024
|
+
Complexity: ${triage.complexity}
|
|
2025
|
+
Requires tools: ${triage.requiresTools}
|
|
2026
|
+
Suggested approach: ${triage.suggestedApproach}`,
|
|
2027
|
+
schema: PlanSchema,
|
|
2028
|
+
temperature: 0.3,
|
|
2029
|
+
maxRetries: 1
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function createExecutionStep(modelName, tools, systemPrompt) {
|
|
2033
|
+
return {
|
|
2034
|
+
name: "pipeline-execute",
|
|
2035
|
+
model: modelName,
|
|
2036
|
+
system: systemPrompt ?? "You are a task executor. Follow the given plan step by step and produce the result.",
|
|
2037
|
+
prompt: (plan) => {
|
|
2038
|
+
const stepsStr = plan.steps.map((s, i) => `${i + 1}. ${s.description}${s.toolRequired ? ` (tool: ${s.toolRequired})` : ""}`).join("\n");
|
|
2039
|
+
return `Execute this plan:
|
|
2040
|
+
|
|
2041
|
+
${stepsStr}
|
|
2042
|
+
|
|
2043
|
+
Reasoning: ${plan.reasoning}`;
|
|
2044
|
+
},
|
|
2045
|
+
schema: ExecutionSchema,
|
|
2046
|
+
tools,
|
|
2047
|
+
temperature: 0.5,
|
|
2048
|
+
maxRetries: 1
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
function createSummaryStep(modelName, systemPrompt) {
|
|
2052
|
+
return {
|
|
2053
|
+
name: "pipeline-summarize",
|
|
2054
|
+
model: modelName,
|
|
2055
|
+
system: systemPrompt ?? "You are a summarizer. Condense the execution results into a clear, concise summary.",
|
|
2056
|
+
prompt: (execution) => `Summarize these results:
|
|
2057
|
+
Response: ${execution.response}
|
|
2058
|
+
Steps completed: ${execution.stepsCompleted}
|
|
2059
|
+
Confidence: ${execution.confidence}
|
|
2060
|
+
` + (execution.toolsUsed?.length ? `Tools used: ${execution.toolsUsed.join(", ")}` : ""),
|
|
2061
|
+
schema: SummarySchema2,
|
|
2062
|
+
temperature: 0.3,
|
|
2063
|
+
maxRetries: 1
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
function createTriagePlanExecutePipeline(modelName, tools) {
|
|
2067
|
+
return Pipeline.from(createTriageStep(modelName)).pipe(createPlanStep(modelName)).pipe(createExecutionStep(modelName, tools));
|
|
2068
|
+
}
|
|
2069
|
+
function createFullPipeline(modelName, tools) {
|
|
2070
|
+
return Pipeline.from(createTriageStep(modelName)).pipe(createPlanStep(modelName)).pipe(createExecutionStep(modelName, tools)).pipe(createSummaryStep(modelName));
|
|
2071
|
+
}
|
|
2072
|
+
function createAnalyzeRespondPipeline(modelName) {
|
|
2073
|
+
const analyzeStep = {
|
|
2074
|
+
name: "pipeline-analyze",
|
|
2075
|
+
model: modelName,
|
|
2076
|
+
system: "You are an analyst. Understand the request and identify the best approach.",
|
|
2077
|
+
prompt: (input) => `Analyze this request:
|
|
2078
|
+
|
|
2079
|
+
${input}`,
|
|
2080
|
+
schema: TriagePipelineSchema,
|
|
2081
|
+
temperature: 0.1,
|
|
2082
|
+
maxRetries: 1
|
|
2083
|
+
};
|
|
2084
|
+
const respondStep = {
|
|
2085
|
+
name: "pipeline-respond",
|
|
2086
|
+
model: modelName,
|
|
2087
|
+
system: "You are a helpful assistant. Based on the analysis, provide a comprehensive response.",
|
|
2088
|
+
prompt: (analysis) => `Respond to this request:
|
|
2089
|
+
Intent: ${analysis.intent}
|
|
2090
|
+
Approach: ${analysis.suggestedApproach}`,
|
|
2091
|
+
schema: ExecutionSchema,
|
|
2092
|
+
temperature: 0.7,
|
|
2093
|
+
maxRetries: 1
|
|
2094
|
+
};
|
|
2095
|
+
return Pipeline.from(analyzeStep).pipe(respondStep);
|
|
2096
|
+
}
|
|
2097
|
+
async function runAgentPipeline(pipeline, input, config) {
|
|
2098
|
+
const result = await pipeline.execute(config.runtime, input);
|
|
2099
|
+
if (config.onStepComplete) {
|
|
2100
|
+
for (const step of result.steps) {
|
|
2101
|
+
config.onStepComplete(step.name, step.data, step.usage);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return result;
|
|
2105
|
+
}
|
|
594
2106
|
export {
|
|
2107
|
+
AgentDynamicTools,
|
|
595
2108
|
AgentEventBus,
|
|
2109
|
+
AgentUsageTracker,
|
|
596
2110
|
ContextManager,
|
|
2111
|
+
ConversationRAG,
|
|
2112
|
+
ExecutionSchema,
|
|
2113
|
+
HookIntegration,
|
|
597
2114
|
ObotoAgent,
|
|
2115
|
+
PermissionGuard,
|
|
2116
|
+
PlanSchema,
|
|
2117
|
+
RouterEventBridge,
|
|
2118
|
+
SessionCompactor,
|
|
2119
|
+
SlashCommandRegistry,
|
|
2120
|
+
SummarySchema2 as SummarySchema,
|
|
2121
|
+
TriagePipelineSchema,
|
|
598
2122
|
TriageSchema,
|
|
2123
|
+
UsageBridge,
|
|
2124
|
+
asTokenUsageToLmscript,
|
|
2125
|
+
createAgentToolTree,
|
|
2126
|
+
createAnalyzeRespondPipeline,
|
|
599
2127
|
createEmptySession,
|
|
2128
|
+
createExecutionStep,
|
|
2129
|
+
createFullPipeline,
|
|
2130
|
+
createPlanStep,
|
|
600
2131
|
createRouterTool,
|
|
2132
|
+
createSummaryStep,
|
|
2133
|
+
createToolAuditMiddleware,
|
|
2134
|
+
createToolTimeoutMiddleware,
|
|
2135
|
+
createToolTimingMiddleware,
|
|
601
2136
|
createTriageFunction,
|
|
2137
|
+
createTriagePlanExecutePipeline,
|
|
2138
|
+
createTriageStep,
|
|
2139
|
+
estimateCostFromAsAgent,
|
|
602
2140
|
fromChat,
|
|
2141
|
+
isLLMRouter,
|
|
2142
|
+
lmscriptToAsTokenUsage,
|
|
2143
|
+
runAgentPipeline,
|
|
603
2144
|
sessionToHistory,
|
|
604
2145
|
toChat,
|
|
605
2146
|
toLmscriptProvider
|