omnius 1.0.591 → 1.0.592

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.
Files changed (131) hide show
  1. package/.aiwg/addons/omnius-docs/README.md +15 -1
  2. package/.aiwg/addons/omnius-docs/manifest.json +28 -68
  3. package/.aiwg/addons/omnius-docs/skills/agent-failure-recovery/SKILL.md +2 -1
  4. package/.aiwg/addons/omnius-docs/skills/browser-interaction-validation/SKILL.md +2 -1
  5. package/.aiwg/addons/omnius-docs/skills/evidence-directed-delivery/SKILL.md +2 -1
  6. package/.aiwg/addons/omnius-docs/skills/hardware-evidence-audit/SKILL.md +2 -1
  7. package/.aiwg/addons/omnius-docs/skills/omnius-docs/SKILL.md +17 -7
  8. package/.aiwg/addons/omnius-docs/skills/omnius-inference-docs/SKILL.md +27 -0
  9. package/.aiwg/addons/omnius-docs/skills/omnius-integration-docs/SKILL.md +21 -0
  10. package/.aiwg/addons/omnius-docs/skills/omnius-ops-docs/SKILL.md +2 -0
  11. package/.aiwg/addons/omnius-docs/skills/omnius-realtime-docs/SKILL.md +2 -0
  12. package/.aiwg/addons/omnius-docs/skills/omnius-sponsor-docs/SKILL.md +2 -0
  13. package/.aiwg/addons/omnius-docs/skills/omnius-telegram-docs/SKILL.md +2 -0
  14. package/.aiwg/addons/omnius-docs/skills/omnius-tools-docs/SKILL.md +23 -0
  15. package/.aiwg/addons/omnius-docs/skills/omnius-version-compatibility-docs/SKILL.md +23 -0
  16. package/.aiwg/addons/omnius-docs/skills/runtime-provenance-audit/SKILL.md +2 -1
  17. package/.aiwg/addons/omnius-docs/skills/secrets-and-config-audit/SKILL.md +2 -1
  18. package/.aiwg/addons/omnius-docs/skills/test-surface-audit/SKILL.md +2 -1
  19. package/.aiwg/addons/omnius-docs/skills/workspace-reality-audit/SKILL.md +2 -1
  20. package/.aiwg/addons/omnius-rest-docs/README.md +3 -0
  21. package/.aiwg/addons/omnius-rest-docs/manifest.json +27 -20
  22. package/.aiwg/addons/omnius-rest-docs/skills/omnius-rest-docs/SKILL.md +9 -5
  23. package/README.md +36 -0
  24. package/dist/discovery.d.ts +50 -0
  25. package/dist/index.js +5975 -4021
  26. package/dist/library.d.ts +7 -0
  27. package/dist/library.js +950 -0
  28. package/dist/postinstall-daemon.cjs +18 -0
  29. package/dist/providerRegistry.d.ts +80 -0
  30. package/dist/service-version.d.ts +35 -0
  31. package/docs/.vitepress/config.mts +8 -0
  32. package/docs/DISCOVERY.json +20224 -0
  33. package/docs/DISCOVERY.md +648 -0
  34. package/docs/HANDOFF-crl-encoder-decoder-fix.md +129 -0
  35. package/docs/agent-memory/INDEX.md +9 -4
  36. package/docs/agent-memory/index.md +7 -0
  37. package/docs/concept-relational-language.md +869 -0
  38. package/docs/context-management-medium-models-proposal.md +449 -0
  39. package/docs/dedup-false-positive-meta-analysis.md +96 -0
  40. package/docs/discovery/catalog-overrides.json +724 -0
  41. package/docs/duplicate-calls-root-cause-analysis.md +91 -0
  42. package/docs/duplicate-calls-root-cause-deep.md +155 -0
  43. package/docs/ephemeral-skill-pack-small-context.md +57 -0
  44. package/docs/explorations/context-window-todo-association.md +156 -0
  45. package/docs/explorations/todo-association-verify.json +30 -0
  46. package/docs/explorations/verification-ledger.json +45 -0
  47. package/docs/explorations/verify-todo-association.sh +30 -0
  48. package/docs/flowstate.md +806 -0
  49. package/docs/getting-started/install.md +24 -0
  50. package/docs/getting-started/model-providers.md +13 -0
  51. package/docs/guides/agent-integration.md +87 -0
  52. package/docs/guides/bring-your-own-inference.md +126 -0
  53. package/docs/guides/tools-and-web-search.md +95 -0
  54. package/docs/index.md +14 -0
  55. package/docs/longhaul-35b-workorders.md +496 -0
  56. package/docs/memory-integration-analysis.md +303 -0
  57. package/docs/model-capability-awareness-and-multimodal-memory-root-fix.md +799 -0
  58. package/docs/multimodal-identity-memory-implementation.md +76 -0
  59. package/docs/omnius-self-edit-eval-2026-06-10.md +169 -0
  60. package/docs/opencode-agentic-loop-comparison.md +290 -0
  61. package/docs/operations/security-and-remote-access.md +2 -2
  62. package/docs/operations/version-compatibility.md +63 -0
  63. package/docs/proposals/git-progress-tracking-strategy.md +289 -0
  64. package/docs/proposals/opencode-modules/backendAdapter.ts +443 -0
  65. package/docs/proposals/opencode-modules/childSession.ts +288 -0
  66. package/docs/proposals/opencode-modules/compactionAgent.ts +101 -0
  67. package/docs/proposals/opencode-modules/orchestrator.ts +387 -0
  68. package/docs/proposals/opencode-modules/runner.ts +258 -0
  69. package/docs/reference/auth-map.md +87 -196
  70. package/docs/reference/configuration.md +27 -0
  71. package/docs/reference/rest-api.md +7 -0
  72. package/docs/reference/slash-commands.md +125 -2
  73. package/docs/research/_archived/README.md +18 -0
  74. package/docs/research/_archived/context_window_attention_model.py +418 -0
  75. package/docs/research/_archived/context_window_attention_spec.md +55 -0
  76. package/docs/research/_archived/context_window_attention_weights.json +68 -0
  77. package/docs/research/k-splanifolds.pdf +0 -0
  78. package/docs/research/personality-verbosity-control.md +293 -0
  79. package/docs/rest/INDEX.md +7 -0
  80. package/docs/rest/QUICKREF.md +18 -0
  81. package/docs/rest/REST-DOCS-MANIFEST.json +1 -0
  82. package/docs/rest/auth-and-scopes.md +7 -1
  83. package/docs/rest/endpoints/discovery.md +44 -0
  84. package/docs/rest/endpoints/events.md +5 -0
  85. package/docs/rest/endpoints/tools.md +9 -0
  86. package/docs/reviews/adversary-system-review.md +42 -0
  87. package/docs/sana-and-video-generation-integration-plan.md +712 -0
  88. package/docs/session-diary-llm-training-analysis.md +218 -0
  89. package/docs/telegram-dmn-curiosity-outreach-scaffold.md +91 -0
  90. package/docs/telegram-mid-horizon-download-loop-handoff.md +468 -0
  91. package/docs/telegram-reflection-corpus-integration-plan.md +306 -0
  92. package/docs/telegram-unified-tooling-architecture.md +332 -0
  93. package/docs/threat-model.md +868 -0
  94. package/docs/trajectory-grounding.md +160 -0
  95. package/docs/voice-flow-architecture.md +489 -0
  96. package/docs/work-orders/WO-AM-GAPS.md +638 -0
  97. package/docs/work-orders/daemon-hud-ui-overhaul.md +82 -0
  98. package/docs/work-orders/hermes-architecture-deltas/01-public-scrutiny-provenance-control/INDEX.md +21 -0
  99. package/docs/work-orders/hermes-architecture-deltas/01-public-scrutiny-provenance-control/WORKORDER.md +225 -0
  100. package/docs/work-orders/hermes-architecture-deltas/02-context-engine-plugin-boundary/INDEX.md +20 -0
  101. package/docs/work-orders/hermes-architecture-deltas/02-context-engine-plugin-boundary/WORKORDER.md +198 -0
  102. package/docs/work-orders/hermes-architecture-deltas/03-typed-gateway-event-stream/INDEX.md +19 -0
  103. package/docs/work-orders/hermes-architecture-deltas/03-typed-gateway-event-stream/WORKORDER.md +172 -0
  104. package/docs/work-orders/hermes-architecture-deltas/04-task-local-gateway-context/INDEX.md +19 -0
  105. package/docs/work-orders/hermes-architecture-deltas/04-task-local-gateway-context/WORKORDER.md +169 -0
  106. package/docs/work-orders/hermes-architecture-deltas/05-process-lifecycle-monitoring-notifications/INDEX.md +22 -0
  107. package/docs/work-orders/hermes-architecture-deltas/05-process-lifecycle-monitoring-notifications/WORKORDER.md +189 -0
  108. package/docs/work-orders/hermes-architecture-deltas/06-vision-evidence-routing-ladder/INDEX.md +22 -0
  109. package/docs/work-orders/hermes-architecture-deltas/06-vision-evidence-routing-ladder/WORKORDER.md +199 -0
  110. package/docs/work-orders/hermes-architecture-deltas/07-durable-multi-agent-kanban/INDEX.md +20 -0
  111. package/docs/work-orders/hermes-architecture-deltas/07-durable-multi-agent-kanban/WORKORDER.md +174 -0
  112. package/docs/work-orders/hermes-architecture-deltas/08-completion-critic-reconciliation-ledger/INDEX.md +22 -0
  113. package/docs/work-orders/hermes-architecture-deltas/08-completion-critic-reconciliation-ledger/WORKORDER.md +226 -0
  114. package/docs/work-orders/hermes-architecture-deltas/INDEX.md +38 -0
  115. package/docs/work-orders/omnius-context-engineering-behavior-fixes.md +281 -0
  116. package/docs/work-orders/telegram-dropbear-context-rca-workorder.md +202 -0
  117. package/docs/work-orders/world-class-memory-compiler/README.md +162 -0
  118. package/docs/work-orders/world-class-memory-compiler/TRACKER.md +179 -0
  119. package/docs/work-orders/world-class-memory-compiler/WO-01-exact-request-budget.md +79 -0
  120. package/docs/work-orders/world-class-memory-compiler/WO-02-typed-memory-fabric.md +65 -0
  121. package/docs/work-orders/world-class-memory-compiler/WO-03-dependency-working-set.md +55 -0
  122. package/docs/work-orders/world-class-memory-compiler/WO-04-inference-memory-compiler.md +67 -0
  123. package/docs/work-orders/world-class-memory-compiler/WO-05-artifact-fidelity-materialization.md +72 -0
  124. package/docs/work-orders/world-class-memory-compiler/WO-06-temporal-hybrid-retrieval.md +49 -0
  125. package/docs/work-orders/world-class-memory-compiler/WO-07-evaluation-harness.md +45 -0
  126. package/docs/work-orders/world-class-memory-compiler/WO-08-rollout-legacy-removal.md +45 -0
  127. package/docs/x402-remote-inference-plan.md +323 -0
  128. package/npm-shrinkwrap.json +108 -117
  129. package/package.json +7 -6
  130. package/templates/AGENTS.md +6 -0
  131. package/templates/OMNIUS.md +20 -0
