@gencode/agents 0.0.4 → 0.0.6

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