@gencode/agents 0.0.3 → 0.0.5

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 (72) hide show
  1. package/dist/commands/help.js +6 -6
  2. package/dist/commands/help.js.map +1 -1
  3. package/dist/commands/new.d.ts.map +1 -1
  4. package/dist/commands/new.js +12 -1
  5. package/dist/commands/new.js.map +1 -1
  6. package/dist/commands/registry.js +5 -5
  7. package/dist/commands/registry.js.map +1 -1
  8. package/dist/plugins/hooks.d.ts +20 -1
  9. package/dist/plugins/hooks.d.ts.map +1 -1
  10. package/dist/plugins/hooks.js.map +1 -1
  11. package/dist/plugins/index.d.ts +1 -1
  12. package/dist/plugins/index.d.ts.map +1 -1
  13. package/dist/plugins/index.js.map +1 -1
  14. package/dist/plugins/loader.js +1 -1
  15. package/dist/plugins/loader.js.map +1 -1
  16. package/dist/plugins/runtime.d.ts +9 -1
  17. package/dist/plugins/runtime.d.ts.map +1 -1
  18. package/dist/plugins/runtime.js +24 -4
  19. package/dist/plugins/runtime.js.map +1 -1
  20. package/dist/runner/agent-runtime.d.ts +62 -0
  21. package/dist/runner/agent-runtime.d.ts.map +1 -0
  22. package/dist/runner/agent-runtime.js +179 -0
  23. package/dist/runner/agent-runtime.js.map +1 -0
  24. package/dist/runner/announce-loop.d.ts +41 -0
  25. package/dist/runner/announce-loop.d.ts.map +1 -0
  26. package/dist/runner/announce-loop.js +94 -0
  27. package/dist/runner/announce-loop.js.map +1 -0
  28. package/dist/runner/event-dispatcher.d.ts +31 -0
  29. package/dist/runner/event-dispatcher.d.ts.map +1 -0
  30. package/dist/runner/event-dispatcher.js +87 -0
  31. package/dist/runner/event-dispatcher.js.map +1 -0
  32. package/dist/runner/finalizer.d.ts +30 -0
  33. package/dist/runner/finalizer.d.ts.map +1 -0
  34. package/dist/runner/finalizer.js +75 -0
  35. package/dist/runner/finalizer.js.map +1 -0
  36. package/dist/runner/invocation-resolver.d.ts +67 -0
  37. package/dist/runner/invocation-resolver.d.ts.map +1 -0
  38. package/dist/runner/invocation-resolver.js +224 -0
  39. package/dist/runner/invocation-resolver.js.map +1 -0
  40. package/dist/runner/plugin-context.d.ts +18 -0
  41. package/dist/runner/plugin-context.d.ts.map +1 -0
  42. package/dist/runner/plugin-context.js +26 -0
  43. package/dist/runner/plugin-context.js.map +1 -0
  44. package/dist/runner/run-context.d.ts +38 -0
  45. package/dist/runner/run-context.d.ts.map +1 -0
  46. package/dist/runner/run-context.js +159 -0
  47. package/dist/runner/run-context.js.map +1 -0
  48. package/dist/runner/runner-session.d.ts +34 -0
  49. package/dist/runner/runner-session.d.ts.map +1 -0
  50. package/dist/runner/runner-session.js +61 -0
  51. package/dist/runner/runner-session.js.map +1 -0
  52. package/dist/runner/runner.d.ts +1 -2
  53. package/dist/runner/runner.d.ts.map +1 -1
  54. package/dist/runner/runner.js +115 -874
  55. package/dist/runner/runner.js.map +1 -1
  56. package/dist/runner/runtime.d.ts +7 -0
  57. package/dist/runner/runtime.d.ts.map +1 -0
  58. package/dist/runner/runtime.js +21 -0
  59. package/dist/runner/runtime.js.map +1 -0
  60. package/dist/runner/session-lifecycle.d.ts +31 -0
  61. package/dist/runner/session-lifecycle.d.ts.map +1 -0
  62. package/dist/runner/session-lifecycle.js +46 -0
  63. package/dist/runner/session-lifecycle.js.map +1 -0
  64. package/dist/runner/title.d.ts +3 -0
  65. package/dist/runner/title.d.ts.map +1 -0
  66. package/dist/runner/title.js +6 -0
  67. package/dist/runner/title.js.map +1 -0
  68. package/dist/runner/turn-executor.d.ts +51 -0
  69. package/dist/runner/turn-executor.d.ts.map +1 -0
  70. package/dist/runner/turn-executor.js +255 -0
  71. package/dist/runner/turn-executor.js.map +1 -0
  72. package/package.json +1 -1
@@ -1,46 +1,17 @@
1
- import { Agent } from "@mariozechner/pi-agent-core";
2
- import { registerApiProvider, registerBuiltInApiProviders } from "@mariozechner/pi-ai";
3
- import os from "node:os";
4
- import { createSession, ensureSession, loadTranscript, appendTranscriptEntry, saveSessionMetadata } from "../session/session.js";
5
1
  import { loadBootstrapFiles, buildBootstrapContextFiles } from "../bootstrap/bootstrap.js";
6
2
  import { loadSkillsWithPluginDirs } from "../skills/skills.js";
7
- import { buildSystemPrompt } from "../system-prompt/builder.js";
8
- import { createAgentTools } from "../tools/index.js";
9
- import { createBuiltinMemoryProvider } from "../memory/builtin-provider.js";
10
- import { resolveMemoryProvider } from "../memory/provider-registry.js";
11
- import { startMemoryWatchBridge } from "../memory/watch-bridge.js";
12
3
  import { SubagentRegistry } from "../subagent/registry.js";
13
- import { buildSubagentAnnounceMessage } from "../tools/subagent-spawn.js";
14
- import { manageHistory } from "../history/index.js";
15
- import { wrapToolsWithLoopDetection, } from "../loop-detection/tool-loop-detection.js";
16
- import { clearLoopDetectionState, getLoopDetectionState } from "../loop-detection/session-state.js";
17
- import path from "node:path";
18
- import { initializePluginSystem } from "../plugins/manager.js";
19
- import { PluginHookRegistry } from "../plugins/hooks.js";
20
- import { wrapToolsWithHooks } from "../plugins/tool-hooks.js";
21
- import { handleSlashCommand, parseResetCommand } from "../commands/index.js";
22
- import { handleNewCommand } from "../commands/new.js";
23
- import { handleResetCommand } from "../commands/reset.js";
24
- import { compactTranscriptNow } from "../commands/compact.js";
25
- import { LlmRequestError } from "../llm/client.js";
26
- import { streamOpenAICompletionsCompat, streamSimpleOpenAICompletionsCompat, } from "../llm/openai-completions-compat.js";
4
+ import { clearLoopDetectionState } from "../loop-detection/session-state.js";
27
5
  import { SkillUsageTracker } from "./skill-usage.js";
