@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.
- package/dist/commands/help.js +6 -6
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +12 -1
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/registry.js +5 -5
- package/dist/commands/registry.js.map +1 -1
- package/dist/plugins/hooks.d.ts +20 -1
- package/dist/plugins/hooks.d.ts.map +1 -1
- package/dist/plugins/hooks.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/loader.js +1 -1
- package/dist/plugins/loader.js.map +1 -1
- package/dist/plugins/runtime.d.ts +9 -1
- package/dist/plugins/runtime.d.ts.map +1 -1
- package/dist/plugins/runtime.js +24 -4
- package/dist/plugins/runtime.js.map +1 -1
- package/dist/runner/agent-runtime.d.ts +62 -0
- package/dist/runner/agent-runtime.d.ts.map +1 -0
- package/dist/runner/agent-runtime.js +179 -0
- package/dist/runner/agent-runtime.js.map +1 -0
- package/dist/runner/announce-loop.d.ts +41 -0
- package/dist/runner/announce-loop.d.ts.map +1 -0
- package/dist/runner/announce-loop.js +94 -0
- package/dist/runner/announce-loop.js.map +1 -0
- package/dist/runner/event-dispatcher.d.ts +31 -0
- package/dist/runner/event-dispatcher.d.ts.map +1 -0
- package/dist/runner/event-dispatcher.js +87 -0
- package/dist/runner/event-dispatcher.js.map +1 -0
- package/dist/runner/finalizer.d.ts +30 -0
- package/dist/runner/finalizer.d.ts.map +1 -0
- package/dist/runner/finalizer.js +75 -0
- package/dist/runner/finalizer.js.map +1 -0
- package/dist/runner/invocation-resolver.d.ts +67 -0
- package/dist/runner/invocation-resolver.d.ts.map +1 -0
- package/dist/runner/invocation-resolver.js +224 -0
- package/dist/runner/invocation-resolver.js.map +1 -0
- package/dist/runner/plugin-context.d.ts +18 -0
- package/dist/runner/plugin-context.d.ts.map +1 -0
- package/dist/runner/plugin-context.js +26 -0
- package/dist/runner/plugin-context.js.map +1 -0
- package/dist/runner/run-context.d.ts +38 -0
- package/dist/runner/run-context.d.ts.map +1 -0
- package/dist/runner/run-context.js +159 -0
- package/dist/runner/run-context.js.map +1 -0
- package/dist/runner/runner-session.d.ts +34 -0
- package/dist/runner/runner-session.d.ts.map +1 -0
- package/dist/runner/runner-session.js +61 -0
- package/dist/runner/runner-session.js.map +1 -0
- package/dist/runner/runner.d.ts +1 -2
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/runner.js +115 -874
- package/dist/runner/runner.js.map +1 -1
- package/dist/runner/runtime.d.ts +7 -0
- package/dist/runner/runtime.d.ts.map +1 -0
- package/dist/runner/runtime.js +21 -0
- package/dist/runner/runtime.js.map +1 -0
- package/dist/runner/session-lifecycle.d.ts +31 -0
- package/dist/runner/session-lifecycle.d.ts.map +1 -0
- package/dist/runner/session-lifecycle.js +46 -0
- package/dist/runner/session-lifecycle.js.map +1 -0
- package/dist/runner/title.d.ts +3 -0
- package/dist/runner/title.d.ts.map +1 -0
- package/dist/runner/title.js +6 -0
- package/dist/runner/title.js.map +1 -0
- package/dist/runner/turn-executor.d.ts +51 -0
- package/dist/runner/turn-executor.d.ts.map +1 -0
- package/dist/runner/turn-executor.js +255 -0
- package/dist/runner/turn-executor.js.map +1 -0
- package/package.json +1 -1
package/dist/runner/runner.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
418
|
-
const
|
|
419
|
-
|
|
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 =
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
|
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
|
|
95
|
+
report: (event) => eventDispatcher.dispatchProgress(sessionId, event),
|
|
606
96
|
});
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
125
|
+
runtimeInputs: {
|
|
126
|
+
contextFiles,
|
|
127
|
+
bootstrapWarnings,
|
|
128
|
+
skills,
|
|
129
|
+
effectivePromptText,
|
|
130
|
+
pluginTools: pluginContext.pluginTools,
|
|
762
131
|
},
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
|
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
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|