@@ -0,0 +1,387 @@
1
+ /**
2
+ * orchestrator.ts — High-level session management, state transitions, and
3
+ * multi-turn coordination. This module owns the *what* and *when* of the
4
+ * agentic loop:
5
+ * 1. Manage session lifecycle (create, resume, terminate)
6
+ * 2. Decide when to compact context
7
+ * 3. Coordinate between multiple agents
8
+ * 4. Handle task decomposition and sub-task routing
9
+ * 5. Track overall progress and completion criteria
10
+ *
11
+ * The runner owns the *how*: turn-by-turn execution, tool dispatch, and
12
+ * event emission.
13
+ */
14
+
15
+ import type { LLMBackend } from './backendAdapter.js';
16
+ import { AgenticRunner, type RunResult, type RunnerEvent, type RunnerConfig, type ToolHandler } from './runner.js';
17
+
18
+ // ─── Session state ────────────────────────────────────────────────────────
19
+
20
+ export enum SessionState {
21
+ /** Session has been created but not yet started */
22
+ Created = 'created',
23
+ /** Session is actively running */
24
+ Running = 'running',
25
+ /** Session is paused (waiting for user input or external event) */
26
+ Paused = 'paused',
27
+ /** Session has completed successfully */
28
+ Completed = 'completed',
29
+ /** Session has failed */
30
+ Failed = 'failed',
31
+ /** Session has been terminated */
32
+ Terminated = 'terminated',
33
+ }
34
+
35
+ /** A single session in the orchestrator */
36
+ export interface Session {
37
+ /** Unique session identifier */
38
+ id: string;
39
+ /** Current session state */
40
+ state: SessionState;
41
+ /** Session name / description */
42
+ name: string;
43
+ /** Task description */
44
+ task: string;
45
+ /** System prompt for the session */
46
+ systemPrompt: string;
47
+ /** Registered tools for this session */
48
+ tools: Map<string, ToolHandler>;
49
+ /** LLM backend for this session */
50
+ backend: LLMBackend;
51
+ /** Messages in the conversation history */
52
+ messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;
53
+ /** Current turn number */
54
+ turnCount: number;
55
+ /** Maximum turns before forced stop */
56
+ maxTurns: number;
57
+ /** Compaction strategy for this session */
58
+ compactionStrategy: CompactionStrategy;
59
+ /** Completion criteria for the session */
60
+ completionCriteria?: string;
61
+ /** Progress tracking */
62
+ progress: SessionProgress;
63
+ /** Created timestamp */
64
+ createdAt: number;
65
+ /** Last updated timestamp */
66
+ updatedAt: number;
67
+ }
68
+
69
+ /** Progress tracking for a session */
70
+ export interface SessionProgress {
71
+ /** Steps completed */
72
+ completedSteps: string[];
73
+ /** Steps pending */
74
+ pendingSteps: string[];
75
+ /** Current step being worked on */
76
+ currentStep: string;
77
+ /** Failed approaches to avoid */
78
+ failedApproaches: string[];
79
+ /** Next action to take */
80
+ nextAction: string;
81
+ /** Modified files tracking */
82
+ modifiedFiles: Map<string, string>;
83
+ /** Total tool calls made */
84
+ toolCallCount: number;
85
+ }
86
+
87
+ /** Compaction strategy for managing context window */
88
+ export interface CompactionStrategy {
89
+ /** Type of compaction */
90
+ type: 'summarize' | 'prune' | 'compress' | 'none';
91
+ /** Threshold for triggering compaction (in turns) */
92
+ threshold: number;
93
+ /** Token budget after compaction */
94
+ tokenBudget: number;
95
+ /** Custom compaction function */
96
+ custom?: (messages: Array<{ role: string; content: string }>) => Promise<Array<{ role: string; content: string }>>;
97
+ }
98
+
99
+ // ─── Orchestrator ────────────────────────────────────────────────────────
100
+
101
+ export class SessionOrchestrator {
102
+ private sessions: Map<string, Session> = new Map();
103
+ private onSessionEvent?: (event: SessionEvent) => void;
104
+
105
+ constructor(onSessionEvent?: (event: SessionEvent) => void) {
106
+ this.onSessionEvent = onSessionEvent;
107
+ }
108
+
109
+ /**
110
+ * Create a new session with the given configuration.
111
+ */
112
+ createSession(options: {
113
+ id?: string;
114
+ name: string;
115
+ task: string;
116
+ systemPrompt?: string;
117
+ backend: LLMBackend;
118
+ tools?: Map<string, ToolHandler>;
119
+ maxTurns?: number;
120
+ compactionStrategy?: CompactionStrategy;
121
+ completionCriteria?: string;
122
+ }): Session {
123
+ const id = options.id ?? `session_${Date.now()}_${Math.random().toString(36).slice(2)}`;
124
+ const session: Session = {
125
+ id,
126
+ state: SessionState.Created,
127
+ name: options.name,
128
+ task: options.task,
129
+ systemPrompt: options.systemPrompt ?? '',
130
+ backend: options.backend,
131
+ tools: options.tools ?? new Map(),
132
+ messages: [],
133
+ turnCount: 0,
134
+ maxTurns: options.maxTurns ?? 50,
135
+ compactionStrategy: options.compactionStrategy ?? { type: 'none', threshold: 10, tokenBudget: 4096 },
136
+ completionCriteria: options.completionCriteria,
137
+ progress: {
138
+ completedSteps: [],
139
+ pendingSteps: [options.task],
140
+ currentStep: options.task,
141
+ failedApproaches: [],
142
+ nextAction: 'start',
143
+ modifiedFiles: new Map(),
144
+ toolCallCount: 0,
145
+ },
146
+ createdAt: Date.now(),
147
+ updatedAt: Date.now(),
148
+ };
149
+
150
+ this.sessions.set(id, session);
151
+ this.emit({ type: 'session-created', sessionId: id, session });
152
+ return session;
153
+ }
154
+
155
+ /**
156
+ * Start a session — transitions it from Created to Running and begins
157
+ * the agentic loop.
158
+ */
159
+ async startSession(sessionId: string): Promise<Session> {
160
+ const session = this.sessions.get(sessionId);
161
+ if (!session) {
162
+ throw new Error(`Session "${sessionId}" not found`);
163
+ }
164
+ if (session.state !== SessionState.Created && session.state !== SessionState.Paused) {
165
+ throw new Error(`Cannot start session in state "${session.state}"`);
166
+ }
167
+
168
+ session.state = SessionState.Running;
169
+ session.updatedAt = Date.now();
170
+
171
+ // Add system prompt as first message
172
+ if (session.systemPrompt) {
173
+ session.messages.push({ role: 'system', content: session.systemPrompt });
174
+ }
175
+
176
+ this.emit({ type: 'session-started', sessionId, session });
177
+
178
+ // Begin the agentic loop
179
+ await this.runLoop(session);
180
+
181
+ return session;
182
+ }
183
+
184
+ /**
185
+ * Run the agentic loop for a session: repeatedly call the runner until
186
+ * completion criteria are met or max turns reached.
187
+ */
188
+ private async runLoop(session: Session): Promise<void> {
189
+ while (session.state === SessionState.Running && session.turnCount < session.maxTurns) {
190
+ session.turnCount++;
191
+
192
+ // Check compaction threshold
193
+ if (session.compactionStrategy.type !== 'none' && session.turnCount % session.compactionStrategy.threshold === 0) {
194
+ await this.compactSession(session);
195
+ }
196
+
197
+ // Check completion criteria
198
+ if (session.completionCriteria && this.checkCompletion(session)) {
199
+ session.state = SessionState.Completed;
200
+ session.updatedAt = Date.now();
201
+ this.emit({ type: 'session-completed', sessionId: session.id, session });
202
+ return;
203
+ }
204
+
205
+ // Run one turn
206
+ const result = await this.runTurn(session);
207
+
208
+ // Check for errors
209
+ if (result.error) {
210
+ session.state = SessionState.Failed;
211
+ session.updatedAt = Date.now();
212
+ this.emit({ type: 'session-failed', sessionId: session.id, session, error: result.error });
213
+ return;
214
+ }
215
+
216
+ // Update progress
217
+ session.progress.toolCallCount += result.toolCalls.length;
218
+ session.updatedAt = Date.now();
219
+ }
220
+
221
+ // Max turns reached
222
+ if (session.turnCount >= session.maxTurns) {
223
+ session.state = SessionState.Completed;
224
+ session.updatedAt = Date.now();
225
+ this.emit({ type: 'session-completed', sessionId: session.id, session });
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Run a single turn: delegate to the runner and process the result.
231
+ */
232
+ private async runTurn(session: Session): Promise<RunResult> {
233
+ const runner = new AgenticRunner(
234
+ {
235
+ backend: session.backend,
236
+ tools: session.tools,
237
+ maxTurns: 1,
238
+ streamEvents: true,
239
+ },
240
+ (event: RunnerEvent) => {
241
+ this.emit({ type: 'runner-event', sessionId: session.id, event });
242
+ },
243
+ );
244
+
245
+ const result = await runner.run(session.messages);
246
+
247
+ // Add assistant message to history
248
+ session.messages.push({ role: 'assistant', content: result.text });
249
+
250
+ // Add tool results to history
251
+ for (const tr of result.toolResults) {
252
+ session.messages.push({
253
+ role: 'system',
254
+ content: `<tool_result id="${tr.id}">${tr.content}</tool_result>`,
255
+ });
256
+ }
257
+
258
+ return result;
259
+ }
260
+
261
+ /**
262
+ * Compact the session's message history according to the compaction strategy.
263
+ */
264
+ private async compactSession(session: Session): Promise<void> {
265
+ if (session.compactionStrategy.type === 'none') return;
266
+
267
+ const messages = session.messages;
268
+
269
+ if (session.compactionStrategy.custom) {
270
+ session.messages = (await session.compactionStrategy.custom(messages)) as typeof session.messages;
271
+ } else if (session.compactionStrategy.type === 'summarize') {
272
+ // Simple summarize: keep system prompt + last N messages
273
+ const keep = Math.min(session.compactionStrategy.tokenBudget, messages.length);
274
+ session.messages = messages.slice(-keep);
275
+ } else if (session.compactionStrategy.type === 'prune') {
276
+ // Simple prune: keep system prompt + every other message
277
+ session.messages = messages.filter((_, i) => i % 2 === 0);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Check if the session has met its completion criteria.
283
+ */
284
+ private checkCompletion(session: Session): boolean {
285
+ if (!session.completionCriteria) return false;
286
+
287
+ // Simple keyword matching for now
288
+ const criteria = session.completionCriteria.toLowerCase();
289
+ const lastMessage = session.messages[session.messages.length - 1];
290
+ if (!lastMessage) return false;
291
+
292
+ return lastMessage.content.toLowerCase().includes(criteria);
293
+ }
294
+
295
+ /**
296
+ * Pause a running session.
297
+ */
298
+ pauseSession(sessionId: string): Session {
299
+ const session = this.sessions.get(sessionId);
300
+ if (!session) {
301
+ throw new Error(`Session "${sessionId}" not found`);
302
+ }
303
+ if (session.state !== SessionState.Running) {
304
+ throw new Error(`Cannot pause session in state "${session.state}"`);
305
+ }
306
+
307
+ session.state = SessionState.Paused;
308
+ session.updatedAt = Date.now();
309
+ this.emit({ type: 'session-paused', sessionId, session });
310
+ return session;
311
+ }
312
+
313
+ /**
314
+ * Resume a paused session.
315
+ */
316
+ async resumeSession(sessionId: string): Promise<Session> {
317
+ const session = this.sessions.get(sessionId);
318
+ if (!session) {
319
+ throw new Error(`Session "${sessionId}" not found`);
320
+ }
321
+ if (session.state !== SessionState.Paused) {
322
+ throw new Error(`Cannot resume session in state "${session.state}"`);
323
+ }
324
+
325
+ session.state = SessionState.Running;
326
+ session.updatedAt = Date.now();
327
+ this.emit({ type: 'session-resumed', sessionId, session });
328
+
329
+ await this.runLoop(session);
330
+ return session;
331
+ }
332
+
333
+ /**
334
+ * Terminate a session.
335
+ */
336
+ terminateSession(sessionId: string): Session {
337
+ const session = this.sessions.get(sessionId);
338
+ if (!session) {
339
+ throw new Error(`Session "${sessionId}" not found`);
340
+ }
341
+
342
+ session.state = SessionState.Terminated;
343
+ session.updatedAt = Date.now();
344
+ this.emit({ type: 'session-terminated', sessionId, session });
345
+ return session;
346
+ }
347
+
348
+ /**
349
+ * Get a session by ID.
350
+ */
351
+ getSession(sessionId: string): Session | undefined {
352
+ return this.sessions.get(sessionId);
353
+ }
354
+
355
+ /**
356
+ * List all sessions.
357
+ */
358
+ listSessions(): Session[] {
359
+ return Array.from(this.sessions.values());
360
+ }
361
+
362
+ /**
363
+ * Delete a session.
364
+ */
365
+ deleteSession(sessionId: string): void {
366
+ this.sessions.delete(sessionId);
367
+ }
368
+
369
+ /**
370
+ * Emit a session event.
371
+ */
372
+ private emit(event: SessionEvent): void {
373
+ if (this.onSessionEvent) {
374
+ this.onSessionEvent(event);
375
+ }
376
+ }
377
+ }
378
+
379
+ // ─── Session events ────────────────────────────────────────────────────────
380
+
381
+ export interface SessionEvent {
382
+ type: 'session-created' | 'session-started' | 'session-paused' | 'session-resumed' | 'session-completed' | 'session-failed' | 'session-terminated' | 'runner-event';
383
+ sessionId: string;
384
+ session?: Session;
385
+ event?: RunnerEvent;
386
+ error?: string;
387
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * runner.ts — Core agentic loop: turn-by-turn execution, tool dispatch,
3
+ * and event emission. This module owns the *how* of a single agent turn:
4
+ * 1. Receive messages from the orchestrator
5
+ * 2. Call the LLM backend (via backendAdapter)
6
+ * 3. Parse tool calls from the response
7
+ * 4. Dispatch each tool and collect results
8
+ * 5. Emit events back to the orchestrator
9
+ *
10
+ * The orchestrator owns the *what* and *when*: session lifecycle, state
11
+ * transitions, compaction decisions, and multi-agent coordination.
12
+ */
13
+
14
+ import type { LLMBackend, LLMEvent } from './backendAdapter.js';
15
+
16
+ // ─── Public types ────────────────────────────────────────────────────────────
17
+
18
+ /** Result returned by a single run cycle */
19
+ export interface RunResult {
20
+ /** Final assistant text (concatenated chunks) */
21
+ text: string;
22
+ /** Tool calls that were made during this turn */
23
+ toolCalls: Array<{ id: string; name: string; args: string }>;
24
+ /** Tool results that were collected */
25
+ toolResults: Array<{ id: string; content: string; error?: string }>;
26
+ /** Whether the run completed normally */
27
+ finished: boolean;
28
+ /** Error message if the run failed */
29
+ error?: string;
30
+ }
31
+
32
+ /** Configuration for a single run cycle */
33
+ export interface RunnerConfig {
34
+ /** LLM backend to use for completions */
35
+ backend: LLMBackend;
36
+ /** Registered tools keyed by name */
37
+ tools: Map<string, ToolHandler>;
38
+ /** Maximum turns before forced stop */
39
+ maxTurns: number;
40
+ /** Whether to stream events to the orchestrator */
41
+ streamEvents: boolean;
42
+ }
43
+
44
+ /** A tool handler that can be invoked by the runner */
45
+ export interface ToolHandler {
46
+ /** Unique tool name */
47
+ name: string;
48
+ /** Description of what the tool does */
49
+ description: string;
50
+ /** Schema describing the tool's parameters */
51
+ parameters: Record<string, unknown>;
52
+ /** Execute the tool with the given arguments */
53
+ execute(args: string): Promise<string>;
54
+ }
55
+
56
+ /** Event emitted by the runner during execution */
57
+ export interface RunnerEvent {
58
+ type: 'chunk' | 'tool-call' | 'tool-result' | 'finish' | 'error' | 'turn-start' | 'turn-end';
59
+ content?: string;
60
+ toolCall?: { id: string; name: string; args: string };
61
+ toolResult?: { id: string; content: string; error?: string };
62
+ turnNumber?: number;
63
+ error?: string;
64
+ }
65
+
66
+ // ─── Core runner ─────────────────────────────────────────────────────────────
67
+
68
+ export class AgenticRunner {
69
+ private config: RunnerConfig;
70
+ private turnCount = 0;
71
+ private currentText = '';
72
+ private currentToolCalls: Array<{ id: string; name: string; args: string }> = [];
73
+ private currentToolResults: Array<{ id: string; content: string; error?: string }> = [];
74
+ private onEvent?: (event: RunnerEvent) => void;
75
+
76
+ constructor(config: RunnerConfig, onEvent?: (event: RunnerEvent) => void) {
77
+ this.config = config;
78
+ this.onEvent = onEvent;
79
+ }
80
+
81
+ /**
82
+ * Run one complete agentic cycle: prompt LLM → parse response → execute tools → return result.
83
+ * This is the core turn loop that the orchestrator calls repeatedly.
84
+ */
85
+ async run(messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>): Promise<RunResult> {
86
+ this.turnCount++;
87
+ this.currentText = '';
88
+ this.currentToolCalls = [];
89
+ this.currentToolResults = [];
90
+
91
+ // Emit turn-start event
92
+ this.emit({ type: 'turn-start', turnNumber: this.turnCount });
93
+
94
+ // Call the LLM backend
95
+ const response = await this.callLLM(messages);
96
+
97
+ // Parse the response into tool calls and text
98
+ const { text, toolCalls } = this.parseResponse(response);
99
+ this.currentText = text;
100
+ this.currentToolCalls = toolCalls;
101
+
102
+ // Execute any tool calls
103
+ if (toolCalls.length > 0) {
104
+ const results = await this.executeTools(toolCalls);
105
+ this.currentToolResults = results;
106
+
107
+ // Build follow-up messages with tool results
108
+ const toolResultMessages = this.buildToolResultMessages(results);
109
+ const finalResponse = await this.callLLM([...messages, ...toolResultMessages]);
110
+ const { text: finalText } = this.parseResponse(finalResponse);
111
+ this.currentText = finalText;
112
+ }
113
+
114
+ // Emit finish event
115
+ this.emit({
116
+ type: 'finish',
117
+ content: this.currentText,
118
+ turnNumber: this.turnCount,
119
+ });
120
+
121
+ return {
122
+ text: this.currentText,
123
+ toolCalls: this.currentToolCalls,
124
+ toolResults: this.currentToolResults,
125
+ finished: true,
126
+ };
127
+ }
128
+
129
+ /** Call the LLM backend and collect the full response */
130
+ private async callLLM(messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>): Promise<string> {
131
+ const stream = this.config.backend.complete(messages, this.buildToolDefinitions());
132
+ let fullResponse = '';
133
+
134
+ for await (const event of stream) {
135
+ if (event.type === 'chunk' && event.content) {
136
+ this.emit({ type: 'chunk', content: event.content });
137
+ fullResponse += event.content;
138
+ } else if (event.type === 'error') {
139
+ this.emit({ type: 'error', error: event.error });
140
+ throw new Error(event.error ?? 'Unknown backend error');
141
+ }
142
+ }
143
+
144
+ return fullResponse;
145
+ }
146
+
147
+ /** Build tool definitions for the LLM backend */
148
+ private buildToolDefinitions(): Array<{ name: string; description?: string; parameters: Record<string, unknown> }> {
149
+ const definitions: Array<{ name: string; description?: string; parameters: Record<string, unknown> }> = [];
150
+
151
+ for (const tool of this.config.tools.values()) {
152
+ definitions.push({
153
+ name: tool.name,
154
+ description: tool.description,
155
+ parameters: tool.parameters,
156
+ });
157
+ }
158
+
159
+ return definitions;
160
+ }
161
+
162
+ /** Parse the LLM response into text and tool calls */
163
+ private parseResponse(response: string): { text: string; toolCalls: Array<{ id: string; name: string; args: string }> } {
164
+ const toolCalls: Array<{ id: string; name: string; args: string }> = [];
165
+ let text = response;
166
+
167
+ // Extract tool calls using regex pattern matching
168
+ const toolCallRegex = /<tool_call>\s*({[\s\S]*?})\s*<\/tool_call>/g;
169
+ let match;
170
+
171
+ while ((match = toolCallRegex.exec(response)) !== null) {
172
+ try {
173
+ const args = JSON.parse(match[1]);
174
+ const id = `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
175
+ const name = args.name || args.function?.name || 'unknown';
176
+ const toolArgs = args.arguments || args.args || args.function?.arguments || '{}';
177
+ const parsedArgs = typeof toolArgs === 'string' ? toolArgs : JSON.stringify(toolArgs);
178
+
179
+ toolCalls.push({ id, name, args: parsedArgs });
180
+
181
+ // Remove the tool call from the text
182
+ text = text.replace(match[0], '').trim();
183
+ } catch {
184
+ // Invalid JSON — skip this match
185
+ }
186
+ }
187
+
188
+ // Emit tool-call events
189
+ for (const tc of toolCalls) {
190
+ this.emit({ type: 'tool-call', toolCall: tc });
191
+ }
192
+
193
+ return { text, toolCalls };
194
+ }
195
+
196
+ /** Execute all tool calls and collect results */
197
+ private async executeTools(toolCalls: Array<{ id: string; name: string; args: string }>): Promise<Array<{ id: string; content: string; error?: string }>> {
198
+ const results: Array<{ id: string; content: string; error?: string }> = [];
199
+
200
+ for (const tc of toolCalls) {
201
+ const tool = this.config.tools.get(tc.name);
202
+ if (!tool) {
203
+ results.push({ id: tc.id, content: `Error: Tool "${tc.name}" not found`, error: 'tool_not_found' });
204
+ this.emit({
205
+ type: 'tool-result',
206
+ toolResult: { id: tc.id, content: `Tool "${tc.name}" not found`, error: 'tool_not_found' },
207
+ });
208
+ continue;
209
+ }
210
+
211
+ try {
212
+ const result = await tool.execute(tc.args);
213
+ results.push({ id: tc.id, content: result });
214
+ this.emit({
215
+ type: 'tool-result',
216
+ toolResult: { id: tc.id, content: result },
217
+ });
218
+ } catch (err) {
219
+ const errorMsg = err instanceof Error ? err.message : String(err);
220
+ results.push({ id: tc.id, content: `Error: ${errorMsg}`, error: 'execution_error' });
221
+ this.emit({
222
+ type: 'tool-result',
223
+ toolResult: { id: tc.id, content: `Error: ${errorMsg}`, error: 'execution_error' },
224
+ });
225
+ }
226
+ }
227
+
228
+ return results;
229
+ }
230
+
231
+ /** Build messages from tool results for the follow-up LLM call */
232
+ private buildToolResultMessages(results: Array<{ id: string; content: string; error?: string }>): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
233
+ return results.map((r) => ({
234
+ role: 'system',
235
+ content: `<tool_result>\n<tool_id>${r.id}</tool_id>\n<content>${r.content}</content>\n</tool_result>`,
236
+ }));
237
+ }
238
+
239
+ /** Emit an event if a handler is registered */
240
+ private emit(event: RunnerEvent): void {
241
+ if (this.onEvent) {
242
+ this.onEvent(event);
243
+ }
244
+ }
245
+
246
+ /** Get the current turn count */
247
+ getTurnCount(): number {
248
+ return this.turnCount;
249
+ }
250
+
251
+ /** Reset the runner state */
252
+ reset(): void {
253
+ this.turnCount = 0;
254
+ this.currentText = '';
255
+ this.currentToolCalls = [];
256
+ this.currentToolResults = [];
257
+ }
258
+ }