28
- // Register built-in API providers (must be called once at startup)
29
- registerBuiltInApiProviders();
30
- registerApiProvider({
31
- api: "openai-completions",
32
- stream: streamOpenAICompletionsCompat,
33
- streamSimple: streamSimpleOpenAICompletionsCompat,
34
- });
35
- /** Maximum rounds of subagent announce to prevent infinite delegation loops */
36
- const MAX_ANNOUNCE_ROUNDS = 10;
37
- const MEMORY_EVENT_DEDUPE_WINDOW_MS = 1_000;
38
- const EXTERNAL_WATCH_SUPPRESS_WINDOW_MS = 3_000;
39
- /** Generates a session title by truncating the first user message */
40
- export function generateSessionTitle(message, maxLength = 80) {
41
- const trimmed = message.trim().replace(/\s+/g, " ");
42
- return trimmed.length <= maxLength ? trimmed : trimmed.slice(0, maxLength - 1) + "…";
43
- }
6
+ import { ensureRunnerRuntimeInitialized } from "./runtime.js";
7
+ import { RunEventDispatcher } from "./event-dispatcher.js";
8
+ import { finalizeCompletedRun } from "./finalizer.js";
9
+ import { handleInvocation, prepareInvocation } from "./invocation-resolver.js";
10
+ import { createAgentRuntime } from "./agent-runtime.js";
11
+ import { executeAgentTurn, persistTurnEntriesWithWriter } from "./turn-executor.js";
12
+ import { runAnnounceLoop } from "./announce-loop.js";
13
+ import { createRunnerSession } from "./runner-session.js";
14
+ export { generateSessionTitle } from "./title.js";
44
15
  /**
45
16
  * Creates a Model object for use with vLLM/OpenAI-compatible APIs.
46
17
  */
@@ -68,519 +39,39 @@ function createOpenAICompatModel(params, sessionId) {
68
39
  },
69
40
  };
70
41
  }
