doer-agent 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +210 -6
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -16,6 +16,7 @@ const sessionRpcCodec = StringCodec();
|
|
|
16
16
|
const codexAuthRpcCodec = StringCodec();
|
|
17
17
|
const settingsRpcCodec = StringCodec();
|
|
18
18
|
const gitRpcCodec = StringCodec();
|
|
19
|
+
const skillRpcCodec = StringCodec();
|
|
19
20
|
const retainedRuns = new Map();
|
|
20
21
|
const activeSessionWatchers = new Map();
|
|
21
22
|
const sessionLineIndexCache = new Map();
|
|
@@ -40,6 +41,9 @@ function buildAgentSettingsRpcSubject(userId, agentId) {
|
|
|
40
41
|
function buildAgentGitRpcSubject(userId, agentId) {
|
|
41
42
|
return `doer.agent.git.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
42
43
|
}
|
|
44
|
+
function buildAgentSkillRpcSubject(userId, agentId) {
|
|
45
|
+
return `doer.agent.skill.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
46
|
+
}
|
|
43
47
|
function normalizeNatsServers(value) {
|
|
44
48
|
if (!Array.isArray(value)) {
|
|
45
49
|
return [];
|
|
@@ -286,6 +290,25 @@ function normalizeEnvPatch(value) {
|
|
|
286
290
|
}
|
|
287
291
|
return out;
|
|
288
292
|
}
|
|
293
|
+
function normalizeRunImagePaths(value) {
|
|
294
|
+
if (!Array.isArray(value)) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
const seen = new Set();
|
|
298
|
+
const out = [];
|
|
299
|
+
for (const item of value) {
|
|
300
|
+
if (typeof item !== "string") {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const normalized = item.trim();
|
|
304
|
+
if (!normalized || seen.has(normalized)) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
seen.add(normalized);
|
|
308
|
+
out.push(normalized);
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
289
312
|
async function prepareTaskRuntimeConfig(args) {
|
|
290
313
|
const bundle = await postJson(`${args.serverBaseUrl}/api/agent/tasks/${encodeURIComponent(args.taskId)}/runtime-config`, {
|
|
291
314
|
userId: args.userId,
|
|
@@ -408,6 +431,7 @@ function normalizeRunRpcRequest(args) {
|
|
|
408
431
|
}
|
|
409
432
|
const runId = typeof args.request.runId === "string" && args.request.runId.trim() ? args.request.runId.trim() : null;
|
|
410
433
|
const prompt = typeof args.request.prompt === "string" && args.request.prompt.trim() ? args.request.prompt.trim() : null;
|
|
434
|
+
const imagePaths = normalizeRunImagePaths(args.request.imagePaths);
|
|
411
435
|
const sessionId = typeof args.request.sessionId === "string" && args.request.sessionId.trim() ? args.request.sessionId.trim() : null;
|
|
412
436
|
const model = normalizeCodexModel(args.request.model);
|
|
413
437
|
if (action === "start" && !prompt) {
|
|
@@ -426,6 +450,7 @@ function normalizeRunRpcRequest(args) {
|
|
|
426
450
|
action,
|
|
427
451
|
runId,
|
|
428
452
|
prompt,
|
|
453
|
+
imagePaths,
|
|
429
454
|
sessionId,
|
|
430
455
|
model,
|
|
431
456
|
cwd,
|
|
@@ -1185,13 +1210,14 @@ function normalizeCodexModel(value) {
|
|
|
1185
1210
|
function buildManagedCodexArgs(args) {
|
|
1186
1211
|
const promptArgs = ["--", args.prompt];
|
|
1187
1212
|
const fixedArgs = ["--dangerously-bypass-approvals-and-sandbox"];
|
|
1213
|
+
const imageArgs = args.imagePaths.flatMap((imagePath) => ["--image", imagePath]);
|
|
1188
1214
|
return [
|
|
1189
1215
|
...fixedArgs,
|
|
1190
1216
|
"--model",
|
|
1191
1217
|
args.model,
|
|
1192
1218
|
...(args.sessionId
|
|
1193
|
-
? ["exec", "resume", "--json", args.sessionId, ...promptArgs]
|
|
1194
|
-
: ["exec", "--json", ...promptArgs]),
|
|
1219
|
+
? ["exec", "resume", "--json", ...imageArgs, args.sessionId, ...promptArgs]
|
|
1220
|
+
: ["exec", "--json", ...imageArgs, ...promptArgs]),
|
|
1195
1221
|
];
|
|
1196
1222
|
}
|
|
1197
1223
|
function buildLocalCodexCliCommand(args) {
|
|
@@ -1234,11 +1260,12 @@ function spawnManagedCodexCommand(args) {
|
|
|
1234
1260
|
child.stderr?.setEncoding("utf8");
|
|
1235
1261
|
return child;
|
|
1236
1262
|
}
|
|
1237
|
-
async function runLocalCodexCli(args, timeoutMs) {
|
|
1263
|
+
async function runLocalCodexCli(args, timeoutMs, envPatch) {
|
|
1238
1264
|
const command = buildLocalCodexCliCommand(args);
|
|
1239
1265
|
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1240
1266
|
const env = {
|
|
1241
1267
|
...process.env,
|
|
1268
|
+
...(envPatch ?? {}),
|
|
1242
1269
|
WORKSPACE: workspaceRoot,
|
|
1243
1270
|
CODEX_HOME: resolveCodexHomePath(),
|
|
1244
1271
|
};
|
|
@@ -1284,6 +1311,152 @@ async function runLocalCodexCli(args, timeoutMs) {
|
|
|
1284
1311
|
});
|
|
1285
1312
|
});
|
|
1286
1313
|
}
|
|
1314
|
+
function buildSkillGeneratorPrompt(userPrompt) {
|
|
1315
|
+
return [
|
|
1316
|
+
"Create a Codex skill from the user's description.",
|
|
1317
|
+
"",
|
|
1318
|
+
"Return JSON only with this shape:",
|
|
1319
|
+
'{ "skillName": "kebab-or-dot-or-underscore-name", "skillMd": "full SKILL.md content" }',
|
|
1320
|
+
"",
|
|
1321
|
+
"Requirements:",
|
|
1322
|
+
"- skillName must be lowercase ASCII and use only letters, numbers, dot, underscore, or dash.",
|
|
1323
|
+
"- skillName must not start with a dot and must not be .system.",
|
|
1324
|
+
"- skillMd must be a complete SKILL.md file.",
|
|
1325
|
+
"- skillMd must start with YAML frontmatter containing name and description.",
|
|
1326
|
+
"- Keep the skill concise and action-oriented.",
|
|
1327
|
+
"- Include when to use the skill, the core workflow, and any important constraints.",
|
|
1328
|
+
"- Do not add README, changelog, or any extra files.",
|
|
1329
|
+
"",
|
|
1330
|
+
"User request:",
|
|
1331
|
+
userPrompt.trim(),
|
|
1332
|
+
].join("\n");
|
|
1333
|
+
}
|
|
1334
|
+
function extractJsonObject(value) {
|
|
1335
|
+
const trimmed = value.trim();
|
|
1336
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
1337
|
+
return trimmed;
|
|
1338
|
+
}
|
|
1339
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1340
|
+
if (fenced?.[1]) {
|
|
1341
|
+
return fenced[1].trim();
|
|
1342
|
+
}
|
|
1343
|
+
const start = trimmed.indexOf("{");
|
|
1344
|
+
const end = trimmed.lastIndexOf("}");
|
|
1345
|
+
if (start >= 0 && end > start) {
|
|
1346
|
+
return trimmed.slice(start, end + 1);
|
|
1347
|
+
}
|
|
1348
|
+
throw new Error("Codex did not return JSON");
|
|
1349
|
+
}
|
|
1350
|
+
function slugifySkillName(value) {
|
|
1351
|
+
return value
|
|
1352
|
+
.trim()
|
|
1353
|
+
.toLowerCase()
|
|
1354
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1355
|
+
.replace(/^-+|-+$/g, "")
|
|
1356
|
+
.replace(/-{2,}/g, "-");
|
|
1357
|
+
}
|
|
1358
|
+
function normalizeGeneratedSkill(value) {
|
|
1359
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1360
|
+
throw new Error("Invalid generated skill payload");
|
|
1361
|
+
}
|
|
1362
|
+
const row = value;
|
|
1363
|
+
const skillName = slugifySkillName(typeof row.skillName === "string" ? row.skillName : "");
|
|
1364
|
+
const skillMd = typeof row.skillMd === "string" ? row.skillMd.trim() : "";
|
|
1365
|
+
if (!skillName || skillName.startsWith(".") || skillName === ".system") {
|
|
1366
|
+
throw new Error("Codex returned an invalid skill name");
|
|
1367
|
+
}
|
|
1368
|
+
if (!skillMd) {
|
|
1369
|
+
throw new Error("Codex returned an empty SKILL.md");
|
|
1370
|
+
}
|
|
1371
|
+
if (!/^---\s*\n[\s\S]*?\n---\s*\n/m.test(skillMd)) {
|
|
1372
|
+
throw new Error("Generated SKILL.md is missing YAML frontmatter");
|
|
1373
|
+
}
|
|
1374
|
+
if (!/\nname:\s*[^\n]+/i.test(skillMd) || !/\ndescription:\s*[^\n]+/i.test(skillMd)) {
|
|
1375
|
+
throw new Error("Generated SKILL.md frontmatter is incomplete");
|
|
1376
|
+
}
|
|
1377
|
+
return { skillName, skillMd };
|
|
1378
|
+
}
|
|
1379
|
+
function buildSkillGeneratorCodexArgs(prompt, model) {
|
|
1380
|
+
return ["--dangerously-bypass-approvals-and-sandbox", "--model", model, "exec", "--", prompt];
|
|
1381
|
+
}
|
|
1382
|
+
async function generateSkillViaCodex(userPrompt) {
|
|
1383
|
+
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
1384
|
+
const envPatch = buildAgentSettingsEnvPatch(localAgentSettings);
|
|
1385
|
+
const prompt = buildSkillGeneratorPrompt(userPrompt);
|
|
1386
|
+
const result = await runLocalCodexCli(buildSkillGeneratorCodexArgs(prompt, localAgentSettings.codex.model || "gpt-5.4"), 120_000, envPatch);
|
|
1387
|
+
if (result.timedOut) {
|
|
1388
|
+
throw new Error("Codex timed out while generating the skill");
|
|
1389
|
+
}
|
|
1390
|
+
if ((result.code ?? 1) !== 0) {
|
|
1391
|
+
const details = stripAnsi(result.stderr || result.stdout).trim();
|
|
1392
|
+
throw new Error(details || `Codex exited with code ${result.code ?? "null"}`);
|
|
1393
|
+
}
|
|
1394
|
+
const payload = JSON.parse(extractJsonObject(stripAnsi(result.stdout)));
|
|
1395
|
+
const generated = normalizeGeneratedSkill(payload);
|
|
1396
|
+
const skillPath = path.join(resolveCodexHomePath(), "skills", generated.skillName);
|
|
1397
|
+
const skillFilePath = path.join(skillPath, "SKILL.md");
|
|
1398
|
+
try {
|
|
1399
|
+
await stat(skillPath);
|
|
1400
|
+
throw new Error("A skill with that name already exists");
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
if (!(error instanceof Error) || !/ENOENT/i.test(error.message)) {
|
|
1404
|
+
throw error;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
await mkdir(skillPath, { recursive: true });
|
|
1408
|
+
await writeFile(skillFilePath, `${generated.skillMd}\n`, "utf8");
|
|
1409
|
+
return {
|
|
1410
|
+
skillName: generated.skillName,
|
|
1411
|
+
skillPath: `.codex/skills/${generated.skillName}`,
|
|
1412
|
+
skillFilePath: `.codex/skills/${generated.skillName}/SKILL.md`,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
async function handleSkillRpcMessage(args) {
|
|
1416
|
+
let payload = {};
|
|
1417
|
+
try {
|
|
1418
|
+
payload = JSON.parse(skillRpcCodec.decode(args.msg.data));
|
|
1419
|
+
if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
|
|
1420
|
+
throw new Error("agent id mismatch");
|
|
1421
|
+
}
|
|
1422
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt.trim() : "";
|
|
1423
|
+
if (!prompt) {
|
|
1424
|
+
throw new Error("prompt is required");
|
|
1425
|
+
}
|
|
1426
|
+
const result = await generateSkillViaCodex(prompt);
|
|
1427
|
+
args.msg.respond(skillRpcCodec.encode(JSON.stringify({
|
|
1428
|
+
ok: true,
|
|
1429
|
+
skillName: result.skillName,
|
|
1430
|
+
skillPath: result.skillPath,
|
|
1431
|
+
skillFilePath: result.skillFilePath,
|
|
1432
|
+
})));
|
|
1433
|
+
}
|
|
1434
|
+
catch (error) {
|
|
1435
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
1436
|
+
args.msg.respond(skillRpcCodec.encode(JSON.stringify({
|
|
1437
|
+
ok: false,
|
|
1438
|
+
error: message,
|
|
1439
|
+
})));
|
|
1440
|
+
writeAgentError(`skill rpc failed error=${message}`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
function subscribeToSkillRpc(args) {
|
|
1444
|
+
const subject = buildAgentSkillRpcSubject(args.userId, args.agentId);
|
|
1445
|
+
args.jetstream.nc.subscribe(subject, {
|
|
1446
|
+
callback: (error, msg) => {
|
|
1447
|
+
if (error) {
|
|
1448
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1449
|
+
writeAgentError(`skill rpc subscription error: ${message}`);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
void handleSkillRpcMessage({
|
|
1453
|
+
msg,
|
|
1454
|
+
agentId: args.agentId,
|
|
1455
|
+
});
|
|
1456
|
+
},
|
|
1457
|
+
});
|
|
1458
|
+
writeAgentInfo(`skill rpc subscribed subject=${subject}`);
|
|
1459
|
+
}
|
|
1287
1460
|
function parseCodexDeviceAuthOutput(raw) {
|
|
1288
1461
|
const text = stripAnsi(raw);
|
|
1289
1462
|
const urlMatch = text.match(/https?:\/\/[^\s]+/i);
|
|
@@ -1962,6 +2135,7 @@ async function handleRunRpcMessage(args) {
|
|
|
1962
2135
|
sessionId: request.sessionId,
|
|
1963
2136
|
codexArgs: buildManagedCodexArgs({
|
|
1964
2137
|
prompt: request.prompt ?? "",
|
|
2138
|
+
imagePaths: request.imagePaths,
|
|
1965
2139
|
sessionId: request.sessionId,
|
|
1966
2140
|
model: request.model,
|
|
1967
2141
|
}),
|
|
@@ -2252,7 +2426,8 @@ function parseFsRpcAction(value) {
|
|
|
2252
2426
|
value === "read_text" ||
|
|
2253
2427
|
value === "read_file" ||
|
|
2254
2428
|
value === "write_file" ||
|
|
2255
|
-
value === "download_file"
|
|
2429
|
+
value === "download_file" ||
|
|
2430
|
+
value === "delete_path") {
|
|
2256
2431
|
return value;
|
|
2257
2432
|
}
|
|
2258
2433
|
throw new Error("unsupported action");
|
|
@@ -2412,6 +2587,15 @@ async function executeFsRpc(args) {
|
|
|
2412
2587
|
mtimeMs: entry.mtimeMs,
|
|
2413
2588
|
};
|
|
2414
2589
|
}
|
|
2590
|
+
if (action === "delete_path") {
|
|
2591
|
+
await rm(abs, { recursive: true, force: true });
|
|
2592
|
+
return {
|
|
2593
|
+
ok: true,
|
|
2594
|
+
action,
|
|
2595
|
+
path: formatPath(abs),
|
|
2596
|
+
absolutePath: abs.split(path.sep).join("/"),
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2415
2599
|
if (action === "download_file") {
|
|
2416
2600
|
const downloadPath = typeof args.request.downloadPath === "string" ? args.request.downloadPath.trim() : "";
|
|
2417
2601
|
if (!downloadPath) {
|
|
@@ -2611,9 +2795,19 @@ function sanitizeSessionRpcRawLine(line) {
|
|
|
2611
2795
|
}
|
|
2612
2796
|
try {
|
|
2613
2797
|
const parsed = JSON.parse(line);
|
|
2614
|
-
if (!isObjectRecord(parsed)
|
|
2798
|
+
if (!isObjectRecord(parsed)) {
|
|
2615
2799
|
return line;
|
|
2616
2800
|
}
|
|
2801
|
+
if (parsed.type === "compacted" || parsed.type === "turn_context" || parsed.type === "session_meta") {
|
|
2802
|
+
return null;
|
|
2803
|
+
}
|
|
2804
|
+
if (!isObjectRecord(parsed.payload) || parsed.type !== "response_item") {
|
|
2805
|
+
return line;
|
|
2806
|
+
}
|
|
2807
|
+
const payloadType = typeof parsed.payload.type === "string" ? parsed.payload.type : "";
|
|
2808
|
+
if (payloadType === "message" || payloadType === "reasoning") {
|
|
2809
|
+
return null;
|
|
2810
|
+
}
|
|
2617
2811
|
return JSON.stringify({
|
|
2618
2812
|
...parsed,
|
|
2619
2813
|
payload: sanitizeSessionRpcPayload(parsed.payload),
|
|
@@ -3007,9 +3201,14 @@ async function getAgentSessionRawRows(args) {
|
|
|
3007
3201
|
let lineNumber = startLineIndex + 1;
|
|
3008
3202
|
for (const line of lines) {
|
|
3009
3203
|
if (line.trim()) {
|
|
3204
|
+
const sanitized = sanitizeSessionRpcRawLine(line);
|
|
3205
|
+
if (!sanitized) {
|
|
3206
|
+
lineNumber += 1;
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3010
3209
|
rawRows.push({
|
|
3011
3210
|
id: lineNumber,
|
|
3012
|
-
raw:
|
|
3211
|
+
raw: sanitized,
|
|
3013
3212
|
});
|
|
3014
3213
|
}
|
|
3015
3214
|
lineNumber += 1;
|
|
@@ -3841,6 +4040,11 @@ async function main() {
|
|
|
3841
4040
|
userId,
|
|
3842
4041
|
agentId: initialAgentId,
|
|
3843
4042
|
});
|
|
4043
|
+
subscribeToSkillRpc({
|
|
4044
|
+
jetstream,
|
|
4045
|
+
userId,
|
|
4046
|
+
agentId: initialAgentId,
|
|
4047
|
+
});
|
|
3844
4048
|
subscribeToRunRpc({
|
|
3845
4049
|
jetstream,
|
|
3846
4050
|
serverBaseUrl,
|