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