71
- function normalizeMemoryChangedFiles(files) {
72
- const result = [];
73
- const seen = new Set();
74
- for (const raw of files) {
75
- const normalized = raw.replace(/\\/g, "/").replace(/^\.\//, "").trim();
76
- if (!normalized) {
77
- continue;
78
- }
79
- const canonical = normalized.toLowerCase() === "memory.md" ? "MEMORY.md" : normalized;
80
- const key = canonical.toLowerCase();
81
- if (seen.has(key)) {
82
- continue;
83
- }
84
- seen.add(key);
85
- result.push(canonical);
86
- }
87
- return result;
88
- }
89
- function isTextRunParams(params) {
90
- return typeof params.message === "string";
91
- }
92
- function extractLeadingUserTextMessage(messages) {
93
- for (let index = 0; index < messages.length; index += 1) {
94
- const candidate = messages[index];
95
- if (candidate.role !== "user") {
96
- continue;
97
- }
98
- if (typeof candidate.content === "string") {
99
- return { index, text: candidate.content };
100
- }
101
- if (!Array.isArray(candidate.content) || candidate.content.length === 0) {
102
- return null;
103
- }
104
- const firstBlock = candidate.content[0];
105
- if (firstBlock?.type === "text" && typeof firstBlock.text === "string") {
106
- return { index, text: firstBlock.text };
107
- }
108
- return null;
109
- }
110
- return null;
111
- }
112
- function replaceLeadingUserTextMessage(messages, replacement) {
113
- const match = extractLeadingUserTextMessage(messages);
114
- if (!match) {
115
- return messages;
116
- }
117
- return messages.map((message, index) => {
118
- if (index !== match.index) {
119
- return message;
120
- }
121
- const candidate = message;
122
- if (typeof candidate.content === "string") {
123
- return { ...message, content: replacement };
124
- }
125
- if (Array.isArray(candidate.content) && candidate.content.length > 0) {
126
- return {
127
- ...message,
128
- content: candidate.content.map((block, blockIndex) => {
129
- if (blockIndex !== 0) {
130
- return block;
131
- }
132
- const typedBlock = block;
133
- if (typedBlock?.type === "text" && typeof typedBlock.text === "string") {
134
- return { ...typedBlock, text: replacement };
135
- }
136
- return block;
137
- }),
138
- };
139
- }
140
- return message;
141
- });
142
- }
143
- function serializeUserInput(input) {
144
- return typeof input === "string" ? input : JSON.stringify(input);
145
- }
146
- /** Convert a captured AssistantMessage to an AssistantEntry for the transcript */
147
- function assistantMessageToEntry(msg) {
148
- const text = msg.content
149
- .filter((b) => b.type === "text")
150
- .map((b) => b.text)
151
- .join("");
152
- const toolCalls = msg.content
153
- .filter((b) => b.type === "toolCall")
154
- .map((b) => ({ id: b.id, name: b.name, arguments: b.arguments }));
155
- if (text.trim().length === 0 && toolCalls.length === 0) {
156
- return null;
157
- }
158
- const entry = { role: "assistant", content: text, timestamp: new Date().toISOString() };
159
- if (toolCalls.length > 0)
160
- entry.toolCalls = toolCalls;
161
- return entry;
162
- }
163
- /** Convert a captured tool result to a ToolResultEntry for the transcript */
164
- function capturedToolResultToEntry(tr) {
165
- return {
166
- role: "tool_result",
167
- toolCallId: tr.toolCallId,
168
- toolName: tr.toolName,
169
- content: tr.content,
170
- isError: tr.isError,
171
- timestamp: new Date().toISOString(),
172
- };
173
- }
174
- // ── Callbacks ─────────────────────────────────────────────────────────────
175
- /**
176
- * Sends an HTTP callback to the given URL.
177
- * Errors are silently ignored to not interfere with the main flow.
178
- */
179
- async function sendCallback(url, payload) {
180
- try {
181
- await fetch(url, {
182
- method: "POST",
183
- headers: { "content-type": "application/json" },
184
- body: JSON.stringify(payload),
185
- });
186
- }
187
- catch {
188
- // Callback failures are non-fatal
189
- }
190
- }
191
- /**
192
- * Dispatches a progress event to both the in-process callback and the HTTP callback.
193
- */
194
- async function dispatchProgress(sessionId, event, params) {
195
- const eventWithMessageId = params.messageId ? { ...event, messageId: params.messageId } : event;
196
- await params.onProgress?.(eventWithMessageId);
197
- if (params.callbackUrl) {
198
- await sendCallback(params.callbackUrl, {
199
- sessionId,
200
- channel: params.channel,
201
- messageId: params.messageId,
202
- type: "progress",
203
- event: eventWithMessageId,
204
- });
205
- }
206
- }
207
- // ── Helpers ───────────────────────────────────────────────────────────────
208
- /**
209
- * Builds a single announce message from a batch of completed subagent runs.
210
- * Batching reduces the number of extra LLM turns needed.
211
- */
212
- function buildBatchAnnounceMessage(completed) {
213
- if (completed.length === 1) {
214
- const r = completed[0];
215
- return buildSubagentAnnounceMessage({
216
- task: r.task,
217
- label: r.label,
218
- status: r.status,
219
- result: r.result,
220
- error: r.error,
221
- });
222
- }
223
- const parts = completed.map((r) => buildSubagentAnnounceMessage({
224
- task: r.task,
225
- label: r.label,
226
- status: r.status,
227
- result: r.result,
228
- error: r.error,
229
- }));
230
- return `[${completed.length} subagents completed]\n\n${parts.join("\n\n---\n\n")}`;
231
- }
232
- // ── Agent turn runner ─────────────────────────────────────────────────────
233
- /**
234
- * Runs one invocation of agent.prompt() and collects text output, usage, and
235
- * full turn records (via `turn_end` events when the SDK fires them).
236
- */
237
- async function runAgentTurn(agent, message, sessionId, modelId, historyMessages, params, skillUsageTracker, hooks, hookCtx, abortSignal) {
238
- let pendingTextChunk = "";
239
- let lastTextChunk = "";
240
- let inputTokens = 0;
241
- let outputTokens = 0;
242
- let runError;
243
- let sdkError;
244
- const turnRecords = [];
245
- const flushPendingTextChunk = async () => {
246
- if (!pendingTextChunk) {
247
- return;
248
- }
249
- lastTextChunk = pendingTextChunk;
250
- await dispatchProgress(sessionId, { type: "text", text: pendingTextChunk }, params);
251
- pendingTextChunk = "";
252
- };
253
- const unsubscribe = agent.subscribe(async (event) => {
254
- if (event.type === "message_update") {
255
- const assistantEvent = event.assistantMessageEvent;
256
- if (assistantEvent.type === "text_delta" && typeof assistantEvent.delta === "string") {
257
- pendingTextChunk += assistantEvent.delta;
258
- }
259
- if (assistantEvent.type === "done") {
260
- const msg = assistantEvent.message;
261
- if (msg.usage) {
262
- inputTokens = msg.usage.input;
263
- outputTokens = msg.usage.output;
264
- }
265
- await flushPendingTextChunk();
266
- }
267
- }
268
- else if (event.type === "message_end") {
269
- const msg = event.message;
270
- if (msg.usage) {
271
- inputTokens = msg.usage.input;
272
- outputTokens = msg.usage.output;
273
- }
274
- }
275
- else if (event.type === "turn_end") {
276
- // Capture full turn for high-fidelity transcript persistence
277
- const msg = event.message;
278
- if (msg.stopReason === "error" || msg.stopReason === "aborted") {
279
- sdkError = msg.errorMessage ?? (msg.stopReason === "aborted" ? "aborted" : "Unknown LLM error");
280
- }
281
- const rawResults = (event.toolResults ?? []);
282
- const toolResults = rawResults.map((r) => ({
283
- toolCallId: r.toolCallId,
284
- toolName: r.toolName,
285
- content: r.content
286
- .filter((c) => c.type === "text")
287
- .map((c) => c.text ?? "")
288
- .join(""),
289
- isError: r.isError,
290
- }));
291
- turnRecords.push({ message: msg, toolResults });
292
- }
293
- else if (event.type === "tool_execution_start") {
294
- await flushPendingTextChunk();
295
- skillUsageTracker.onToolExecutionStart({ toolName: event.toolName, args: event.args });
296
- await dispatchProgress(sessionId, { type: "tool_start", name: event.toolName, input: event.args }, params);
297
- }
298
- else if (event.type === "tool_execution_end") {
299
- await skillUsageTracker.onToolExecutionEnd({ toolName: event.toolName, isError: event.isError });
300
- const output = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
301
- await dispatchProgress(sessionId, { type: "tool_end", name: event.toolName, output, isError: event.isError }, params);
302
- }
303
- });
304
- try {
305
- await hooks.dispatch("llm_input", {
306
- sessionId,
307
- model: modelId,
308
- prompt: typeof message === "string" ? message : JSON.stringify(message),
309
- historyMessages,
310
- }, hookCtx);
311
- if (abortSignal?.aborted) {
312
- throw new Error("aborted");
313
- }
314
- if (typeof message === "string") {
315
- await agent.prompt(message);
316
- }
317
- else {
318
- await agent.prompt(message);
319
- }
320
- if (!runError) {
321
- const stateError = typeof agent.state.error === "string" ? agent.state.error : undefined;
322
- const latentError = sdkError ?? stateError;
323
- if (latentError) {
324
- runError = formatRunError(latentError);
325
- await dispatchProgress(sessionId, { type: "error", message: runError }, params);
326
- }
327
- }
328
- }
329
- catch (err) {
330
- runError = formatRunError(err);
331
- await dispatchProgress(sessionId, { type: "error", message: runError }, params);
332
- }
333
- finally {
334
- unsubscribe();
335
- }
336
- const lastAssistant = turnRecords.length > 0 ? turnRecords[turnRecords.length - 1]?.message : undefined;
337
- await hooks.dispatch("llm_output", {
338
- sessionId,
339
- model: modelId,
340
- assistantTexts: lastTextChunk ? [lastTextChunk] : [],
341
- lastAssistant,
342
- usage: { input: inputTokens, output: outputTokens, total: inputTokens + outputTokens },
343
- }, hookCtx);
344
- return { text: lastTextChunk, inputTokens, outputTokens, error: runError, turnRecords };
345
- }
346
- function formatRunError(error) {
347
- const llmError = toLlmRequestErrorLike(error);
348
- if (llmError) {
349
- if (llmError.code === "http_error" && llmError.statusCode) {
350
- return llmError.providerMessage
351
- ? `LLM upstream returned HTTP ${llmError.statusCode}${llmError.statusText ? ` ${llmError.statusText}` : ""}: ${llmError.providerMessage}`
352
- : `LLM upstream returned HTTP ${llmError.statusCode}${llmError.statusText ? ` ${llmError.statusText}` : ""}`;
353
- }
354
- if (llmError.code === "timeout") {
355
- return "LLM request timed out";
356
- }
357
- if (llmError.code === "network_error") {
358
- return llmError.message;
359
- }
360
- if (llmError.code === "aborted") {
361
- return "LLM request was aborted";
362
- }
363
- return llmError.message;
364
- }
365
- return error instanceof Error ? error.message : String(error);
366
- }
367
- function toLlmRequestErrorLike(error) {
368
- if (error instanceof LlmRequestError) {
369
- return error;
370
- }
371
- if (!(error instanceof Error)) {
372
- return null;
373
- }
374
- const candidate = error;
375
- if (error.name !== "LlmRequestError" && !candidate.code) {
376
- return null;
377
- }
378
- return {
379
- message: error.message,
380
- code: candidate.code,
381
- statusCode: candidate.statusCode,
382
- statusText: candidate.statusText,
383
- providerMessage: candidate.providerMessage,
384
- };
385
- }
386
- /**
387
- * Persist the assistant's response to the transcript.
388
- * Uses turn_end records (with tool calls/results) when available;
389
- * falls back to a plain text entry otherwise.
390
- */
391
- async function persistTurnEntries(dataDir, sessionId, turn, options) {
392
- if (turn.turnRecords.length > 0) {
393
- for (const record of turn.turnRecords) {
394
- const assistantEntry = assistantMessageToEntry(record.message);
395
- if (assistantEntry) {
396
- await appendTranscriptEntry(dataDir, sessionId, assistantEntry, options);
397
- }
398
- for (const tr of record.toolResults) {
399
- await appendTranscriptEntry(dataDir, sessionId, capturedToolResultToEntry(tr), options);
400
- }
401
- }
402
- }
403
- else if (turn.text) {
404
- await appendTranscriptEntry(dataDir, sessionId, {
405
- role: "assistant",
406
- content: turn.text,
407
- timestamp: new Date().toISOString(),
408
- }, options);
409
- }
410
- }
411
42
  // ── Internal runner ───────────────────────────────────────────────────────
412
43
  /**
413
44
  * Internal runner shared by both top-level and subagent invocations.
414
45
  */
415
46
  async function runAgentInternal(params, registry) {
47
+ ensureRunnerRuntimeInitialized();
416
48
  const startTime = Date.now();
417
- const inputMode = isTextRunParams(params) ? "text" : "messages";
418
- const rawMessage = inputMode === "text" ? params.message : undefined;
419
- const inputMessages = inputMode === "messages" ? params.messages : undefined;
420
- const structuredUserText = inputMessages ? extractLeadingUserTextMessage(inputMessages) : null;
421
- const slashCommandSource = rawMessage ?? structuredUserText?.text;
422
- const resetCommand = slashCommandSource ? parseResetCommand(slashCommandSource) : null;
423
- const resetRemainder = resetCommand?.remainder ?? "";
424
- const resetShortCircuit = Boolean(resetCommand && !resetRemainder);
425
- const messageForRun = resetCommand && resetRemainder ? resetRemainder : slashCommandSource;
426
- const promptInput = inputMode === "text"
427
- ? (messageForRun ?? "")
428
- : (resetCommand && resetRemainder && inputMessages
429
- ? replaceLeadingUserTextMessage(inputMessages, resetRemainder)
430
- : inputMessages);
431
- let transcriptMessage = inputMode === "text"
432
- ? (resetShortCircuit ? rawMessage : (messageForRun ?? ""))
433
- : serializeUserInput(promptInput);
49
+ const eventDispatcher = new RunEventDispatcher(params);
50
+ const invocation = prepareInvocation(params);
51
+ let transcriptMessage = invocation.transcriptMessage;
434
52
  let initialUserEntryPersisted = false;
435
- const previousSessionId = resetCommand ? params.sessionId : undefined;
436
- const workspaceDir = path.join(params.dataDir, "workspace");
437
- // 1. Resolve or create session
438
- let sessionId = resetCommand ? undefined : params.sessionId;
439
- const isNewSession = !sessionId;
440
- if (sessionId) {
441
- await ensureSession(params.dataDir, sessionId);
442
- }
443
- else {
444
- sessionId = await createSession(params.dataDir, params.channel);
445
- }
446
- const hookContext = {
447
- sessionId,
448
- workspaceDir,
449
- channel: params.channel,
450
- };
451
- const pluginSystem = params.plugins
452
- ? initializePluginSystem({
453
- ...params.plugins,
454
- runtime: {
455
- llm: params.llm,
456
- hookCtx: hookContext,
457
- llmAllowlist: params.plugins.llmAllowlist,
458
- },
459
- })
460
- : undefined;
461
- const hookRegistry = pluginSystem?.registry.hooks ?? new PluginHookRegistry();
462
- const memoryProviderId = params.memory?.providerId;
463
- const memoryPluginId = params.memory?.pluginId ?? pluginSystem?.normalizedConfig.slots?.memory;
464
- const recentMemoryWrites = new Map();
465
- const recentMemoryEvents = new Map();
466
- const emitMemoryChanged = async (event) => {
467
- const now = Date.now();
468
- const normalizedFiles = normalizeMemoryChangedFiles(event.files);
469
- if (normalizedFiles.length === 0) {
470
- return;
471
- }
472
- const filteredFiles = event.reason === "external-watch" && event.source === "memory"
473
- ? normalizedFiles.filter((file) => {
474
- const lastWriteAt = recentMemoryWrites.get(file.toLowerCase()) ?? 0;
475
- return now - lastWriteAt > EXTERNAL_WATCH_SUPPRESS_WINDOW_MS;
476
- })
477
- : normalizedFiles;
478
- if (filteredFiles.length === 0) {
479
- return;
480
- }
481
- if (event.reason !== "external-watch" && event.source === "memory") {
482
- for (const file of filteredFiles) {
483
- recentMemoryWrites.set(file.toLowerCase(), now);
484
- }
485
- }
486
- const dedupeKey = `${event.reason}|${event.source}|${filteredFiles
487
- .map((file) => file.toLowerCase())
488
- .sort()
489
- .join("|")}`;
490
- const previousEventAt = recentMemoryEvents.get(dedupeKey) ?? 0;
491
- if (now - previousEventAt < MEMORY_EVENT_DEDUPE_WINDOW_MS) {
492
- return;
493
- }
494
- recentMemoryEvents.set(dedupeKey, now);
495
- const eventToEmit = {
496
- ...event,
497
- files: filteredFiles,
498
- };
499
- await dispatchProgress(event.sessionId ?? sessionId, {
500
- type: "memory_changed",
501
- reason: eventToEmit.reason,
502
- files: eventToEmit.files,
503
- source: eventToEmit.source,
504
- providerId: eventToEmit.providerId,
505
- timestamp: eventToEmit.timestamp,
506
- }, params).catch(() => { });
507
- await hookRegistry
508
- .dispatch("memory_changed", eventToEmit, {
509
- ...hookContext,
510
- sessionId: eventToEmit.sessionId ?? hookContext.sessionId,
511
- })
512
- .catch(() => { });
513
- };
514
- const transcriptOptions = memoryProviderId || memoryPluginId
515
- ? {
516
- providerId: memoryProviderId,
517
- pluginId: memoryPluginId,
518
- onMemoryChanged: emitMemoryChanged,
519
- }
520
- : { onMemoryChanged: emitMemoryChanged };
521
- const persistInitialUserEntry = async () => {
522
- if (initialUserEntryPersisted) {
523
- return;
524
- }
525
- await appendTranscriptEntry(params.dataDir, sessionId, {
526
- role: "user",
527
- content: transcriptMessage,
528
- timestamp: new Date().toISOString(),
529
- }, transcriptOptions);
530
- initialUserEntryPersisted = true;
531
- };
532
- const memoryRoot = path.join(params.dataDir, ".aimax");
533
- const resolvedProvider = resolveMemoryProvider({
534
- providerId: memoryProviderId,
535
- pluginId: memoryPluginId,
536
- dataDir: params.dataDir,
537
- memoryDir: memoryRoot,
538
- sessionId,
53
+ const previousSessionId = invocation.previousSessionId;
54
+ const session = await createRunnerSession({
55
+ runParams: params,
56
+ requestedSessionId: invocation.requestedSessionId,
57
+ eventDispatcher,
539
58
  });
540
- const provider = resolvedProvider?.provider ??
541
- createBuiltinMemoryProvider({ dataDir: params.dataDir, memoryDir: memoryRoot, sessionId });
542
- const stopMemoryWatchBridge = startMemoryWatchBridge({
543
- dataDir: params.dataDir,
544
- sessionId,
545
- providerId: memoryProviderId ?? memoryPluginId ?? provider.id,
546
- provider,
547
- onMemoryChanged: emitMemoryChanged,
59
+ const sessionId = session.sessionId;
60
+ const isNewSession = session.isNewSession;
61
+ const workspaceDir = session.workspaceDir;
62
+ const hookContext = session.hookContext;
63
+ const hookRegistry = session.hookRegistry;
64
+ const pluginContext = session.pluginContext;
65
+ const runContext = session.runContext;
66
+ if (invocation.resetCommand) {
67
+ initialUserEntryPersisted = await runContext.persistInitialUserEntry(transcriptMessage);
68
+ }
69
+ await session.start({
70
+ resetCommand: invocation.resetCommand,
71
+ previousSessionId,
72
+ rawMessage: invocation.rawMessage,
73
+ startMessage: typeof invocation.promptInput === "string" ? invocation.promptInput : transcriptMessage,
548
74
  });
549
- if (provider.sync) {
550
- void provider.sync("session-start").catch(() => { });
551
- }
552
- if (resetCommand) {
553
- await persistInitialUserEntry();
554
- }
555
- if (resetCommand && params.callbackUrl) {
556
- await sendCallback(params.callbackUrl, {
557
- sessionId,
558
- channel: params.channel,
559
- messageId: params.messageId,
560
- type: "session_reset",
561
- action: resetCommand.action,
562
- previousSessionId,
563
- message: rawMessage,
564
- });
565
- }
566
- if (resetCommand) {
567
- await hookRegistry.dispatch("session_reset", {
568
- action: resetCommand.action,
569
- sessionId,
570
- previousSessionId,
571
- message: rawMessage,
572
- }, hookContext);
573
- }
574
- if (params.callbackUrl) {
575
- await sendCallback(params.callbackUrl, {
576
- sessionId,
577
- channel: params.channel,
578
- messageId: params.messageId,
579
- type: "start",
580
- message: typeof promptInput === "string" ? promptInput : transcriptMessage,
581
- });
582
- }
583
- await hookRegistry.dispatch("session_start", { sessionId }, hookContext);
584
75
  // 2. Set up abort controller early (needed for manageHistory signal)
585
76
  const abortController = new AbortController();
586
77
  if (params.abortSignal?.aborted) {
@@ -596,219 +87,57 @@ async function runAgentInternal(params, registry) {
596
87
  warn: (message) => bootstrapWarnings.push(message),
597
88
  });
598
89
  // 4. Load skills (user + plugin skills)
599
- const pluginSkillDirs = pluginSystem?.registry.skills ?? [];
600
- const skills = await loadSkillsWithPluginDirs(params.dataDir, pluginSkillDirs);
90
+ const skills = await loadSkillsWithPluginDirs(params.dataDir, pluginContext.pluginSkillDirs);
601
91
  const skillUsageTracker = new SkillUsageTracker({
602
92
  workspaceDir,
603
93
  sessionId,
604
94
  skills,
605
- report: (event) => dispatchProgress(sessionId, event, params),
95
+ report: (event) => eventDispatcher.dispatchProgress(sessionId, event),
606
96
  });
607
- if (resetShortCircuit) {
608
- const resetReply = resetCommand?.action === "reset" ? handleResetCommand() : handleNewCommand();
609
- return finalizeSlashCommandResult({
610
- replyText: resetReply.text,
611
- sessionId,
612
- isNewSession,
613
- transcriptMessage,
614
- initialUserEntryPersisted,
615
- params,
616
- hookRegistry,
617
- hookContext,
618
- startTime,
619
- });
620
- }
621
- let effectivePrompt = promptInput;
622
- if (slashCommandSource) {
623
- const slashResult = handleSlashCommand({ message: messageForRun ?? "", skills });
624
- if (slashResult.kind === "reply") {
625
- return finalizeSlashCommandResult({
626
- replyText: slashResult.text,
627
- sessionId,
628
- isNewSession,
629
- transcriptMessage,
630
- params,
631
- hookRegistry,
632
- hookContext,
633
- startTime,
634
- });
635
- }
636
- if (slashResult.kind === "compact") {
637
- const rawHistory = params.channel === "CRON" ? [] : await loadTranscript(params.dataDir, sessionId);
638
- const compactResult = await compactTranscriptNow({
639
- entries: rawHistory,
640
- contextWindowTokens: params.llm.contextWindow ?? 200_000,
641
- llm: {
642
- baseUrl: params.llm.baseUrl,
643
- apiKey: params.llm.apiKey,
644
- model: params.llm.model,
645
- },
646
- instructions: slashResult.instructions,
647
- signal: params.abortSignal,
648
- hooks: hookRegistry,
649
- hookCtx: hookContext,
650
- });
651
- const compactText = compactResult.status === "compacted"
652
- ? `⚙️ Compacted (kept ${compactResult.keptCount}, dropped ${compactResult.droppedCount}).`
653
- : `⚙️ Compaction skipped: ${compactResult.reason}`;
654
- return finalizeSlashCommandResult({
655
- replyText: compactText,
656
- sessionId,
657
- isNewSession,
658
- transcriptMessage,
659
- initialUserEntryPersisted,
660
- params,
661
- hookRegistry,
662
- hookContext,
663
- startTime,
664
- compactionEntry: compactResult.status === "compacted" ? compactResult.entry : undefined,
665
- });
666
- }
667
- if (slashResult.kind === "rewrite") {
668
- effectivePrompt = inputMode === "text"
669
- ? slashResult.message
670
- : replaceLeadingUserTextMessage(promptInput, slashResult.message);
671
- transcriptMessage = serializeUserInput(effectivePrompt);
672
- }
673
- else if (inputMode === "text") {
674
- effectivePrompt = messageForRun ?? "";
675
- transcriptMessage = typeof effectivePrompt === "string" ? effectivePrompt : transcriptMessage;
676
- }
97
+ const appendEntry = (entry) => runContext.appendTranscriptEntry(entry);
98
+ const invocationExecution = await handleInvocation({
99
+ invocation,
100
+ skills,
101
+ sessionId,
102
+ isNewSession,
103
+ initialUserEntryPersisted,
104
+ runParams: params,
105
+ hookRegistry,
106
+ hookContext,
107
+ startTime,
108
+ eventDispatcher,
109
+ });
110
+ if (invocationExecution.kind === "completed") {
111
+ return invocationExecution.result;
677
112
  }
113
+ const effectivePrompt = invocationExecution.effectivePrompt;
114
+ transcriptMessage = invocationExecution.transcriptMessage;
678
115
  const effectivePromptText = typeof effectivePrompt === "string" ? effectivePrompt : transcriptMessage;
679
- // 5. Load and manage conversation history
680
- const skipHistory = params.channel === "CRON";
681
- const rawHistory = skipHistory ? [] : await loadTranscript(params.dataDir, sessionId);
682
- await hookRegistry.dispatch("before_compaction", { messageCount: rawHistory.length }, hookContext);
683
- const modelInfo = { model: params.llm.model, api: "openai-completions" };
684
- const historyResult = skipHistory
685
- ? {
686
- messages: [],
687
- priorSummary: undefined,
688
- compactionEntry: undefined,
689
- stats: { originalCount: 0, keptCount: 0, estimatedTokens: 0, compacted: false },
690
- }
691
- : await manageHistory({
692
- entries: rawHistory,
693
- modelInfo,
694
- contextWindowTokens: params.llm.contextWindow ?? 200_000,
695
- llm: {
696
- baseUrl: params.llm.baseUrl,
697
- apiKey: params.llm.apiKey,
698
- model: params.llm.model,
699
- },
700
- historyLimit: params.historyLimit,
701
- compactionEnabled: true,
702
- signal: abortController.signal,
703
- hooks: hookRegistry,
704
- hookCtx: hookContext,
705
- });
706
- const depth = params.subagentContext?.depth ?? 0;
707
- const baseTools = createAgentTools(params.dataDir, {
708
- registry,
709
- parentSessionId: sessionId,
710
- depth,
711
- channel: params.channel,
712
- llm: params.llm,
713
- loopDetection: params.loopDetection,
714
- memoryOptions: {
715
- providerId: memoryProviderId,
716
- pluginId: memoryPluginId,
116
+ const runtime = await createAgentRuntime({
117
+ session: {
118
+ runParams: params,
717
119
  sessionId,
718
- onMemoryChanged: emitMemoryChanged,
719
- },
720
- spawnFn: (childParams) => runAgentInternal(childParams, new SubagentRegistry()),
721
- });
722
- const pluginTools = pluginSystem?.registry.tools.resolveEnabled(params.plugins?.toolAllowlist) ?? [];
723
- const rawTools = [...baseTools, ...pluginTools];
724
- const toolNames = rawTools
725
- .map((tool) => (typeof tool.name === "string" ? tool.name.trim() : ""))
726
- .filter(Boolean);
727
- const toolSummaries = {};
728
- for (const tool of rawTools) {
729
- const name = typeof tool.name === "string" ? tool.name.trim() : "";
730
- const description = typeof tool.description === "string" ? tool.description.trim() : "";
731
- if (!name || !description || toolSummaries[name])
732
- continue;
733
- toolSummaries[name] = description;
734
- }
735
- const messagingEnabled = params.messaging?.enabled ?? params.channel !== "CRON";
736
- const messagingChannels = params.messaging?.channels && params.messaging.channels.length > 0
737
- ? params.messaging.channels
738
- : [params.channel];
739
- // 6. Build system prompt (with prior summary injected when available)
740
- let systemPrompt = buildSystemPrompt({
741
- dataDir: params.dataDir,
742
- skills,
743
- contextFiles,
744
- toolNames,
745
- toolSummaries,
746
- promptMode: depth > 0 ? "minimal" : "full",
747
- bootstrapWarnings,
748
- memoryCitationsMode: params.memory?.citationsMode ?? "off",
749
- messaging: {
750
- enabled: messagingEnabled,
751
- channels: messagingChannels,
752
- },
753
- docs: {
754
- localPath: params.docs?.localPath,
755
- webUrl: params.docs?.webUrl,
756
- sourceUrl: params.docs?.sourceUrl,
120
+ hookRegistry,
121
+ hookContext,
122
+ runContext,
123
+ eventDispatcher,
757
124
  },
758
- sandboxInfo: {
759
- enabled: true,
760
- hostWorkspaceDir: workspaceDir,
761
- containerWorkspaceDir: workspaceDir,
125
+ runtimeInputs: {
126
+ contextFiles,
127
+ bootstrapWarnings,
128
+ skills,
129
+ effectivePromptText,
130
+ pluginTools: pluginContext.pluginTools,
762
131
  },
763
- runtimeInfo: {
764
- os: process.platform,
765
- node: process.version,
766
- model: params.llm.model,
767
- hostname: os.hostname(),
132
+ dependencies: {
133
+ registry,
134
+ spawnFn: (childParams) => runAgentInternal(childParams, new SubagentRegistry()),
135
+ createModel: createOpenAICompatModel,
136
+ abortSignal: abortController.signal,
768
137
  },
769
- currentDate: new Date().toISOString().split("T")[0],
770
- priorConversationSummary: historyResult.priorSummary,
771
- });
772
- // 7. Persist new compaction entry (if history was summarised this run)
773
- if (historyResult.compactionEntry) {
774
- await appendTranscriptEntry(params.dataDir, sessionId, historyResult.compactionEntry, transcriptOptions);
775
- await dispatchProgress(sessionId, {
776
- type: "compaction",
777
- reason: `Summarised ${historyResult.stats.originalCount - historyResult.stats.keptCount} older entries`,
778
- }, params);
779
- await hookRegistry.dispatch("after_compaction", {
780
- messageCount: historyResult.stats.originalCount,
781
- compactedCount: historyResult.stats.originalCount - historyResult.stats.keptCount,
782
- }, hookContext);
783
- }
784
- const beforePromptResults = await hookRegistry.dispatch("before_prompt_build", { prompt: effectivePromptText }, hookContext);
785
- for (const result of beforePromptResults) {
786
- if (!result)
787
- continue;
788
- if (result.systemPrompt) {
789
- systemPrompt = result.systemPrompt;
790
- }
791
- if (result.prependContext) {
792
- systemPrompt = `${result.prependContext}\n\n${systemPrompt}`;
793
- }
794
- }
795
- // 8. Create tools (subagent tools included for all agents)
796
- const toolsWithHooks = wrapToolsWithHooks(rawTools, hookRegistry, hookContext);
797
- const tools = wrapToolsWithLoopDetection(toolsWithHooks, { sessionId, config: params.loopDetection }, getLoopDetectionState);
798
- // 9. Create the model and agent
799
- let resolvedModelId = params.llm.model;
800
- const beforeModelResults = await hookRegistry.dispatch("before_model_resolve", { prompt: effectivePromptText }, hookContext);
801
- for (const result of beforeModelResults) {
802
- if (result && result.modelOverride) {
803
- resolvedModelId = result.modelOverride;
804
- }
805
- }
806
- const model = createOpenAICompatModel({ ...params.llm, model: resolvedModelId }, sessionId);
807
- const apiKey = params.llm.apiKey;
808
- const agent = new Agent({
809
- initialState: { systemPrompt, model, tools, messages: [] },
810
- getApiKey: (_provider) => apiKey,
811
138
  });
139
+ const agent = runtime.agent;
140
+ const resolvedModelId = runtime.resolvedModelId;
812
141
  // 10. Wire timeout to abort the agent
813
142
  const timeoutMs = params.timeoutMs ?? 600_000;
814
143
  const timeoutHandle = setTimeout(() => {
@@ -817,10 +146,6 @@ async function runAgentInternal(params, registry) {
817
146
  }, timeoutMs);
818
147
  // Also propagate future aborts to the agent
819
148
  abortController.signal.addEventListener("abort", () => agent.abort());
820
- // 11. Restore managed history into the agent
821
- if (historyResult.messages.length > 0) {
822
- agent.replaceMessages(historyResult.messages);
823
- }
824
149
  let totalInputTokens = 0;
825
150
  let totalOutputTokens = 0;
826
151
  let lastResponseText = "";
@@ -828,155 +153,71 @@ async function runAgentInternal(params, registry) {
828
153
  try {
829
154
  // 12. Persist the initial user turn before the first model call so the
830
155
  // transcript still records the request if the turn aborts or fails.
831
- await persistInitialUserEntry();
156
+ initialUserEntryPersisted = (await runContext.persistInitialUserEntry(transcriptMessage)) || initialUserEntryPersisted;
832
157
  // 13. First agent turn
833
- const turn = await runAgentTurn(agent, effectivePrompt, sessionId, resolvedModelId, historyResult.messages, params, skillUsageTracker, hookRegistry, hookContext, abortController.signal);
158
+ const turn = await executeAgentTurn({
159
+ agent,
160
+ message: effectivePrompt,
161
+ sessionId,
162
+ modelId: resolvedModelId,
163
+ historyMessages: runtime.historyMessages,
164
+ eventDispatcher,
165
+ skillUsageTracker,
166
+ hooks: hookRegistry,
167
+ hookCtx: hookContext,
168
+ abortSignal: abortController.signal,
169
+ });
834
170
  lastResponseText = turn.text;
835
171
  totalInputTokens += turn.inputTokens;
836
172
  totalOutputTokens += turn.outputTokens;
837
173
  if (turn.error)
838
174
  runError = turn.error;
839
175
  // 14. Persist the assistant/tool outputs from the first turn.
840
- await persistTurnEntries(params.dataDir, sessionId, turn, transcriptOptions);
841
- // 15. Announce loop — deliver subagent results back to the parent agent.
842
- let announceRound = 0;
843
- while (!abortController.signal.aborted &&
844
- registry.needsAnnounce(sessionId) &&
845
- announceRound < MAX_ANNOUNCE_ROUNDS) {
846
- announceRound++;
847
- await registry.waitForAll(sessionId);
848
- const completed = registry.consumeCompleted(sessionId);
849
- if (completed.length === 0)
850
- break;
851
- for (const r of completed) {
852
- await dispatchProgress(sessionId, {
853
- type: "subagent_complete",
854
- childSessionId: r.childSessionId,
855
- task: r.task,
856
- status: r.status,
857
- }, params);
858
- }
859
- const announceMsg = buildBatchAnnounceMessage(completed);
860
- await appendTranscriptEntry(params.dataDir, sessionId, {
861
- role: "user",
862
- content: announceMsg,
863
- timestamp: new Date().toISOString(),
864
- }, transcriptOptions);
865
- const announceTurn = await runAgentTurn(agent, announceMsg, sessionId, resolvedModelId, [], params, skillUsageTracker, hookRegistry, hookContext, abortController.signal);
866
- lastResponseText = announceTurn.text;
867
- totalInputTokens += announceTurn.inputTokens;
868
- totalOutputTokens += announceTurn.outputTokens;
869
- if (announceTurn.error && !runError)
870
- runError = announceTurn.error;
871
- await persistTurnEntries(params.dataDir, sessionId, announceTurn, transcriptOptions);
176
+ await persistTurnEntriesWithWriter(appendEntry, turn);
177
+ const announceResult = await runAnnounceLoop({
178
+ agent,
179
+ registry,
180
+ sessionId,
181
+ resolvedModelId,
182
+ eventDispatcher,
183
+ skillUsageTracker,
184
+ hookRegistry,
185
+ hookContext,
186
+ abortSignal: abortController.signal,
187
+ appendEntry,
188
+ });
189
+ if (announceResult.text) {
190
+ lastResponseText = announceResult.text;
191
+ }
192
+ totalInputTokens += announceResult.inputTokens;
193
+ totalOutputTokens += announceResult.outputTokens;
194
+ if (announceResult.error && !runError) {
195
+ runError = announceResult.error;
872
196
  }
873
197
  }
874
198
  finally {
875
199
  clearTimeout(timeoutHandle);
876
200
  clearLoopDetectionState(sessionId);
877
- stopMemoryWatchBridge();
201
+ runContext.stop();
878
202
  }
879
- // 15. Save session metadata for new sessions
880
- if (isNewSession) {
881
- const title = generateSessionTitle(transcriptMessage);
882
- const now = new Date().toISOString();
883
- await saveSessionMetadata(params.dataDir, {
884
- id: sessionId,
885
- title,
886
- channel: params.channel,
887
- createdAt: now,
888
- updatedAt: now,
889
- });
890
- }
891
- const durationMs = Date.now() - startTime;
892
203
  const usage = {
893
204
  input: totalInputTokens,
894
205
  output: totalOutputTokens,
895
206
  total: totalInputTokens + totalOutputTokens,
896
207
  };
897
- const result = {
208
+ return finalizeCompletedRun({
898
209
  sessionId,
210
+ isNewSession,
211
+ transcriptMessage,
212
+ runParams: params,
213
+ hookRegistry,
214
+ hookContext,
215
+ startTime,
216
+ eventDispatcher,
899
217
  text: lastResponseText,
900
218
  usage,
901
- durationMs,
902
219
  error: runError,
903
- };
904
- await hookRegistry.dispatch("agent_end", { success: !runError, error: runError, durationMs }, hookContext);
905
- const finalTranscript = await loadTranscript(params.dataDir, sessionId);
906
- await hookRegistry.dispatch("session_end", { sessionId, messageCount: finalTranscript.length, durationMs }, hookContext);
907
- // 16. Send final callback
908
- if (params.callbackUrl) {
909
- if (runError) {
910
- await sendCallback(params.callbackUrl, {
911
- sessionId,
912
- channel: params.channel,
913
- messageId: params.messageId,
914
- type: "error",
915
- message: runError,
916
- });
917
- }
918
- else {
919
- await sendCallback(params.callbackUrl, {
920
- sessionId,
921
- channel: params.channel,
922
- messageId: params.messageId,
923
- type: "done",
924
- result: { text: lastResponseText, usage, durationMs },
925
- });
926
- }
927
- }
928
- return result;
929
- }
930
- async function finalizeSlashCommandResult(params) {
931
- const { replyText, sessionId, isNewSession, transcriptMessage, initialUserEntryPersisted = false, params: runParams, hookRegistry, hookContext, startTime, compactionEntry, } = params;
932
- if (!initialUserEntryPersisted) {
933
- await appendTranscriptEntry(runParams.dataDir, sessionId, {
934
- role: "user",
935
- content: transcriptMessage,
936
- timestamp: new Date().toISOString(),
937
- });
938
- }
939
- if (compactionEntry) {
940
- await appendTranscriptEntry(runParams.dataDir, sessionId, compactionEntry);
941
- }
942
- await appendTranscriptEntry(runParams.dataDir, sessionId, {
943
- role: "assistant",
944
- content: replyText,
945
- timestamp: new Date().toISOString(),
946
220
  });
947
- if (isNewSession) {
948
- const titleSource = transcriptMessage.trim() ? transcriptMessage : "New session";
949
- const title = generateSessionTitle(titleSource);
950
- const now = new Date().toISOString();
951
- await saveSessionMetadata(runParams.dataDir, {
952
- id: sessionId,
953
- title,
954
- channel: runParams.channel,
955
- createdAt: now,
956
- updatedAt: now,
957
- });
958
- }
959
- const durationMs = Date.now() - startTime;
960
- const usage = { input: 0, output: 0, total: 0 };
961
- const result = {
962
- sessionId,
963
- text: replyText,
964
- usage,
965
- durationMs,
966
- };
967
- await hookRegistry.dispatch("agent_end", { success: true, error: undefined, durationMs }, hookContext);
968
- const finalTranscript = await loadTranscript(runParams.dataDir, sessionId);
969
- await hookRegistry.dispatch("session_end", { sessionId, messageCount: finalTranscript.length, durationMs }, hookContext);
970
- if (runParams.callbackUrl) {
971
- await sendCallback(runParams.callbackUrl, {
972
- sessionId,
973
- channel: runParams.channel,
974
- messageId: runParams.messageId,
975
- type: "done",
976
- result: { text: replyText, usage, durationMs },
977
- });
978
- }
979
- return result;
980
221
  }
981
222
  /**
982
223
  * Runs an agent session end-to-end.