@sschepis/oboto-agent 0.1.5 → 0.2.2

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