@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.js CHANGED
@@ -1,8 +1,15 @@
1
1
  // src/oboto-agent.ts
2
- import { LScriptRuntime } from "@sschepis/lmscript";
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 { zodToJsonSchema } from "zod-to-json-schema";
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().optional().describe("Direct answer if the request can be handled without escalation")
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
- - If the request is a greeting, simple question, or casual conversation: respond directly.
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 messages = request.messages.map((m) => ({
171
- role: m.role,
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 _ObotoAgent {
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
- this.localRuntime = new LScriptRuntime({ provider: localLmscript });
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: MessageRole2.User,
1305
+ role: MessageRole4.User,
338
1306
  blocks: [{ kind: "text", text: `[INTERRUPTION] ${newDirectives}` }]
339
1307
  };
340
- this.session.messages.push(msg);
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
- /** Remove all event listeners. */
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: MessageRole2.User,
1432
+ role: MessageRole4.User,
362
1433
  blocks: [{ kind: "text", text: userInput }]
363
1434
  };
364
- this.session.messages.push(userMsg);
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: MessageRole2.Assistant,
1457
+ role: MessageRole4.Assistant,
375
1458
  blocks: [{ kind: "text", text: response }]
376
1459
  };
377
- this.session.messages.push(assistantMsg);
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
- await this.executeWithModel(provider, modelName, userInput);
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 the agent loop using llm-wrapper directly.
413
- * When onToken is configured, uses streaming for real-time token output.
414
- * No JSON mode, no schema enforcement — just natural chat with tool calling.
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 executeWithModel(provider, modelName, _userInput) {
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 useStreaming = !!this.onToken;
437
- for (let iteration = 1; iteration <= this.maxIterations; iteration++) {
438
- if (this.interrupted) break;
439
- const isLastIteration = iteration === this.maxIterations;
440
- if (isLastIteration) {
441
- messages.push({
442
- role: "user",
443
- 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."
444
- });
445
- }
446
- const params = {
447
- model: modelName,
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
- let args;
493
- try {
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
- const command = args.command ?? tc.function.name;
499
- const kwargs = args.kwargs ?? {};
500
- const callSig = JSON.stringify({ command, kwargs });
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: "tool",
506
- tool_call_id: tc.id,
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
- this.bus.emit("tool_execution_start", { command, kwargs });
519
- let result;
520
- let success = true;
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
- result = await tool.execute(args);
1634
+ response = await this.streamAndAggregate(provider, params);
523
1635
  } catch (err) {
524
- result = `Error: ${err instanceof Error ? err.message : String(err)}`;
525
- success = false;
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: "tool",
536
- tool_call_id: tc.id,
537
- content: truncated
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
- this.bus.emit("tool_round_complete", {
541
- iteration,
542
- tools: toolResults,
543
- totalToolCalls
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