vole-agent 0.1.0
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/README.md +83 -0
- package/README.zh-CN.md +81 -0
- package/dist/app.js +702 -0
- package/dist/chunk-RPVF2IWG.js +4580 -0
- package/dist/index.js +21 -0
- package/dist/web/client/assets/index-CjJBdA5w.js +148 -0
- package/dist/web/client/index.html +17 -0
- package/dist/web/server.js +417 -0
- package/package.json +46 -0
|
@@ -0,0 +1,4580 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import "dotenv/config";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { createInterface } from "readline";
|
|
7
|
+
import { readdir as readdir4, readFile as readFile6, stat } from "fs/promises";
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
// ../../packages/config/dist/index.js
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
var openRouterDefaults = {
|
|
15
|
+
baseURL: "https://openrouter.ai/api/v1"
|
|
16
|
+
};
|
|
17
|
+
var anthropicDefaults = {
|
|
18
|
+
model: "claude-haiku-4-5-20251001"
|
|
19
|
+
};
|
|
20
|
+
var ConfigValidationError = class extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "ConfigValidationError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var defaultConfig = {
|
|
27
|
+
model: {
|
|
28
|
+
provider: "openai-compatible",
|
|
29
|
+
baseURL: "https://api.openai.com/v1",
|
|
30
|
+
model: "gpt-4.1-mini",
|
|
31
|
+
temperature: 0.2,
|
|
32
|
+
maxTokens: 4096
|
|
33
|
+
},
|
|
34
|
+
workspace: {
|
|
35
|
+
root: "."
|
|
36
|
+
},
|
|
37
|
+
runtime: {
|
|
38
|
+
defaultMode: "confirm",
|
|
39
|
+
maxSteps: 12
|
|
40
|
+
},
|
|
41
|
+
trace: {
|
|
42
|
+
verbosity: "explainable"
|
|
43
|
+
},
|
|
44
|
+
tools: {
|
|
45
|
+
fileSystem: true,
|
|
46
|
+
shell: true,
|
|
47
|
+
web: false
|
|
48
|
+
},
|
|
49
|
+
permissions: {
|
|
50
|
+
allowLowRisk: true
|
|
51
|
+
},
|
|
52
|
+
sessions: {
|
|
53
|
+
directory: "~/.vole/sessions"
|
|
54
|
+
},
|
|
55
|
+
memory: {
|
|
56
|
+
longTermFiles: "disabled",
|
|
57
|
+
writes: "disabled"
|
|
58
|
+
},
|
|
59
|
+
secrets: {
|
|
60
|
+
apiKey: void 0
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
function loadConfig(input = {}) {
|
|
64
|
+
const config = cloneConfig(defaultConfig);
|
|
65
|
+
applyConfig(config, input.userConfig);
|
|
66
|
+
applyConfig(config, input.projectConfig);
|
|
67
|
+
applyEnv(config, input.env ?? {});
|
|
68
|
+
validateConfig(config);
|
|
69
|
+
return config;
|
|
70
|
+
}
|
|
71
|
+
function redactedConfig(config) {
|
|
72
|
+
return {
|
|
73
|
+
...config,
|
|
74
|
+
model: { ...config.model },
|
|
75
|
+
workspace: { ...config.workspace },
|
|
76
|
+
runtime: { ...config.runtime },
|
|
77
|
+
trace: { ...config.trace },
|
|
78
|
+
tools: { ...config.tools },
|
|
79
|
+
permissions: { ...config.permissions },
|
|
80
|
+
sessions: { ...config.sessions },
|
|
81
|
+
memory: { ...config.memory },
|
|
82
|
+
secrets: {
|
|
83
|
+
apiKey: config.secrets.apiKey === void 0 ? "missing" : "configured"
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function cloneConfig(config) {
|
|
88
|
+
return {
|
|
89
|
+
model: { ...config.model },
|
|
90
|
+
workspace: { ...config.workspace },
|
|
91
|
+
runtime: { ...config.runtime },
|
|
92
|
+
trace: { ...config.trace },
|
|
93
|
+
tools: { ...config.tools },
|
|
94
|
+
permissions: { ...config.permissions },
|
|
95
|
+
sessions: { ...config.sessions },
|
|
96
|
+
memory: { ...config.memory },
|
|
97
|
+
secrets: { ...config.secrets }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function applyConfig(config, value) {
|
|
101
|
+
if (value === void 0) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!isRecord(value)) {
|
|
105
|
+
throw new ConfigValidationError("Configuration must be an object.");
|
|
106
|
+
}
|
|
107
|
+
applyObject(config.model, value.model);
|
|
108
|
+
applyObject(config.workspace, value.workspace);
|
|
109
|
+
applyObject(config.runtime, value.runtime);
|
|
110
|
+
applyObject(config.trace, value.trace);
|
|
111
|
+
applyObject(config.tools, value.tools);
|
|
112
|
+
applyObject(config.permissions, value.permissions);
|
|
113
|
+
applyObject(config.sessions, value.sessions);
|
|
114
|
+
applyObject(config.memory, value.memory);
|
|
115
|
+
}
|
|
116
|
+
function applyObject(target, value) {
|
|
117
|
+
if (value === void 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!isRecord(value)) {
|
|
121
|
+
throw new ConfigValidationError("Configuration sections must be objects.");
|
|
122
|
+
}
|
|
123
|
+
for (const [key, sectionValue] of Object.entries(value)) {
|
|
124
|
+
if (key in target) {
|
|
125
|
+
target[key] = sectionValue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function applyEnv(config, env) {
|
|
130
|
+
if (env.OPENROUTER_API_KEY !== void 0) {
|
|
131
|
+
config.model.provider = "openai-compatible";
|
|
132
|
+
config.model.baseURL = openRouterDefaults.baseURL;
|
|
133
|
+
config.model.model = "";
|
|
134
|
+
config.secrets.apiKey = env.OPENROUTER_API_KEY;
|
|
135
|
+
}
|
|
136
|
+
if (env.ANTHROPIC_API_KEY !== void 0) {
|
|
137
|
+
config.model.provider = "anthropic";
|
|
138
|
+
config.model.model = anthropicDefaults.model;
|
|
139
|
+
config.secrets.apiKey = env.ANTHROPIC_API_KEY;
|
|
140
|
+
}
|
|
141
|
+
if (env.VOLE_BASE_URL !== void 0) {
|
|
142
|
+
config.model.baseURL = env.VOLE_BASE_URL;
|
|
143
|
+
}
|
|
144
|
+
if (env.VOLE_MODEL !== void 0) {
|
|
145
|
+
config.model.model = env.VOLE_MODEL;
|
|
146
|
+
}
|
|
147
|
+
if (env.VOLE_DEFAULT_MODE !== void 0) {
|
|
148
|
+
config.runtime.defaultMode = env.VOLE_DEFAULT_MODE;
|
|
149
|
+
}
|
|
150
|
+
if (env.VOLE_WORKSPACE_ROOT !== void 0) {
|
|
151
|
+
config.workspace.root = env.VOLE_WORKSPACE_ROOT;
|
|
152
|
+
}
|
|
153
|
+
if (env.VOLE_LONG_TERM_MEMORY !== void 0) {
|
|
154
|
+
config.memory.longTermFiles = env.VOLE_LONG_TERM_MEMORY;
|
|
155
|
+
}
|
|
156
|
+
if (env.VOLE_API_KEY !== void 0) {
|
|
157
|
+
config.secrets.apiKey = env.VOLE_API_KEY;
|
|
158
|
+
}
|
|
159
|
+
if (env.VOLE_PROMPT_MODE !== void 0) {
|
|
160
|
+
config.runtime.promptMode = env.VOLE_PROMPT_MODE;
|
|
161
|
+
}
|
|
162
|
+
if (env.VOLE_EXECUTION_CONTRACT !== void 0) {
|
|
163
|
+
config.runtime.executionContract = env.VOLE_EXECUTION_CONTRACT;
|
|
164
|
+
}
|
|
165
|
+
if (env.VOLE_TOOL_PROFILE !== void 0) {
|
|
166
|
+
config.runtime.toolProfile = env.VOLE_TOOL_PROFILE;
|
|
167
|
+
}
|
|
168
|
+
if (env.VOLE_SANDBOX !== void 0) {
|
|
169
|
+
config.runtime.sandboxed = env.VOLE_SANDBOX === "true";
|
|
170
|
+
}
|
|
171
|
+
if (env.VOLE_THINKING_BUDGET !== void 0) {
|
|
172
|
+
config.model.thinkingBudget = env.VOLE_THINKING_BUDGET;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function validateConfig(config) {
|
|
176
|
+
if (config.model.provider !== "openai-compatible" && config.model.provider !== "anthropic") {
|
|
177
|
+
throw new ConfigValidationError(`Invalid model.provider "${String(config.model.provider)}". Expected openai-compatible or anthropic.`);
|
|
178
|
+
}
|
|
179
|
+
if (config.model.model.trim().length === 0) {
|
|
180
|
+
throw new ConfigValidationError("No model configured. Set VOLE_MODEL=<model-name> (e.g. VOLE_MODEL=openai/gpt-4o for OpenRouter).");
|
|
181
|
+
}
|
|
182
|
+
if (!isAutonomyMode(config.runtime.defaultMode)) {
|
|
183
|
+
throw new ConfigValidationError(`Invalid runtime.defaultMode "${String(config.runtime.defaultMode)}". Expected observe, confirm, or auto.`);
|
|
184
|
+
}
|
|
185
|
+
if (!isTraceVerbosity(config.trace.verbosity)) {
|
|
186
|
+
throw new ConfigValidationError(`Invalid trace.verbosity "${String(config.trace.verbosity)}". Expected explainable or debug.`);
|
|
187
|
+
}
|
|
188
|
+
if (!isLongTermMemoryFilePolicy(config.memory.longTermFiles)) {
|
|
189
|
+
throw new ConfigValidationError(`Invalid memory.longTermFiles "${String(config.memory.longTermFiles)}". Expected disabled or read-only.`);
|
|
190
|
+
}
|
|
191
|
+
if (config.memory.writes !== "disabled") {
|
|
192
|
+
throw new ConfigValidationError(`Invalid memory.writes "${String(config.memory.writes)}". Only disabled is supported.`);
|
|
193
|
+
}
|
|
194
|
+
if (config.runtime.promptMode !== void 0 && !isPromptMode(config.runtime.promptMode)) {
|
|
195
|
+
throw new ConfigValidationError(`Invalid runtime.promptMode "${String(config.runtime.promptMode)}". Expected full, minimal, or none.`);
|
|
196
|
+
}
|
|
197
|
+
if (config.runtime.executionContract !== void 0 && !isExecutionContract(config.runtime.executionContract)) {
|
|
198
|
+
throw new ConfigValidationError(`Invalid runtime.executionContract "${String(config.runtime.executionContract)}". Expected default or strict-agentic.`);
|
|
199
|
+
}
|
|
200
|
+
if (config.runtime.toolProfile !== void 0 && !isToolProfileConfig(config.runtime.toolProfile)) {
|
|
201
|
+
throw new ConfigValidationError(`Invalid runtime.toolProfile "${String(config.runtime.toolProfile)}". Expected coding, full, messaging, or background.`);
|
|
202
|
+
}
|
|
203
|
+
if (config.model.thinkingBudget !== void 0 && !isThinkingBudget(config.model.thinkingBudget)) {
|
|
204
|
+
throw new ConfigValidationError(`Invalid model.thinkingBudget "${String(config.model.thinkingBudget)}". Expected off, minimal, low, medium, high, max, or adaptive.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function isRecord(value) {
|
|
208
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
209
|
+
}
|
|
210
|
+
function isAutonomyMode(value) {
|
|
211
|
+
return value === "observe" || value === "confirm" || value === "auto";
|
|
212
|
+
}
|
|
213
|
+
function isTraceVerbosity(value) {
|
|
214
|
+
return value === "explainable" || value === "debug";
|
|
215
|
+
}
|
|
216
|
+
function isLongTermMemoryFilePolicy(value) {
|
|
217
|
+
return value === "disabled" || value === "read-only" || value === "write";
|
|
218
|
+
}
|
|
219
|
+
function isPromptMode(value) {
|
|
220
|
+
return value === "full" || value === "minimal" || value === "none";
|
|
221
|
+
}
|
|
222
|
+
function isExecutionContract(value) {
|
|
223
|
+
return value === "default" || value === "strict-agentic";
|
|
224
|
+
}
|
|
225
|
+
function isToolProfileConfig(value) {
|
|
226
|
+
return value === "coding" || value === "full" || value === "messaging" || value === "background";
|
|
227
|
+
}
|
|
228
|
+
function isThinkingBudget(value) {
|
|
229
|
+
return value === "off" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "max" || value === "adaptive";
|
|
230
|
+
}
|
|
231
|
+
function resolveSessionsDirectory(config, env) {
|
|
232
|
+
const directory = config.sessions.directory;
|
|
233
|
+
if (!directory.startsWith("~/")) {
|
|
234
|
+
return directory;
|
|
235
|
+
}
|
|
236
|
+
const home = env?.HOME ?? process.env.HOME;
|
|
237
|
+
return home === void 0 ? directory : join(home, directory.slice(2));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ../../packages/context/dist/index.js
|
|
241
|
+
import { readFile } from "fs/promises";
|
|
242
|
+
import { join as join2 } from "path";
|
|
243
|
+
var MinimalContextAssembler = class {
|
|
244
|
+
async assemble(input) {
|
|
245
|
+
const messages = [];
|
|
246
|
+
if (input.systemInstruction) {
|
|
247
|
+
messages.push({ role: "system", content: input.systemInstruction });
|
|
248
|
+
}
|
|
249
|
+
if (input.recentMessages) {
|
|
250
|
+
messages.push(...input.recentMessages);
|
|
251
|
+
}
|
|
252
|
+
messages.push({ role: "user", content: input.userMessage });
|
|
253
|
+
return {
|
|
254
|
+
modelInput: { messages },
|
|
255
|
+
report: {
|
|
256
|
+
includedSections: input.systemInstruction ? ["identity"] : [],
|
|
257
|
+
omittedSections: ["runtime", "tooling", "safety", "skills", "workspace"],
|
|
258
|
+
sections: []
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
var DefaultContextAssembler = class {
|
|
264
|
+
#workspacePromptFiles;
|
|
265
|
+
#readWorkspaceFile;
|
|
266
|
+
constructor(dependencies = {}) {
|
|
267
|
+
this.#workspacePromptFiles = dependencies.workspacePromptFiles ?? [];
|
|
268
|
+
this.#readWorkspaceFile = dependencies.readWorkspaceFile ?? ((path) => readFile(path, "utf8"));
|
|
269
|
+
}
|
|
270
|
+
async assemble(input) {
|
|
271
|
+
const mode = input.promptMode ?? "full";
|
|
272
|
+
const sectionReports = [];
|
|
273
|
+
const systemParts = [];
|
|
274
|
+
if (mode === "none") {
|
|
275
|
+
const recentMessages2 = input.recentMessages ?? [];
|
|
276
|
+
if (recentMessages2.length > 0) {
|
|
277
|
+
sectionReports.push({ name: "conversation_history", included: true });
|
|
278
|
+
}
|
|
279
|
+
sectionReports.push({ name: "user_message", included: true });
|
|
280
|
+
const includedSections2 = sectionReports.filter((s) => s.included).map((s) => s.name);
|
|
281
|
+
const omittedSections2 = sectionReports.filter((s) => !s.included).map((s) => s.name);
|
|
282
|
+
return {
|
|
283
|
+
modelInput: {
|
|
284
|
+
messages: [
|
|
285
|
+
...recentMessages2.map((message) => ({ ...message })),
|
|
286
|
+
{ role: "user", content: input.userMessage }
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
report: { includedSections: includedSections2, omittedSections: omittedSections2, sections: sectionReports }
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
systemParts.push(`<identity>
|
|
293
|
+
${input.systemInstruction}
|
|
294
|
+
</identity>`);
|
|
295
|
+
sectionReports.push({ name: "identity", included: true });
|
|
296
|
+
if (mode === "full") {
|
|
297
|
+
if (input.runtime) {
|
|
298
|
+
systemParts.push("", `<runtime>
|
|
299
|
+
- Mode: ${input.runtime.mode}
|
|
300
|
+
- Workspace: ${input.runtime.workspace}
|
|
301
|
+
- Date: ${input.runtime.currentDate}
|
|
302
|
+
</runtime>`);
|
|
303
|
+
sectionReports.push({ name: "runtime", included: true });
|
|
304
|
+
} else {
|
|
305
|
+
sectionReports.push({ name: "runtime", included: false, reason: "No runtime metadata provided." });
|
|
306
|
+
}
|
|
307
|
+
if (input.tools !== void 0 && input.tools.length > 0) {
|
|
308
|
+
const toolLines = input.tools.map((t) => `- ${t.name} [${t.risk}]: ${t.description}`).join("\n");
|
|
309
|
+
systemParts.push("", `<tooling>
|
|
310
|
+
${toolLines}
|
|
311
|
+
</tooling>`);
|
|
312
|
+
sectionReports.push({ name: "tooling", included: true });
|
|
313
|
+
} else {
|
|
314
|
+
sectionReports.push({ name: "tooling", included: false, reason: "No tools registered." });
|
|
315
|
+
}
|
|
316
|
+
if (input.permissionGuidance !== void 0 && input.permissionGuidance.length > 0) {
|
|
317
|
+
systemParts.push("", `<safety>
|
|
318
|
+
${input.permissionGuidance}
|
|
319
|
+
</safety>`);
|
|
320
|
+
sectionReports.push({ name: "safety", included: true });
|
|
321
|
+
} else {
|
|
322
|
+
sectionReports.push({ name: "safety", included: false, reason: "No permission guidance provided." });
|
|
323
|
+
}
|
|
324
|
+
if (input.skillIndex !== void 0 && input.skillIndex.length > 0) {
|
|
325
|
+
const skillLines = input.skillIndex.map((s) => `- ${s.name}: ${s.description}`).join("\n");
|
|
326
|
+
systemParts.push("", `<skills>
|
|
327
|
+
${skillLines}
|
|
328
|
+
</skills>`);
|
|
329
|
+
sectionReports.push({ name: "skills", included: true });
|
|
330
|
+
} else {
|
|
331
|
+
sectionReports.push({ name: "skills", included: false, reason: "No skills loaded." });
|
|
332
|
+
}
|
|
333
|
+
const workspaceContent = await this.#loadWorkspacePromptSections(input.runtime?.workspace);
|
|
334
|
+
if (workspaceContent.length > 0) {
|
|
335
|
+
systemParts.push("", `<workspace>${workspaceContent.join("\n")}
|
|
336
|
+
</workspace>`);
|
|
337
|
+
sectionReports.push({ name: "workspace", included: true });
|
|
338
|
+
} else if (this.#workspacePromptFiles.length > 0) {
|
|
339
|
+
sectionReports.push({ name: "workspace", included: false, reason: "No workspace prompt files found." });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const recentMessages = input.recentMessages ?? [];
|
|
343
|
+
if (recentMessages.length > 0) {
|
|
344
|
+
sectionReports.push({ name: "conversation_history", included: true });
|
|
345
|
+
}
|
|
346
|
+
sectionReports.push({ name: "user_message", included: true });
|
|
347
|
+
const includedSections = sectionReports.filter((s) => s.included).map((s) => s.name);
|
|
348
|
+
const omittedSections = sectionReports.filter((s) => !s.included).map((s) => s.name);
|
|
349
|
+
return {
|
|
350
|
+
modelInput: {
|
|
351
|
+
messages: [
|
|
352
|
+
{ role: "system", content: systemParts.join("\n") },
|
|
353
|
+
...recentMessages.map((message) => ({ ...message })),
|
|
354
|
+
{ role: "user", content: input.userMessage }
|
|
355
|
+
]
|
|
356
|
+
},
|
|
357
|
+
report: { includedSections, omittedSections, sections: sectionReports }
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async #loadWorkspacePromptSections(workspace) {
|
|
361
|
+
if (workspace === void 0 || this.#workspacePromptFiles.length === 0) {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
const sections = [];
|
|
365
|
+
for (const fileName of this.#workspacePromptFiles) {
|
|
366
|
+
try {
|
|
367
|
+
const content = await this.#readWorkspaceFile(join2(workspace, fileName));
|
|
368
|
+
const trimmedContent = content.trim();
|
|
369
|
+
if (trimmedContent.length > 0) {
|
|
370
|
+
sections.push("", `### ${fileName}`, trimmedContent);
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return sections;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
function isNodeError(error) {
|
|
383
|
+
return error instanceof Error && "code" in error;
|
|
384
|
+
}
|
|
385
|
+
var DEFAULT_COMPACTION_OPTIONS = {
|
|
386
|
+
maxTokens: 6e4,
|
|
387
|
+
maxMessages: 400,
|
|
388
|
+
keepRecent: 12,
|
|
389
|
+
summarySystemPrompt: "You are a context distiller for an AI agent. The conversation history has grown too long and must be reduced. Extract only what the agent needs to continue working: tools called and their key outcomes, decisions reached, important facts discovered, files created or modified, errors encountered, and the current task state. Discard pleasantries, repetition, and details that no longer affect the agent's ability to proceed. Output concise factual statements only."
|
|
390
|
+
};
|
|
391
|
+
function estimateMessageTokens(messages) {
|
|
392
|
+
let chars = 0;
|
|
393
|
+
for (const msg of messages) {
|
|
394
|
+
if (typeof msg.content === "string")
|
|
395
|
+
chars += msg.content.length;
|
|
396
|
+
if (msg.toolCalls !== void 0)
|
|
397
|
+
chars += JSON.stringify(msg.toolCalls).length;
|
|
398
|
+
if (msg.toolCallId !== void 0)
|
|
399
|
+
chars += msg.toolCallId.length;
|
|
400
|
+
}
|
|
401
|
+
return Math.ceil(chars / 4);
|
|
402
|
+
}
|
|
403
|
+
async function compactMessages(messages, modelProvider, options) {
|
|
404
|
+
const opts = { ...DEFAULT_COMPACTION_OPTIONS, ...options };
|
|
405
|
+
const shouldCompact = estimateMessageTokens(messages) > opts.maxTokens || messages.length > opts.maxMessages;
|
|
406
|
+
if (!shouldCompact) {
|
|
407
|
+
return messages;
|
|
408
|
+
}
|
|
409
|
+
const leadingSystem = messages[0]?.role === "system" ? messages[0] : void 0;
|
|
410
|
+
const conversation = leadingSystem !== void 0 ? messages.slice(1) : messages;
|
|
411
|
+
const old = conversation.slice(0, conversation.length - opts.keepRecent);
|
|
412
|
+
const recent = conversation.slice(-opts.keepRecent);
|
|
413
|
+
if (old.length === 0) {
|
|
414
|
+
return messages;
|
|
415
|
+
}
|
|
416
|
+
const thinnedOld = old.map(thinToolMessage);
|
|
417
|
+
const thinnedMessages = [
|
|
418
|
+
...leadingSystem !== void 0 ? [leadingSystem] : [],
|
|
419
|
+
...thinnedOld,
|
|
420
|
+
...recent
|
|
421
|
+
];
|
|
422
|
+
const transcript = thinnedOld.map((m) => `${m.role.toUpperCase()}: ${m.content ?? "(tool call)"}`).join("\n");
|
|
423
|
+
try {
|
|
424
|
+
const output = await modelProvider.generate({
|
|
425
|
+
messages: [
|
|
426
|
+
{ role: "system", content: opts.summarySystemPrompt },
|
|
427
|
+
{ role: "user", content: `Conversation to distil:
|
|
428
|
+
|
|
429
|
+
${transcript}` }
|
|
430
|
+
]
|
|
431
|
+
});
|
|
432
|
+
if (output.type !== "message" || !output.content) {
|
|
433
|
+
return thinnedMessages;
|
|
434
|
+
}
|
|
435
|
+
return [
|
|
436
|
+
...leadingSystem !== void 0 ? [leadingSystem] : [],
|
|
437
|
+
{ role: "system", content: `Conversation summary:
|
|
438
|
+
${output.content}` },
|
|
439
|
+
...recent
|
|
440
|
+
];
|
|
441
|
+
} catch {
|
|
442
|
+
return thinnedMessages;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function thinToolMessage(message) {
|
|
446
|
+
if (message.role !== "tool" || message.content === null) {
|
|
447
|
+
return message;
|
|
448
|
+
}
|
|
449
|
+
let parsed;
|
|
450
|
+
try {
|
|
451
|
+
parsed = JSON.parse(message.content);
|
|
452
|
+
} catch {
|
|
453
|
+
return message.content.length > 400 ? { ...message, content: `${message.content.slice(0, 400)}
|
|
454
|
+
[${message.content.length - 400} chars omitted]` } : message;
|
|
455
|
+
}
|
|
456
|
+
const slim = {};
|
|
457
|
+
if ("ok" in parsed)
|
|
458
|
+
slim["ok"] = parsed["ok"];
|
|
459
|
+
if ("summary" in parsed && typeof parsed["summary"] === "string")
|
|
460
|
+
slim["summary"] = parsed["summary"];
|
|
461
|
+
if ("exitCode" in parsed)
|
|
462
|
+
slim["exitCode"] = parsed["exitCode"];
|
|
463
|
+
if ("error" in parsed)
|
|
464
|
+
slim["error"] = parsed["error"];
|
|
465
|
+
if ("type" in parsed)
|
|
466
|
+
slim["type"] = parsed["type"];
|
|
467
|
+
if ("result" in parsed && typeof parsed["result"] === "string" && parsed["result"].length <= 200) {
|
|
468
|
+
slim["result"] = parsed["result"];
|
|
469
|
+
}
|
|
470
|
+
return { ...message, content: JSON.stringify(slim) };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ../../packages/models/dist/index.js
|
|
474
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
475
|
+
function isStreamingProvider(provider) {
|
|
476
|
+
return "generateStream" in provider && typeof provider.generateStream === "function";
|
|
477
|
+
}
|
|
478
|
+
var OpenAICompatibleProvider = class {
|
|
479
|
+
#baseURL;
|
|
480
|
+
#apiKey;
|
|
481
|
+
#model;
|
|
482
|
+
#temperature;
|
|
483
|
+
#maxTokens;
|
|
484
|
+
#fetch;
|
|
485
|
+
constructor(config) {
|
|
486
|
+
this.#baseURL = config.baseURL.replace(/\/+$/, "");
|
|
487
|
+
this.#apiKey = config.apiKey;
|
|
488
|
+
this.#model = config.model;
|
|
489
|
+
this.#temperature = config.temperature;
|
|
490
|
+
this.#maxTokens = config.maxTokens;
|
|
491
|
+
this.#fetch = config.fetch ?? fetch;
|
|
492
|
+
}
|
|
493
|
+
async generate(input) {
|
|
494
|
+
try {
|
|
495
|
+
const response = await this.#fetch(`${this.#baseURL}/chat/completions`, {
|
|
496
|
+
method: "POST",
|
|
497
|
+
headers: this.#headers(),
|
|
498
|
+
body: JSON.stringify(this.#body(input))
|
|
499
|
+
});
|
|
500
|
+
if (!response.ok) {
|
|
501
|
+
return {
|
|
502
|
+
type: "error",
|
|
503
|
+
category: this.#errorCategory(response.status),
|
|
504
|
+
message: `Provider request failed with status ${response.status}.`,
|
|
505
|
+
recoverable: response.status === 408 || response.status === 409 || response.status === 429 || response.status >= 500
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const data = await response.json();
|
|
509
|
+
const choice = data.choices?.[0];
|
|
510
|
+
const finishReason = choice?.finish_reason;
|
|
511
|
+
const message = choice?.message;
|
|
512
|
+
const rawToolCalls = message?.tool_calls;
|
|
513
|
+
if (finishReason === "tool_calls" && rawToolCalls !== void 0 && rawToolCalls.length > 0) {
|
|
514
|
+
return {
|
|
515
|
+
type: "tool_calls",
|
|
516
|
+
calls: rawToolCalls.map((tc) => ({
|
|
517
|
+
id: tc.id,
|
|
518
|
+
name: tc.function.name,
|
|
519
|
+
input: parseToolCallArguments(tc.function.arguments)
|
|
520
|
+
})),
|
|
521
|
+
...data.usage ? { usage: this.#usage(data.usage) } : {}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
type: "message",
|
|
526
|
+
content: message?.content ?? "",
|
|
527
|
+
...data.usage ? { usage: this.#usage(data.usage) } : {}
|
|
528
|
+
};
|
|
529
|
+
} catch {
|
|
530
|
+
return {
|
|
531
|
+
type: "error",
|
|
532
|
+
category: "network",
|
|
533
|
+
message: "Provider network request failed.",
|
|
534
|
+
recoverable: true
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async *generateStream(input) {
|
|
539
|
+
try {
|
|
540
|
+
const response = await this.#fetch(`${this.#baseURL}/chat/completions`, {
|
|
541
|
+
method: "POST",
|
|
542
|
+
headers: this.#headers(),
|
|
543
|
+
body: JSON.stringify({ ...this.#body(input), stream: true })
|
|
544
|
+
});
|
|
545
|
+
if (!response.ok) {
|
|
546
|
+
yield {
|
|
547
|
+
type: "error",
|
|
548
|
+
category: this.#errorCategory(response.status),
|
|
549
|
+
message: `Provider request failed with status ${response.status}.`,
|
|
550
|
+
recoverable: response.status === 408 || response.status === 409 || response.status === 429 || response.status >= 500
|
|
551
|
+
};
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (response.body === null) {
|
|
555
|
+
yield { type: "error", category: "network", message: "No response body for streaming request.", recoverable: true };
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const toolAccumulators = /* @__PURE__ */ new Map();
|
|
559
|
+
let textContent = "";
|
|
560
|
+
let usage;
|
|
561
|
+
let finishReason = null;
|
|
562
|
+
for await (const data of parseSSEStream(response.body)) {
|
|
563
|
+
if (data === "[DONE]")
|
|
564
|
+
break;
|
|
565
|
+
let chunk;
|
|
566
|
+
try {
|
|
567
|
+
chunk = JSON.parse(data);
|
|
568
|
+
} catch {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const choice = chunk.choices?.[0];
|
|
572
|
+
if (choice !== void 0) {
|
|
573
|
+
if (choice.finish_reason !== null && choice.finish_reason !== void 0) {
|
|
574
|
+
finishReason = choice.finish_reason;
|
|
575
|
+
}
|
|
576
|
+
const delta = choice.delta;
|
|
577
|
+
if (delta !== void 0) {
|
|
578
|
+
if (delta.content) {
|
|
579
|
+
textContent += delta.content;
|
|
580
|
+
yield { type: "token_delta", delta: delta.content };
|
|
581
|
+
}
|
|
582
|
+
if (delta.tool_calls) {
|
|
583
|
+
for (const tc of delta.tool_calls) {
|
|
584
|
+
if (!toolAccumulators.has(tc.index)) {
|
|
585
|
+
toolAccumulators.set(tc.index, { id: "", name: "", arguments: "" });
|
|
586
|
+
}
|
|
587
|
+
const acc = toolAccumulators.get(tc.index);
|
|
588
|
+
if (tc.id)
|
|
589
|
+
acc.id = tc.id;
|
|
590
|
+
if (tc.function?.name)
|
|
591
|
+
acc.name = tc.function.name;
|
|
592
|
+
if (tc.function?.arguments)
|
|
593
|
+
acc.arguments += tc.function.arguments;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (chunk.usage) {
|
|
599
|
+
usage = this.#usage(chunk.usage);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (finishReason === "tool_calls" && toolAccumulators.size > 0) {
|
|
603
|
+
const calls = Array.from(toolAccumulators.entries()).sort(([a], [b]) => a - b).map(([, acc]) => ({
|
|
604
|
+
id: acc.id,
|
|
605
|
+
name: acc.name,
|
|
606
|
+
input: parseToolCallArguments(acc.arguments)
|
|
607
|
+
}));
|
|
608
|
+
yield { type: "tool_calls", calls, ...usage !== void 0 ? { usage } : {} };
|
|
609
|
+
} else {
|
|
610
|
+
yield { type: "message_done", content: textContent, ...usage !== void 0 ? { usage } : {} };
|
|
611
|
+
}
|
|
612
|
+
} catch {
|
|
613
|
+
yield { type: "error", category: "network", message: "Provider network request failed.", recoverable: true };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
#headers() {
|
|
617
|
+
return {
|
|
618
|
+
"content-type": "application/json",
|
|
619
|
+
...this.#apiKey ? { authorization: `Bearer ${this.#apiKey}` } : {}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
#body(input) {
|
|
623
|
+
return {
|
|
624
|
+
model: this.#model,
|
|
625
|
+
messages: input.messages.map((m) => this.#formatMessage(m)),
|
|
626
|
+
...input.tools !== void 0 && input.tools.length > 0 ? { tools: input.tools } : {},
|
|
627
|
+
...this.#temperature === void 0 ? {} : { temperature: this.#temperature },
|
|
628
|
+
...this.#maxTokens === void 0 ? {} : { max_tokens: this.#maxTokens }
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
#formatMessage(message) {
|
|
632
|
+
if (message.role === "tool") {
|
|
633
|
+
return {
|
|
634
|
+
role: "tool",
|
|
635
|
+
tool_call_id: message.toolCallId ?? "",
|
|
636
|
+
content: message.content ?? ""
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
if (message.role === "assistant" && message.toolCalls !== void 0 && message.toolCalls.length > 0) {
|
|
640
|
+
return {
|
|
641
|
+
role: "assistant",
|
|
642
|
+
content: message.content,
|
|
643
|
+
tool_calls: message.toolCalls.map((call) => ({
|
|
644
|
+
id: call.id,
|
|
645
|
+
type: "function",
|
|
646
|
+
function: {
|
|
647
|
+
name: call.name,
|
|
648
|
+
arguments: typeof call.input === "string" ? call.input : JSON.stringify(call.input)
|
|
649
|
+
}
|
|
650
|
+
}))
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return { role: message.role, content: message.content };
|
|
654
|
+
}
|
|
655
|
+
#errorCategory(status) {
|
|
656
|
+
if (status === 401 || status === 403) {
|
|
657
|
+
return "authentication";
|
|
658
|
+
}
|
|
659
|
+
if (status === 429) {
|
|
660
|
+
return "rate_limit";
|
|
661
|
+
}
|
|
662
|
+
if (status === 400 || status === 422) {
|
|
663
|
+
return "invalid_request";
|
|
664
|
+
}
|
|
665
|
+
if (status === 404 || status === 503) {
|
|
666
|
+
return "model_unavailable";
|
|
667
|
+
}
|
|
668
|
+
return "unknown";
|
|
669
|
+
}
|
|
670
|
+
#usage(usage) {
|
|
671
|
+
return {
|
|
672
|
+
...usage.prompt_tokens === void 0 ? {} : { inputTokens: usage.prompt_tokens },
|
|
673
|
+
...usage.completion_tokens === void 0 ? {} : { outputTokens: usage.completion_tokens },
|
|
674
|
+
...usage.total_tokens === void 0 ? {} : { totalTokens: usage.total_tokens }
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
async function* parseSSEStream(body) {
|
|
679
|
+
const reader = body.getReader();
|
|
680
|
+
const decoder = new TextDecoder();
|
|
681
|
+
let buffer = "";
|
|
682
|
+
try {
|
|
683
|
+
while (true) {
|
|
684
|
+
const { done, value } = await reader.read();
|
|
685
|
+
if (done)
|
|
686
|
+
break;
|
|
687
|
+
buffer += decoder.decode(value, { stream: true });
|
|
688
|
+
const lines = buffer.split("\n");
|
|
689
|
+
buffer = lines.pop() ?? "";
|
|
690
|
+
for (const line of lines) {
|
|
691
|
+
const trimmed = line.trim();
|
|
692
|
+
if (trimmed.startsWith("data: ")) {
|
|
693
|
+
yield trimmed.slice(6);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
buffer += decoder.decode();
|
|
698
|
+
for (const line of buffer.split("\n")) {
|
|
699
|
+
const trimmed = line.trim();
|
|
700
|
+
if (trimmed.startsWith("data: ")) {
|
|
701
|
+
yield trimmed.slice(6);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
} finally {
|
|
705
|
+
reader.releaseLock();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function parseToolCallArguments(args) {
|
|
709
|
+
try {
|
|
710
|
+
return JSON.parse(args);
|
|
711
|
+
} catch {
|
|
712
|
+
return args;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
var FakeModelProvider = class {
|
|
716
|
+
requests = [];
|
|
717
|
+
#outputs;
|
|
718
|
+
constructor(outputs) {
|
|
719
|
+
this.#outputs = [...outputs];
|
|
720
|
+
}
|
|
721
|
+
async generate(input) {
|
|
722
|
+
this.requests.push(input);
|
|
723
|
+
const output = this.#outputs.shift();
|
|
724
|
+
return output ?? {
|
|
725
|
+
type: "error",
|
|
726
|
+
category: "unknown",
|
|
727
|
+
message: "FakeModelProvider has no queued output.",
|
|
728
|
+
recoverable: false
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
var THINKING_BUDGET_TOKENS = {
|
|
733
|
+
minimal: 1024,
|
|
734
|
+
low: 2048,
|
|
735
|
+
medium: 4096,
|
|
736
|
+
high: 8192,
|
|
737
|
+
max: 16384
|
|
738
|
+
};
|
|
739
|
+
var AnthropicProvider = class {
|
|
740
|
+
#client;
|
|
741
|
+
#streamClient;
|
|
742
|
+
#model;
|
|
743
|
+
#maxTokens;
|
|
744
|
+
#temperature;
|
|
745
|
+
#thinkingBudget;
|
|
746
|
+
constructor(config) {
|
|
747
|
+
this.#model = config.model;
|
|
748
|
+
this.#maxTokens = config.maxTokens ?? 4096;
|
|
749
|
+
this.#temperature = config.temperature;
|
|
750
|
+
this.#thinkingBudget = config.thinkingBudget;
|
|
751
|
+
if (config.client !== void 0 || config.streamClient !== void 0) {
|
|
752
|
+
this.#client = config.client ?? { messages: { create: async () => {
|
|
753
|
+
throw new Error("No client provided.");
|
|
754
|
+
} } };
|
|
755
|
+
this.#streamClient = config.streamClient;
|
|
756
|
+
} else {
|
|
757
|
+
const sdk = new Anthropic({ apiKey: config.apiKey });
|
|
758
|
+
this.#client = sdk;
|
|
759
|
+
this.#streamClient = {
|
|
760
|
+
messages: {
|
|
761
|
+
stream: (params) => sdk.messages.create({ ...params, stream: true })
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
#buildThinkingParam() {
|
|
767
|
+
const budget = this.#thinkingBudget;
|
|
768
|
+
if (budget === void 0 || budget === "off")
|
|
769
|
+
return void 0;
|
|
770
|
+
if (budget === "adaptive")
|
|
771
|
+
return { type: "adaptive" };
|
|
772
|
+
const tokens = THINKING_BUDGET_TOKENS[budget];
|
|
773
|
+
return { type: "enabled", budget_tokens: tokens };
|
|
774
|
+
}
|
|
775
|
+
async generate(input) {
|
|
776
|
+
try {
|
|
777
|
+
const { system, messages } = translateMessagesToAnthropic(input.messages);
|
|
778
|
+
const systemBlocks = system !== void 0 ? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }] : void 0;
|
|
779
|
+
const thinkingParam = this.#buildThinkingParam();
|
|
780
|
+
const response = await this.#client.messages.create({
|
|
781
|
+
model: this.#model,
|
|
782
|
+
max_tokens: this.#maxTokens,
|
|
783
|
+
...systemBlocks !== void 0 ? { system: systemBlocks } : {},
|
|
784
|
+
messages,
|
|
785
|
+
...input.tools !== void 0 && input.tools.length > 0 ? { tools: translateToolsToAnthropic(input.tools) } : {},
|
|
786
|
+
...this.#temperature !== void 0 ? { temperature: this.#temperature } : {},
|
|
787
|
+
...thinkingParam !== void 0 ? { thinking: thinkingParam } : {}
|
|
788
|
+
});
|
|
789
|
+
const toolUseBlocks = response.content.filter(isToolUseBlock);
|
|
790
|
+
if (response.stop_reason === "tool_use" && toolUseBlocks.length > 0) {
|
|
791
|
+
return {
|
|
792
|
+
type: "tool_calls",
|
|
793
|
+
calls: toolUseBlocks.map((b) => ({ id: b.id, name: b.name, input: b.input })),
|
|
794
|
+
usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens }
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const textBlock = response.content.find(isTextBlock);
|
|
798
|
+
return {
|
|
799
|
+
type: "message",
|
|
800
|
+
content: textBlock?.text ?? "",
|
|
801
|
+
usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens }
|
|
802
|
+
};
|
|
803
|
+
} catch (error) {
|
|
804
|
+
return normalizeAnthropicError(error);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async *generateStream(input) {
|
|
808
|
+
if (this.#streamClient === void 0) {
|
|
809
|
+
const output = await this.generate(input);
|
|
810
|
+
if (output.type === "message") {
|
|
811
|
+
yield { type: "token_delta", delta: output.content };
|
|
812
|
+
yield { type: "message_done", content: output.content, ...output.usage ? { usage: output.usage } : {} };
|
|
813
|
+
} else if (output.type === "tool_calls") {
|
|
814
|
+
yield { type: "tool_calls", calls: output.calls, ...output.usage ? { usage: output.usage } : {} };
|
|
815
|
+
} else {
|
|
816
|
+
yield { type: "error", category: output.category, message: output.message, recoverable: output.recoverable };
|
|
817
|
+
}
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const { system, messages } = translateMessagesToAnthropic(input.messages);
|
|
822
|
+
const systemBlocks = system !== void 0 ? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }] : void 0;
|
|
823
|
+
const thinkingParam = this.#buildThinkingParam();
|
|
824
|
+
const params = {
|
|
825
|
+
model: this.#model,
|
|
826
|
+
max_tokens: this.#maxTokens,
|
|
827
|
+
...systemBlocks !== void 0 ? { system: systemBlocks } : {},
|
|
828
|
+
messages,
|
|
829
|
+
...input.tools !== void 0 && input.tools.length > 0 ? { tools: translateToolsToAnthropic(input.tools) } : {},
|
|
830
|
+
...this.#temperature !== void 0 ? { temperature: this.#temperature } : {},
|
|
831
|
+
...thinkingParam !== void 0 ? { thinking: thinkingParam } : {}
|
|
832
|
+
};
|
|
833
|
+
const stream = await this.#streamClient.messages.stream(params);
|
|
834
|
+
let textContent = "";
|
|
835
|
+
let inputTokens = 0;
|
|
836
|
+
let outputTokens = 0;
|
|
837
|
+
let stopReason = null;
|
|
838
|
+
const toolBlocks = /* @__PURE__ */ new Map();
|
|
839
|
+
for await (const event of stream) {
|
|
840
|
+
if (event.type === "message_start") {
|
|
841
|
+
inputTokens = event.message.usage.input_tokens;
|
|
842
|
+
} else if (event.type === "content_block_start") {
|
|
843
|
+
if (event.content_block.type === "tool_use") {
|
|
844
|
+
toolBlocks.set(event.index, {
|
|
845
|
+
id: event.content_block.id ?? "",
|
|
846
|
+
name: event.content_block.name ?? "",
|
|
847
|
+
inputJson: ""
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
} else if (event.type === "content_block_delta") {
|
|
851
|
+
if (event.delta.type === "text_delta") {
|
|
852
|
+
textContent += event.delta.text;
|
|
853
|
+
yield { type: "token_delta", delta: event.delta.text };
|
|
854
|
+
} else if (event.delta.type === "input_json_delta") {
|
|
855
|
+
const block = toolBlocks.get(event.index);
|
|
856
|
+
if (block !== void 0) {
|
|
857
|
+
block.inputJson += event.delta.partial_json;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
} else if (event.type === "message_delta") {
|
|
861
|
+
outputTokens = event.usage.output_tokens;
|
|
862
|
+
stopReason = event.delta.stop_reason;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const usage = { inputTokens, outputTokens };
|
|
866
|
+
if (stopReason === "tool_use" && toolBlocks.size > 0) {
|
|
867
|
+
const calls = Array.from(toolBlocks.entries()).sort(([a], [b]) => a - b).map(([, block]) => ({
|
|
868
|
+
id: block.id,
|
|
869
|
+
name: block.name,
|
|
870
|
+
input: parseToolCallArguments(block.inputJson)
|
|
871
|
+
}));
|
|
872
|
+
yield { type: "tool_calls", calls, usage };
|
|
873
|
+
} else {
|
|
874
|
+
yield { type: "message_done", content: textContent, usage };
|
|
875
|
+
}
|
|
876
|
+
} catch (error) {
|
|
877
|
+
yield normalizeAnthropicError(error);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
function translateMessagesToAnthropic(messages) {
|
|
882
|
+
let system;
|
|
883
|
+
const result = [];
|
|
884
|
+
let i = 0;
|
|
885
|
+
while (i < messages.length) {
|
|
886
|
+
const msg = messages[i];
|
|
887
|
+
if (msg === void 0)
|
|
888
|
+
break;
|
|
889
|
+
if (msg.role === "system") {
|
|
890
|
+
system = msg.content ?? void 0;
|
|
891
|
+
i++;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (msg.role === "tool") {
|
|
895
|
+
const blocks = [];
|
|
896
|
+
while (i < messages.length) {
|
|
897
|
+
const tm = messages[i];
|
|
898
|
+
if (tm === void 0 || tm.role !== "tool")
|
|
899
|
+
break;
|
|
900
|
+
blocks.push({ type: "tool_result", tool_use_id: tm.toolCallId ?? "", content: tm.content ?? "" });
|
|
901
|
+
i++;
|
|
902
|
+
}
|
|
903
|
+
result.push({ role: "user", content: blocks });
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (msg.role === "assistant") {
|
|
907
|
+
if (msg.toolCalls !== void 0 && msg.toolCalls.length > 0) {
|
|
908
|
+
const blocks = [
|
|
909
|
+
...msg.content ? [{ type: "text", text: msg.content }] : [],
|
|
910
|
+
...msg.toolCalls.map((c) => ({ type: "tool_use", id: c.id, name: c.name, input: c.input }))
|
|
911
|
+
];
|
|
912
|
+
result.push({ role: "assistant", content: blocks });
|
|
913
|
+
} else {
|
|
914
|
+
result.push({ role: "assistant", content: msg.content ?? "" });
|
|
915
|
+
}
|
|
916
|
+
i++;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
result.push({ role: "user", content: msg.content ?? "" });
|
|
920
|
+
i++;
|
|
921
|
+
}
|
|
922
|
+
return { system, messages: result };
|
|
923
|
+
}
|
|
924
|
+
function translateToolsToAnthropic(tools) {
|
|
925
|
+
return tools.map((t) => ({
|
|
926
|
+
name: t.function.name,
|
|
927
|
+
description: t.function.description,
|
|
928
|
+
input_schema: {
|
|
929
|
+
type: "object",
|
|
930
|
+
...t.function.parameters.properties !== void 0 ? { properties: t.function.parameters.properties } : {},
|
|
931
|
+
...t.function.parameters.required !== void 0 ? { required: t.function.parameters.required } : {}
|
|
932
|
+
}
|
|
933
|
+
}));
|
|
934
|
+
}
|
|
935
|
+
function isToolUseBlock(block) {
|
|
936
|
+
return block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string";
|
|
937
|
+
}
|
|
938
|
+
function isTextBlock(block) {
|
|
939
|
+
return block.type === "text" && typeof block.text === "string";
|
|
940
|
+
}
|
|
941
|
+
function normalizeAnthropicError(error) {
|
|
942
|
+
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") {
|
|
943
|
+
const apiError = error;
|
|
944
|
+
return {
|
|
945
|
+
type: "error",
|
|
946
|
+
category: anthropicErrorCategory(apiError.status),
|
|
947
|
+
message: apiError.message ?? `Anthropic API error ${apiError.status}.`,
|
|
948
|
+
recoverable: apiError.status === 429 || apiError.status >= 500
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
type: "error",
|
|
953
|
+
category: "network",
|
|
954
|
+
message: "Provider network request failed.",
|
|
955
|
+
recoverable: true
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function anthropicErrorCategory(status) {
|
|
959
|
+
if (status === 401 || status === 403)
|
|
960
|
+
return "authentication";
|
|
961
|
+
if (status === 429)
|
|
962
|
+
return "rate_limit";
|
|
963
|
+
if (status === 400)
|
|
964
|
+
return "invalid_request";
|
|
965
|
+
if (status === 404)
|
|
966
|
+
return "model_unavailable";
|
|
967
|
+
if (status === 413 || status === 422)
|
|
968
|
+
return "context_length";
|
|
969
|
+
return "unknown";
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// ../../packages/permissions/dist/index.js
|
|
973
|
+
var DefaultPermissionPolicy = class {
|
|
974
|
+
evaluate(input) {
|
|
975
|
+
const risk = input.action.risk;
|
|
976
|
+
if (risk === "blocked") {
|
|
977
|
+
return {
|
|
978
|
+
decision: "deny",
|
|
979
|
+
risk,
|
|
980
|
+
reason: "Blocked actions are denied."
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
if (input.mode === "observe") {
|
|
984
|
+
return {
|
|
985
|
+
decision: "ask",
|
|
986
|
+
risk,
|
|
987
|
+
reason: "Observe mode asks before external actions."
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
if (input.mode === "auto") {
|
|
991
|
+
return risk === "high" ? {
|
|
992
|
+
decision: "ask",
|
|
993
|
+
risk,
|
|
994
|
+
reason: "High-risk action requires approval in auto mode."
|
|
995
|
+
} : {
|
|
996
|
+
decision: "allow",
|
|
997
|
+
risk,
|
|
998
|
+
reason: "Low and medium-risk actions are allowed in auto mode."
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
return risk === "low" ? {
|
|
1002
|
+
decision: "allow",
|
|
1003
|
+
risk,
|
|
1004
|
+
reason: "Low-risk action is allowed in confirm mode."
|
|
1005
|
+
} : {
|
|
1006
|
+
decision: "ask",
|
|
1007
|
+
risk,
|
|
1008
|
+
reason: "Medium and high-risk actions require approval in confirm mode."
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
// ../../packages/tools/dist/index.js
|
|
1014
|
+
import { exec } from "child_process";
|
|
1015
|
+
import { access, readdir, readFile as readFile2, stat as statFs, writeFile as writeFileFs, mkdir } from "fs/promises";
|
|
1016
|
+
import { resolve, relative, basename, extname, dirname, join as join3 } from "path";
|
|
1017
|
+
function createReadFileTool() {
|
|
1018
|
+
return {
|
|
1019
|
+
name: "read_file",
|
|
1020
|
+
description: "Read a UTF-8 file inside the workspace.",
|
|
1021
|
+
inputSchema: {
|
|
1022
|
+
type: "object",
|
|
1023
|
+
properties: {
|
|
1024
|
+
path: {
|
|
1025
|
+
type: "string"
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
required: ["path"]
|
|
1029
|
+
},
|
|
1030
|
+
risk: "low",
|
|
1031
|
+
async execute(input, context) {
|
|
1032
|
+
const path = getPathInput(input);
|
|
1033
|
+
if (path === void 0) {
|
|
1034
|
+
return inputError("Tool input must include a string path.");
|
|
1035
|
+
}
|
|
1036
|
+
const target = resolveWorkspacePath(context.workspaceRoot, path);
|
|
1037
|
+
if (target === void 0) {
|
|
1038
|
+
return outsideWorkspaceError();
|
|
1039
|
+
}
|
|
1040
|
+
if (isSecretLikePath(target.absolutePath)) {
|
|
1041
|
+
return secretFileError();
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
return {
|
|
1045
|
+
ok: true,
|
|
1046
|
+
content: await readFile2(target.absolutePath, "utf8"),
|
|
1047
|
+
summary: `Read file ${target.displayPath}.`
|
|
1048
|
+
};
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
return fileSystemError(error);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function createListDirectoryTool() {
|
|
1056
|
+
return {
|
|
1057
|
+
name: "list_directory",
|
|
1058
|
+
description: "List entries in a workspace directory.",
|
|
1059
|
+
inputSchema: {
|
|
1060
|
+
type: "object",
|
|
1061
|
+
properties: {
|
|
1062
|
+
path: {
|
|
1063
|
+
type: "string"
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
required: ["path"]
|
|
1067
|
+
},
|
|
1068
|
+
risk: "low",
|
|
1069
|
+
async execute(input, context) {
|
|
1070
|
+
const path = getPathInput(input);
|
|
1071
|
+
if (path === void 0) {
|
|
1072
|
+
return inputError("Tool input must include a string path.");
|
|
1073
|
+
}
|
|
1074
|
+
const target = resolveWorkspacePath(context.workspaceRoot, path);
|
|
1075
|
+
if (target === void 0) {
|
|
1076
|
+
return outsideWorkspaceError();
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
const entries = await readdir(target.absolutePath, { withFileTypes: true });
|
|
1080
|
+
return {
|
|
1081
|
+
ok: true,
|
|
1082
|
+
entries: entries.map((entry) => ({
|
|
1083
|
+
name: entry.name,
|
|
1084
|
+
type: entry.isDirectory() ? "directory" : entry.isFile() ? "file" : "other"
|
|
1085
|
+
})).sort((left, right) => left.name.localeCompare(right.name)),
|
|
1086
|
+
summary: `Listed directory ${target.displayPath}.`
|
|
1087
|
+
};
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
return fileSystemError(error);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function getPathInput(input) {
|
|
1095
|
+
if (typeof input !== "object" || input === null || !("path" in input)) {
|
|
1096
|
+
return void 0;
|
|
1097
|
+
}
|
|
1098
|
+
const path = input.path;
|
|
1099
|
+
return typeof path === "string" ? path : void 0;
|
|
1100
|
+
}
|
|
1101
|
+
function resolveWorkspacePath(workspaceRoot, path) {
|
|
1102
|
+
const root = resolve(workspaceRoot);
|
|
1103
|
+
const absolutePath = resolve(root, path);
|
|
1104
|
+
const relativePath = relative(root, absolutePath);
|
|
1105
|
+
if (relativePath.startsWith("..") || relativePath === ".." || absolutePath !== root && relativePath === "") {
|
|
1106
|
+
return void 0;
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
absolutePath,
|
|
1110
|
+
displayPath: relativePath === "" ? "." : relativePath
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
function inputError(message) {
|
|
1114
|
+
return {
|
|
1115
|
+
ok: false,
|
|
1116
|
+
error: {
|
|
1117
|
+
code: "invalid_input",
|
|
1118
|
+
message
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function outsideWorkspaceError() {
|
|
1123
|
+
return {
|
|
1124
|
+
ok: false,
|
|
1125
|
+
error: {
|
|
1126
|
+
code: "path_outside_workspace",
|
|
1127
|
+
message: "Tool path must stay inside the workspace."
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function fileSystemError(error) {
|
|
1132
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "fs_error";
|
|
1133
|
+
return {
|
|
1134
|
+
ok: false,
|
|
1135
|
+
error: {
|
|
1136
|
+
code,
|
|
1137
|
+
message: "File system operation failed."
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function isSecretLikePath(absolutePath) {
|
|
1142
|
+
const name = basename(absolutePath).toLowerCase();
|
|
1143
|
+
if (name === ".env" || name.startsWith(".env."))
|
|
1144
|
+
return true;
|
|
1145
|
+
if (name === ".netrc")
|
|
1146
|
+
return true;
|
|
1147
|
+
const ext = extname(name);
|
|
1148
|
+
if (ext === ".key" || ext === ".pem" || ext === ".p12" || ext === ".pfx")
|
|
1149
|
+
return true;
|
|
1150
|
+
return name === "id_rsa" || name === "id_ed25519" || name === "id_ecdsa" || name === "id_dsa";
|
|
1151
|
+
}
|
|
1152
|
+
function secretFileError() {
|
|
1153
|
+
return {
|
|
1154
|
+
ok: false,
|
|
1155
|
+
error: {
|
|
1156
|
+
code: "path_not_permitted",
|
|
1157
|
+
message: "Tool path is not permitted."
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function getWriteInput(input) {
|
|
1162
|
+
if (typeof input !== "object" || input === null)
|
|
1163
|
+
return void 0;
|
|
1164
|
+
const path = input.path;
|
|
1165
|
+
const content = input.content;
|
|
1166
|
+
return typeof path === "string" && typeof content === "string" ? { path, content } : void 0;
|
|
1167
|
+
}
|
|
1168
|
+
var SHELL_DEFAULT_TIMEOUT_MS = 3e4;
|
|
1169
|
+
var SHELL_MAX_OUTPUT_CHARS = 4e3;
|
|
1170
|
+
var BLOCKED_COMMAND_PATTERNS = [
|
|
1171
|
+
/\brm\b.*-[a-zA-Z]*r[a-zA-Z]*.*\s+\/\s*$/,
|
|
1172
|
+
// rm -r* targeting root /
|
|
1173
|
+
/\brm\b.*-[a-zA-Z]*r[a-zA-Z]*.*\s+~\/?$/,
|
|
1174
|
+
// rm -r* targeting home ~
|
|
1175
|
+
/:\(\)\s*\{/,
|
|
1176
|
+
// fork bomb
|
|
1177
|
+
/[|>]\s*\/dev\/(sd|hd|nvme|vd)[a-z0-9]?/,
|
|
1178
|
+
// write/pipe to block devices
|
|
1179
|
+
/\b(mkfs(\.[a-z0-9]+)?|fdisk|parted|shred)\b/
|
|
1180
|
+
// disk tools
|
|
1181
|
+
];
|
|
1182
|
+
function isBlockedCommand(command) {
|
|
1183
|
+
return BLOCKED_COMMAND_PATTERNS.some((pattern) => pattern.test(command));
|
|
1184
|
+
}
|
|
1185
|
+
function truncateShellOutput(output) {
|
|
1186
|
+
if (output.length <= SHELL_MAX_OUTPUT_CHARS)
|
|
1187
|
+
return output;
|
|
1188
|
+
return `${output.slice(0, SHELL_MAX_OUTPUT_CHARS)}
|
|
1189
|
+
[truncated ${output.length - SHELL_MAX_OUTPUT_CHARS} characters]`;
|
|
1190
|
+
}
|
|
1191
|
+
function runShellCommand(command, cwd, timeoutMs) {
|
|
1192
|
+
return new Promise((resolve2) => {
|
|
1193
|
+
const start = Date.now();
|
|
1194
|
+
exec(command, { cwd, timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
1195
|
+
const durationMs = Date.now() - start;
|
|
1196
|
+
if (error?.killed === true) {
|
|
1197
|
+
resolve2({ completed: false });
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const exitCode = error === null ? 0 : typeof error.code === "number" ? error.code : 1;
|
|
1201
|
+
resolve2({
|
|
1202
|
+
completed: true,
|
|
1203
|
+
exitCode,
|
|
1204
|
+
stdout: truncateShellOutput(stdout),
|
|
1205
|
+
stderr: truncateShellOutput(stderr),
|
|
1206
|
+
durationMs
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
function getShellInput(input) {
|
|
1212
|
+
if (typeof input !== "object" || input === null)
|
|
1213
|
+
return void 0;
|
|
1214
|
+
const command = input.command;
|
|
1215
|
+
if (typeof command !== "string")
|
|
1216
|
+
return void 0;
|
|
1217
|
+
const timeoutMs = input.timeoutMs;
|
|
1218
|
+
return { command, ...typeof timeoutMs === "number" ? { timeoutMs } : {} };
|
|
1219
|
+
}
|
|
1220
|
+
function blockedCommandError() {
|
|
1221
|
+
return {
|
|
1222
|
+
ok: false,
|
|
1223
|
+
error: {
|
|
1224
|
+
code: "command_blocked",
|
|
1225
|
+
message: "Command matches a blocked pattern."
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
function sandboxRejectionError() {
|
|
1230
|
+
return {
|
|
1231
|
+
ok: false,
|
|
1232
|
+
error: {
|
|
1233
|
+
code: "sandbox_rejected",
|
|
1234
|
+
message: "Command rejected: workspace sandbox prevents execution outside workspace."
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
var SANDBOX_ESCAPE_PATTERNS = [
|
|
1239
|
+
/\/\.\.\//,
|
|
1240
|
+
// /../ path traversal
|
|
1241
|
+
/\bcd\s+\/(\s|$)/,
|
|
1242
|
+
// cd / or cd /... (absolute root)
|
|
1243
|
+
/\bcd\s+~\/?(\s|$)/
|
|
1244
|
+
// cd ~ or cd ~/...
|
|
1245
|
+
];
|
|
1246
|
+
function isSandboxEscape(command) {
|
|
1247
|
+
return SANDBOX_ESCAPE_PATTERNS.some((pattern) => pattern.test(command));
|
|
1248
|
+
}
|
|
1249
|
+
function createShellTool(options) {
|
|
1250
|
+
return {
|
|
1251
|
+
name: "run_shell",
|
|
1252
|
+
description: "Run a shell command in the workspace directory. Requires approval.",
|
|
1253
|
+
inputSchema: {
|
|
1254
|
+
type: "object",
|
|
1255
|
+
properties: {
|
|
1256
|
+
command: { type: "string" },
|
|
1257
|
+
timeoutMs: { type: "number" }
|
|
1258
|
+
},
|
|
1259
|
+
required: ["command"]
|
|
1260
|
+
},
|
|
1261
|
+
risk: "high",
|
|
1262
|
+
async execute(input, context) {
|
|
1263
|
+
const parsed = getShellInput(input);
|
|
1264
|
+
if (parsed === void 0) {
|
|
1265
|
+
return inputError("Tool input must include a string command.");
|
|
1266
|
+
}
|
|
1267
|
+
if (isBlockedCommand(parsed.command)) {
|
|
1268
|
+
return blockedCommandError();
|
|
1269
|
+
}
|
|
1270
|
+
if (options?.sandboxed === true && isSandboxEscape(parsed.command)) {
|
|
1271
|
+
return sandboxRejectionError();
|
|
1272
|
+
}
|
|
1273
|
+
const timeoutMs = parsed.timeoutMs ?? SHELL_DEFAULT_TIMEOUT_MS;
|
|
1274
|
+
const result = await runShellCommand(parsed.command, context.workspaceRoot, timeoutMs);
|
|
1275
|
+
if (!result.completed) {
|
|
1276
|
+
return {
|
|
1277
|
+
ok: false,
|
|
1278
|
+
error: {
|
|
1279
|
+
code: "timeout",
|
|
1280
|
+
message: `Command exceeded ${timeoutMs}ms timeout.`
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
ok: true,
|
|
1286
|
+
exitCode: result.exitCode,
|
|
1287
|
+
stdout: result.stdout,
|
|
1288
|
+
stderr: result.stderr,
|
|
1289
|
+
durationMs: result.durationMs,
|
|
1290
|
+
summary: `Ran command in ${result.durationMs}ms with exit code ${result.exitCode}.`
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function createWriteFileTool() {
|
|
1296
|
+
return {
|
|
1297
|
+
name: "write_file",
|
|
1298
|
+
description: "Write or overwrite a UTF-8 file inside the workspace. Requires approval.",
|
|
1299
|
+
inputSchema: {
|
|
1300
|
+
type: "object",
|
|
1301
|
+
properties: {
|
|
1302
|
+
path: { type: "string" },
|
|
1303
|
+
content: { type: "string" }
|
|
1304
|
+
},
|
|
1305
|
+
required: ["path", "content"]
|
|
1306
|
+
},
|
|
1307
|
+
risk: "medium",
|
|
1308
|
+
async execute(input, context) {
|
|
1309
|
+
const parsed = getWriteInput(input);
|
|
1310
|
+
if (parsed === void 0) {
|
|
1311
|
+
return inputError("Tool input must include a string path and string content.");
|
|
1312
|
+
}
|
|
1313
|
+
const target = resolveWorkspacePath(context.workspaceRoot, parsed.path);
|
|
1314
|
+
if (target === void 0) {
|
|
1315
|
+
return outsideWorkspaceError();
|
|
1316
|
+
}
|
|
1317
|
+
if (isSecretLikePath(target.absolutePath)) {
|
|
1318
|
+
return secretFileError();
|
|
1319
|
+
}
|
|
1320
|
+
try {
|
|
1321
|
+
await mkdir(dirname(target.absolutePath), { recursive: true });
|
|
1322
|
+
await writeFileFs(target.absolutePath, parsed.content, "utf8");
|
|
1323
|
+
return {
|
|
1324
|
+
ok: true,
|
|
1325
|
+
summary: `Wrote file ${target.displayPath}.`
|
|
1326
|
+
};
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
return fileSystemError(error);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
function countOccurrences(text, needle) {
|
|
1334
|
+
if (needle.length === 0)
|
|
1335
|
+
return 0;
|
|
1336
|
+
let count = 0;
|
|
1337
|
+
let pos = 0;
|
|
1338
|
+
while ((pos = text.indexOf(needle, pos)) !== -1) {
|
|
1339
|
+
count++;
|
|
1340
|
+
pos += needle.length;
|
|
1341
|
+
}
|
|
1342
|
+
return count;
|
|
1343
|
+
}
|
|
1344
|
+
function createEditFileTool() {
|
|
1345
|
+
return {
|
|
1346
|
+
name: "edit_file",
|
|
1347
|
+
description: "Make a precise edit to an existing file by replacing an exact string. old_string must appear exactly once in the file unless replace_all is true. Prefer this over write_file when modifying existing content \u2014 it never loses surrounding code.",
|
|
1348
|
+
risk: "medium",
|
|
1349
|
+
inputSchema: {
|
|
1350
|
+
type: "object",
|
|
1351
|
+
properties: {
|
|
1352
|
+
path: { type: "string", description: "File path relative to workspace root." },
|
|
1353
|
+
old_string: { type: "string", description: "The exact string to replace. Must be unique in the file." },
|
|
1354
|
+
new_string: { type: "string", description: "The replacement string." },
|
|
1355
|
+
replace_all: { type: "boolean", description: "Replace every occurrence. Default false (errors on multiple matches)." }
|
|
1356
|
+
},
|
|
1357
|
+
required: ["path", "old_string", "new_string"]
|
|
1358
|
+
},
|
|
1359
|
+
async execute(rawInput, context) {
|
|
1360
|
+
const input = rawInput;
|
|
1361
|
+
if (typeof input["path"] !== "string" || typeof input["old_string"] !== "string" || typeof input["new_string"] !== "string") {
|
|
1362
|
+
return inputError("path, old_string, and new_string must be strings.");
|
|
1363
|
+
}
|
|
1364
|
+
const filePath = input["path"];
|
|
1365
|
+
const oldStr = input["old_string"];
|
|
1366
|
+
const newStr = input["new_string"];
|
|
1367
|
+
const replaceAll = input["replace_all"] === true;
|
|
1368
|
+
if (oldStr.length === 0)
|
|
1369
|
+
return inputError("old_string must not be empty.");
|
|
1370
|
+
const abs = resolve(context.workspaceRoot, filePath);
|
|
1371
|
+
if (!abs.startsWith(resolve(context.workspaceRoot) + "/") && abs !== resolve(context.workspaceRoot)) {
|
|
1372
|
+
return outsideWorkspaceError();
|
|
1373
|
+
}
|
|
1374
|
+
if (isSecretLikePath(abs))
|
|
1375
|
+
return secretFileError();
|
|
1376
|
+
let content;
|
|
1377
|
+
try {
|
|
1378
|
+
content = await readFile2(abs, "utf8");
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
return fileSystemError(err);
|
|
1381
|
+
}
|
|
1382
|
+
const count = countOccurrences(content, oldStr);
|
|
1383
|
+
if (count === 0) {
|
|
1384
|
+
return { ok: false, error: { code: "string_not_found", message: `old_string not found in ${filePath}.` } };
|
|
1385
|
+
}
|
|
1386
|
+
if (count > 1 && !replaceAll) {
|
|
1387
|
+
return { ok: false, error: { code: "multiple_matches", message: `old_string appears ${count} times in ${filePath}. Use replace_all: true or add more surrounding context to make it unique.` } };
|
|
1388
|
+
}
|
|
1389
|
+
const newContent = content.split(oldStr).join(newStr);
|
|
1390
|
+
try {
|
|
1391
|
+
await writeFileFs(abs, newContent, "utf8");
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
return fileSystemError(err);
|
|
1394
|
+
}
|
|
1395
|
+
return { ok: true, path: filePath, replacements: count, summary: `Edited ${filePath}: ${count} replacement${count === 1 ? "" : "s"}.` };
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function createAppendFileTool() {
|
|
1400
|
+
return {
|
|
1401
|
+
name: "append_file",
|
|
1402
|
+
description: "Append text to the end of a file. Creates the file (and parent directories) if it does not exist. Use this to add new code, tests, or entries without touching existing content.",
|
|
1403
|
+
risk: "medium",
|
|
1404
|
+
inputSchema: {
|
|
1405
|
+
type: "object",
|
|
1406
|
+
properties: {
|
|
1407
|
+
path: { type: "string", description: "File path relative to workspace root." },
|
|
1408
|
+
content: { type: "string", description: "Text to append." }
|
|
1409
|
+
},
|
|
1410
|
+
required: ["path", "content"]
|
|
1411
|
+
},
|
|
1412
|
+
async execute(rawInput, context) {
|
|
1413
|
+
const input = rawInput;
|
|
1414
|
+
if (typeof input["path"] !== "string" || typeof input["content"] !== "string") {
|
|
1415
|
+
return inputError("path and content must be strings.");
|
|
1416
|
+
}
|
|
1417
|
+
const filePath = input["path"];
|
|
1418
|
+
const text = input["content"];
|
|
1419
|
+
const abs = resolve(context.workspaceRoot, filePath);
|
|
1420
|
+
if (!abs.startsWith(resolve(context.workspaceRoot) + "/") && abs !== resolve(context.workspaceRoot)) {
|
|
1421
|
+
return outsideWorkspaceError();
|
|
1422
|
+
}
|
|
1423
|
+
if (isSecretLikePath(abs))
|
|
1424
|
+
return secretFileError();
|
|
1425
|
+
try {
|
|
1426
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
1427
|
+
await writeFileFs(abs, text, { flag: "a" });
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
return fileSystemError(err);
|
|
1430
|
+
}
|
|
1431
|
+
return { ok: true, path: filePath, summary: `Appended to ${filePath}.` };
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
var WEB_PAGE_MAX_CHARS = 8e3;
|
|
1436
|
+
function parseHttpUrl(url) {
|
|
1437
|
+
try {
|
|
1438
|
+
const parsed = new URL(url);
|
|
1439
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed : void 0;
|
|
1440
|
+
} catch {
|
|
1441
|
+
return void 0;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function extractTextFromHtml(html) {
|
|
1445
|
+
return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
1446
|
+
}
|
|
1447
|
+
function truncateWebContent(content) {
|
|
1448
|
+
if (content.length <= WEB_PAGE_MAX_CHARS)
|
|
1449
|
+
return content;
|
|
1450
|
+
return `${content.slice(0, WEB_PAGE_MAX_CHARS)}
|
|
1451
|
+
[truncated ${content.length - WEB_PAGE_MAX_CHARS} characters]`;
|
|
1452
|
+
}
|
|
1453
|
+
function getUrlInput(input) {
|
|
1454
|
+
if (typeof input !== "object" || input === null)
|
|
1455
|
+
return void 0;
|
|
1456
|
+
const url = input.url;
|
|
1457
|
+
return typeof url === "string" ? url : void 0;
|
|
1458
|
+
}
|
|
1459
|
+
function createReadWebPageTool(fetchFn = fetch) {
|
|
1460
|
+
return {
|
|
1461
|
+
name: "read_web_page",
|
|
1462
|
+
description: "Read a public web page and return its text content.",
|
|
1463
|
+
inputSchema: {
|
|
1464
|
+
type: "object",
|
|
1465
|
+
properties: {
|
|
1466
|
+
url: { type: "string" }
|
|
1467
|
+
},
|
|
1468
|
+
required: ["url"]
|
|
1469
|
+
},
|
|
1470
|
+
risk: "low",
|
|
1471
|
+
async execute(input, _context) {
|
|
1472
|
+
const url = getUrlInput(input);
|
|
1473
|
+
if (url === void 0) {
|
|
1474
|
+
return inputError("Tool input must include a string url.");
|
|
1475
|
+
}
|
|
1476
|
+
const parsed = parseHttpUrl(url);
|
|
1477
|
+
if (parsed === void 0) {
|
|
1478
|
+
return inputError("Tool url must use http or https.");
|
|
1479
|
+
}
|
|
1480
|
+
try {
|
|
1481
|
+
const response = await fetchFn(url);
|
|
1482
|
+
if (!response.ok) {
|
|
1483
|
+
return {
|
|
1484
|
+
ok: false,
|
|
1485
|
+
error: {
|
|
1486
|
+
code: "http_error",
|
|
1487
|
+
message: `Page request failed with status ${response.status}.`
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
const html = await response.text();
|
|
1492
|
+
const content = truncateWebContent(extractTextFromHtml(html));
|
|
1493
|
+
return {
|
|
1494
|
+
ok: true,
|
|
1495
|
+
url,
|
|
1496
|
+
content,
|
|
1497
|
+
summary: `Read web page ${parsed.hostname}.`
|
|
1498
|
+
};
|
|
1499
|
+
} catch {
|
|
1500
|
+
return {
|
|
1501
|
+
ok: false,
|
|
1502
|
+
error: {
|
|
1503
|
+
code: "network_error",
|
|
1504
|
+
message: "Web page request failed."
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function createUpdateTodosTool(onUpdate) {
|
|
1512
|
+
return {
|
|
1513
|
+
name: "update_todos",
|
|
1514
|
+
description: "Update the task list to track progress on multi-step work. Call this when starting a complex task or after completing each step. At most one item may be in_progress at a time.",
|
|
1515
|
+
inputSchema: {
|
|
1516
|
+
type: "object",
|
|
1517
|
+
properties: {
|
|
1518
|
+
todos: {
|
|
1519
|
+
type: "array",
|
|
1520
|
+
items: {
|
|
1521
|
+
type: "object",
|
|
1522
|
+
properties: {
|
|
1523
|
+
content: { type: "string" },
|
|
1524
|
+
status: { type: "string", enum: ["pending", "in_progress", "completed"] }
|
|
1525
|
+
},
|
|
1526
|
+
required: ["content", "status"]
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
},
|
|
1530
|
+
required: ["todos"]
|
|
1531
|
+
},
|
|
1532
|
+
risk: "low",
|
|
1533
|
+
async execute(input, _context) {
|
|
1534
|
+
const raw = input;
|
|
1535
|
+
if (!Array.isArray(raw.todos)) {
|
|
1536
|
+
return { ok: false, error: { code: "invalid_input", message: "todos must be an array." } };
|
|
1537
|
+
}
|
|
1538
|
+
const todos = [];
|
|
1539
|
+
for (const item of raw.todos) {
|
|
1540
|
+
if (typeof item !== "object" || item === null) {
|
|
1541
|
+
return { ok: false, error: { code: "invalid_input", message: "Each todo must be an object." } };
|
|
1542
|
+
}
|
|
1543
|
+
const { content, status } = item;
|
|
1544
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
1545
|
+
return { ok: false, error: { code: "invalid_input", message: "Each todo must have a non-empty content string." } };
|
|
1546
|
+
}
|
|
1547
|
+
if (status !== "pending" && status !== "in_progress" && status !== "completed") {
|
|
1548
|
+
return { ok: false, error: { code: "invalid_input", message: `Invalid status "${String(status)}". Must be pending, in_progress, or completed.` } };
|
|
1549
|
+
}
|
|
1550
|
+
todos.push({ content, status });
|
|
1551
|
+
}
|
|
1552
|
+
const inProgressCount = todos.filter((t) => t.status === "in_progress").length;
|
|
1553
|
+
if (inProgressCount > 1) {
|
|
1554
|
+
return { ok: false, error: { code: "invalid_input", message: "At most one todo may be in_progress at a time." } };
|
|
1555
|
+
}
|
|
1556
|
+
onUpdate?.(todos);
|
|
1557
|
+
return { ok: true };
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function createLoadSkillTool(skillFileMap) {
|
|
1562
|
+
return {
|
|
1563
|
+
name: "load_skill",
|
|
1564
|
+
description: "Load the full instructions for a named skill. Call this when you need to follow a skill's detailed guidance. Available skills are listed in the <skills> section.",
|
|
1565
|
+
risk: "low",
|
|
1566
|
+
inputSchema: {
|
|
1567
|
+
type: "object",
|
|
1568
|
+
properties: {
|
|
1569
|
+
name: { type: "string", description: "The exact skill name to load." }
|
|
1570
|
+
},
|
|
1571
|
+
required: ["name"]
|
|
1572
|
+
},
|
|
1573
|
+
async execute(rawInput) {
|
|
1574
|
+
const { name } = rawInput;
|
|
1575
|
+
const filePath = skillFileMap.get(name);
|
|
1576
|
+
if (filePath === void 0) {
|
|
1577
|
+
return { ok: false, error: `Skill "${name}" not found. Check the skills list for available names.` };
|
|
1578
|
+
}
|
|
1579
|
+
try {
|
|
1580
|
+
const { readFile: readFile7 } = await import("fs/promises");
|
|
1581
|
+
const content = await readFile7(filePath, "utf-8");
|
|
1582
|
+
return { ok: true, content };
|
|
1583
|
+
} catch {
|
|
1584
|
+
return { ok: false, error: `Could not read skill file for "${name}".` };
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function createMemorySearchTool(memoryDir) {
|
|
1590
|
+
return {
|
|
1591
|
+
name: "memory_search",
|
|
1592
|
+
description: "Search over memory files (MEMORY.md, USER.md, memory/YYYY-MM-DD.md) for relevant content. Returns matching excerpts.",
|
|
1593
|
+
risk: "low",
|
|
1594
|
+
inputSchema: {
|
|
1595
|
+
type: "object",
|
|
1596
|
+
properties: {
|
|
1597
|
+
query: { type: "string" },
|
|
1598
|
+
maxResults: { type: "number" }
|
|
1599
|
+
},
|
|
1600
|
+
required: ["query"]
|
|
1601
|
+
},
|
|
1602
|
+
async execute(input) {
|
|
1603
|
+
const raw = input;
|
|
1604
|
+
const query = typeof raw.query === "string" ? raw.query : "";
|
|
1605
|
+
const maxResults = typeof raw.maxResults === "number" ? raw.maxResults : 5;
|
|
1606
|
+
const candidateFiles = [];
|
|
1607
|
+
const rootMemoryMd = join3(memoryDir, "MEMORY.md");
|
|
1608
|
+
const rootUserMd = join3(memoryDir, "USER.md");
|
|
1609
|
+
const memorySubdir = join3(memoryDir, "memory");
|
|
1610
|
+
for (const candidatePath of [rootMemoryMd, rootUserMd]) {
|
|
1611
|
+
try {
|
|
1612
|
+
await access(candidatePath);
|
|
1613
|
+
candidateFiles.push(candidatePath);
|
|
1614
|
+
} catch {
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
try {
|
|
1618
|
+
const entries = await readdir(memorySubdir, { recursive: true, withFileTypes: true });
|
|
1619
|
+
for (const entry of entries) {
|
|
1620
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1621
|
+
const dir = typeof entry.parentPath === "string" ? entry.parentPath : entry.path;
|
|
1622
|
+
candidateFiles.push(join3(dir, entry.name));
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
} catch {
|
|
1626
|
+
}
|
|
1627
|
+
if (candidateFiles.length === 0) {
|
|
1628
|
+
return { ok: true, results: [], total: 0 };
|
|
1629
|
+
}
|
|
1630
|
+
const queryWords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 0);
|
|
1631
|
+
const matches = [];
|
|
1632
|
+
for (const filePath of candidateFiles) {
|
|
1633
|
+
let content;
|
|
1634
|
+
try {
|
|
1635
|
+
content = await readFile2(filePath, "utf8");
|
|
1636
|
+
} catch {
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
const paragraphs = content.split(/\n\n+/);
|
|
1640
|
+
const relPath = relative(memoryDir, filePath);
|
|
1641
|
+
for (const paragraph of paragraphs) {
|
|
1642
|
+
const lowerParagraph = paragraph.toLowerCase();
|
|
1643
|
+
if (queryWords.some((word) => lowerParagraph.includes(word))) {
|
|
1644
|
+
matches.push({ file: relPath, excerpt: paragraph.trim() });
|
|
1645
|
+
if (matches.length >= maxResults)
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (matches.length >= maxResults)
|
|
1650
|
+
break;
|
|
1651
|
+
}
|
|
1652
|
+
return { ok: true, results: matches, total: matches.length };
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
function createMemoryGetTool(memoryDir) {
|
|
1657
|
+
return {
|
|
1658
|
+
name: "memory_get",
|
|
1659
|
+
description: "Read the full contents of a specific memory file. Valid paths: MEMORY.md, USER.md, memory/YYYY-MM-DD.md",
|
|
1660
|
+
risk: "low",
|
|
1661
|
+
inputSchema: {
|
|
1662
|
+
type: "object",
|
|
1663
|
+
properties: {
|
|
1664
|
+
path: { type: "string", description: "Relative path within the memory workspace, e.g. MEMORY.md or memory/2026-05-05.md" }
|
|
1665
|
+
},
|
|
1666
|
+
required: ["path"]
|
|
1667
|
+
},
|
|
1668
|
+
async execute(input) {
|
|
1669
|
+
const raw = input;
|
|
1670
|
+
const requestedPath = typeof raw.path === "string" ? raw.path : "";
|
|
1671
|
+
if (requestedPath.includes("..")) {
|
|
1672
|
+
return { ok: true, error: "Path traversal is not permitted." };
|
|
1673
|
+
}
|
|
1674
|
+
if (requestedPath.startsWith("/")) {
|
|
1675
|
+
return { ok: true, error: "Absolute paths are not permitted." };
|
|
1676
|
+
}
|
|
1677
|
+
if (!requestedPath.endsWith(".md")) {
|
|
1678
|
+
return { ok: true, error: "Only .md files are permitted." };
|
|
1679
|
+
}
|
|
1680
|
+
const resolvedMemoryDir = resolve(memoryDir);
|
|
1681
|
+
const resolvedPath = resolve(resolvedMemoryDir, requestedPath);
|
|
1682
|
+
if (!resolvedPath.startsWith(resolvedMemoryDir + "/") && resolvedPath !== resolvedMemoryDir) {
|
|
1683
|
+
return { ok: true, error: "Path traversal is not permitted." };
|
|
1684
|
+
}
|
|
1685
|
+
try {
|
|
1686
|
+
const content = await readFile2(resolvedPath, "utf8");
|
|
1687
|
+
return { ok: true, content };
|
|
1688
|
+
} catch {
|
|
1689
|
+
return { ok: true, error: `File not found: ${requestedPath}` };
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
function createAppendDailyMemoryTool(options) {
|
|
1695
|
+
return {
|
|
1696
|
+
name: "append_daily_memory",
|
|
1697
|
+
description: "Append a note to today's daily memory file (memory/YYYY-MM-DD.md) in the workspace. Use this to record facts, decisions, or observations worth remembering across sessions.",
|
|
1698
|
+
inputSchema: {
|
|
1699
|
+
type: "object",
|
|
1700
|
+
properties: {
|
|
1701
|
+
content: { type: "string" }
|
|
1702
|
+
},
|
|
1703
|
+
required: ["content"]
|
|
1704
|
+
},
|
|
1705
|
+
risk: "medium",
|
|
1706
|
+
async execute(input, context) {
|
|
1707
|
+
const raw = input;
|
|
1708
|
+
if (typeof raw.content !== "string" || raw.content.trim().length === 0) {
|
|
1709
|
+
return { ok: false, error: { code: "invalid_input", message: "content must be a non-empty string." } };
|
|
1710
|
+
}
|
|
1711
|
+
const today = options?.getCurrentDate?.() ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1712
|
+
const memoryDir = resolve(context.workspaceRoot, "memory");
|
|
1713
|
+
const filePath = resolve(memoryDir, `${today}.md`);
|
|
1714
|
+
try {
|
|
1715
|
+
await mkdir(memoryDir, { recursive: true });
|
|
1716
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16);
|
|
1717
|
+
const entry = `
|
|
1718
|
+
## ${timestamp}
|
|
1719
|
+
|
|
1720
|
+
${raw.content.trim()}
|
|
1721
|
+
`;
|
|
1722
|
+
await writeFileFs(filePath, entry, { flag: "a" });
|
|
1723
|
+
return {
|
|
1724
|
+
ok: true,
|
|
1725
|
+
filePath: `memory/${today}.md`,
|
|
1726
|
+
summary: `Appended note to memory/${today}.md.`
|
|
1727
|
+
};
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
const message = error instanceof Error ? error.message : "Failed to write daily memory.";
|
|
1730
|
+
return { ok: false, error: { code: "write_error", message } };
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
var SEARCH_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1736
|
+
"node_modules",
|
|
1737
|
+
".git",
|
|
1738
|
+
"dist",
|
|
1739
|
+
"build",
|
|
1740
|
+
"coverage",
|
|
1741
|
+
".pnpm-store",
|
|
1742
|
+
".nyc_output",
|
|
1743
|
+
".turbo",
|
|
1744
|
+
".cache"
|
|
1745
|
+
]);
|
|
1746
|
+
var SEARCH_BINARY_EXTS = /* @__PURE__ */ new Set([
|
|
1747
|
+
".png",
|
|
1748
|
+
".jpg",
|
|
1749
|
+
".jpeg",
|
|
1750
|
+
".gif",
|
|
1751
|
+
".svg",
|
|
1752
|
+
".ico",
|
|
1753
|
+
".webp",
|
|
1754
|
+
".avif",
|
|
1755
|
+
".woff",
|
|
1756
|
+
".woff2",
|
|
1757
|
+
".ttf",
|
|
1758
|
+
".eot",
|
|
1759
|
+
".otf",
|
|
1760
|
+
".pdf",
|
|
1761
|
+
".zip",
|
|
1762
|
+
".tar",
|
|
1763
|
+
".gz",
|
|
1764
|
+
".bz2",
|
|
1765
|
+
".7z",
|
|
1766
|
+
".rar",
|
|
1767
|
+
".exe",
|
|
1768
|
+
".bin",
|
|
1769
|
+
".dll",
|
|
1770
|
+
".so",
|
|
1771
|
+
".dylib",
|
|
1772
|
+
".class",
|
|
1773
|
+
".mp3",
|
|
1774
|
+
".mp4",
|
|
1775
|
+
".wav",
|
|
1776
|
+
".ogg",
|
|
1777
|
+
".webm",
|
|
1778
|
+
".flac",
|
|
1779
|
+
".db",
|
|
1780
|
+
".sqlite",
|
|
1781
|
+
".sqlite3"
|
|
1782
|
+
]);
|
|
1783
|
+
var SEARCH_MAX_FILE_BYTES = 512 * 1024;
|
|
1784
|
+
var SEARCH_DEFAULT_MAX_RESULTS = 50;
|
|
1785
|
+
async function* walkSearchFiles(dir) {
|
|
1786
|
+
let entries;
|
|
1787
|
+
try {
|
|
1788
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1789
|
+
} catch {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
for (const entry of entries) {
|
|
1793
|
+
if (SEARCH_SKIP_DIRS.has(entry.name))
|
|
1794
|
+
continue;
|
|
1795
|
+
const full = join3(dir, entry.name);
|
|
1796
|
+
if (entry.isDirectory()) {
|
|
1797
|
+
yield* walkSearchFiles(full);
|
|
1798
|
+
} else if (entry.isFile() && !SEARCH_BINARY_EXTS.has(extname(entry.name).toLowerCase())) {
|
|
1799
|
+
yield full;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function matchesInclude(relPath, pattern) {
|
|
1804
|
+
const base = basename(relPath);
|
|
1805
|
+
const hasPathSep = pattern.includes("/");
|
|
1806
|
+
const target = hasPathSep ? relPath.replace(/\\/g, "/") : base;
|
|
1807
|
+
const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "(.*/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
1808
|
+
return new RegExp(`^${regexStr}$`).test(target);
|
|
1809
|
+
}
|
|
1810
|
+
function createUpdateHeartbeatTool() {
|
|
1811
|
+
return {
|
|
1812
|
+
name: "update_heartbeat",
|
|
1813
|
+
description: "Write current execution status to HEARTBEAT.md in the workspace. Use during long-running background tasks to signal progress and liveness. Status must be one of: running, completed, failed, idle.",
|
|
1814
|
+
risk: "low",
|
|
1815
|
+
inputSchema: {
|
|
1816
|
+
type: "object",
|
|
1817
|
+
properties: {
|
|
1818
|
+
status: {
|
|
1819
|
+
type: "string",
|
|
1820
|
+
enum: ["running", "completed", "failed", "idle"],
|
|
1821
|
+
description: "Current execution status."
|
|
1822
|
+
},
|
|
1823
|
+
message: {
|
|
1824
|
+
type: "string",
|
|
1825
|
+
description: "Human-readable progress note or status message."
|
|
1826
|
+
}
|
|
1827
|
+
},
|
|
1828
|
+
required: ["status", "message"]
|
|
1829
|
+
},
|
|
1830
|
+
async execute(rawInput, context) {
|
|
1831
|
+
const input = rawInput;
|
|
1832
|
+
const validStatuses = ["running", "completed", "failed", "idle"];
|
|
1833
|
+
const status = validStatuses.find((s) => s === input.status) ?? "running";
|
|
1834
|
+
const message = typeof input.message === "string" ? input.message.trim() : "";
|
|
1835
|
+
const filePath = resolve(context.workspaceRoot, "HEARTBEAT.md");
|
|
1836
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1837
|
+
const lines = [
|
|
1838
|
+
"# Heartbeat",
|
|
1839
|
+
"",
|
|
1840
|
+
`**Status**: ${status}`,
|
|
1841
|
+
`**Last updated**: ${now}`,
|
|
1842
|
+
...message.length > 0 ? ["", message] : []
|
|
1843
|
+
];
|
|
1844
|
+
try {
|
|
1845
|
+
await writeFileFs(filePath, lines.join("\n") + "\n");
|
|
1846
|
+
return { ok: true, filePath: "HEARTBEAT.md" };
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
const msg = error instanceof Error ? error.message : "Failed to write HEARTBEAT.md.";
|
|
1849
|
+
return { ok: false, error: { code: "write_error", message: msg } };
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
function createSearchFilesTool() {
|
|
1855
|
+
return {
|
|
1856
|
+
name: "search_files",
|
|
1857
|
+
description: "Search for a text or regex pattern across files in the workspace. Returns matching file paths, line numbers, and line content. Skips node_modules, .git, dist, and binary files.",
|
|
1858
|
+
risk: "low",
|
|
1859
|
+
inputSchema: {
|
|
1860
|
+
type: "object",
|
|
1861
|
+
properties: {
|
|
1862
|
+
pattern: {
|
|
1863
|
+
type: "string",
|
|
1864
|
+
description: "Text or regex pattern to search for."
|
|
1865
|
+
},
|
|
1866
|
+
path: {
|
|
1867
|
+
type: "string",
|
|
1868
|
+
description: "Directory to search in, relative to workspace root. Defaults to '.' (workspace root)."
|
|
1869
|
+
},
|
|
1870
|
+
include: {
|
|
1871
|
+
type: "string",
|
|
1872
|
+
description: "Glob pattern to filter files, e.g. '*.ts' or '**/*.md'. Defaults to all non-binary files."
|
|
1873
|
+
},
|
|
1874
|
+
case_sensitive: {
|
|
1875
|
+
type: "boolean",
|
|
1876
|
+
description: "Case-sensitive search. Defaults to false."
|
|
1877
|
+
},
|
|
1878
|
+
max_results: {
|
|
1879
|
+
type: "number",
|
|
1880
|
+
description: `Maximum matching lines to return. Defaults to ${SEARCH_DEFAULT_MAX_RESULTS}.`
|
|
1881
|
+
}
|
|
1882
|
+
},
|
|
1883
|
+
required: ["pattern"]
|
|
1884
|
+
},
|
|
1885
|
+
async execute(rawInput, context) {
|
|
1886
|
+
const input = rawInput;
|
|
1887
|
+
const root = context.workspaceRoot;
|
|
1888
|
+
const searchDir = input.path ? resolve(root, input.path) : root;
|
|
1889
|
+
const maxResults = input.max_results ?? SEARCH_DEFAULT_MAX_RESULTS;
|
|
1890
|
+
const flags = input.case_sensitive === true ? "" : "i";
|
|
1891
|
+
let regex;
|
|
1892
|
+
try {
|
|
1893
|
+
regex = new RegExp(input.pattern, flags);
|
|
1894
|
+
} catch {
|
|
1895
|
+
const escaped = input.pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1896
|
+
regex = new RegExp(escaped, flags);
|
|
1897
|
+
}
|
|
1898
|
+
const matches = [];
|
|
1899
|
+
let searchedFiles = 0;
|
|
1900
|
+
let matchedFiles = 0;
|
|
1901
|
+
let truncated = false;
|
|
1902
|
+
outer: for await (const filePath of walkSearchFiles(searchDir)) {
|
|
1903
|
+
const relPath = relative(root, filePath).replace(/\\/g, "/");
|
|
1904
|
+
if (input.include !== void 0 && !matchesInclude(relPath, input.include))
|
|
1905
|
+
continue;
|
|
1906
|
+
let text;
|
|
1907
|
+
try {
|
|
1908
|
+
const s = await statFs(filePath);
|
|
1909
|
+
if (s.size > SEARCH_MAX_FILE_BYTES)
|
|
1910
|
+
continue;
|
|
1911
|
+
text = await readFile2(filePath, "utf8");
|
|
1912
|
+
} catch {
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
searchedFiles++;
|
|
1916
|
+
const lines = text.split("\n");
|
|
1917
|
+
let hit = false;
|
|
1918
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1919
|
+
if (regex.test(lines[i])) {
|
|
1920
|
+
if (matches.length >= maxResults) {
|
|
1921
|
+
truncated = true;
|
|
1922
|
+
break outer;
|
|
1923
|
+
}
|
|
1924
|
+
matches.push({ file: relPath, line: i + 1, content: lines[i].trimEnd() });
|
|
1925
|
+
hit = true;
|
|
1926
|
+
}
|
|
1927
|
+
regex.lastIndex = 0;
|
|
1928
|
+
}
|
|
1929
|
+
if (hit)
|
|
1930
|
+
matchedFiles++;
|
|
1931
|
+
}
|
|
1932
|
+
return { type: "search_files_result", matches, truncated, matchedFiles, searchedFiles };
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// ../../packages/core/dist/index.js
|
|
1938
|
+
function createRuntimeEvent(event) {
|
|
1939
|
+
return event;
|
|
1940
|
+
}
|
|
1941
|
+
var InMemoryRuntimeTraceStore = class {
|
|
1942
|
+
#events = [];
|
|
1943
|
+
async append(event) {
|
|
1944
|
+
this.#events.push(event);
|
|
1945
|
+
}
|
|
1946
|
+
async listRecent(query = {}) {
|
|
1947
|
+
const events = query.limit === void 0 ? this.#events : this.#events.slice(-query.limit);
|
|
1948
|
+
return [...events];
|
|
1949
|
+
}
|
|
1950
|
+
async listByRun(runId) {
|
|
1951
|
+
return this.#events.filter((event) => event.runId === runId);
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
var DEFAULT_MAX_STEPS = 12;
|
|
1955
|
+
var DEFAULT_MAX_PLANNING_STALL_RETRIES = 2;
|
|
1956
|
+
var DEFAULT_PERMISSION_GUIDANCE = "Low-risk actions run automatically. Medium and high-risk actions require approval. Blocked actions are never permitted.";
|
|
1957
|
+
var PLANNING_ONLY_RETRY_INSTRUCTION = "Do not restate the plan. Act now: take the first concrete tool action you can.";
|
|
1958
|
+
var PLAN_PROMISE_RE = /\b(?:i(?:'ll| will)|let me|i(?:'m| am)\s+going to|first[, ]+i(?:'ll| will)|next[, ]+i(?:'ll| will)|i can do that)\b/i;
|
|
1959
|
+
var PLAN_COMPLETION_RE = /\b(?:done|finished|implemented|updated|fixed|changed|ran|verified|found|here(?:'s| is) what|blocked by|the blocker is)\b/i;
|
|
1960
|
+
var PLAN_HEADING_RE = /^(?:plan|steps?|next steps?)\s*:/im;
|
|
1961
|
+
var PLAN_BULLET_RE = /^(?:[-*•]\s+|\d+[.)]\s+)/u;
|
|
1962
|
+
var PLAN_ACTION_VERB_RE = /\b(?:inspect|investigate|check|look(?:\s+into|\s+at)?|read|search|find|debug|fix|patch|update|change|edit|write|implement|run|test|verify|review|analy(?:s|z)e|summari(?:s|z)e|explain|answer|show|share|report|prepare|refactor|deploy)\b/i;
|
|
1963
|
+
var PLAN_MAX_CHARS = 700;
|
|
1964
|
+
var AgentRuntime = class {
|
|
1965
|
+
#contextAssembler;
|
|
1966
|
+
#modelProvider;
|
|
1967
|
+
#permissionPolicy;
|
|
1968
|
+
#approvalResolver;
|
|
1969
|
+
#tools;
|
|
1970
|
+
#skillIndex;
|
|
1971
|
+
#maxSteps;
|
|
1972
|
+
#maxPlanningStallRetries;
|
|
1973
|
+
#systemInstruction;
|
|
1974
|
+
#runtime;
|
|
1975
|
+
#preferStreaming;
|
|
1976
|
+
#compaction;
|
|
1977
|
+
#promptMode;
|
|
1978
|
+
#hooks;
|
|
1979
|
+
#sessionMutex;
|
|
1980
|
+
#executionContract;
|
|
1981
|
+
#createRunId;
|
|
1982
|
+
#createEventId;
|
|
1983
|
+
#now;
|
|
1984
|
+
#currentTodos = [];
|
|
1985
|
+
constructor(dependencies) {
|
|
1986
|
+
this.#contextAssembler = dependencies.contextAssembler ?? new MinimalContextAssembler();
|
|
1987
|
+
this.#modelProvider = dependencies.modelProvider;
|
|
1988
|
+
this.#permissionPolicy = dependencies.permissionPolicy ?? new DefaultPermissionPolicy();
|
|
1989
|
+
this.#approvalResolver = dependencies.approvalResolver;
|
|
1990
|
+
this.#maxSteps = dependencies.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
1991
|
+
this.#executionContract = dependencies.executionContract ?? "default";
|
|
1992
|
+
this.#maxPlanningStallRetries = dependencies.executionContract === "strict-agentic" ? dependencies.maxPlanningStallRetries ?? 3 : dependencies.maxPlanningStallRetries ?? DEFAULT_MAX_PLANNING_STALL_RETRIES;
|
|
1993
|
+
this.#skillIndex = dependencies.skillIndex ?? [];
|
|
1994
|
+
const baseInstruction = dependencies.systemInstruction ?? "";
|
|
1995
|
+
this.#systemInstruction = dependencies.executionContract === "strict-agentic" ? `${baseInstruction}
|
|
1996
|
+
|
|
1997
|
+
Execution contract: strict-agentic. Act immediately. Do not narrate plans. Call tools now.` : baseInstruction;
|
|
1998
|
+
this.#runtime = dependencies.runtime;
|
|
1999
|
+
this.#preferStreaming = dependencies.preferStreaming ?? false;
|
|
2000
|
+
this.#compaction = dependencies.compaction;
|
|
2001
|
+
this.#promptMode = dependencies.promptMode;
|
|
2002
|
+
this.#hooks = dependencies.hooks;
|
|
2003
|
+
this.#sessionMutex = dependencies.sessionMutex;
|
|
2004
|
+
this.#createRunId = dependencies.createRunId ?? randomId("run");
|
|
2005
|
+
this.#createEventId = dependencies.createEventId ?? randomId("evt");
|
|
2006
|
+
this.#now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
2007
|
+
const updateTodos = createUpdateTodosTool((todos) => {
|
|
2008
|
+
this.#currentTodos = todos;
|
|
2009
|
+
});
|
|
2010
|
+
const userTools = dependencies.tools ?? [];
|
|
2011
|
+
this.#tools = new Map([updateTodos, ...userTools].map((tool) => [tool.name, tool]));
|
|
2012
|
+
}
|
|
2013
|
+
async *runTurn(input) {
|
|
2014
|
+
const runId = this.#createRunId();
|
|
2015
|
+
const base = input.sessionId ? { runId, sessionId: input.sessionId } : { runId };
|
|
2016
|
+
const collectedEvents = [];
|
|
2017
|
+
const release = this.#sessionMutex ? await this.#sessionMutex.acquire(input.sessionId ?? "global") : void 0;
|
|
2018
|
+
try {
|
|
2019
|
+
if (this.#hooks?.beforeTurn !== void 0) {
|
|
2020
|
+
try {
|
|
2021
|
+
await this.#hooks.beforeTurn(input);
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
if (typeof process !== "undefined" && process.env["NODE_ENV"] !== "production") {
|
|
2024
|
+
console.warn("[AgentRuntime] beforeTurn hook threw:", err);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
const emitAndCollect = (event) => {
|
|
2029
|
+
collectedEvents.push(event);
|
|
2030
|
+
return event;
|
|
2031
|
+
};
|
|
2032
|
+
yield emitAndCollect(this.#event({ ...base, type: "run_started", userMessage: input.message }));
|
|
2033
|
+
const contextToolSummaries = this.#buildContextToolSummaries();
|
|
2034
|
+
const assembled = await this.#contextAssembler.assemble({
|
|
2035
|
+
systemInstruction: this.#systemInstruction,
|
|
2036
|
+
...this.#runtime ? { runtime: this.#runtime } : {},
|
|
2037
|
+
...contextToolSummaries.length > 0 ? { tools: contextToolSummaries } : {},
|
|
2038
|
+
permissionGuidance: DEFAULT_PERMISSION_GUIDANCE,
|
|
2039
|
+
...this.#skillIndex.length > 0 ? { skillIndex: this.#skillIndex } : {},
|
|
2040
|
+
...input.recentMessages ? { recentMessages: input.recentMessages } : {},
|
|
2041
|
+
...this.#promptMode !== void 0 ? { promptMode: this.#promptMode } : {},
|
|
2042
|
+
userMessage: input.message
|
|
2043
|
+
});
|
|
2044
|
+
yield emitAndCollect(this.#event({
|
|
2045
|
+
...base,
|
|
2046
|
+
type: "context_assembled",
|
|
2047
|
+
messageCount: assembled.modelInput.messages.length,
|
|
2048
|
+
systemInstructionIncluded: assembled.report.includedSections.includes("identity")
|
|
2049
|
+
}));
|
|
2050
|
+
const toolDefinitions = this.#buildToolDefinitions();
|
|
2051
|
+
let messages = assembled.modelInput.messages;
|
|
2052
|
+
let steps = 0;
|
|
2053
|
+
let stallCount = 0;
|
|
2054
|
+
let hadRealToolCallThisTurn = false;
|
|
2055
|
+
this.#currentTodos = [];
|
|
2056
|
+
const turnNewMessages = [];
|
|
2057
|
+
const userMsg = assembled.modelInput.messages.at(-1);
|
|
2058
|
+
if (userMsg)
|
|
2059
|
+
turnNewMessages.push({ ...userMsg });
|
|
2060
|
+
while (steps < this.#maxSteps) {
|
|
2061
|
+
if (input.signal?.aborted) {
|
|
2062
|
+
yield emitAndCollect(this.#event({ ...base, type: "run_failed", error: { message: "Aborted by user.", recoverable: false } }));
|
|
2063
|
+
await this.#callAfterTurn(collectedEvents);
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
if (this.#compaction !== void 0) {
|
|
2067
|
+
const before = messages.length;
|
|
2068
|
+
messages = await compactMessages(messages, this.#modelProvider, this.#compaction);
|
|
2069
|
+
const after = messages.length;
|
|
2070
|
+
if (after < before) {
|
|
2071
|
+
const summaryMsg = messages.find((m) => m.role === "system" && typeof m.content === "string" && m.content.startsWith("Conversation summary:\n"));
|
|
2072
|
+
const summary = summaryMsg && typeof summaryMsg.content === "string" ? summaryMsg.content.slice("Conversation summary:\n".length) : "";
|
|
2073
|
+
yield emitAndCollect(this.#event({ ...base, type: "compaction_triggered", messagesBefore: before, messagesAfter: after, summary }));
|
|
2074
|
+
}
|
|
2075
|
+
if (this.#hooks?.onCompaction !== void 0) {
|
|
2076
|
+
try {
|
|
2077
|
+
await this.#hooks.onCompaction(before, after);
|
|
2078
|
+
} catch (err) {
|
|
2079
|
+
if (typeof process !== "undefined" && process.env["NODE_ENV"] !== "production") {
|
|
2080
|
+
console.warn("[AgentRuntime] onCompaction hook threw:", err);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
yield emitAndCollect(this.#event({ ...base, type: "model_request_started", provider: "configured" }));
|
|
2086
|
+
const modelInput = {
|
|
2087
|
+
messages,
|
|
2088
|
+
...toolDefinitions.length > 0 ? { tools: toolDefinitions } : {},
|
|
2089
|
+
...assembled.modelInput.options !== void 0 ? { options: assembled.modelInput.options } : {}
|
|
2090
|
+
};
|
|
2091
|
+
let output;
|
|
2092
|
+
if (this.#preferStreaming && isStreamingProvider(this.#modelProvider)) {
|
|
2093
|
+
let textContent = "";
|
|
2094
|
+
let streamedToolCalls;
|
|
2095
|
+
let streamedUsage;
|
|
2096
|
+
let streamError;
|
|
2097
|
+
for await (const streamEvent of this.#modelProvider.generateStream(modelInput)) {
|
|
2098
|
+
if (input.signal?.aborted)
|
|
2099
|
+
break;
|
|
2100
|
+
if (streamEvent.type === "token_delta") {
|
|
2101
|
+
yield emitAndCollect(this.#event({ ...base, type: "token_delta", delta: streamEvent.delta }));
|
|
2102
|
+
} else if (streamEvent.type === "message_done") {
|
|
2103
|
+
textContent = streamEvent.content;
|
|
2104
|
+
streamedUsage = streamEvent.usage;
|
|
2105
|
+
} else if (streamEvent.type === "tool_calls") {
|
|
2106
|
+
streamedToolCalls = streamEvent.calls;
|
|
2107
|
+
streamedUsage = streamEvent.usage;
|
|
2108
|
+
} else if (streamEvent.type === "error") {
|
|
2109
|
+
streamError = { category: streamEvent.category, message: streamEvent.message, recoverable: streamEvent.recoverable };
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (streamError !== void 0) {
|
|
2113
|
+
output = { type: "error", category: streamError.category, message: streamError.message, recoverable: streamError.recoverable };
|
|
2114
|
+
} else if (streamedToolCalls !== void 0) {
|
|
2115
|
+
output = { type: "tool_calls", calls: streamedToolCalls, ...streamedUsage !== void 0 ? { usage: streamedUsage } : {} };
|
|
2116
|
+
} else {
|
|
2117
|
+
output = { type: "message", content: textContent, ...streamedUsage !== void 0 ? { usage: streamedUsage } : {} };
|
|
2118
|
+
}
|
|
2119
|
+
} else {
|
|
2120
|
+
output = await this.#modelProvider.generate(modelInput);
|
|
2121
|
+
}
|
|
2122
|
+
yield emitAndCollect(this.#event({ ...base, type: "model_request_completed", provider: "configured" }));
|
|
2123
|
+
steps++;
|
|
2124
|
+
if (output.type === "error") {
|
|
2125
|
+
const ev = emitAndCollect(this.#event({ ...base, type: "run_failed", error: { message: output.message, recoverable: output.recoverable } }));
|
|
2126
|
+
yield ev;
|
|
2127
|
+
await this.#callAfterTurn(collectedEvents);
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
if (output.type === "message") {
|
|
2131
|
+
const hasActionableTools = [...this.#tools.keys()].some((n) => n !== "update_todos");
|
|
2132
|
+
if (hasActionableTools && !hadRealToolCallThisTurn && isPlanningOnly(output.content)) {
|
|
2133
|
+
stallCount++;
|
|
2134
|
+
yield emitAndCollect(this.#event({ ...base, type: "planning_stall_detected", stallCount, maxRetries: this.#maxPlanningStallRetries }));
|
|
2135
|
+
if (stallCount >= this.#maxPlanningStallRetries) {
|
|
2136
|
+
const ev = emitAndCollect(this.#event({ ...base, type: "run_failed", error: { message: "Agent stopped after repeated plan-only turns without taking action.", recoverable: false } }));
|
|
2137
|
+
yield ev;
|
|
2138
|
+
await this.#callAfterTurn(collectedEvents);
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
messages = [
|
|
2142
|
+
...messages,
|
|
2143
|
+
{ role: "assistant", content: output.content },
|
|
2144
|
+
{ role: "user", content: PLANNING_ONLY_RETRY_INSTRUCTION }
|
|
2145
|
+
];
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
stallCount = 0;
|
|
2149
|
+
turnNewMessages.push({ role: "assistant", content: output.content });
|
|
2150
|
+
yield emitAndCollect(this.#event({ ...base, type: "assistant_message_created", message: { role: "assistant", content: output.content } }));
|
|
2151
|
+
yield emitAndCollect(this.#event({ ...base, type: "turn_complete", messages: [...turnNewMessages] }));
|
|
2152
|
+
const completedEv = emitAndCollect(this.#event({ ...base, type: "run_completed" }));
|
|
2153
|
+
yield completedEv;
|
|
2154
|
+
await this.#callAfterTurn(collectedEvents);
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
stallCount = 0;
|
|
2158
|
+
if (output.calls.some((c) => c.name !== "update_todos")) {
|
|
2159
|
+
hadRealToolCallThisTurn = true;
|
|
2160
|
+
}
|
|
2161
|
+
const assistantToolCallMsg = { role: "assistant", content: null, toolCalls: output.calls };
|
|
2162
|
+
messages = [...messages, assistantToolCallMsg];
|
|
2163
|
+
turnNewMessages.push({ ...assistantToolCallMsg });
|
|
2164
|
+
const toolResultMessages = [];
|
|
2165
|
+
let hardTerminate = false;
|
|
2166
|
+
let terminationError = "";
|
|
2167
|
+
for (const call of output.calls) {
|
|
2168
|
+
yield emitAndCollect(this.#event({ ...base, type: "tool_call_requested", call }));
|
|
2169
|
+
const tool = this.#tools.get(call.name);
|
|
2170
|
+
if (tool === void 0) {
|
|
2171
|
+
const errorMessage = `Tool "${call.name}" is not registered.`;
|
|
2172
|
+
yield emitAndCollect(this.#event({ ...base, type: "tool_failed", callId: call.id, toolName: call.name, error: { message: errorMessage } }));
|
|
2173
|
+
const errResultMsg = { role: "tool", toolCallId: call.id, content: `Error: ${errorMessage}` };
|
|
2174
|
+
toolResultMessages.push(errResultMsg);
|
|
2175
|
+
turnNewMessages.push({ ...errResultMsg });
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
const decision = this.#permissionPolicy.evaluate({
|
|
2179
|
+
mode: normalizeAutonomyMode(this.#runtime?.mode),
|
|
2180
|
+
action: createToolPermissionAction(call, tool.risk)
|
|
2181
|
+
});
|
|
2182
|
+
yield emitAndCollect(this.#event({ ...base, type: "tool_call_permission_evaluated", callId: call.id, toolName: call.name, decision }));
|
|
2183
|
+
if (decision.decision === "deny") {
|
|
2184
|
+
hardTerminate = true;
|
|
2185
|
+
terminationError = `Tool call ${call.name} was denied.`;
|
|
2186
|
+
break;
|
|
2187
|
+
}
|
|
2188
|
+
if (decision.decision === "ask") {
|
|
2189
|
+
yield emitAndCollect(this.#event({ ...base, type: "approval_requested", callId: call.id, toolName: call.name, decision }));
|
|
2190
|
+
const resolution = this.#approvalResolver === void 0 ? { approved: false, reason: "No approval resolver was configured." } : await this.#approvalResolver.resolve({ call, decision });
|
|
2191
|
+
yield emitAndCollect(this.#event({ ...base, type: "approval_resolved", callId: call.id, toolName: call.name, resolution }));
|
|
2192
|
+
if (!resolution.approved) {
|
|
2193
|
+
hardTerminate = true;
|
|
2194
|
+
terminationError = `Tool call ${call.name} was denied.`;
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (this.#hooks?.beforeToolCall !== void 0) {
|
|
2199
|
+
let hookResult = void 0;
|
|
2200
|
+
try {
|
|
2201
|
+
hookResult = await this.#hooks.beforeToolCall(call);
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
if (typeof process !== "undefined" && process.env["NODE_ENV"] !== "production") {
|
|
2204
|
+
console.warn("[AgentRuntime] beforeToolCall hook threw:", err);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
if (hookResult === "abort") {
|
|
2208
|
+
const abortMessage = "Tool call aborted by hook.";
|
|
2209
|
+
yield emitAndCollect(this.#event({ ...base, type: "tool_failed", callId: call.id, toolName: call.name, error: { message: abortMessage } }));
|
|
2210
|
+
const abortResultMsg = { role: "tool", toolCallId: call.id, content: `Error: ${abortMessage}` };
|
|
2211
|
+
toolResultMessages.push(abortResultMsg);
|
|
2212
|
+
turnNewMessages.push({ ...abortResultMsg });
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
yield emitAndCollect(this.#event({ ...base, type: "tool_started", callId: call.id, toolName: call.name }));
|
|
2217
|
+
let result;
|
|
2218
|
+
try {
|
|
2219
|
+
result = await tool.execute(call.input, {
|
|
2220
|
+
workspaceRoot: this.#runtime?.workspace ?? process.cwd()
|
|
2221
|
+
});
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown tool execution error.";
|
|
2224
|
+
yield emitAndCollect(this.#event({ ...base, type: "tool_failed", callId: call.id, toolName: call.name, error: { message: errorMessage } }));
|
|
2225
|
+
const execErrMsg = { role: "tool", toolCallId: call.id, content: `Error: ${errorMessage}` };
|
|
2226
|
+
toolResultMessages.push(execErrMsg);
|
|
2227
|
+
turnNewMessages.push({ ...execErrMsg });
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
yield emitAndCollect(this.#event({ ...base, type: "tool_completed", callId: call.id, toolName: call.name, result }));
|
|
2231
|
+
const toolSuccessMsg = { role: "tool", toolCallId: call.id, content: JSON.stringify(result) };
|
|
2232
|
+
toolResultMessages.push(toolSuccessMsg);
|
|
2233
|
+
turnNewMessages.push({ ...toolSuccessMsg });
|
|
2234
|
+
if (this.#hooks?.afterToolCall !== void 0) {
|
|
2235
|
+
try {
|
|
2236
|
+
await this.#hooks.afterToolCall(call, result);
|
|
2237
|
+
} catch (err) {
|
|
2238
|
+
if (typeof process !== "undefined" && process.env["NODE_ENV"] !== "production") {
|
|
2239
|
+
console.warn("[AgentRuntime] afterToolCall hook threw:", err);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (hardTerminate) {
|
|
2245
|
+
const ev = emitAndCollect(this.#event({ ...base, type: "run_failed", error: { message: terminationError, recoverable: false } }));
|
|
2246
|
+
yield ev;
|
|
2247
|
+
await this.#callAfterTurn(collectedEvents);
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
if (output.calls.some((c) => c.name === "update_todos")) {
|
|
2251
|
+
yield emitAndCollect(this.#event({ ...base, type: "todos_updated", todos: [...this.#currentTodos] }));
|
|
2252
|
+
}
|
|
2253
|
+
messages = [...messages, ...toolResultMessages];
|
|
2254
|
+
}
|
|
2255
|
+
const stepLimitEv = emitAndCollect(this.#event({ ...base, type: "run_failed", error: { message: `Agent loop reached the step limit of ${this.#maxSteps}.`, recoverable: false } }));
|
|
2256
|
+
yield stepLimitEv;
|
|
2257
|
+
await this.#callAfterTurn(collectedEvents);
|
|
2258
|
+
} finally {
|
|
2259
|
+
release?.();
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
async #callAfterTurn(events) {
|
|
2263
|
+
if (this.#hooks?.afterTurn === void 0)
|
|
2264
|
+
return;
|
|
2265
|
+
try {
|
|
2266
|
+
await this.#hooks.afterTurn(events);
|
|
2267
|
+
} catch (err) {
|
|
2268
|
+
if (typeof process !== "undefined" && process.env["NODE_ENV"] !== "production") {
|
|
2269
|
+
console.warn("[AgentRuntime] afterTurn hook threw:", err);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
#buildContextToolSummaries() {
|
|
2274
|
+
return [...this.#tools.values()].map((tool) => ({
|
|
2275
|
+
name: tool.name,
|
|
2276
|
+
description: tool.description,
|
|
2277
|
+
risk: tool.risk
|
|
2278
|
+
}));
|
|
2279
|
+
}
|
|
2280
|
+
#buildToolDefinitions() {
|
|
2281
|
+
return [...this.#tools.values()].map((tool) => ({
|
|
2282
|
+
type: "function",
|
|
2283
|
+
function: {
|
|
2284
|
+
name: tool.name,
|
|
2285
|
+
description: tool.description,
|
|
2286
|
+
parameters: tool.inputSchema
|
|
2287
|
+
}
|
|
2288
|
+
}));
|
|
2289
|
+
}
|
|
2290
|
+
#event(event) {
|
|
2291
|
+
return createRuntimeEvent({
|
|
2292
|
+
...event,
|
|
2293
|
+
eventId: this.#createEventId(),
|
|
2294
|
+
timestamp: this.#now()
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
function randomId(prefix) {
|
|
2299
|
+
return () => `${prefix}_${crypto.randomUUID()}`;
|
|
2300
|
+
}
|
|
2301
|
+
function createToolPermissionAction(call, risk) {
|
|
2302
|
+
return {
|
|
2303
|
+
kind: "tool",
|
|
2304
|
+
name: call.name,
|
|
2305
|
+
summary: `Model requested tool ${call.name}.`,
|
|
2306
|
+
risk
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
function normalizeAutonomyMode(mode) {
|
|
2310
|
+
return mode === "observe" || mode === "auto" ? mode : "confirm";
|
|
2311
|
+
}
|
|
2312
|
+
function hasStructuredPlanFormat(text) {
|
|
2313
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
2314
|
+
if (lines.length === 0)
|
|
2315
|
+
return false;
|
|
2316
|
+
const bulletCount = lines.filter((l) => PLAN_BULLET_RE.test(l)).length;
|
|
2317
|
+
const hasPromise = lines.some((l) => PLAN_PROMISE_RE.test(l));
|
|
2318
|
+
const hasHeading = PLAN_HEADING_RE.test(lines[0] ?? "");
|
|
2319
|
+
return hasHeading && hasPromise || bulletCount >= 2 && hasPromise;
|
|
2320
|
+
}
|
|
2321
|
+
function isPlanningOnly(content) {
|
|
2322
|
+
const text = content.trim();
|
|
2323
|
+
if (!text || text.length > PLAN_MAX_CHARS || text.includes("```"))
|
|
2324
|
+
return false;
|
|
2325
|
+
if (PLAN_COMPLETION_RE.test(text))
|
|
2326
|
+
return false;
|
|
2327
|
+
const hasStructured = hasStructuredPlanFormat(text);
|
|
2328
|
+
if (!PLAN_PROMISE_RE.test(text) && !hasStructured)
|
|
2329
|
+
return false;
|
|
2330
|
+
if (!hasStructured && !PLAN_ACTION_VERB_RE.test(text))
|
|
2331
|
+
return false;
|
|
2332
|
+
return true;
|
|
2333
|
+
}
|
|
2334
|
+
function createSpawnSubagentAsyncTool(factory, options) {
|
|
2335
|
+
return {
|
|
2336
|
+
name: "spawn_subagent_async",
|
|
2337
|
+
description: "Spawn a sub-agent to handle a subtask asynchronously. Returns a taskId immediately; the sub-agent runs in the background. Use spawn_subagent for synchronous execution.",
|
|
2338
|
+
risk: "medium",
|
|
2339
|
+
inputSchema: {
|
|
2340
|
+
type: "object",
|
|
2341
|
+
properties: {
|
|
2342
|
+
goal: { type: "string", description: "The complete goal for the sub-agent." },
|
|
2343
|
+
context: { type: "string", description: "Optional background context." }
|
|
2344
|
+
},
|
|
2345
|
+
required: ["goal"]
|
|
2346
|
+
},
|
|
2347
|
+
async execute(rawInput) {
|
|
2348
|
+
const input = rawInput;
|
|
2349
|
+
const taskId = `task_${crypto.randomUUID()}`;
|
|
2350
|
+
if (options?.taskStore !== void 0) {
|
|
2351
|
+
await options.taskStore.create({
|
|
2352
|
+
id: taskId,
|
|
2353
|
+
runtime: "subagent",
|
|
2354
|
+
task: input.goal,
|
|
2355
|
+
status: "queued",
|
|
2356
|
+
...options.parentTaskId !== void 0 ? { parentId: options.parentTaskId } : {}
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
const message = input.context !== void 0 ? `${input.goal}
|
|
2360
|
+
|
|
2361
|
+
Context:
|
|
2362
|
+
${input.context}` : input.goal;
|
|
2363
|
+
void (async () => {
|
|
2364
|
+
if (options?.taskStore !== void 0) {
|
|
2365
|
+
await options.taskStore.update(taskId, { status: "running" });
|
|
2366
|
+
}
|
|
2367
|
+
const subRuntime = factory.create(input.goal);
|
|
2368
|
+
let assistantText = "";
|
|
2369
|
+
let failed = false;
|
|
2370
|
+
for await (const event of subRuntime.runTurn({ message })) {
|
|
2371
|
+
if (event.type === "assistant_message_created")
|
|
2372
|
+
assistantText = event.message.content;
|
|
2373
|
+
if (event.type === "run_failed")
|
|
2374
|
+
failed = true;
|
|
2375
|
+
}
|
|
2376
|
+
if (options?.taskStore !== void 0) {
|
|
2377
|
+
await options.taskStore.update(taskId, {
|
|
2378
|
+
status: failed ? "failed" : "succeeded",
|
|
2379
|
+
...assistantText.length > 0 ? { terminalSummary: assistantText } : {}
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
})();
|
|
2383
|
+
const result = { type: "spawn_subagent_async_result", taskId, status: "queued" };
|
|
2384
|
+
return result;
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
function createCheckSubagentTool(taskStore) {
|
|
2389
|
+
return {
|
|
2390
|
+
name: "check_subagent",
|
|
2391
|
+
description: "Check the status and result of an async sub-agent by taskId. Returns status (queued/running/succeeded/failed) and result when complete. Use after spawn_subagent_async.",
|
|
2392
|
+
risk: "low",
|
|
2393
|
+
inputSchema: {
|
|
2394
|
+
type: "object",
|
|
2395
|
+
properties: {
|
|
2396
|
+
taskId: { type: "string", description: "The taskId returned by spawn_subagent_async." }
|
|
2397
|
+
},
|
|
2398
|
+
required: ["taskId"]
|
|
2399
|
+
},
|
|
2400
|
+
async execute(rawInput) {
|
|
2401
|
+
const input = rawInput;
|
|
2402
|
+
const taskId = typeof input.taskId === "string" ? input.taskId.trim() : "";
|
|
2403
|
+
if (taskId === "") {
|
|
2404
|
+
return { ok: false, error: { code: "invalid_input", message: "taskId is required." } };
|
|
2405
|
+
}
|
|
2406
|
+
const record = await taskStore.get(taskId);
|
|
2407
|
+
if (record === void 0) {
|
|
2408
|
+
return { ok: false, error: { code: "not_found", message: `No subagent task found with id "${taskId}".` } };
|
|
2409
|
+
}
|
|
2410
|
+
return {
|
|
2411
|
+
type: "check_subagent_result",
|
|
2412
|
+
taskId: record.id,
|
|
2413
|
+
status: record.status,
|
|
2414
|
+
result: record.terminalSummary
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
function createSpawnSubagentTool(factory) {
|
|
2420
|
+
return {
|
|
2421
|
+
name: "spawn_subagent",
|
|
2422
|
+
description: "Spawn a focused sub-agent to handle a complex subtask and return its result. Use when the current task requires a separate focused execution context.",
|
|
2423
|
+
risk: "medium",
|
|
2424
|
+
inputSchema: {
|
|
2425
|
+
type: "object",
|
|
2426
|
+
properties: {
|
|
2427
|
+
goal: { type: "string", description: "The complete goal for the sub-agent." },
|
|
2428
|
+
context: { type: "string", description: "Optional background context to share." }
|
|
2429
|
+
},
|
|
2430
|
+
required: ["goal"]
|
|
2431
|
+
},
|
|
2432
|
+
async execute(rawInput, _execContext) {
|
|
2433
|
+
const input = rawInput;
|
|
2434
|
+
const subRuntime = factory.create(input.goal);
|
|
2435
|
+
let assistantText = "";
|
|
2436
|
+
let failed = false;
|
|
2437
|
+
let errorMsg = "";
|
|
2438
|
+
for await (const event of subRuntime.runTurn({ message: input.goal })) {
|
|
2439
|
+
if (event.type === "assistant_message_created") {
|
|
2440
|
+
assistantText = event.message.content;
|
|
2441
|
+
}
|
|
2442
|
+
if (event.type === "run_failed") {
|
|
2443
|
+
failed = true;
|
|
2444
|
+
errorMsg = event.error.message;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
if (failed) {
|
|
2448
|
+
return { type: "spawn_subagent_result", ok: false, error: errorMsg };
|
|
2449
|
+
}
|
|
2450
|
+
return { type: "spawn_subagent_result", ok: true, result: assistantText };
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// ../../packages/gateway/dist/index.js
|
|
2456
|
+
var SessionGateway = class {
|
|
2457
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
2458
|
+
register(session) {
|
|
2459
|
+
this.#sessions.set(session.id, session);
|
|
2460
|
+
}
|
|
2461
|
+
unregister(sessionId) {
|
|
2462
|
+
this.#sessions.delete(sessionId);
|
|
2463
|
+
}
|
|
2464
|
+
touch(sessionId) {
|
|
2465
|
+
const s = this.#sessions.get(sessionId);
|
|
2466
|
+
if (s !== void 0) {
|
|
2467
|
+
this.#sessions.set(sessionId, { ...s, lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
get(sessionId) {
|
|
2471
|
+
return this.#sessions.get(sessionId);
|
|
2472
|
+
}
|
|
2473
|
+
list() {
|
|
2474
|
+
return Array.from(this.#sessions.values());
|
|
2475
|
+
}
|
|
2476
|
+
listByAdapter(adapterName) {
|
|
2477
|
+
return this.list().filter((s) => s.adapterName === adapterName);
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
|
|
2481
|
+
// ../../packages/adapters/dist/index.js
|
|
2482
|
+
var CLI_CAPABILITIES = {
|
|
2483
|
+
streaming: true,
|
|
2484
|
+
approvalPrompts: true,
|
|
2485
|
+
background: false
|
|
2486
|
+
};
|
|
2487
|
+
var TOOL_PROFILES = {
|
|
2488
|
+
full: {
|
|
2489
|
+
name: "full",
|
|
2490
|
+
description: "All available tools.",
|
|
2491
|
+
allowedTools: []
|
|
2492
|
+
},
|
|
2493
|
+
coding: {
|
|
2494
|
+
name: "coding",
|
|
2495
|
+
description: "File system, search, and shell tools for coding tasks.",
|
|
2496
|
+
allowedTools: ["read_file", "list_directory", "write_file", "edit_file", "append_file", "run_shell", "search_files", "load_skill", "update_todos", "spawn_subagent", "spawn_subagent_async", "check_subagent"]
|
|
2497
|
+
},
|
|
2498
|
+
messaging: {
|
|
2499
|
+
name: "messaging",
|
|
2500
|
+
description: "Read-only tools for informational tasks without file writes or shell execution.",
|
|
2501
|
+
allowedTools: ["read_file", "list_directory", "read_web_page", "memory_search", "memory_get", "load_skill", "update_todos"]
|
|
2502
|
+
},
|
|
2503
|
+
background: {
|
|
2504
|
+
name: "background",
|
|
2505
|
+
description: "File system tools only for unattended background tasks.",
|
|
2506
|
+
allowedTools: ["read_file", "list_directory", "write_file", "memory_search", "memory_get", "append_daily_memory", "update_todos", "spawn_subagent", "update_heartbeat"]
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2509
|
+
function filterToolsByProfile(tools, profile) {
|
|
2510
|
+
const def = TOOL_PROFILES[profile];
|
|
2511
|
+
if (def.allowedTools.length === 0)
|
|
2512
|
+
return tools;
|
|
2513
|
+
return tools.filter((t) => def.allowedTools.includes(t.name));
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// ../../packages/scheduler/dist/index.js
|
|
2517
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile } from "fs/promises";
|
|
2518
|
+
import { dirname as dirname2 } from "path";
|
|
2519
|
+
var JsonlTaskStore = class {
|
|
2520
|
+
#filePath;
|
|
2521
|
+
constructor(filePath) {
|
|
2522
|
+
this.#filePath = filePath;
|
|
2523
|
+
}
|
|
2524
|
+
async saveRun(record) {
|
|
2525
|
+
await mkdir2(dirname2(this.#filePath), { recursive: true });
|
|
2526
|
+
await writeFile(this.#filePath, `${JSON.stringify(record)}
|
|
2527
|
+
`, { flag: "a" });
|
|
2528
|
+
}
|
|
2529
|
+
async updateRun(id, updates) {
|
|
2530
|
+
const records = await this.#readAll();
|
|
2531
|
+
const updated = records.map((record) => record.id === id ? { ...record, ...updates } : record);
|
|
2532
|
+
await mkdir2(dirname2(this.#filePath), { recursive: true });
|
|
2533
|
+
await writeFile(this.#filePath, updated.map((r) => JSON.stringify(r)).join("\n") + (updated.length > 0 ? "\n" : ""));
|
|
2534
|
+
}
|
|
2535
|
+
async listRuns(query = {}) {
|
|
2536
|
+
const records = await this.#readAll();
|
|
2537
|
+
const filtered = query.taskName === void 0 ? records : records.filter((record) => record.taskName === query.taskName);
|
|
2538
|
+
return query.limit === void 0 ? filtered : filtered.slice(-query.limit);
|
|
2539
|
+
}
|
|
2540
|
+
async #readAll() {
|
|
2541
|
+
let content = "";
|
|
2542
|
+
try {
|
|
2543
|
+
content = await readFile3(this.#filePath, "utf8");
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
2546
|
+
return [];
|
|
2547
|
+
}
|
|
2548
|
+
throw error;
|
|
2549
|
+
}
|
|
2550
|
+
const records = [];
|
|
2551
|
+
for (const line of content.split("\n")) {
|
|
2552
|
+
if (line.trim() === "") {
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
records.push(JSON.parse(line));
|
|
2556
|
+
}
|
|
2557
|
+
return records;
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
var BackgroundApprovalResolver = class {
|
|
2561
|
+
#mode;
|
|
2562
|
+
constructor(mode = "confirm") {
|
|
2563
|
+
this.#mode = mode;
|
|
2564
|
+
}
|
|
2565
|
+
async resolve(_request) {
|
|
2566
|
+
if (this.#mode === "auto") {
|
|
2567
|
+
return {
|
|
2568
|
+
approved: true,
|
|
2569
|
+
reason: "Auto-approved in background auto mode."
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
return {
|
|
2573
|
+
approved: false,
|
|
2574
|
+
reason: `Auto-denied in background ${this.#mode} mode: no user is present to approve.`
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
function isNodeError2(error) {
|
|
2579
|
+
return error instanceof Error && "code" in error;
|
|
2580
|
+
}
|
|
2581
|
+
function matchesCronField(field, value) {
|
|
2582
|
+
if (field === "*")
|
|
2583
|
+
return true;
|
|
2584
|
+
const num = parseInt(field, 10);
|
|
2585
|
+
return !isNaN(num) && num === value;
|
|
2586
|
+
}
|
|
2587
|
+
function matchesCron(expression, date) {
|
|
2588
|
+
const parts = expression.trim().split(/\s+/);
|
|
2589
|
+
if (parts.length !== 5)
|
|
2590
|
+
return false;
|
|
2591
|
+
const [min, hour, dom, month, dow] = parts;
|
|
2592
|
+
return matchesCronField(min, date.getMinutes()) && matchesCronField(hour, date.getHours()) && matchesCronField(dom, date.getDate()) && matchesCronField(month, date.getMonth() + 1) && matchesCronField(dow, date.getDay());
|
|
2593
|
+
}
|
|
2594
|
+
async function writeHeartbeat(filePath, state) {
|
|
2595
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
2596
|
+
const lines = [
|
|
2597
|
+
"# Heartbeat",
|
|
2598
|
+
"",
|
|
2599
|
+
`**Status**: ${state.status}`,
|
|
2600
|
+
`**Last updated**: ${state.lastUpdatedAt}`,
|
|
2601
|
+
...state.taskName !== void 0 ? [`**Task**: ${state.taskName}`] : [],
|
|
2602
|
+
...state.runId !== void 0 ? [`**Run ID**: ${state.runId}`] : [],
|
|
2603
|
+
...state.message !== void 0 ? ["", state.message] : []
|
|
2604
|
+
];
|
|
2605
|
+
await writeFile(filePath, lines.join("\n") + "\n");
|
|
2606
|
+
}
|
|
2607
|
+
var CronScheduler = class {
|
|
2608
|
+
#tasks;
|
|
2609
|
+
#runner;
|
|
2610
|
+
#intervalMs;
|
|
2611
|
+
#getNow;
|
|
2612
|
+
#timer;
|
|
2613
|
+
#lastRun = /* @__PURE__ */ new Map();
|
|
2614
|
+
// task name → last run minute key
|
|
2615
|
+
constructor(tasks, runner, options) {
|
|
2616
|
+
this.#tasks = tasks;
|
|
2617
|
+
this.#runner = runner;
|
|
2618
|
+
this.#intervalMs = options?.checkIntervalMs ?? 3e4;
|
|
2619
|
+
this.#getNow = options?.getNow ?? (() => /* @__PURE__ */ new Date());
|
|
2620
|
+
}
|
|
2621
|
+
start() {
|
|
2622
|
+
if (this.#timer !== void 0)
|
|
2623
|
+
return;
|
|
2624
|
+
this.#timer = setInterval(() => void this.#tick(), this.#intervalMs);
|
|
2625
|
+
void this.#tick();
|
|
2626
|
+
}
|
|
2627
|
+
stop() {
|
|
2628
|
+
if (this.#timer !== void 0) {
|
|
2629
|
+
clearInterval(this.#timer);
|
|
2630
|
+
this.#timer = void 0;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
get isRunning() {
|
|
2634
|
+
return this.#timer !== void 0;
|
|
2635
|
+
}
|
|
2636
|
+
async #tick() {
|
|
2637
|
+
const now = this.#getNow();
|
|
2638
|
+
for (const task of this.#tasks) {
|
|
2639
|
+
if (!task.cron)
|
|
2640
|
+
continue;
|
|
2641
|
+
if (!matchesCron(task.cron, now))
|
|
2642
|
+
continue;
|
|
2643
|
+
const lastRun = this.#lastRun.get(task.name) ?? 0;
|
|
2644
|
+
const minuteKey = Math.floor(now.getTime() / 6e4);
|
|
2645
|
+
if (lastRun === minuteKey)
|
|
2646
|
+
continue;
|
|
2647
|
+
this.#lastRun.set(task.name, minuteKey);
|
|
2648
|
+
try {
|
|
2649
|
+
await this.#runner(task);
|
|
2650
|
+
} catch {
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
|
|
2656
|
+
// ../../packages/sessions/dist/index.js
|
|
2657
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
2658
|
+
import { join as join4 } from "path";
|
|
2659
|
+
var InMemorySessionStore = class {
|
|
2660
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
2661
|
+
#messages = /* @__PURE__ */ new Map();
|
|
2662
|
+
#traceEvents = /* @__PURE__ */ new Map();
|
|
2663
|
+
#createSessionId;
|
|
2664
|
+
#createMessageId;
|
|
2665
|
+
#now;
|
|
2666
|
+
constructor(dependencies = {}) {
|
|
2667
|
+
this.#createSessionId = dependencies.createSessionId ?? randomId2("sess");
|
|
2668
|
+
this.#createMessageId = dependencies.createMessageId ?? randomId2("msg");
|
|
2669
|
+
this.#now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
2670
|
+
}
|
|
2671
|
+
async createSession(input = {}) {
|
|
2672
|
+
const timestamp = this.#now();
|
|
2673
|
+
const session = {
|
|
2674
|
+
id: this.#createSessionId(),
|
|
2675
|
+
...input.title === void 0 ? {} : { title: input.title },
|
|
2676
|
+
createdAt: timestamp,
|
|
2677
|
+
updatedAt: timestamp
|
|
2678
|
+
};
|
|
2679
|
+
this.#sessions.set(session.id, session);
|
|
2680
|
+
this.#messages.set(session.id, []);
|
|
2681
|
+
this.#traceEvents.set(session.id, []);
|
|
2682
|
+
return { ...session };
|
|
2683
|
+
}
|
|
2684
|
+
async getSession(sessionId) {
|
|
2685
|
+
const session = this.#sessions.get(sessionId);
|
|
2686
|
+
return session === void 0 ? void 0 : { ...session };
|
|
2687
|
+
}
|
|
2688
|
+
async listSessions(query = {}) {
|
|
2689
|
+
const sessions = [...this.#sessions.values()].sort(compareSessionsByRecentUpdate);
|
|
2690
|
+
const selectedSessions = query.limit === void 0 ? sessions : sessions.slice(0, query.limit);
|
|
2691
|
+
return selectedSessions.map((session) => ({ ...session }));
|
|
2692
|
+
}
|
|
2693
|
+
async appendMessage(input) {
|
|
2694
|
+
const session = this.#sessions.get(input.sessionId);
|
|
2695
|
+
if (session === void 0) {
|
|
2696
|
+
throw new Error(`Unknown session "${input.sessionId}".`);
|
|
2697
|
+
}
|
|
2698
|
+
const timestamp = this.#now();
|
|
2699
|
+
const message = {
|
|
2700
|
+
id: this.#createMessageId(),
|
|
2701
|
+
sessionId: input.sessionId,
|
|
2702
|
+
role: input.role,
|
|
2703
|
+
content: input.content,
|
|
2704
|
+
createdAt: timestamp,
|
|
2705
|
+
...input.toolCalls !== void 0 ? { toolCalls: input.toolCalls } : {},
|
|
2706
|
+
...input.toolCallId !== void 0 ? { toolCallId: input.toolCallId } : {}
|
|
2707
|
+
};
|
|
2708
|
+
const messages = this.#messages.get(input.sessionId) ?? [];
|
|
2709
|
+
messages.push(message);
|
|
2710
|
+
this.#messages.set(input.sessionId, messages);
|
|
2711
|
+
this.#sessions.set(input.sessionId, {
|
|
2712
|
+
...session,
|
|
2713
|
+
updatedAt: timestamp
|
|
2714
|
+
});
|
|
2715
|
+
return { ...message };
|
|
2716
|
+
}
|
|
2717
|
+
async listMessages(sessionId, query = {}) {
|
|
2718
|
+
const messages = this.#messages.get(sessionId) ?? [];
|
|
2719
|
+
const selectedMessages = query.limit === void 0 ? messages : messages.slice(-query.limit);
|
|
2720
|
+
return selectedMessages.map((message) => ({ ...message }));
|
|
2721
|
+
}
|
|
2722
|
+
async appendCompactBoundary(input) {
|
|
2723
|
+
const session = this.#sessions.get(input.sessionId);
|
|
2724
|
+
if (session === void 0) {
|
|
2725
|
+
throw new Error(`Unknown session "${input.sessionId}".`);
|
|
2726
|
+
}
|
|
2727
|
+
const messages = [];
|
|
2728
|
+
if (input.summary) {
|
|
2729
|
+
const timestamp = this.#now();
|
|
2730
|
+
messages.push({
|
|
2731
|
+
id: this.#createMessageId(),
|
|
2732
|
+
sessionId: input.sessionId,
|
|
2733
|
+
role: "system",
|
|
2734
|
+
content: input.summary,
|
|
2735
|
+
createdAt: timestamp
|
|
2736
|
+
});
|
|
2737
|
+
this.#sessions.set(input.sessionId, {
|
|
2738
|
+
...session,
|
|
2739
|
+
updatedAt: timestamp
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
this.#messages.set(input.sessionId, messages);
|
|
2743
|
+
}
|
|
2744
|
+
async appendTraceEvent(input) {
|
|
2745
|
+
const session = this.#sessions.get(input.sessionId);
|
|
2746
|
+
if (session === void 0) {
|
|
2747
|
+
throw new Error(`Unknown session "${input.sessionId}".`);
|
|
2748
|
+
}
|
|
2749
|
+
const timestamp = this.#now();
|
|
2750
|
+
const traceEvent = {
|
|
2751
|
+
sessionId: input.sessionId,
|
|
2752
|
+
event: input.event,
|
|
2753
|
+
createdAt: timestamp
|
|
2754
|
+
};
|
|
2755
|
+
const traceEvents = this.#traceEvents.get(input.sessionId) ?? [];
|
|
2756
|
+
traceEvents.push(traceEvent);
|
|
2757
|
+
this.#traceEvents.set(input.sessionId, traceEvents);
|
|
2758
|
+
this.#sessions.set(input.sessionId, {
|
|
2759
|
+
...session,
|
|
2760
|
+
updatedAt: timestamp
|
|
2761
|
+
});
|
|
2762
|
+
return cloneTraceEventRecord(traceEvent);
|
|
2763
|
+
}
|
|
2764
|
+
async listTraceEvents(sessionId, query = {}) {
|
|
2765
|
+
const traceEvents = this.#traceEvents.get(sessionId) ?? [];
|
|
2766
|
+
const selectedTraceEvents = query.limit === void 0 ? traceEvents : traceEvents.slice(-query.limit);
|
|
2767
|
+
return selectedTraceEvents.map((traceEvent) => cloneTraceEventRecord(traceEvent));
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
var JsonlSessionStore = class {
|
|
2771
|
+
#directory;
|
|
2772
|
+
#createSessionId;
|
|
2773
|
+
#createMessageId;
|
|
2774
|
+
#now;
|
|
2775
|
+
constructor(dependencies) {
|
|
2776
|
+
this.#directory = dependencies.directory;
|
|
2777
|
+
this.#createSessionId = dependencies.createSessionId ?? randomId2("sess");
|
|
2778
|
+
this.#createMessageId = dependencies.createMessageId ?? randomId2("msg");
|
|
2779
|
+
this.#now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
2780
|
+
}
|
|
2781
|
+
async createSession(input = {}) {
|
|
2782
|
+
const timestamp = this.#now();
|
|
2783
|
+
const session = {
|
|
2784
|
+
id: this.#createSessionId(),
|
|
2785
|
+
...input.title === void 0 ? {} : { title: input.title },
|
|
2786
|
+
createdAt: timestamp,
|
|
2787
|
+
updatedAt: timestamp
|
|
2788
|
+
};
|
|
2789
|
+
await this.#append(session.id, {
|
|
2790
|
+
type: "session",
|
|
2791
|
+
session
|
|
2792
|
+
});
|
|
2793
|
+
return { ...session };
|
|
2794
|
+
}
|
|
2795
|
+
async getSession(sessionId) {
|
|
2796
|
+
const replay = await this.#replay(sessionId);
|
|
2797
|
+
return replay.session === void 0 ? void 0 : { ...replay.session };
|
|
2798
|
+
}
|
|
2799
|
+
async listSessions(query = {}) {
|
|
2800
|
+
const sessionIds = await this.#sessionIds();
|
|
2801
|
+
const sessions = [];
|
|
2802
|
+
for (const sessionId of sessionIds) {
|
|
2803
|
+
const session = await this.getSession(sessionId);
|
|
2804
|
+
if (session !== void 0) {
|
|
2805
|
+
sessions.push(session);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
const sortedSessions = sessions.sort(compareSessionsByRecentUpdate);
|
|
2809
|
+
const selectedSessions = query.limit === void 0 ? sortedSessions : sortedSessions.slice(0, query.limit);
|
|
2810
|
+
return selectedSessions.map((session) => ({ ...session }));
|
|
2811
|
+
}
|
|
2812
|
+
async appendMessage(input) {
|
|
2813
|
+
const replay = await this.#replay(input.sessionId);
|
|
2814
|
+
if (replay.session === void 0) {
|
|
2815
|
+
throw new Error(`Unknown session "${input.sessionId}".`);
|
|
2816
|
+
}
|
|
2817
|
+
const timestamp = this.#now();
|
|
2818
|
+
const message = {
|
|
2819
|
+
id: this.#createMessageId(),
|
|
2820
|
+
sessionId: input.sessionId,
|
|
2821
|
+
role: input.role,
|
|
2822
|
+
content: input.content,
|
|
2823
|
+
createdAt: timestamp,
|
|
2824
|
+
...input.toolCalls !== void 0 ? { toolCalls: input.toolCalls } : {},
|
|
2825
|
+
...input.toolCallId !== void 0 ? { toolCallId: input.toolCallId } : {}
|
|
2826
|
+
};
|
|
2827
|
+
await this.#append(input.sessionId, {
|
|
2828
|
+
type: "message",
|
|
2829
|
+
message
|
|
2830
|
+
});
|
|
2831
|
+
return { ...message };
|
|
2832
|
+
}
|
|
2833
|
+
async listMessages(sessionId, query = {}) {
|
|
2834
|
+
const replay = await this.#replay(sessionId);
|
|
2835
|
+
const selectedMessages = query.limit === void 0 ? replay.messages : replay.messages.slice(-query.limit);
|
|
2836
|
+
return selectedMessages.map((message) => ({ ...message }));
|
|
2837
|
+
}
|
|
2838
|
+
async appendTraceEvent(input) {
|
|
2839
|
+
const replay = await this.#replay(input.sessionId);
|
|
2840
|
+
if (replay.session === void 0) {
|
|
2841
|
+
throw new Error(`Unknown session "${input.sessionId}".`);
|
|
2842
|
+
}
|
|
2843
|
+
const traceEvent = {
|
|
2844
|
+
sessionId: input.sessionId,
|
|
2845
|
+
event: input.event,
|
|
2846
|
+
createdAt: this.#now()
|
|
2847
|
+
};
|
|
2848
|
+
await this.#append(input.sessionId, {
|
|
2849
|
+
type: "trace",
|
|
2850
|
+
traceEvent
|
|
2851
|
+
});
|
|
2852
|
+
return cloneTraceEventRecord(traceEvent);
|
|
2853
|
+
}
|
|
2854
|
+
async listTraceEvents(sessionId, query = {}) {
|
|
2855
|
+
const replay = await this.#replay(sessionId);
|
|
2856
|
+
const selectedTraceEvents = query.limit === void 0 ? replay.traceEvents : replay.traceEvents.slice(-query.limit);
|
|
2857
|
+
return selectedTraceEvents.map((traceEvent) => cloneTraceEventRecord(traceEvent));
|
|
2858
|
+
}
|
|
2859
|
+
async appendCompactBoundary(input) {
|
|
2860
|
+
const replay = await this.#replay(input.sessionId);
|
|
2861
|
+
if (replay.session === void 0) {
|
|
2862
|
+
throw new Error(`Unknown session "${input.sessionId}".`);
|
|
2863
|
+
}
|
|
2864
|
+
const timestamp = this.#now();
|
|
2865
|
+
await this.#append(input.sessionId, {
|
|
2866
|
+
type: "compact_boundary",
|
|
2867
|
+
summary: input.summary,
|
|
2868
|
+
messagesBefore: input.messagesBefore,
|
|
2869
|
+
messagesAfter: input.messagesAfter,
|
|
2870
|
+
createdAt: timestamp
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
async #append(sessionId, record) {
|
|
2874
|
+
await mkdir3(this.#directory, { recursive: true });
|
|
2875
|
+
await writeFile2(this.#filePath(sessionId), `${JSON.stringify(record)}
|
|
2876
|
+
`, { flag: "a" });
|
|
2877
|
+
}
|
|
2878
|
+
async #replay(sessionId) {
|
|
2879
|
+
let content = "";
|
|
2880
|
+
try {
|
|
2881
|
+
content = await readFile4(this.#filePath(sessionId), "utf8");
|
|
2882
|
+
} catch (error) {
|
|
2883
|
+
if (isNodeError3(error) && error.code === "ENOENT") {
|
|
2884
|
+
return { messages: [], traceEvents: [] };
|
|
2885
|
+
}
|
|
2886
|
+
throw error;
|
|
2887
|
+
}
|
|
2888
|
+
let messages = [];
|
|
2889
|
+
const traceEvents = [];
|
|
2890
|
+
let session;
|
|
2891
|
+
for (const line of content.split("\n")) {
|
|
2892
|
+
if (line.trim() === "") {
|
|
2893
|
+
continue;
|
|
2894
|
+
}
|
|
2895
|
+
const record = JSON.parse(line);
|
|
2896
|
+
if (record.type === "session") {
|
|
2897
|
+
session = record.session;
|
|
2898
|
+
} else if (record.type === "compact_boundary") {
|
|
2899
|
+
messages = [];
|
|
2900
|
+
if (record.summary) {
|
|
2901
|
+
messages.push({
|
|
2902
|
+
id: `cmpct_${record.createdAt}`,
|
|
2903
|
+
sessionId: session?.id ?? "",
|
|
2904
|
+
role: "system",
|
|
2905
|
+
content: record.summary,
|
|
2906
|
+
createdAt: record.createdAt
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
if (session && record.createdAt > session.updatedAt) {
|
|
2910
|
+
session = {
|
|
2911
|
+
...session,
|
|
2912
|
+
updatedAt: record.createdAt
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
} else if (record.type === "message") {
|
|
2916
|
+
messages.push(record.message);
|
|
2917
|
+
if (session && record.message.createdAt > session.updatedAt) {
|
|
2918
|
+
session = {
|
|
2919
|
+
...session,
|
|
2920
|
+
updatedAt: record.message.createdAt
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
} else {
|
|
2924
|
+
traceEvents.push(record.traceEvent);
|
|
2925
|
+
if (session && record.traceEvent.createdAt > session.updatedAt) {
|
|
2926
|
+
session = {
|
|
2927
|
+
...session,
|
|
2928
|
+
updatedAt: record.traceEvent.createdAt
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return {
|
|
2934
|
+
...session === void 0 ? {} : { session },
|
|
2935
|
+
messages,
|
|
2936
|
+
traceEvents
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
#filePath(sessionId) {
|
|
2940
|
+
assertSafeSessionId(sessionId);
|
|
2941
|
+
return join4(this.#directory, `${sessionId}.jsonl`);
|
|
2942
|
+
}
|
|
2943
|
+
async #sessionIds() {
|
|
2944
|
+
try {
|
|
2945
|
+
const entries = await readdir2(this.#directory);
|
|
2946
|
+
return entries.filter((entry) => entry.endsWith(".jsonl")).map((entry) => entry.slice(0, -".jsonl".length)).filter((sessionId) => /^[A-Za-z0-9_-]+$/.test(sessionId));
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
if (isNodeError3(error) && error.code === "ENOENT") {
|
|
2949
|
+
return [];
|
|
2950
|
+
}
|
|
2951
|
+
throw error;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
};
|
|
2955
|
+
function assertSafeSessionId(sessionId) {
|
|
2956
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
|
|
2957
|
+
throw new Error(`Unsafe session id "${sessionId}".`);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
function isNodeError3(error) {
|
|
2961
|
+
return error instanceof Error && "code" in error;
|
|
2962
|
+
}
|
|
2963
|
+
function compareSessionsByRecentUpdate(left, right) {
|
|
2964
|
+
return right.updatedAt.localeCompare(left.updatedAt);
|
|
2965
|
+
}
|
|
2966
|
+
function cloneTraceEventRecord(traceEvent) {
|
|
2967
|
+
return {
|
|
2968
|
+
...traceEvent,
|
|
2969
|
+
event: structuredClone(traceEvent.event)
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
function randomId2(prefix) {
|
|
2973
|
+
return () => `${prefix}_${crypto.randomUUID()}`;
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// ../../packages/taskflow/dist/index.js
|
|
2977
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
2978
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
2979
|
+
var JsonlTaskFlowStore = class {
|
|
2980
|
+
#filePath;
|
|
2981
|
+
constructor(filePath) {
|
|
2982
|
+
this.#filePath = filePath;
|
|
2983
|
+
}
|
|
2984
|
+
async #readAll() {
|
|
2985
|
+
try {
|
|
2986
|
+
const content = await readFile5(this.#filePath, "utf-8");
|
|
2987
|
+
return content.split("\n").filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
|
|
2988
|
+
} catch {
|
|
2989
|
+
return [];
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
async #writeAll(records) {
|
|
2993
|
+
await mkdir4(dirname3(this.#filePath), { recursive: true });
|
|
2994
|
+
await writeFile3(this.#filePath, records.map((r) => JSON.stringify(r)).join("\n") + "\n", "utf-8");
|
|
2995
|
+
}
|
|
2996
|
+
async create(record) {
|
|
2997
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2998
|
+
const full = { ...record, createdAt: now, updatedAt: now };
|
|
2999
|
+
const all = await this.#readAll();
|
|
3000
|
+
all.push(full);
|
|
3001
|
+
await this.#writeAll(all);
|
|
3002
|
+
return full;
|
|
3003
|
+
}
|
|
3004
|
+
async update(id, updates) {
|
|
3005
|
+
const all = await this.#readAll();
|
|
3006
|
+
const idx = all.findIndex((r) => r.id === id);
|
|
3007
|
+
if (idx === -1)
|
|
3008
|
+
return void 0;
|
|
3009
|
+
const updated = { ...all[idx], ...updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3010
|
+
all[idx] = updated;
|
|
3011
|
+
await this.#writeAll(all);
|
|
3012
|
+
return updated;
|
|
3013
|
+
}
|
|
3014
|
+
async get(id) {
|
|
3015
|
+
const all = await this.#readAll();
|
|
3016
|
+
return all.find((r) => r.id === id);
|
|
3017
|
+
}
|
|
3018
|
+
async list(query) {
|
|
3019
|
+
let records = await this.#readAll();
|
|
3020
|
+
if (query?.status !== void 0)
|
|
3021
|
+
records = records.filter((r) => r.status === query.status);
|
|
3022
|
+
if (query?.parentId !== void 0)
|
|
3023
|
+
records = records.filter((r) => r.parentId === query.parentId);
|
|
3024
|
+
if (query?.limit !== void 0)
|
|
3025
|
+
records = records.slice(-query.limit);
|
|
3026
|
+
return records;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
|
|
3030
|
+
// ../../packages/skills/dist/index.js
|
|
3031
|
+
import { copyFile, mkdir as mkdir5, readFile as fsReadFile, readdir as readdir3, writeFile as writeFile4 } from "fs/promises";
|
|
3032
|
+
import { homedir } from "os";
|
|
3033
|
+
import { basename as basename2, join as join6 } from "path";
|
|
3034
|
+
var BUILTIN_SKILLS = [
|
|
3035
|
+
{
|
|
3036
|
+
name: "research",
|
|
3037
|
+
description: "Use when investigating external information, comparing sources, or summarizing findings. Guides web search, source reading, source comparison, and citation-aware output.",
|
|
3038
|
+
body: "Search for relevant sources, read and compare at least two, and summarize findings with source links. Prefer primary sources. Flag conflicting evidence.",
|
|
3039
|
+
source: "built-in",
|
|
3040
|
+
filePath: ""
|
|
3041
|
+
},
|
|
3042
|
+
{
|
|
3043
|
+
name: "project-inspector",
|
|
3044
|
+
description: "Use when understanding a codebase, identifying technologies, or summarizing module responsibilities. Guides project structure inspection and technology detection.",
|
|
3045
|
+
body: "Read README, list top-level directories, inspect package files, and summarize each module's role. Identify entry points and dependency boundaries.",
|
|
3046
|
+
source: "built-in",
|
|
3047
|
+
filePath: ""
|
|
3048
|
+
},
|
|
3049
|
+
{
|
|
3050
|
+
name: "safe-shell",
|
|
3051
|
+
description: "Use when planning to run shell commands, especially destructive or irreversible ones. Guides shell command risk assessment and command purpose explanation.",
|
|
3052
|
+
body: "State the purpose before running. Prefer read-only commands. Avoid rm -rf, force flags, or piped untrusted input. Confirm intent before destructive operations.",
|
|
3053
|
+
source: "built-in",
|
|
3054
|
+
filePath: ""
|
|
3055
|
+
}
|
|
3056
|
+
];
|
|
3057
|
+
var SkillLoader = class {
|
|
3058
|
+
async load(options = {}) {
|
|
3059
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3060
|
+
const skills = [];
|
|
3061
|
+
const add = (skill) => {
|
|
3062
|
+
if (!seen.has(skill.name)) {
|
|
3063
|
+
seen.add(skill.name);
|
|
3064
|
+
skills.push(skill);
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
if (options.workspaceRoot !== void 0) {
|
|
3068
|
+
const workspaceDir = join6(options.workspaceRoot, "skills");
|
|
3069
|
+
for (const skill of await this.#loadFromDir(workspaceDir, "workspace", options)) {
|
|
3070
|
+
add(skill);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
const userDir = options.userSkillsDir ?? join6(homedir(), ".vole", "skills");
|
|
3074
|
+
const manifest = await loadManifestFromDir(userDir, options.readFile);
|
|
3075
|
+
for (const skill of await this.#loadFromDir(userDir, "user", options, manifest)) {
|
|
3076
|
+
add(skill);
|
|
3077
|
+
}
|
|
3078
|
+
for (const skill of BUILTIN_SKILLS) {
|
|
3079
|
+
add(skill);
|
|
3080
|
+
}
|
|
3081
|
+
return skills;
|
|
3082
|
+
}
|
|
3083
|
+
async #loadFromDir(dirPath, source, options, manifest) {
|
|
3084
|
+
const doReadDir = options.readDir ?? ((p) => readdir3(p));
|
|
3085
|
+
const doReadFile = options.readFile ?? ((p) => fsReadFile(p, "utf8"));
|
|
3086
|
+
let entries;
|
|
3087
|
+
try {
|
|
3088
|
+
entries = await doReadDir(dirPath);
|
|
3089
|
+
} catch (error) {
|
|
3090
|
+
if (isNodeError4(error) && error.code === "ENOENT")
|
|
3091
|
+
return [];
|
|
3092
|
+
throw error;
|
|
3093
|
+
}
|
|
3094
|
+
const skills = [];
|
|
3095
|
+
for (const entry of entries) {
|
|
3096
|
+
if (entry === "skills-index.json")
|
|
3097
|
+
continue;
|
|
3098
|
+
const filePath = join6(dirPath, entry, "SKILL.md");
|
|
3099
|
+
if (source === "user" && manifest !== void 0) {
|
|
3100
|
+
const manifestEntry = manifest.skills.find((e) => e.name === entry);
|
|
3101
|
+
if (manifestEntry !== void 0 && manifestEntry.enabled === false) {
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
try {
|
|
3106
|
+
const content = await doReadFile(filePath);
|
|
3107
|
+
const parsed = parseSKILLMd(content);
|
|
3108
|
+
if (parsed !== null) {
|
|
3109
|
+
let trusted;
|
|
3110
|
+
let enabled;
|
|
3111
|
+
if (source === "user") {
|
|
3112
|
+
const manifestEntry = manifest?.skills.find((e) => e.name === entry);
|
|
3113
|
+
trusted = manifestEntry?.trusted ?? false;
|
|
3114
|
+
enabled = manifestEntry?.enabled ?? true;
|
|
3115
|
+
}
|
|
3116
|
+
const def = {
|
|
3117
|
+
...parsed,
|
|
3118
|
+
source,
|
|
3119
|
+
filePath,
|
|
3120
|
+
...trusted !== void 0 ? { trusted } : {},
|
|
3121
|
+
...enabled !== void 0 ? { enabled } : {}
|
|
3122
|
+
};
|
|
3123
|
+
skills.push(def);
|
|
3124
|
+
}
|
|
3125
|
+
} catch {
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
return skills;
|
|
3129
|
+
}
|
|
3130
|
+
};
|
|
3131
|
+
async function loadManifestFromDir(dirPath, readFileFn) {
|
|
3132
|
+
const doReadFile = readFileFn ?? ((p) => fsReadFile(p, "utf8"));
|
|
3133
|
+
const manifestPath = join6(dirPath, "skills-index.json");
|
|
3134
|
+
try {
|
|
3135
|
+
const content = await doReadFile(manifestPath);
|
|
3136
|
+
return JSON.parse(content);
|
|
3137
|
+
} catch {
|
|
3138
|
+
return void 0;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
var SkillManager = class {
|
|
3142
|
+
#skillsDirectory;
|
|
3143
|
+
constructor(skillsDirectory) {
|
|
3144
|
+
this.#skillsDirectory = skillsDirectory;
|
|
3145
|
+
}
|
|
3146
|
+
async loadManifest() {
|
|
3147
|
+
const manifestPath = join6(this.#skillsDirectory, "skills-index.json");
|
|
3148
|
+
try {
|
|
3149
|
+
const content = await fsReadFile(manifestPath, "utf8");
|
|
3150
|
+
return JSON.parse(content);
|
|
3151
|
+
} catch {
|
|
3152
|
+
return { skills: [] };
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
async saveManifest(manifest) {
|
|
3156
|
+
await mkdir5(this.#skillsDirectory, { recursive: true });
|
|
3157
|
+
const manifestPath = join6(this.#skillsDirectory, "skills-index.json");
|
|
3158
|
+
await writeFile4(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
3159
|
+
}
|
|
3160
|
+
async install(sourcePath) {
|
|
3161
|
+
const fileName = basename2(sourcePath);
|
|
3162
|
+
const nameWithoutExt = fileName.endsWith(".md") ? fileName.slice(0, -3) : fileName;
|
|
3163
|
+
const content = await fsReadFile(sourcePath, "utf8");
|
|
3164
|
+
const parsed = parseSKILLMd(content);
|
|
3165
|
+
const name = parsed?.name ?? nameWithoutExt;
|
|
3166
|
+
const skillDir = join6(this.#skillsDirectory, name);
|
|
3167
|
+
await mkdir5(skillDir, { recursive: true });
|
|
3168
|
+
const destPath = join6(skillDir, "SKILL.md");
|
|
3169
|
+
await copyFile(sourcePath, destPath);
|
|
3170
|
+
const manifest = await this.loadManifest();
|
|
3171
|
+
const existingIndex = manifest.skills.findIndex((e) => e.name === name);
|
|
3172
|
+
const entry = {
|
|
3173
|
+
name,
|
|
3174
|
+
filePath: destPath,
|
|
3175
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3176
|
+
...parsed?.origin !== void 0 ? { origin: parsed.origin } : {},
|
|
3177
|
+
trusted: false,
|
|
3178
|
+
enabled: true
|
|
3179
|
+
};
|
|
3180
|
+
if (existingIndex !== -1) {
|
|
3181
|
+
manifest.skills[existingIndex] = entry;
|
|
3182
|
+
} else {
|
|
3183
|
+
manifest.skills.push(entry);
|
|
3184
|
+
}
|
|
3185
|
+
await this.saveManifest(manifest);
|
|
3186
|
+
return entry;
|
|
3187
|
+
}
|
|
3188
|
+
async enable(name) {
|
|
3189
|
+
const manifest = await this.loadManifest();
|
|
3190
|
+
const entry = manifest.skills.find((e) => e.name === name);
|
|
3191
|
+
if (entry === void 0)
|
|
3192
|
+
throw new Error(`Skill "${name}" not found in manifest.`);
|
|
3193
|
+
entry.enabled = true;
|
|
3194
|
+
await this.saveManifest(manifest);
|
|
3195
|
+
}
|
|
3196
|
+
async disable(name) {
|
|
3197
|
+
const manifest = await this.loadManifest();
|
|
3198
|
+
const entry = manifest.skills.find((e) => e.name === name);
|
|
3199
|
+
if (entry === void 0)
|
|
3200
|
+
throw new Error(`Skill "${name}" not found in manifest.`);
|
|
3201
|
+
entry.enabled = false;
|
|
3202
|
+
await this.saveManifest(manifest);
|
|
3203
|
+
}
|
|
3204
|
+
async trust(name) {
|
|
3205
|
+
const manifest = await this.loadManifest();
|
|
3206
|
+
const entry = manifest.skills.find((e) => e.name === name);
|
|
3207
|
+
if (entry === void 0)
|
|
3208
|
+
throw new Error(`Skill "${name}" not found in manifest.`);
|
|
3209
|
+
entry.trusted = true;
|
|
3210
|
+
await this.saveManifest(manifest);
|
|
3211
|
+
}
|
|
3212
|
+
async review(name) {
|
|
3213
|
+
const manifest = await this.loadManifest();
|
|
3214
|
+
const entry = manifest.skills.find((e) => e.name === name);
|
|
3215
|
+
if (entry === void 0)
|
|
3216
|
+
return void 0;
|
|
3217
|
+
try {
|
|
3218
|
+
const content = await fsReadFile(entry.filePath, "utf8");
|
|
3219
|
+
const parsed = parseSKILLMd(content);
|
|
3220
|
+
if (parsed === null)
|
|
3221
|
+
return void 0;
|
|
3222
|
+
return {
|
|
3223
|
+
...parsed,
|
|
3224
|
+
source: "user",
|
|
3225
|
+
filePath: entry.filePath,
|
|
3226
|
+
trusted: entry.trusted,
|
|
3227
|
+
enabled: entry.enabled
|
|
3228
|
+
};
|
|
3229
|
+
} catch {
|
|
3230
|
+
return void 0;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
async listEntries() {
|
|
3234
|
+
const manifest = await this.loadManifest();
|
|
3235
|
+
return manifest.skills;
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
function parseSKILLMd(content) {
|
|
3239
|
+
const lines = content.split("\n");
|
|
3240
|
+
if (lines[0]?.trim() !== "---")
|
|
3241
|
+
return null;
|
|
3242
|
+
const closingIndex = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
|
|
3243
|
+
if (closingIndex === -1)
|
|
3244
|
+
return null;
|
|
3245
|
+
const frontmatterLines = lines.slice(1, closingIndex);
|
|
3246
|
+
const body = lines.slice(closingIndex + 1).join("\n").trim();
|
|
3247
|
+
const fields = {};
|
|
3248
|
+
const arrayFields = {};
|
|
3249
|
+
let currentArrayKey = null;
|
|
3250
|
+
for (const line of frontmatterLines) {
|
|
3251
|
+
const arrayItemMatch = /^\s+-\s+(.+)$/.exec(line);
|
|
3252
|
+
if (arrayItemMatch !== null && currentArrayKey !== null) {
|
|
3253
|
+
const value = arrayItemMatch[1]?.trim() ?? "";
|
|
3254
|
+
if (value.length > 0) {
|
|
3255
|
+
arrayFields[currentArrayKey] = [...arrayFields[currentArrayKey] ?? [], value];
|
|
3256
|
+
}
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
const colonIndex = line.indexOf(":");
|
|
3260
|
+
if (colonIndex === -1) {
|
|
3261
|
+
currentArrayKey = null;
|
|
3262
|
+
continue;
|
|
3263
|
+
}
|
|
3264
|
+
const key = line.slice(0, colonIndex).trim();
|
|
3265
|
+
const rawValue = line.slice(colonIndex + 1).trim();
|
|
3266
|
+
if (key.length === 0) {
|
|
3267
|
+
currentArrayKey = null;
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3270
|
+
if (rawValue === "") {
|
|
3271
|
+
currentArrayKey = key;
|
|
3272
|
+
} else {
|
|
3273
|
+
currentArrayKey = null;
|
|
3274
|
+
fields[key] = rawValue;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
const { name, description, version, origin, permissions: permissionsStr } = fields;
|
|
3278
|
+
if (!name || !description)
|
|
3279
|
+
return null;
|
|
3280
|
+
let permissions;
|
|
3281
|
+
if (permissionsStr !== void 0 && permissionsStr.length > 0) {
|
|
3282
|
+
permissions = permissionsStr.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
3283
|
+
} else if (arrayFields["permissions"] !== void 0) {
|
|
3284
|
+
permissions = arrayFields["permissions"];
|
|
3285
|
+
}
|
|
3286
|
+
return {
|
|
3287
|
+
name,
|
|
3288
|
+
description,
|
|
3289
|
+
body,
|
|
3290
|
+
...version !== void 0 ? { version } : {},
|
|
3291
|
+
...origin !== void 0 ? { origin } : {},
|
|
3292
|
+
...permissions !== void 0 ? { permissions } : {}
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
function toSkillSummary(skill) {
|
|
3296
|
+
return { name: skill.name, description: skill.description, source: skill.source };
|
|
3297
|
+
}
|
|
3298
|
+
function isNodeError4(error) {
|
|
3299
|
+
return error instanceof Error && "code" in error;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
// src/index.ts
|
|
3303
|
+
var cliPackageName = "@vole/cli";
|
|
3304
|
+
var AGENT_SYSTEM_INSTRUCTION = `You are Vole, a capable coding and general-purpose agent.
|
|
3305
|
+
|
|
3306
|
+
## Tool Call Style
|
|
3307
|
+
Do not narrate routine, low-risk tool calls \u2014 just call the tool.
|
|
3308
|
+
Narrate only when it genuinely helps: multi-step work, sensitive actions, or when explaining a non-obvious choice.
|
|
3309
|
+
Keep narration brief; avoid restating what tool output already shows.
|
|
3310
|
+
|
|
3311
|
+
## Execution Bias
|
|
3312
|
+
- Actionable request: act in this turn, do not describe what you plan to do.
|
|
3313
|
+
- Non-final turn: use tools to advance, or ask for the one decision that blocks safe progress.
|
|
3314
|
+
- Continue until done or genuinely blocked; do not end with a plan or promise when tools can move work forward.
|
|
3315
|
+
- Weak or empty tool result: vary the query, path, command, or source before concluding.
|
|
3316
|
+
- Mutable facts require live checks: files, git state, versions, running processes, package state.
|
|
3317
|
+
- Final answer requires evidence: test output, lint result, file inspection, or a named concrete blocker.
|
|
3318
|
+
- Longer work: brief progress note, then keep going.
|
|
3319
|
+
|
|
3320
|
+
## File Editing
|
|
3321
|
+
- Modify existing code: edit_file (precise string replacement, preserves surrounding content).
|
|
3322
|
+
- Add to end of file: append_file.
|
|
3323
|
+
- Create new files or intentional full replacement: write_file.`;
|
|
3324
|
+
var cliGateway = new SessionGateway();
|
|
3325
|
+
async function runCli(args, packageVersion, options = {}) {
|
|
3326
|
+
let capturedOut = "";
|
|
3327
|
+
let actionResult = null;
|
|
3328
|
+
const program = new Command().name("vole").description("A capable coding and general-purpose agent.").version(packageVersion, "-v, --version", "Show version number").exitOverride().configureOutput({
|
|
3329
|
+
writeOut: (str) => {
|
|
3330
|
+
capturedOut += str;
|
|
3331
|
+
},
|
|
3332
|
+
writeErr: (str) => {
|
|
3333
|
+
capturedOut += str;
|
|
3334
|
+
}
|
|
3335
|
+
}).addHelpText("after", "\nRun `vole <command> --help` for command-specific options.");
|
|
3336
|
+
program.command("chat").description("Start an interactive chat session").argument("[extra...]", "Slash commands to run after a --fake turn").option("-s, --session <id>", "Continue a named session").option("-r, --resume", "Continue the most recently updated session").option("--fake <message>", "Run one turn with a fake provider and exit").option("--fake-interactive", "Interactive chat with a fake provider").action(async (extra, opts) => {
|
|
3337
|
+
const slashCmds = extra.filter((a) => a.startsWith("/"));
|
|
3338
|
+
if (opts["fake"] !== void 0) {
|
|
3339
|
+
actionResult = await runFakeChatTurn({ message: opts["fake"], slashCommands: slashCmds }, options);
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
if (opts["fakeInteractive"]) {
|
|
3343
|
+
const sid2 = typeof opts["session"] === "string" ? opts["session"] : void 0;
|
|
3344
|
+
actionResult = await runInteractiveFakeChat(options, {
|
|
3345
|
+
fakeInteractive: true,
|
|
3346
|
+
resume: false,
|
|
3347
|
+
...sid2 !== void 0 ? { sessionId: sid2 } : {}
|
|
3348
|
+
});
|
|
3349
|
+
return;
|
|
3350
|
+
}
|
|
3351
|
+
const sid = typeof opts["session"] === "string" ? opts["session"] : void 0;
|
|
3352
|
+
actionResult = await runInteractiveConfiguredChat(options, {
|
|
3353
|
+
fakeInteractive: false,
|
|
3354
|
+
resume: opts["resume"] === true,
|
|
3355
|
+
...sid !== void 0 ? { sessionId: sid } : {}
|
|
3356
|
+
});
|
|
3357
|
+
});
|
|
3358
|
+
program.command("sessions").description("List stored chat sessions").action(async () => {
|
|
3359
|
+
actionResult = await runListSessions(options);
|
|
3360
|
+
});
|
|
3361
|
+
program.command("run").description('Run a one-shot background task e.g. vole run "fix the tests"').argument("[goal]", "Goal for the task").option("--mode <mode>", "Autonomy mode: auto | confirm | observe", "confirm").option("--dream", "Consolidate daily memory notes into MEMORY.md").action(async (goal, opts) => {
|
|
3362
|
+
if (opts["dream"]) {
|
|
3363
|
+
actionResult = await runMemoryDreaming(options);
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
const g = (goal ?? "").trim();
|
|
3367
|
+
if (g === "") {
|
|
3368
|
+
actionResult = { exitCode: 1, stdout: "", stderr: 'Missing goal. Usage: vole run "<goal>"\n' };
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
const raw = opts["mode"];
|
|
3372
|
+
const mode = raw === "auto" ? "auto" : raw === "observe" ? "observe" : "confirm";
|
|
3373
|
+
actionResult = await runBackgroundTask(g, mode, options);
|
|
3374
|
+
});
|
|
3375
|
+
program.command("tasks").description("List recent background task runs").option("-n, --limit <n>", "Number of runs to show", (v) => parseInt(v, 10)).action(async (opts) => {
|
|
3376
|
+
actionResult = await runListTasks(options, opts["limit"]);
|
|
3377
|
+
});
|
|
3378
|
+
const skillsCmd = program.command("skills").description("Manage agent skills");
|
|
3379
|
+
skillsCmd.action(async () => {
|
|
3380
|
+
actionResult = await runSkillsList(options);
|
|
3381
|
+
});
|
|
3382
|
+
skillsCmd.command("install <path>").description("Install a skill from a local .md file").action(async (p) => {
|
|
3383
|
+
actionResult = await runSkillsInstall(p, options);
|
|
3384
|
+
});
|
|
3385
|
+
skillsCmd.command("enable <name>").description("Enable a disabled skill").action(async (n) => {
|
|
3386
|
+
actionResult = await runSkillsLifecycle("enable", n, options);
|
|
3387
|
+
});
|
|
3388
|
+
skillsCmd.command("disable <name>").description("Disable a skill").action(async (n) => {
|
|
3389
|
+
actionResult = await runSkillsLifecycle("disable", n, options);
|
|
3390
|
+
});
|
|
3391
|
+
skillsCmd.command("trust <name>").description("Mark an installed skill as trusted").action(async (n) => {
|
|
3392
|
+
actionResult = await runSkillsLifecycle("trust", n, options);
|
|
3393
|
+
});
|
|
3394
|
+
skillsCmd.command("review <name>").description("Show full skill metadata and permissions").action(async (n) => {
|
|
3395
|
+
actionResult = await runSkillsReview(n, options);
|
|
3396
|
+
});
|
|
3397
|
+
program.command("daemon").description("Start the task scheduler daemon").option("--once", "Run all due tasks once and exit").action(async (opts) => {
|
|
3398
|
+
actionResult = await runDaemon(options, opts["once"] === true);
|
|
3399
|
+
});
|
|
3400
|
+
program.command("web").description("Start the Vole web dashboard").option("-p, --port <port>", "Port to listen on", "3120").option("--no-open", "Don't open the browser automatically").action(async (opts) => {
|
|
3401
|
+
const port = parseInt(opts["port"], 10) || 3120;
|
|
3402
|
+
const openBrowser = opts["open"] !== false;
|
|
3403
|
+
actionResult = await runWebDashboard(port, openBrowser);
|
|
3404
|
+
});
|
|
3405
|
+
const tfCmd = program.command("taskflow").description("Inspect cross-session task records");
|
|
3406
|
+
tfCmd.action(async () => {
|
|
3407
|
+
actionResult = await runTaskflowList(options, void 0);
|
|
3408
|
+
});
|
|
3409
|
+
tfCmd.command("list").description("List recent task records").option("-n, --limit <n>", "Number of records to show", (v) => parseInt(v, 10)).action(async (opts) => {
|
|
3410
|
+
actionResult = await runTaskflowList(options, opts["limit"]);
|
|
3411
|
+
});
|
|
3412
|
+
tfCmd.command("show <id>").description("Show details of a task").action(async (id) => {
|
|
3413
|
+
actionResult = await runTaskflowShow(id, options);
|
|
3414
|
+
});
|
|
3415
|
+
tfCmd.command("cancel <id>").description("Mark a task as cancelled").action(async (id) => {
|
|
3416
|
+
actionResult = await runTaskflowCancel(id, options);
|
|
3417
|
+
});
|
|
3418
|
+
try {
|
|
3419
|
+
const commanderArgs = args[0] === "--" ? args.slice(1) : args;
|
|
3420
|
+
await program.parseAsync(commanderArgs, { from: "user" });
|
|
3421
|
+
} catch (err) {
|
|
3422
|
+
if (err instanceof Error && "code" in err) {
|
|
3423
|
+
const code = err.code;
|
|
3424
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
3425
|
+
return { exitCode: 0, stdout: capturedOut, stderr: "" };
|
|
3426
|
+
}
|
|
3427
|
+
if (code === "commander.unknownCommand") {
|
|
3428
|
+
const match = err.message.match(/unknown command '(.+)'/);
|
|
3429
|
+
const cmdName = match ? match[1] : args[0] ?? "unknown";
|
|
3430
|
+
return { exitCode: 1, stdout: program.helpInformation(), stderr: `Unknown command "${cmdName}".
|
|
3431
|
+
` };
|
|
3432
|
+
}
|
|
3433
|
+
return { exitCode: err.exitCode ?? 1, stdout: capturedOut, stderr: `${err.message}
|
|
3434
|
+
` };
|
|
3435
|
+
}
|
|
3436
|
+
throw err;
|
|
3437
|
+
}
|
|
3438
|
+
return actionResult ?? { exitCode: 0, stdout: capturedOut, stderr: "" };
|
|
3439
|
+
}
|
|
3440
|
+
async function runFakeChatTurn(input, options) {
|
|
3441
|
+
const { message, slashCommands } = input;
|
|
3442
|
+
if (message.trim() === "") {
|
|
3443
|
+
return {
|
|
3444
|
+
exitCode: 1,
|
|
3445
|
+
stdout: "",
|
|
3446
|
+
stderr: "Missing message for `chat --fake`.\n"
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
const session = CliChatSession.createFake(`Fake response to: ${message}`, options);
|
|
3450
|
+
const turn = await session.sendMessage(message);
|
|
3451
|
+
const commandOutput = await renderSlashCommands(session, slashCommands);
|
|
3452
|
+
const assistantText = turn.assistantText;
|
|
3453
|
+
const events = turn.events;
|
|
3454
|
+
const traceLines = renderCompactTrace(events).join("\n");
|
|
3455
|
+
return {
|
|
3456
|
+
exitCode: events.some((event) => event.type === "run_failed") ? 1 : 0,
|
|
3457
|
+
stdout: `Assistant: ${assistantText}
|
|
3458
|
+
|
|
3459
|
+
Trace:
|
|
3460
|
+
${traceLines}
|
|
3461
|
+
${commandOutput}`,
|
|
3462
|
+
stderr: ""
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
async function runInteractiveFakeChat(options, args) {
|
|
3466
|
+
const session = CliChatSession.createFake((message) => `Fake response to: ${message}`, options, {
|
|
3467
|
+
...args.sessionId === void 0 ? {} : { sessionId: args.sessionId }
|
|
3468
|
+
});
|
|
3469
|
+
return runInteractiveLoop(session, "Vole chat (fake provider)", options);
|
|
3470
|
+
}
|
|
3471
|
+
async function runInteractiveConfiguredChat(options, args) {
|
|
3472
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3473
|
+
if (config.secrets.apiKey === void 0) {
|
|
3474
|
+
return {
|
|
3475
|
+
exitCode: 1,
|
|
3476
|
+
stdout: "",
|
|
3477
|
+
stderr: "Missing VOLE_API_KEY or OPENROUTER_API_KEY. Set one to start `vole chat`, or use `vole chat --fake-interactive` for local learning.\n"
|
|
3478
|
+
};
|
|
3479
|
+
}
|
|
3480
|
+
if (args.resume && args.sessionId !== void 0) {
|
|
3481
|
+
return {
|
|
3482
|
+
exitCode: 1,
|
|
3483
|
+
stdout: "",
|
|
3484
|
+
stderr: "Use either `chat --resume` or `chat --session <id>`, not both.\n"
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
const resumedSessionId = args.resume ? await findMostRecentSessionId(config, options) : void 0;
|
|
3488
|
+
if (args.resume && resumedSessionId === void 0) {
|
|
3489
|
+
return {
|
|
3490
|
+
exitCode: 1,
|
|
3491
|
+
stdout: "",
|
|
3492
|
+
stderr: "No stored sessions to resume. Start one with `vole chat` or `vole chat --session <id>`.\n"
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
const sessionId = args.sessionId ?? resumedSessionId;
|
|
3496
|
+
return runInteractiveLoop(
|
|
3497
|
+
await CliChatSession.createConfigured(config, options, {
|
|
3498
|
+
...sessionId === void 0 ? {} : { sessionId }
|
|
3499
|
+
}),
|
|
3500
|
+
resumedSessionId === void 0 ? "Vole chat" : `Vole chat
|
|
3501
|
+
Resumed session: ${resumedSessionId}`,
|
|
3502
|
+
options
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
async function runListSessions(options) {
|
|
3506
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3507
|
+
const store = createConfiguredSessionStore(config, options, createSessionId());
|
|
3508
|
+
const sessions = await store.listSessions();
|
|
3509
|
+
if (sessions.length === 0) {
|
|
3510
|
+
return {
|
|
3511
|
+
exitCode: 0,
|
|
3512
|
+
stdout: "Sessions:\nNo sessions found.\n",
|
|
3513
|
+
stderr: ""
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
return {
|
|
3517
|
+
exitCode: 0,
|
|
3518
|
+
stdout: ["Sessions:", ...sessions.map((session) => `${session.id} ${session.updatedAt}${session.title ? ` ${session.title}` : ""}`)].join("\n") + "\n",
|
|
3519
|
+
stderr: ""
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
async function runMemoryDreaming(options) {
|
|
3523
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3524
|
+
if (config.secrets.apiKey === void 0) {
|
|
3525
|
+
return {
|
|
3526
|
+
exitCode: 1,
|
|
3527
|
+
stdout: "",
|
|
3528
|
+
stderr: "Missing VOLE_API_KEY or OPENROUTER_API_KEY. Set one to run memory dreaming.\n"
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
if (config.memory.longTermFiles !== "write") {
|
|
3532
|
+
return {
|
|
3533
|
+
exitCode: 1,
|
|
3534
|
+
stdout: "",
|
|
3535
|
+
stderr: "Memory dreaming requires VOLE_LONG_TERM_MEMORY=write\n"
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
const dreamGoal = `You are a memory consolidation agent.
|
|
3539
|
+
Review the recent daily memory files and the current MEMORY.md in the workspace.
|
|
3540
|
+
Identify key facts, decisions, and patterns worth preserving long-term.
|
|
3541
|
+
Append a consolidation summary to MEMORY.md using the write_file tool.
|
|
3542
|
+
Be concise and factual. Do not duplicate what is already in MEMORY.md.`;
|
|
3543
|
+
return runBackgroundTask(dreamGoal, "auto", options);
|
|
3544
|
+
}
|
|
3545
|
+
async function runBackgroundTask(goal, mode, options) {
|
|
3546
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3547
|
+
if (config.secrets.apiKey === void 0) {
|
|
3548
|
+
return {
|
|
3549
|
+
exitCode: 1,
|
|
3550
|
+
stdout: "",
|
|
3551
|
+
stderr: "Missing VOLE_API_KEY or OPENROUTER_API_KEY. Set one to run background tasks.\n"
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
const effectiveConfig = options.sessionsDirectory ? { ...config, sessions: { directory: options.sessionsDirectory } } : config;
|
|
3555
|
+
const sessionsDir = resolveSessionsDirectory(effectiveConfig, options.env);
|
|
3556
|
+
const sessionId = createSessionId();
|
|
3557
|
+
const taskRunId = `task_${crypto.randomUUID()}`;
|
|
3558
|
+
const taskName = goal.slice(0, 40).replace(/\s+/g, "-").replace(/[^A-Za-z0-9-]/g, "").toLowerCase() || "task";
|
|
3559
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3560
|
+
const sessionStore = new JsonlSessionStore({
|
|
3561
|
+
directory: sessionsDir,
|
|
3562
|
+
createSessionId: () => sessionId
|
|
3563
|
+
});
|
|
3564
|
+
const taskStore = new JsonlTaskStore(join7(sessionsDir, "task-runs.jsonl"));
|
|
3565
|
+
const initialRecord = {
|
|
3566
|
+
id: taskRunId,
|
|
3567
|
+
taskName,
|
|
3568
|
+
goal,
|
|
3569
|
+
sessionId,
|
|
3570
|
+
startedAt,
|
|
3571
|
+
status: "running",
|
|
3572
|
+
assistantText: ""
|
|
3573
|
+
};
|
|
3574
|
+
await taskStore.saveRun(initialRecord);
|
|
3575
|
+
const configuredProvider = createConfiguredProvider(config, options);
|
|
3576
|
+
const approvalResolver = new BackgroundApprovalResolver(mode);
|
|
3577
|
+
const currentDate = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3578
|
+
const backgroundTools = (() => {
|
|
3579
|
+
const allTools = createCliBuiltInTools(options, config);
|
|
3580
|
+
return config.runtime.toolProfile !== void 0 ? filterToolsByProfile(allTools, config.runtime.toolProfile) : allTools;
|
|
3581
|
+
})();
|
|
3582
|
+
const runtime = new AgentRuntime({
|
|
3583
|
+
contextAssembler: createCliContextAssembler(config, currentDate),
|
|
3584
|
+
modelProvider: configuredProvider,
|
|
3585
|
+
systemInstruction: AGENT_SYSTEM_INSTRUCTION,
|
|
3586
|
+
runtime: {
|
|
3587
|
+
mode,
|
|
3588
|
+
workspace: config.workspace.root,
|
|
3589
|
+
currentDate
|
|
3590
|
+
},
|
|
3591
|
+
tools: backgroundTools,
|
|
3592
|
+
preferStreaming: false,
|
|
3593
|
+
approvalResolver,
|
|
3594
|
+
maxSteps: 20,
|
|
3595
|
+
...config.runtime.promptMode !== void 0 ? { promptMode: config.runtime.promptMode } : {}
|
|
3596
|
+
});
|
|
3597
|
+
const events = [];
|
|
3598
|
+
await sessionStore.createSession({ title: `task: ${goal.slice(0, 60)}` });
|
|
3599
|
+
for await (const event of runtime.runTurn({ sessionId, message: goal })) {
|
|
3600
|
+
await sessionStore.appendTraceEvent({ sessionId, event });
|
|
3601
|
+
events.push(event);
|
|
3602
|
+
}
|
|
3603
|
+
const traceLines = renderCompactTrace(events);
|
|
3604
|
+
const assistantMessageEvent = events.find((e) => e.type === "assistant_message_created");
|
|
3605
|
+
const assistantText = assistantMessageEvent?.type === "assistant_message_created" ? assistantMessageEvent.message.content : "No assistant message was produced.";
|
|
3606
|
+
const failedEvent = events.find((e) => e.type === "run_failed");
|
|
3607
|
+
const status = failedEvent ? "failed" : "completed";
|
|
3608
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3609
|
+
const updates = {
|
|
3610
|
+
status,
|
|
3611
|
+
assistantText,
|
|
3612
|
+
completedAt,
|
|
3613
|
+
...failedEvent?.type === "run_failed" ? { errorMessage: failedEvent.error.message } : {}
|
|
3614
|
+
};
|
|
3615
|
+
await taskStore.updateRun(taskRunId, updates);
|
|
3616
|
+
const traceOutput = traceLines.join("\n");
|
|
3617
|
+
const resultLine = status === "completed" ? `Done: ${assistantText}` : `Failed: ${failedEvent?.type === "run_failed" ? failedEvent.error.message : "Unknown error"}`;
|
|
3618
|
+
return {
|
|
3619
|
+
exitCode: status === "completed" ? 0 : 1,
|
|
3620
|
+
stdout: `Trace:
|
|
3621
|
+
${traceOutput}
|
|
3622
|
+
|
|
3623
|
+
${resultLine}
|
|
3624
|
+
`,
|
|
3625
|
+
stderr: ""
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
async function runListTasks(options, limit) {
|
|
3629
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3630
|
+
const effectiveConfig = options.sessionsDirectory ? { ...config, sessions: { directory: options.sessionsDirectory } } : config;
|
|
3631
|
+
const sessionsDir = resolveSessionsDirectory(effectiveConfig, options.env);
|
|
3632
|
+
const taskStore = new JsonlTaskStore(join7(sessionsDir, "task-runs.jsonl"));
|
|
3633
|
+
const runs = await taskStore.listRuns(limit !== void 0 ? { limit } : {});
|
|
3634
|
+
if (runs.length === 0) {
|
|
3635
|
+
return {
|
|
3636
|
+
exitCode: 0,
|
|
3637
|
+
stdout: "No task runs found.\n",
|
|
3638
|
+
stderr: ""
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
const lines = runs.map((run) => {
|
|
3642
|
+
const idSuffix = run.id.slice(-8);
|
|
3643
|
+
return `${idSuffix} ${run.taskName} ${run.status} ${run.startedAt}`;
|
|
3644
|
+
});
|
|
3645
|
+
return {
|
|
3646
|
+
exitCode: 0,
|
|
3647
|
+
stdout: lines.join("\n") + "\n",
|
|
3648
|
+
stderr: ""
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
async function loadTaskDefinitions(tasksDir) {
|
|
3652
|
+
try {
|
|
3653
|
+
await stat(tasksDir);
|
|
3654
|
+
} catch {
|
|
3655
|
+
return null;
|
|
3656
|
+
}
|
|
3657
|
+
const entries = await readdir4(tasksDir);
|
|
3658
|
+
const taskFiles = entries.filter((e) => e.endsWith(".task.json"));
|
|
3659
|
+
const tasks = [];
|
|
3660
|
+
for (const file of taskFiles) {
|
|
3661
|
+
const content = await readFile6(join7(tasksDir, file), "utf8");
|
|
3662
|
+
tasks.push(JSON.parse(content));
|
|
3663
|
+
}
|
|
3664
|
+
return tasks;
|
|
3665
|
+
}
|
|
3666
|
+
async function runDaemonTask(task, config, options, taskStore) {
|
|
3667
|
+
const runId = `run_${crypto.randomUUID()}`;
|
|
3668
|
+
const sessionId = createSessionId();
|
|
3669
|
+
const mode = task.mode ?? "auto";
|
|
3670
|
+
const record = {
|
|
3671
|
+
id: runId,
|
|
3672
|
+
taskName: task.name,
|
|
3673
|
+
goal: task.goal,
|
|
3674
|
+
sessionId,
|
|
3675
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3676
|
+
status: "running",
|
|
3677
|
+
assistantText: ""
|
|
3678
|
+
};
|
|
3679
|
+
await taskStore.saveRun(record);
|
|
3680
|
+
const heartbeatPath = join7(config.workspace.root, "HEARTBEAT.md");
|
|
3681
|
+
const startHeartbeat = {
|
|
3682
|
+
status: "running",
|
|
3683
|
+
taskName: task.name,
|
|
3684
|
+
runId,
|
|
3685
|
+
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3686
|
+
};
|
|
3687
|
+
await writeHeartbeat(heartbeatPath, startHeartbeat);
|
|
3688
|
+
const provider = options.fakeModelOutputs ? new FakeModelProvider(options.fakeModelOutputs) : createConfiguredProvider(config, options);
|
|
3689
|
+
const approvalResolver = new BackgroundApprovalResolver(mode);
|
|
3690
|
+
const currentDate = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3691
|
+
const backgroundTools = (() => {
|
|
3692
|
+
const allTools = createCliBuiltInTools(options, config);
|
|
3693
|
+
return config.runtime.toolProfile !== void 0 ? filterToolsByProfile(allTools, config.runtime.toolProfile) : allTools;
|
|
3694
|
+
})();
|
|
3695
|
+
const runtime = new AgentRuntime({
|
|
3696
|
+
contextAssembler: createCliContextAssembler(config, currentDate),
|
|
3697
|
+
modelProvider: provider,
|
|
3698
|
+
systemInstruction: AGENT_SYSTEM_INSTRUCTION,
|
|
3699
|
+
runtime: {
|
|
3700
|
+
mode,
|
|
3701
|
+
workspace: config.workspace.root,
|
|
3702
|
+
currentDate
|
|
3703
|
+
},
|
|
3704
|
+
tools: backgroundTools,
|
|
3705
|
+
preferStreaming: false,
|
|
3706
|
+
approvalResolver,
|
|
3707
|
+
...task.maxSteps !== void 0 ? { maxSteps: task.maxSteps } : {},
|
|
3708
|
+
...config.runtime.promptMode !== void 0 ? { promptMode: config.runtime.promptMode } : {}
|
|
3709
|
+
});
|
|
3710
|
+
const events = [];
|
|
3711
|
+
for await (const event of runtime.runTurn({ sessionId, message: task.goal })) {
|
|
3712
|
+
events.push(event);
|
|
3713
|
+
}
|
|
3714
|
+
const assistantMessageEvent = events.find((e) => e.type === "assistant_message_created");
|
|
3715
|
+
const assistantText = assistantMessageEvent?.type === "assistant_message_created" ? assistantMessageEvent.message.content : "No assistant message was produced.";
|
|
3716
|
+
const failedEvent = events.find((e) => e.type === "run_failed");
|
|
3717
|
+
const status = failedEvent ? "failed" : "completed";
|
|
3718
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3719
|
+
const updates = {
|
|
3720
|
+
status,
|
|
3721
|
+
assistantText,
|
|
3722
|
+
completedAt,
|
|
3723
|
+
...failedEvent?.type === "run_failed" ? { errorMessage: failedEvent.error.message } : {}
|
|
3724
|
+
};
|
|
3725
|
+
await taskStore.updateRun(runId, updates);
|
|
3726
|
+
const endHeartbeat = {
|
|
3727
|
+
status,
|
|
3728
|
+
taskName: task.name,
|
|
3729
|
+
runId,
|
|
3730
|
+
lastUpdatedAt: completedAt,
|
|
3731
|
+
...failedEvent?.type === "run_failed" ? { message: `Error: ${failedEvent.error.message}` } : {}
|
|
3732
|
+
};
|
|
3733
|
+
await writeHeartbeat(heartbeatPath, endHeartbeat);
|
|
3734
|
+
}
|
|
3735
|
+
async function runDaemon(options, once) {
|
|
3736
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3737
|
+
if (config.secrets.apiKey === void 0) {
|
|
3738
|
+
return {
|
|
3739
|
+
exitCode: 1,
|
|
3740
|
+
stdout: "",
|
|
3741
|
+
stderr: "Missing VOLE_API_KEY or OPENROUTER_API_KEY. Set one to run the daemon.\n"
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
const effectiveConfig = options.sessionsDirectory ? { ...config, sessions: { directory: options.sessionsDirectory } } : config;
|
|
3745
|
+
const sessionsDir = resolveSessionsDirectory(effectiveConfig, options.env);
|
|
3746
|
+
const tasksDir = join7(dirname4(sessionsDir), "tasks");
|
|
3747
|
+
const taskStore = new JsonlTaskStore(join7(sessionsDir, "task-runs.jsonl"));
|
|
3748
|
+
const tasks = await loadTaskDefinitions(tasksDir);
|
|
3749
|
+
if (tasks === null) {
|
|
3750
|
+
return {
|
|
3751
|
+
exitCode: 0,
|
|
3752
|
+
stdout: `No tasks directory found at ${tasksDir}.
|
|
3753
|
+
`,
|
|
3754
|
+
stderr: ""
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
const cronTasks = tasks.filter((t) => t.cron !== void 0);
|
|
3758
|
+
if (once) {
|
|
3759
|
+
const now = /* @__PURE__ */ new Date();
|
|
3760
|
+
const output = [];
|
|
3761
|
+
for (const task of cronTasks) {
|
|
3762
|
+
output.push(`Running: ${task.name}`);
|
|
3763
|
+
await runDaemonTask(task, config, options, taskStore);
|
|
3764
|
+
}
|
|
3765
|
+
output.push("Done.");
|
|
3766
|
+
return {
|
|
3767
|
+
exitCode: 0,
|
|
3768
|
+
stdout: output.join("\n") + "\n",
|
|
3769
|
+
stderr: ""
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
const runner = async (task) => {
|
|
3773
|
+
await runDaemonTask(task, config, options, taskStore);
|
|
3774
|
+
};
|
|
3775
|
+
const scheduler = new CronScheduler(cronTasks, runner);
|
|
3776
|
+
scheduler.start();
|
|
3777
|
+
return new Promise((resolve2) => {
|
|
3778
|
+
const shutdown = () => {
|
|
3779
|
+
scheduler.stop();
|
|
3780
|
+
resolve2({
|
|
3781
|
+
exitCode: 0,
|
|
3782
|
+
stdout: "Daemon stopped.\n",
|
|
3783
|
+
stderr: ""
|
|
3784
|
+
});
|
|
3785
|
+
};
|
|
3786
|
+
process.once("SIGTERM", shutdown);
|
|
3787
|
+
process.once("SIGINT", shutdown);
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
function resolveTaskflowFilePath(options) {
|
|
3791
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3792
|
+
const effectiveConfig = options.sessionsDirectory ? { ...config, sessions: { directory: options.sessionsDirectory } } : config;
|
|
3793
|
+
const sessionsDir = resolveSessionsDirectory(effectiveConfig, options.env);
|
|
3794
|
+
return join7(dirname4(sessionsDir), "taskflow.jsonl");
|
|
3795
|
+
}
|
|
3796
|
+
async function runTaskflowList(options, limit) {
|
|
3797
|
+
const filePath = resolveTaskflowFilePath(options);
|
|
3798
|
+
const store = new JsonlTaskFlowStore(filePath);
|
|
3799
|
+
const records = await store.list(limit !== void 0 ? { limit } : {});
|
|
3800
|
+
if (records.length === 0) {
|
|
3801
|
+
return {
|
|
3802
|
+
exitCode: 0,
|
|
3803
|
+
stdout: "No task records found.\n",
|
|
3804
|
+
stderr: ""
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3807
|
+
const lines = records.map((r) => {
|
|
3808
|
+
const idSuffix = r.id.slice(-8);
|
|
3809
|
+
return `${idSuffix} ${r.status} ${r.runtime} ${r.createdAt} ${r.task.slice(0, 60)}`;
|
|
3810
|
+
});
|
|
3811
|
+
return {
|
|
3812
|
+
exitCode: 0,
|
|
3813
|
+
stdout: ["Task records:", ...lines].join("\n") + "\n",
|
|
3814
|
+
stderr: ""
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
async function runTaskflowShow(id, options) {
|
|
3818
|
+
const filePath = resolveTaskflowFilePath(options);
|
|
3819
|
+
const store = new JsonlTaskFlowStore(filePath);
|
|
3820
|
+
const record = await store.get(id);
|
|
3821
|
+
if (record === void 0) {
|
|
3822
|
+
return {
|
|
3823
|
+
exitCode: 1,
|
|
3824
|
+
stdout: "",
|
|
3825
|
+
stderr: `Task "${id}" not found.
|
|
3826
|
+
`
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
const lines = [
|
|
3830
|
+
`ID: ${record.id}`,
|
|
3831
|
+
`Runtime: ${record.runtime}`,
|
|
3832
|
+
`Status: ${record.status}`,
|
|
3833
|
+
`Task: ${record.task}`,
|
|
3834
|
+
`Created: ${record.createdAt}`,
|
|
3835
|
+
`Updated: ${record.updatedAt}`,
|
|
3836
|
+
...record.parentId !== void 0 ? [`Parent: ${record.parentId}`] : [],
|
|
3837
|
+
...record.sessionId !== void 0 ? [`Session: ${record.sessionId}`] : [],
|
|
3838
|
+
...record.progressSummary !== void 0 ? [`Progress: ${record.progressSummary}`] : [],
|
|
3839
|
+
...record.terminalSummary !== void 0 ? [`Terminal summary: ${record.terminalSummary}`] : []
|
|
3840
|
+
];
|
|
3841
|
+
return {
|
|
3842
|
+
exitCode: 0,
|
|
3843
|
+
stdout: lines.join("\n") + "\n",
|
|
3844
|
+
stderr: ""
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
async function runTaskflowCancel(id, options) {
|
|
3848
|
+
const filePath = resolveTaskflowFilePath(options);
|
|
3849
|
+
const store = new JsonlTaskFlowStore(filePath);
|
|
3850
|
+
const updated = await store.update(id, { status: "cancelled" });
|
|
3851
|
+
if (updated === void 0) {
|
|
3852
|
+
return {
|
|
3853
|
+
exitCode: 1,
|
|
3854
|
+
stdout: "",
|
|
3855
|
+
stderr: `Task "${id}" not found.
|
|
3856
|
+
`
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
return {
|
|
3860
|
+
exitCode: 0,
|
|
3861
|
+
stdout: `Cancelled: ${id}
|
|
3862
|
+
`,
|
|
3863
|
+
stderr: ""
|
|
3864
|
+
};
|
|
3865
|
+
}
|
|
3866
|
+
function resolveSkillsDirectory(config, options) {
|
|
3867
|
+
const effectiveConfig = options.sessionsDirectory ? { ...config, sessions: { directory: options.sessionsDirectory } } : config;
|
|
3868
|
+
const sessionsDir = resolveSessionsDirectory(effectiveConfig, options.env);
|
|
3869
|
+
return join7(dirname4(sessionsDir), "skills");
|
|
3870
|
+
}
|
|
3871
|
+
async function runSkillsList(options) {
|
|
3872
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3873
|
+
const skillsDir = resolveSkillsDirectory(config, options);
|
|
3874
|
+
const loader = new SkillLoader();
|
|
3875
|
+
const skills = await loader.load({
|
|
3876
|
+
workspaceRoot: config.workspace.root,
|
|
3877
|
+
userSkillsDir: skillsDir
|
|
3878
|
+
});
|
|
3879
|
+
const lines = renderSkillIndex(skills);
|
|
3880
|
+
return {
|
|
3881
|
+
exitCode: 0,
|
|
3882
|
+
stdout: ["Skills:", ...lines].join("\n") + "\n",
|
|
3883
|
+
stderr: ""
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
async function runSkillsInstall(sourcePath, options) {
|
|
3887
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3888
|
+
const skillsDir = resolveSkillsDirectory(config, options);
|
|
3889
|
+
const manager = new SkillManager(skillsDir);
|
|
3890
|
+
try {
|
|
3891
|
+
const entry = await manager.install(sourcePath);
|
|
3892
|
+
return {
|
|
3893
|
+
exitCode: 0,
|
|
3894
|
+
stdout: `Installed: ${entry.name}
|
|
3895
|
+
`,
|
|
3896
|
+
stderr: ""
|
|
3897
|
+
};
|
|
3898
|
+
} catch (error) {
|
|
3899
|
+
return {
|
|
3900
|
+
exitCode: 1,
|
|
3901
|
+
stdout: "",
|
|
3902
|
+
stderr: `Failed to install skill: ${error instanceof Error ? error.message : String(error)}
|
|
3903
|
+
`
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
async function runSkillsLifecycle(action, name, options) {
|
|
3908
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3909
|
+
const skillsDir = resolveSkillsDirectory(config, options);
|
|
3910
|
+
const manager = new SkillManager(skillsDir);
|
|
3911
|
+
try {
|
|
3912
|
+
if (action === "enable") {
|
|
3913
|
+
await manager.enable(name);
|
|
3914
|
+
return { exitCode: 0, stdout: `Enabled: ${name}
|
|
3915
|
+
`, stderr: "" };
|
|
3916
|
+
} else if (action === "disable") {
|
|
3917
|
+
await manager.disable(name);
|
|
3918
|
+
return { exitCode: 0, stdout: `Disabled: ${name}
|
|
3919
|
+
`, stderr: "" };
|
|
3920
|
+
} else {
|
|
3921
|
+
await manager.trust(name);
|
|
3922
|
+
return { exitCode: 0, stdout: `Trusted: ${name}
|
|
3923
|
+
`, stderr: "" };
|
|
3924
|
+
}
|
|
3925
|
+
} catch (error) {
|
|
3926
|
+
return {
|
|
3927
|
+
exitCode: 1,
|
|
3928
|
+
stdout: "",
|
|
3929
|
+
stderr: `Failed to ${action} skill: ${error instanceof Error ? error.message : String(error)}
|
|
3930
|
+
`
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
async function runSkillsReview(name, options) {
|
|
3935
|
+
const config = loadConfig(options.env ? { env: options.env } : {});
|
|
3936
|
+
const skillsDir = resolveSkillsDirectory(config, options);
|
|
3937
|
+
const manager = new SkillManager(skillsDir);
|
|
3938
|
+
const def = await manager.review(name);
|
|
3939
|
+
if (def === void 0) {
|
|
3940
|
+
return {
|
|
3941
|
+
exitCode: 1,
|
|
3942
|
+
stdout: "",
|
|
3943
|
+
stderr: `Skill "${name}" not found.
|
|
3944
|
+
`
|
|
3945
|
+
};
|
|
3946
|
+
}
|
|
3947
|
+
const entries = await manager.listEntries();
|
|
3948
|
+
const entry = entries.find((e) => e.name === name);
|
|
3949
|
+
const lines = [
|
|
3950
|
+
`Name: ${def.name}`,
|
|
3951
|
+
`Source: ${def.source}`,
|
|
3952
|
+
...def.version !== void 0 ? [`Version: ${def.version}`] : [],
|
|
3953
|
+
...def.origin !== void 0 ? [`Origin: ${def.origin}`] : [],
|
|
3954
|
+
`Permissions: ${def.permissions !== void 0 && def.permissions.length > 0 ? def.permissions.join(", ") : "(none)"}`,
|
|
3955
|
+
`Trusted: ${String(def.trusted ?? false)}`,
|
|
3956
|
+
`Enabled: ${String(def.enabled ?? true)}`,
|
|
3957
|
+
...entry?.installedAt !== void 0 ? [`Installed: ${entry.installedAt}`] : [],
|
|
3958
|
+
"",
|
|
3959
|
+
"--- Body ---",
|
|
3960
|
+
def.body
|
|
3961
|
+
];
|
|
3962
|
+
return {
|
|
3963
|
+
exitCode: 0,
|
|
3964
|
+
stdout: lines.join("\n") + "\n",
|
|
3965
|
+
stderr: ""
|
|
3966
|
+
};
|
|
3967
|
+
}
|
|
3968
|
+
async function findMostRecentSessionId(config, options) {
|
|
3969
|
+
const store = createConfiguredSessionStore(config, options, createSessionId());
|
|
3970
|
+
const [session] = await store.listSessions({ limit: 1 });
|
|
3971
|
+
return session?.id;
|
|
3972
|
+
}
|
|
3973
|
+
async function runInteractiveLoop(session, title, options) {
|
|
3974
|
+
const output = [];
|
|
3975
|
+
const emit = (...lines) => {
|
|
3976
|
+
if (options.write) {
|
|
3977
|
+
options.write(`${lines.join("\n")}
|
|
3978
|
+
`);
|
|
3979
|
+
} else {
|
|
3980
|
+
output.push(...lines);
|
|
3981
|
+
}
|
|
3982
|
+
};
|
|
3983
|
+
emit(title, "Type /help for commands or /exit to leave.", "");
|
|
3984
|
+
while (true) {
|
|
3985
|
+
const line = await options.readLine?.("> ");
|
|
3986
|
+
if (line === void 0) {
|
|
3987
|
+
break;
|
|
3988
|
+
}
|
|
3989
|
+
const message = line.trim();
|
|
3990
|
+
if (message === "") {
|
|
3991
|
+
continue;
|
|
3992
|
+
}
|
|
3993
|
+
if (message === "/exit") {
|
|
3994
|
+
emit("Goodbye.");
|
|
3995
|
+
break;
|
|
3996
|
+
}
|
|
3997
|
+
if (message === "/help") {
|
|
3998
|
+
emit(...renderInteractiveHelp(), "");
|
|
3999
|
+
continue;
|
|
4000
|
+
}
|
|
4001
|
+
if (message === "/clear") {
|
|
4002
|
+
emit("(conversation display cleared)", "");
|
|
4003
|
+
continue;
|
|
4004
|
+
}
|
|
4005
|
+
if (message.startsWith("/")) {
|
|
4006
|
+
emit(...renderInteractiveSlashCommand(message, await session.runSlashCommand(message)), "");
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
const turn = await session.sendMessage(message);
|
|
4010
|
+
if (turn.todosLines.length > 0) {
|
|
4011
|
+
emit(...turn.todosLines, "");
|
|
4012
|
+
}
|
|
4013
|
+
if (turn.approvalLines.length > 0) {
|
|
4014
|
+
emit(...turn.approvalLines, "");
|
|
4015
|
+
}
|
|
4016
|
+
emit(`Assistant: ${turn.assistantText}`, "");
|
|
4017
|
+
}
|
|
4018
|
+
return {
|
|
4019
|
+
exitCode: 0,
|
|
4020
|
+
stdout: options.write ? "" : `${output.join("\n")}
|
|
4021
|
+
`,
|
|
4022
|
+
stderr: ""
|
|
4023
|
+
};
|
|
4024
|
+
}
|
|
4025
|
+
var SLASH_COMMAND_LABELS = {
|
|
4026
|
+
"/trace": "Recent Trace:",
|
|
4027
|
+
"/config": "Config:",
|
|
4028
|
+
"/skills": "Skills:",
|
|
4029
|
+
"/help": "Commands:"
|
|
4030
|
+
};
|
|
4031
|
+
async function renderSlashCommands(session, slashCommands) {
|
|
4032
|
+
const rendered = [];
|
|
4033
|
+
for (const command of slashCommands) {
|
|
4034
|
+
const label = SLASH_COMMAND_LABELS[command];
|
|
4035
|
+
if (label !== void 0) {
|
|
4036
|
+
rendered.push(["", label, ...await session.runSlashCommand(command)].join("\n"));
|
|
4037
|
+
} else {
|
|
4038
|
+
rendered.push(["", `Unknown slash command: ${command}`].join("\n"));
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
return rendered.length === 0 ? "" : `${rendered.join("\n")}
|
|
4042
|
+
`;
|
|
4043
|
+
}
|
|
4044
|
+
function renderInteractiveSlashCommand(command, lines) {
|
|
4045
|
+
if (lines[0]?.startsWith("Unknown slash command")) {
|
|
4046
|
+
return lines;
|
|
4047
|
+
}
|
|
4048
|
+
const label = SLASH_COMMAND_LABELS[command];
|
|
4049
|
+
return label !== void 0 ? [label, ...lines] : lines;
|
|
4050
|
+
}
|
|
4051
|
+
function renderInteractiveHelp() {
|
|
4052
|
+
return [
|
|
4053
|
+
"Commands:",
|
|
4054
|
+
"/help Show commands",
|
|
4055
|
+
"/trace Show recent trace events",
|
|
4056
|
+
"/config Show redacted configuration",
|
|
4057
|
+
"/skills List loaded skills",
|
|
4058
|
+
"/clear Clear conversation display",
|
|
4059
|
+
"/exit Leave chat"
|
|
4060
|
+
];
|
|
4061
|
+
}
|
|
4062
|
+
var CliChatSession = class _CliChatSession {
|
|
4063
|
+
#runtime;
|
|
4064
|
+
#traceStore;
|
|
4065
|
+
#sessionStore;
|
|
4066
|
+
#sessionId;
|
|
4067
|
+
#config;
|
|
4068
|
+
#approvalPromptLog;
|
|
4069
|
+
#skillDefinitions;
|
|
4070
|
+
#gateway;
|
|
4071
|
+
constructor(runtime, config = redactedConfig(loadConfig()), traceStore = new InMemoryRuntimeTraceStore(), sessionId = createSessionId(), sessionStore = new InMemorySessionStore({ createSessionId: () => sessionId }), _recentMessageLimit = 12, approvalPromptLog = [], skillDefinitions = [], gateway) {
|
|
4072
|
+
this.#runtime = runtime;
|
|
4073
|
+
this.#config = config;
|
|
4074
|
+
this.#traceStore = traceStore;
|
|
4075
|
+
this.#sessionStore = sessionStore;
|
|
4076
|
+
this.#sessionId = sessionId;
|
|
4077
|
+
this.#approvalPromptLog = approvalPromptLog;
|
|
4078
|
+
this.#skillDefinitions = skillDefinitions;
|
|
4079
|
+
this.#gateway = gateway;
|
|
4080
|
+
}
|
|
4081
|
+
get sessionId() {
|
|
4082
|
+
return this.#sessionId;
|
|
4083
|
+
}
|
|
4084
|
+
async listSessions(query) {
|
|
4085
|
+
return this.#sessionStore.listSessions(query);
|
|
4086
|
+
}
|
|
4087
|
+
async loadMessages() {
|
|
4088
|
+
return this.#sessionStore.listMessages(this.#sessionId);
|
|
4089
|
+
}
|
|
4090
|
+
close() {
|
|
4091
|
+
this.#gateway?.unregister(this.#sessionId);
|
|
4092
|
+
}
|
|
4093
|
+
static createFake(responseContent = "Fake response to: Hello trace", options = {}, sessionOptions = {}) {
|
|
4094
|
+
const config = redactedConfig(loadConfig(options.env ? { env: options.env } : {}));
|
|
4095
|
+
const approvalPromptLog = [];
|
|
4096
|
+
const provider = options.fakeModelOutputs ? new FakeModelProvider(options.fakeModelOutputs) : typeof responseContent === "function" ? new MessageMappedFakeModelProvider(responseContent) : new FakeModelProvider([
|
|
4097
|
+
{
|
|
4098
|
+
type: "message",
|
|
4099
|
+
content: responseContent
|
|
4100
|
+
}
|
|
4101
|
+
]);
|
|
4102
|
+
return new _CliChatSession(
|
|
4103
|
+
new AgentRuntime({
|
|
4104
|
+
contextAssembler: createCliContextAssembler(config, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)),
|
|
4105
|
+
modelProvider: provider,
|
|
4106
|
+
systemInstruction: AGENT_SYSTEM_INSTRUCTION,
|
|
4107
|
+
runtime: {
|
|
4108
|
+
mode: "confirm",
|
|
4109
|
+
workspace: config.workspace.root,
|
|
4110
|
+
currentDate: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
4111
|
+
},
|
|
4112
|
+
tools: createCliBuiltInTools(options, config),
|
|
4113
|
+
approvalResolver: createCliApprovalResolver(options, approvalPromptLog),
|
|
4114
|
+
maxSteps: 20,
|
|
4115
|
+
compaction: {}
|
|
4116
|
+
}),
|
|
4117
|
+
config,
|
|
4118
|
+
new InMemoryRuntimeTraceStore(),
|
|
4119
|
+
sessionOptions.sessionId ?? createSessionId(),
|
|
4120
|
+
void 0,
|
|
4121
|
+
12,
|
|
4122
|
+
approvalPromptLog
|
|
4123
|
+
);
|
|
4124
|
+
}
|
|
4125
|
+
static async createConfigured(config, options = {}, sessionOptions = {}) {
|
|
4126
|
+
if (config.secrets.apiKey === void 0) {
|
|
4127
|
+
throw new Error("Configured chat requires an API key.");
|
|
4128
|
+
}
|
|
4129
|
+
const sessionId = sessionOptions.sessionId ?? createSessionId();
|
|
4130
|
+
const currentDate = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4131
|
+
const approvalPromptLog = [];
|
|
4132
|
+
const skillDefinitions = await new SkillLoader().load({ workspaceRoot: config.workspace.root });
|
|
4133
|
+
const skillIndex = skillDefinitions.map(toSkillSummary);
|
|
4134
|
+
const skillFileMap = new Map(skillDefinitions.map((s) => [s.name, s.filePath]));
|
|
4135
|
+
const configuredProvider = createConfiguredProvider(config, options);
|
|
4136
|
+
const approvalResolver = sessionOptions.approvalResolver ?? createCliApprovalResolver(options, approvalPromptLog);
|
|
4137
|
+
const builtInTools = createCliBuiltInTools(options, config, skillFileMap);
|
|
4138
|
+
const factory = {
|
|
4139
|
+
create: (goal) => new AgentRuntime({
|
|
4140
|
+
contextAssembler: createCliContextAssembler(redactedConfig(config), currentDate),
|
|
4141
|
+
modelProvider: configuredProvider,
|
|
4142
|
+
systemInstruction: `You are Vole, a sub-agent handling: ${goal}`,
|
|
4143
|
+
runtime: { mode: config.runtime.defaultMode, workspace: config.workspace.root, currentDate },
|
|
4144
|
+
tools: createCliBuiltInTools(options, config),
|
|
4145
|
+
maxSteps: 8
|
|
4146
|
+
})
|
|
4147
|
+
};
|
|
4148
|
+
const taskflowPath = join7(dirname4(resolveSessionsDirectory(config, options.env)), "taskflow.jsonl");
|
|
4149
|
+
const taskFlowStore = new JsonlTaskFlowStore(taskflowPath);
|
|
4150
|
+
const allToolsRaw = [
|
|
4151
|
+
...builtInTools,
|
|
4152
|
+
createSpawnSubagentTool(factory),
|
|
4153
|
+
createSpawnSubagentAsyncTool(factory, { taskStore: taskFlowStore }),
|
|
4154
|
+
createCheckSubagentTool(taskFlowStore)
|
|
4155
|
+
];
|
|
4156
|
+
const allTools = config.runtime.toolProfile !== void 0 ? filterToolsByProfile(allToolsRaw, config.runtime.toolProfile) : allToolsRaw;
|
|
4157
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4158
|
+
const gatewaySession = {
|
|
4159
|
+
id: sessionId,
|
|
4160
|
+
adapterName: "cli",
|
|
4161
|
+
capabilities: CLI_CAPABILITIES,
|
|
4162
|
+
registeredAt: now,
|
|
4163
|
+
lastActivityAt: now
|
|
4164
|
+
};
|
|
4165
|
+
cliGateway.register(gatewaySession);
|
|
4166
|
+
return new _CliChatSession(
|
|
4167
|
+
new AgentRuntime({
|
|
4168
|
+
contextAssembler: createCliContextAssembler(config, currentDate),
|
|
4169
|
+
modelProvider: configuredProvider,
|
|
4170
|
+
systemInstruction: AGENT_SYSTEM_INSTRUCTION,
|
|
4171
|
+
runtime: {
|
|
4172
|
+
mode: config.runtime.defaultMode,
|
|
4173
|
+
workspace: config.workspace.root,
|
|
4174
|
+
currentDate
|
|
4175
|
+
},
|
|
4176
|
+
tools: allTools,
|
|
4177
|
+
skillIndex,
|
|
4178
|
+
preferStreaming: sessionOptions.preferStreaming ?? false,
|
|
4179
|
+
approvalResolver,
|
|
4180
|
+
maxSteps: 20,
|
|
4181
|
+
compaction: {},
|
|
4182
|
+
...config.runtime.promptMode !== void 0 ? { promptMode: config.runtime.promptMode } : {},
|
|
4183
|
+
...config.runtime.executionContract !== void 0 ? { executionContract: config.runtime.executionContract } : {}
|
|
4184
|
+
}),
|
|
4185
|
+
redactedConfig(config),
|
|
4186
|
+
new InMemoryRuntimeTraceStore(),
|
|
4187
|
+
sessionId,
|
|
4188
|
+
createConfiguredSessionStore(config, options, sessionId),
|
|
4189
|
+
12,
|
|
4190
|
+
approvalPromptLog,
|
|
4191
|
+
skillDefinitions,
|
|
4192
|
+
cliGateway
|
|
4193
|
+
);
|
|
4194
|
+
}
|
|
4195
|
+
async sendMessage(message, opts = {}) {
|
|
4196
|
+
const events = [];
|
|
4197
|
+
const approvalStartIndex = this.#approvalPromptLog.length;
|
|
4198
|
+
await this.#ensureSession();
|
|
4199
|
+
const recentMessages = (await this.#sessionStore.listMessages(this.#sessionId)).map(
|
|
4200
|
+
(sessionMessage) => ({
|
|
4201
|
+
role: sessionMessage.role,
|
|
4202
|
+
content: sessionMessage.content,
|
|
4203
|
+
...sessionMessage.toolCalls !== void 0 ? { toolCalls: sessionMessage.toolCalls } : {},
|
|
4204
|
+
...sessionMessage.toolCallId !== void 0 ? { toolCallId: sessionMessage.toolCallId } : {}
|
|
4205
|
+
})
|
|
4206
|
+
);
|
|
4207
|
+
for await (const event of this.#runtime.runTurn({ sessionId: this.#sessionId, recentMessages, message, ...opts.signal !== void 0 ? { signal: opts.signal } : {} })) {
|
|
4208
|
+
await this.#traceStore.append(event);
|
|
4209
|
+
await this.#sessionStore.appendTraceEvent({ sessionId: this.#sessionId, event });
|
|
4210
|
+
events.push(event);
|
|
4211
|
+
opts.onEvent?.(event);
|
|
4212
|
+
if (event.type === "compaction_triggered" && event.summary) {
|
|
4213
|
+
await this.#sessionStore.appendCompactBoundary({
|
|
4214
|
+
sessionId: this.#sessionId,
|
|
4215
|
+
summary: event.summary,
|
|
4216
|
+
messagesBefore: event.messagesBefore,
|
|
4217
|
+
messagesAfter: event.messagesAfter
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
if (event.type === "turn_complete") {
|
|
4221
|
+
for (const msg of event.messages) {
|
|
4222
|
+
await this.#sessionStore.appendMessage({
|
|
4223
|
+
sessionId: this.#sessionId,
|
|
4224
|
+
role: msg.role,
|
|
4225
|
+
content: msg.content ?? null,
|
|
4226
|
+
...msg.toolCalls !== void 0 ? { toolCalls: msg.toolCalls } : {},
|
|
4227
|
+
...msg.toolCallId !== void 0 ? { toolCallId: msg.toolCallId } : {}
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
const assistantMessage = events.find((event) => event.type === "assistant_message_created");
|
|
4233
|
+
const assistantText = assistantMessage?.type === "assistant_message_created" ? assistantMessage.message.content : "No assistant message was produced.";
|
|
4234
|
+
return {
|
|
4235
|
+
assistantText,
|
|
4236
|
+
approvalLines: this.#approvalPromptLog.slice(approvalStartIndex),
|
|
4237
|
+
todosLines: renderTodosProgress(events),
|
|
4238
|
+
events
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
async runSlashCommand(command) {
|
|
4242
|
+
if (command === "/trace") {
|
|
4243
|
+
const traceEvents = await this.#sessionStore.listTraceEvents(this.#sessionId);
|
|
4244
|
+
return renderCompactTrace(traceEvents.map((traceEvent) => traceEvent.event));
|
|
4245
|
+
}
|
|
4246
|
+
if (command === "/config") {
|
|
4247
|
+
return renderRedactedConfig(this.#config);
|
|
4248
|
+
}
|
|
4249
|
+
if (command === "/skills") {
|
|
4250
|
+
return renderSkillIndex(this.#skillDefinitions);
|
|
4251
|
+
}
|
|
4252
|
+
if (command === "/help") {
|
|
4253
|
+
return renderInteractiveHelp();
|
|
4254
|
+
}
|
|
4255
|
+
return [`Unknown slash command: ${command}`];
|
|
4256
|
+
}
|
|
4257
|
+
async #ensureSession() {
|
|
4258
|
+
if (await this.#sessionStore.getSession(this.#sessionId) !== void 0) {
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
await this.#sessionStore.createSession({ title: this.#sessionId });
|
|
4262
|
+
}
|
|
4263
|
+
};
|
|
4264
|
+
function createCliApprovalResolver(options, approvalPromptLog) {
|
|
4265
|
+
return {
|
|
4266
|
+
async resolve(request) {
|
|
4267
|
+
approvalPromptLog.push(
|
|
4268
|
+
"Approval required:",
|
|
4269
|
+
`Tool: ${request.call.name}`,
|
|
4270
|
+
`Risk: ${request.decision.risk}`,
|
|
4271
|
+
`Reason: ${request.decision.reason}`
|
|
4272
|
+
);
|
|
4273
|
+
const answer = (await options.readLine?.("Approve once? [y/N/details] "))?.trim().toLowerCase();
|
|
4274
|
+
if (answer === "y" || answer === "yes") {
|
|
4275
|
+
approvalPromptLog.push("Decision: approved once.");
|
|
4276
|
+
return {
|
|
4277
|
+
approved: true,
|
|
4278
|
+
reason: "Approved once from CLI prompt."
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
approvalPromptLog.push("Decision: denied");
|
|
4282
|
+
return {
|
|
4283
|
+
approved: false,
|
|
4284
|
+
reason: "Denied from CLI prompt."
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
4287
|
+
};
|
|
4288
|
+
}
|
|
4289
|
+
function createCliBuiltInTools(options, config, skillFileMap) {
|
|
4290
|
+
const tools = [
|
|
4291
|
+
createReadFileTool(),
|
|
4292
|
+
createListDirectoryTool(),
|
|
4293
|
+
createWriteFileTool(),
|
|
4294
|
+
createEditFileTool(),
|
|
4295
|
+
createAppendFileTool(),
|
|
4296
|
+
createShellTool(config?.runtime.sandboxed !== void 0 ? { sandboxed: config.runtime.sandboxed } : void 0),
|
|
4297
|
+
createReadWebPageTool(options.fetch),
|
|
4298
|
+
createSearchFilesTool()
|
|
4299
|
+
];
|
|
4300
|
+
if (config?.memory.longTermFiles === "write") {
|
|
4301
|
+
tools.push(createAppendDailyMemoryTool());
|
|
4302
|
+
}
|
|
4303
|
+
if (config?.memory.longTermFiles === "read-only" || config?.memory.longTermFiles === "write") {
|
|
4304
|
+
const workspaceRoot = config.workspace.root;
|
|
4305
|
+
tools.push(createMemorySearchTool(workspaceRoot));
|
|
4306
|
+
tools.push(createMemoryGetTool(workspaceRoot));
|
|
4307
|
+
}
|
|
4308
|
+
tools.push(createUpdateHeartbeatTool());
|
|
4309
|
+
if (skillFileMap !== void 0 && skillFileMap.size > 0) {
|
|
4310
|
+
tools.push(createLoadSkillTool(skillFileMap));
|
|
4311
|
+
}
|
|
4312
|
+
return tools;
|
|
4313
|
+
}
|
|
4314
|
+
var MessageMappedFakeModelProvider = class {
|
|
4315
|
+
requests = [];
|
|
4316
|
+
#mapMessage;
|
|
4317
|
+
constructor(mapMessage) {
|
|
4318
|
+
this.#mapMessage = mapMessage;
|
|
4319
|
+
}
|
|
4320
|
+
async generate(input) {
|
|
4321
|
+
this.requests.push(input);
|
|
4322
|
+
const lastUserMessage = [...input.messages].reverse().find((message) => message.role === "user")?.content ?? "";
|
|
4323
|
+
return {
|
|
4324
|
+
type: "message",
|
|
4325
|
+
content: this.#mapMessage(lastUserMessage)
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
};
|
|
4329
|
+
function createConfiguredProvider(config, options) {
|
|
4330
|
+
if (config.model.provider === "anthropic") {
|
|
4331
|
+
return new AnthropicProvider({
|
|
4332
|
+
...config.secrets.apiKey !== void 0 ? { apiKey: config.secrets.apiKey } : {},
|
|
4333
|
+
model: config.model.model,
|
|
4334
|
+
temperature: config.model.temperature,
|
|
4335
|
+
maxTokens: config.model.maxTokens,
|
|
4336
|
+
...config.model.thinkingBudget !== void 0 ? { thinkingBudget: config.model.thinkingBudget } : {}
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
return new OpenAICompatibleProvider({
|
|
4340
|
+
baseURL: config.model.baseURL,
|
|
4341
|
+
...config.secrets.apiKey !== void 0 ? { apiKey: config.secrets.apiKey } : {},
|
|
4342
|
+
model: config.model.model,
|
|
4343
|
+
temperature: config.model.temperature,
|
|
4344
|
+
maxTokens: config.model.maxTokens,
|
|
4345
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
4346
|
+
});
|
|
4347
|
+
}
|
|
4348
|
+
function createConfiguredSessionStore(config, options, sessionId) {
|
|
4349
|
+
const effectiveConfig = options.sessionsDirectory ? { ...config, sessions: { directory: options.sessionsDirectory } } : config;
|
|
4350
|
+
const directory = resolveSessionsDirectory(effectiveConfig, options.env);
|
|
4351
|
+
return new JsonlSessionStore({
|
|
4352
|
+
directory,
|
|
4353
|
+
createSessionId: () => sessionId
|
|
4354
|
+
});
|
|
4355
|
+
}
|
|
4356
|
+
function createCliContextAssembler(config, currentDate) {
|
|
4357
|
+
const workspacePromptFiles = [
|
|
4358
|
+
"AGENTS.md",
|
|
4359
|
+
"SOUL.md",
|
|
4360
|
+
"TOOLS.md",
|
|
4361
|
+
"IDENTITY.md",
|
|
4362
|
+
"HEARTBEAT.md",
|
|
4363
|
+
"BOOTSTRAP.md"
|
|
4364
|
+
];
|
|
4365
|
+
if (config.memory.longTermFiles === "read-only") {
|
|
4366
|
+
workspacePromptFiles.push("USER.md", "MEMORY.md", `memory/${currentDate}.md`, `memory/${previousIsoDate(currentDate)}.md`);
|
|
4367
|
+
}
|
|
4368
|
+
return new DefaultContextAssembler({
|
|
4369
|
+
workspacePromptFiles
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
function previousIsoDate(currentDate) {
|
|
4373
|
+
const date = /* @__PURE__ */ new Date(`${currentDate}T00:00:00.000Z`);
|
|
4374
|
+
date.setUTCDate(date.getUTCDate() - 1);
|
|
4375
|
+
return date.toISOString().slice(0, 10);
|
|
4376
|
+
}
|
|
4377
|
+
function createSessionId() {
|
|
4378
|
+
return `session_${crypto.randomUUID()}`;
|
|
4379
|
+
}
|
|
4380
|
+
function renderTodosProgress(events) {
|
|
4381
|
+
const todosEvent = [...events].reverse().find((e) => e.type === "todos_updated");
|
|
4382
|
+
if (todosEvent?.type !== "todos_updated" || todosEvent.todos.length === 0) return [];
|
|
4383
|
+
const lines = ["Todo:"];
|
|
4384
|
+
for (const todo of todosEvent.todos) {
|
|
4385
|
+
const icon = todo.status === "completed" ? "\u2713" : todo.status === "in_progress" ? "\u2192" : "\xB7";
|
|
4386
|
+
lines.push(` ${icon} ${todo.content}`);
|
|
4387
|
+
}
|
|
4388
|
+
return lines;
|
|
4389
|
+
}
|
|
4390
|
+
function renderSkillIndex(skills) {
|
|
4391
|
+
if (skills.length === 0) return ["No skills loaded."];
|
|
4392
|
+
const lines = [];
|
|
4393
|
+
const untrustedNames = [];
|
|
4394
|
+
for (const s of skills) {
|
|
4395
|
+
const trustBadge = s.source === "user" && s.trusted === false ? " \u26A0 untrusted" : "";
|
|
4396
|
+
const versionBadge = s.version !== void 0 ? ` v${s.version}` : "";
|
|
4397
|
+
const permsBadge = s.permissions !== void 0 && s.permissions.length > 0 ? ` [${s.permissions.join(", ")}]` : "";
|
|
4398
|
+
lines.push(`[${s.source}]${trustBadge}${versionBadge} ${s.name}: ${s.description}${permsBadge}`);
|
|
4399
|
+
if (s.source === "user" && s.trusted === false) {
|
|
4400
|
+
untrustedNames.push(s.name);
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
if (untrustedNames.length > 0) {
|
|
4404
|
+
lines.push("");
|
|
4405
|
+
for (const name of untrustedNames) {
|
|
4406
|
+
lines.push(`This skill was installed from an external source and has not been trusted. Run \`vole skills trust ${name}\` to trust it.`);
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
return lines;
|
|
4410
|
+
}
|
|
4411
|
+
function renderRedactedConfig(config) {
|
|
4412
|
+
return [
|
|
4413
|
+
`Provider: ${config.model.provider}`,
|
|
4414
|
+
`Model: ${config.model.model}`,
|
|
4415
|
+
`Base URL: ${config.model.baseURL}`,
|
|
4416
|
+
`Default mode: ${config.runtime.defaultMode}`,
|
|
4417
|
+
`Trace verbosity: ${config.trace.verbosity}`,
|
|
4418
|
+
`Long-term memory files: ${config.memory.longTermFiles}`,
|
|
4419
|
+
`Memory writes: ${config.memory.writes}`,
|
|
4420
|
+
`API key: ${config.secrets.apiKey}`
|
|
4421
|
+
];
|
|
4422
|
+
}
|
|
4423
|
+
function renderToolResult(result) {
|
|
4424
|
+
if ("entries" in result && Array.isArray(result.entries)) {
|
|
4425
|
+
return result.entries.map((e) => ` ${e.type === "directory" ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`).join("\n");
|
|
4426
|
+
}
|
|
4427
|
+
if ("content" in result && typeof result.content === "string") {
|
|
4428
|
+
const lines = result.content.split("\n");
|
|
4429
|
+
return lines.length > 30 ? lines.slice(0, 30).join("\n") + `
|
|
4430
|
+
\u2026 (${lines.length - 30} more lines)` : result.content;
|
|
4431
|
+
}
|
|
4432
|
+
if ("stdout" in result) {
|
|
4433
|
+
const out = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
4434
|
+
return out || "(no output)";
|
|
4435
|
+
}
|
|
4436
|
+
if ("error" in result) {
|
|
4437
|
+
const err = result.error;
|
|
4438
|
+
return `Error: ${typeof err === "object" && err !== null && "message" in err ? err.message : String(err)}`;
|
|
4439
|
+
}
|
|
4440
|
+
return JSON.stringify(result, null, 2);
|
|
4441
|
+
}
|
|
4442
|
+
function renderCompactTrace(events) {
|
|
4443
|
+
return events.map((event, index) => `${index + 1}. ${traceEventLabel(event)} (${event.type})`);
|
|
4444
|
+
}
|
|
4445
|
+
function traceEventLabel(event) {
|
|
4446
|
+
switch (event.type) {
|
|
4447
|
+
case "run_started":
|
|
4448
|
+
return "Received user message";
|
|
4449
|
+
case "context_assembled":
|
|
4450
|
+
return "Assembled context";
|
|
4451
|
+
case "compaction_triggered":
|
|
4452
|
+
return `Compacted context (${event.messagesBefore} \u2192 ${event.messagesAfter} messages)`;
|
|
4453
|
+
case "todos_updated":
|
|
4454
|
+
return `Updated todos (${event.todos.length} items)`;
|
|
4455
|
+
case "planning_stall_detected":
|
|
4456
|
+
return `Planning stall detected (${event.stallCount}/${event.maxRetries})`;
|
|
4457
|
+
case "model_request_started":
|
|
4458
|
+
return "Started model request";
|
|
4459
|
+
case "token_delta":
|
|
4460
|
+
return `Token delta: "${event.delta.slice(0, 20)}${event.delta.length > 20 ? "\u2026" : ""}"`;
|
|
4461
|
+
case "model_request_completed":
|
|
4462
|
+
return "Completed model request";
|
|
4463
|
+
case "tool_call_requested":
|
|
4464
|
+
return "Requested tool call";
|
|
4465
|
+
case "tool_call_permission_evaluated":
|
|
4466
|
+
return "Evaluated tool permission";
|
|
4467
|
+
case "approval_requested":
|
|
4468
|
+
return "Requested approval";
|
|
4469
|
+
case "approval_resolved":
|
|
4470
|
+
return "Resolved approval";
|
|
4471
|
+
case "tool_started":
|
|
4472
|
+
return `Tool: ${event.toolName}`;
|
|
4473
|
+
case "tool_completed":
|
|
4474
|
+
return `Result [${event.toolName}]:
|
|
4475
|
+
${renderToolResult(event.result)}`;
|
|
4476
|
+
case "tool_failed":
|
|
4477
|
+
return `Tool failed [${event.toolName}]: ${event.error.message}`;
|
|
4478
|
+
case "assistant_message_created":
|
|
4479
|
+
return "Created assistant message";
|
|
4480
|
+
case "turn_complete":
|
|
4481
|
+
return `Turn complete (${event.messages.length} messages)`;
|
|
4482
|
+
case "run_completed":
|
|
4483
|
+
return "Completed run";
|
|
4484
|
+
case "run_failed":
|
|
4485
|
+
return "Failed run";
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
async function runWebDashboard(port, openBrowser) {
|
|
4489
|
+
const selfDir = dirname4(fileURLToPath(import.meta.url));
|
|
4490
|
+
const candidates = [
|
|
4491
|
+
{ server: join7(selfDir, "web", "server.js"), cwd: join7(selfDir, "web") },
|
|
4492
|
+
{ server: join7(selfDir, "../../web", "dist", "server.js"), cwd: join7(selfDir, "../../web") }
|
|
4493
|
+
];
|
|
4494
|
+
let resolved;
|
|
4495
|
+
for (const c of candidates) {
|
|
4496
|
+
try {
|
|
4497
|
+
await stat(c.server);
|
|
4498
|
+
resolved = c;
|
|
4499
|
+
break;
|
|
4500
|
+
} catch {
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
if (resolved === void 0) {
|
|
4504
|
+
return {
|
|
4505
|
+
exitCode: 1,
|
|
4506
|
+
stdout: "",
|
|
4507
|
+
stderr: `Web app not built. Run first:
|
|
4508
|
+
pnpm --filter @vole/web build
|
|
4509
|
+
`
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
const url = `http://localhost:${port}`;
|
|
4513
|
+
const child = spawn("node", [resolved.server], {
|
|
4514
|
+
env: { ...process.env, PORT: String(port) },
|
|
4515
|
+
stdio: "inherit",
|
|
4516
|
+
cwd: resolved.cwd
|
|
4517
|
+
});
|
|
4518
|
+
process.stdout.write(`Vole web dashboard \u2192 ${url}
|
|
4519
|
+
`);
|
|
4520
|
+
process.stdout.write("Press Ctrl+C to stop.\n");
|
|
4521
|
+
if (openBrowser) {
|
|
4522
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
4523
|
+
const openerArgs = process.platform === "win32" ? ["/c", "start", url] : [url];
|
|
4524
|
+
setTimeout(() => {
|
|
4525
|
+
spawn(opener, openerArgs, { stdio: "ignore", detached: true }).unref();
|
|
4526
|
+
}, 800);
|
|
4527
|
+
}
|
|
4528
|
+
return new Promise((resolve2) => {
|
|
4529
|
+
const shutdown = () => {
|
|
4530
|
+
child.kill();
|
|
4531
|
+
resolve2({ exitCode: 0, stdout: "Web server stopped.\n", stderr: "" });
|
|
4532
|
+
};
|
|
4533
|
+
process.once("SIGTERM", shutdown);
|
|
4534
|
+
process.once("SIGINT", shutdown);
|
|
4535
|
+
child.once("exit", (code) => resolve2({ exitCode: code ?? 0, stdout: "", stderr: "" }));
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4538
|
+
async function main() {
|
|
4539
|
+
const args = process.argv.slice(2);
|
|
4540
|
+
const [command] = args;
|
|
4541
|
+
const effectiveCommand = args.find((a) => a !== "--");
|
|
4542
|
+
if (effectiveCommand === "chat" && !args.includes("--fake") && !args.includes("--fake-interactive")) {
|
|
4543
|
+
const { runInkChat } = await import("./app.js");
|
|
4544
|
+
await runInkChat({ args, env: process.env });
|
|
4545
|
+
return;
|
|
4546
|
+
}
|
|
4547
|
+
const terminal = createInterface({
|
|
4548
|
+
input: process.stdin,
|
|
4549
|
+
output: process.stdout
|
|
4550
|
+
});
|
|
4551
|
+
const lineIterator = terminal[Symbol.asyncIterator]();
|
|
4552
|
+
const result = await runCli(args, "0.0.0", {
|
|
4553
|
+
env: process.env,
|
|
4554
|
+
readLine: async (prompt) => {
|
|
4555
|
+
process.stdout.write(prompt);
|
|
4556
|
+
const line = await lineIterator.next();
|
|
4557
|
+
return line.done ? void 0 : line.value;
|
|
4558
|
+
},
|
|
4559
|
+
write: (text) => process.stdout.write(text)
|
|
4560
|
+
});
|
|
4561
|
+
terminal.close();
|
|
4562
|
+
process.stdout.write(result.stdout);
|
|
4563
|
+
process.stderr.write(result.stderr);
|
|
4564
|
+
process.exitCode = result.exitCode;
|
|
4565
|
+
}
|
|
4566
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
4567
|
+
void main();
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
export {
|
|
4571
|
+
loadConfig,
|
|
4572
|
+
cliPackageName,
|
|
4573
|
+
runCli,
|
|
4574
|
+
CliChatSession,
|
|
4575
|
+
renderTodosProgress,
|
|
4576
|
+
renderSkillIndex,
|
|
4577
|
+
renderRedactedConfig,
|
|
4578
|
+
renderToolResult,
|
|
4579
|
+
renderCompactTrace
|
|
4580
|
+
};
|