doer-agent 0.4.2 → 0.4.4
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-codex-auth-rpc.js +322 -0
- package/dist/agent-codex-cli.js +210 -0
- package/dist/agent-fs-rpc.js +405 -0
- package/dist/agent-git-rpc.js +299 -0
- package/dist/agent-jetstream.js +120 -0
- package/dist/agent-run-execution.js +39 -0
- package/dist/agent-run-lifecycle.js +67 -0
- package/dist/agent-run-rpc.js +93 -0
- package/dist/agent-run-state.js +229 -0
- package/dist/agent-runtime-env.js +147 -0
- package/dist/agent-runtime-io.js +112 -0
- package/dist/agent-runtime-utils.js +253 -0
- package/dist/agent-session-loop.js +53 -0
- package/dist/agent-session-rpc.js +867 -0
- package/dist/agent-settings-rpc.js +75 -0
- package/dist/agent-settings.js +397 -0
- package/dist/agent-skill-rpc.js +164 -0
- package/dist/agent-task-execution.js +275 -0
- package/dist/agent.js +376 -4275
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { StringCodec } from "nats";
|
|
3
|
+
const gitRpcCodec = StringCodec();
|
|
4
|
+
function runLocalCommand(command, args, cwd) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const child = spawn(command, args, {
|
|
7
|
+
cwd,
|
|
8
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9
|
+
});
|
|
10
|
+
let stdout = "";
|
|
11
|
+
let stderr = "";
|
|
12
|
+
child.stdout.setEncoding("utf8");
|
|
13
|
+
child.stderr.setEncoding("utf8");
|
|
14
|
+
child.stdout.on("data", (chunk) => {
|
|
15
|
+
stdout += chunk;
|
|
16
|
+
});
|
|
17
|
+
child.stderr.on("data", (chunk) => {
|
|
18
|
+
stderr += chunk;
|
|
19
|
+
});
|
|
20
|
+
child.once("error", reject);
|
|
21
|
+
child.once("close", (code) => {
|
|
22
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function sanitizeGitRef(value) {
|
|
27
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
if (trimmed.startsWith("-") || /\s/.test(trimmed) || trimmed.includes("..") || trimmed.includes(":")) {
|
|
32
|
+
throw new Error(`Invalid git ref: ${trimmed}`);
|
|
33
|
+
}
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
function sanitizeGitPathspec(value) {
|
|
37
|
+
if (typeof value !== "string") {
|
|
38
|
+
throw new Error("Invalid pathspec");
|
|
39
|
+
}
|
|
40
|
+
const trimmed = value.trim().replace(/\\/g, "/");
|
|
41
|
+
if (!trimmed || trimmed.startsWith("-") || trimmed.includes("\0")) {
|
|
42
|
+
throw new Error(`Invalid pathspec: ${trimmed}`);
|
|
43
|
+
}
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
function normalizeGitRpcRequest(args) {
|
|
47
|
+
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
48
|
+
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
49
|
+
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
50
|
+
const targetPath = typeof args.request.targetPath === "string" ? args.request.targetPath.trim() : "";
|
|
51
|
+
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId || !targetPath) {
|
|
52
|
+
throw new Error("invalid git rpc request");
|
|
53
|
+
}
|
|
54
|
+
const format = args.request.format === "name-only" ||
|
|
55
|
+
args.request.format === "name-status" ||
|
|
56
|
+
args.request.format === "stat" ||
|
|
57
|
+
args.request.format === "numstat" ||
|
|
58
|
+
args.request.format === "raw"
|
|
59
|
+
? args.request.format
|
|
60
|
+
: "patch";
|
|
61
|
+
const ignoreWhitespace = args.request.ignoreWhitespace === "at-eol" ||
|
|
62
|
+
args.request.ignoreWhitespace === "change" ||
|
|
63
|
+
args.request.ignoreWhitespace === "all"
|
|
64
|
+
? args.request.ignoreWhitespace
|
|
65
|
+
: "none";
|
|
66
|
+
const diffAlgorithm = args.request.diffAlgorithm === "minimal" ||
|
|
67
|
+
args.request.diffAlgorithm === "patience" ||
|
|
68
|
+
args.request.diffAlgorithm === "histogram"
|
|
69
|
+
? args.request.diffAlgorithm
|
|
70
|
+
: "default";
|
|
71
|
+
const contextRaw = Number(args.request.contextLines);
|
|
72
|
+
const contextLines = Number.isFinite(contextRaw) ? Math.max(0, Math.min(200, Math.trunc(contextRaw))) : null;
|
|
73
|
+
const pathspecs = Array.isArray(args.request.pathspecs) ? args.request.pathspecs.map((item) => sanitizeGitPathspec(item)) : [];
|
|
74
|
+
return {
|
|
75
|
+
requestId,
|
|
76
|
+
responseSubject,
|
|
77
|
+
targetPath,
|
|
78
|
+
base: sanitizeGitRef(args.request.base),
|
|
79
|
+
target: sanitizeGitRef(args.request.target),
|
|
80
|
+
mergeBase: args.request.mergeBase === true,
|
|
81
|
+
staged: args.request.staged === true,
|
|
82
|
+
format,
|
|
83
|
+
contextLines,
|
|
84
|
+
ignoreWhitespace,
|
|
85
|
+
diffAlgorithm,
|
|
86
|
+
findRenames: args.request.findRenames === true,
|
|
87
|
+
pathspecs,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function buildAgentGitDiffArgs(repoRootAbs, request) {
|
|
91
|
+
const args = ["-C", repoRootAbs, "diff", "--no-color"];
|
|
92
|
+
const displayParts = ["git", "diff", "--no-color"];
|
|
93
|
+
if (request.staged) {
|
|
94
|
+
args.push("--cached");
|
|
95
|
+
displayParts.push("--cached");
|
|
96
|
+
}
|
|
97
|
+
if (typeof request.contextLines === "number") {
|
|
98
|
+
args.push(`-U${request.contextLines}`);
|
|
99
|
+
displayParts.push(`-U${request.contextLines}`);
|
|
100
|
+
}
|
|
101
|
+
if (request.ignoreWhitespace === "at-eol") {
|
|
102
|
+
args.push("--ignore-space-at-eol");
|
|
103
|
+
displayParts.push("--ignore-space-at-eol");
|
|
104
|
+
}
|
|
105
|
+
else if (request.ignoreWhitespace === "change") {
|
|
106
|
+
args.push("--ignore-space-change");
|
|
107
|
+
displayParts.push("--ignore-space-change");
|
|
108
|
+
}
|
|
109
|
+
else if (request.ignoreWhitespace === "all") {
|
|
110
|
+
args.push("--ignore-all-space");
|
|
111
|
+
displayParts.push("--ignore-all-space");
|
|
112
|
+
}
|
|
113
|
+
if (request.diffAlgorithm !== "default") {
|
|
114
|
+
args.push(`--diff-algorithm=${request.diffAlgorithm}`);
|
|
115
|
+
displayParts.push(`--diff-algorithm=${request.diffAlgorithm}`);
|
|
116
|
+
}
|
|
117
|
+
if (request.findRenames) {
|
|
118
|
+
args.push("--find-renames");
|
|
119
|
+
displayParts.push("--find-renames");
|
|
120
|
+
}
|
|
121
|
+
if (request.format === "name-only") {
|
|
122
|
+
args.push("--name-only");
|
|
123
|
+
displayParts.push("--name-only");
|
|
124
|
+
}
|
|
125
|
+
else if (request.format === "name-status") {
|
|
126
|
+
args.push("--name-status");
|
|
127
|
+
displayParts.push("--name-status");
|
|
128
|
+
}
|
|
129
|
+
else if (request.format === "stat") {
|
|
130
|
+
args.push("--stat");
|
|
131
|
+
displayParts.push("--stat");
|
|
132
|
+
}
|
|
133
|
+
else if (request.format === "numstat") {
|
|
134
|
+
args.push("--numstat");
|
|
135
|
+
displayParts.push("--numstat");
|
|
136
|
+
}
|
|
137
|
+
else if (request.format === "raw") {
|
|
138
|
+
args.push("--raw");
|
|
139
|
+
displayParts.push("--raw");
|
|
140
|
+
}
|
|
141
|
+
if (request.mergeBase) {
|
|
142
|
+
if (!request.base || !request.target) {
|
|
143
|
+
throw new Error("mergeBase mode requires both base and target");
|
|
144
|
+
}
|
|
145
|
+
const merged = `${request.base}...${request.target}`;
|
|
146
|
+
args.push(merged);
|
|
147
|
+
displayParts.push(merged);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (request.base) {
|
|
151
|
+
args.push(request.base);
|
|
152
|
+
displayParts.push(request.base);
|
|
153
|
+
}
|
|
154
|
+
if (request.target) {
|
|
155
|
+
args.push(request.target);
|
|
156
|
+
displayParts.push(request.target);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (request.pathspecs.length > 0) {
|
|
160
|
+
args.push("--", ...request.pathspecs);
|
|
161
|
+
displayParts.push("--", ...request.pathspecs);
|
|
162
|
+
}
|
|
163
|
+
return { args, display: displayParts.join(" ") };
|
|
164
|
+
}
|
|
165
|
+
function buildUntrackedText(format, untrackedPaths) {
|
|
166
|
+
if (untrackedPaths.length === 0) {
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
if (format === "name-status" || format === "raw") {
|
|
170
|
+
return `${untrackedPaths.map((item) => `??\t${item}`).join("\n")}\n`;
|
|
171
|
+
}
|
|
172
|
+
if (format === "name-only") {
|
|
173
|
+
return `${untrackedPaths.join("\n")}\n`;
|
|
174
|
+
}
|
|
175
|
+
return `\n# Untracked files\n${untrackedPaths.join("\n")}\n`;
|
|
176
|
+
}
|
|
177
|
+
async function appendAgentLocalUntrackedDiff(repoRootAbs, request, baseOutput) {
|
|
178
|
+
const listArgs = ["-C", repoRootAbs, "ls-files", "--others", "--exclude-standard"];
|
|
179
|
+
if (request.pathspecs.length > 0) {
|
|
180
|
+
listArgs.push("--", ...request.pathspecs);
|
|
181
|
+
}
|
|
182
|
+
const listResult = await runLocalCommand("git", listArgs, repoRootAbs);
|
|
183
|
+
if (listResult.code !== 0) {
|
|
184
|
+
return { output: baseOutput, hasUntracked: false };
|
|
185
|
+
}
|
|
186
|
+
const untrackedPaths = listResult.stdout.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
187
|
+
if (untrackedPaths.length === 0) {
|
|
188
|
+
return { output: baseOutput, hasUntracked: false };
|
|
189
|
+
}
|
|
190
|
+
if (request.format !== "patch") {
|
|
191
|
+
return { output: `${baseOutput}${buildUntrackedText(request.format, untrackedPaths)}`, hasUntracked: true };
|
|
192
|
+
}
|
|
193
|
+
let output = baseOutput;
|
|
194
|
+
for (const relPath of untrackedPaths) {
|
|
195
|
+
const diffResult = await runLocalCommand("git", ["-C", repoRootAbs, "diff", "--no-color", "--no-index", "--", "/dev/null", relPath], repoRootAbs);
|
|
196
|
+
if (diffResult.code !== 0 && diffResult.code !== 1) {
|
|
197
|
+
throw new Error(diffResult.stderr.trim() || `Failed to render agent untracked diff: ${relPath}`);
|
|
198
|
+
}
|
|
199
|
+
if (diffResult.stdout) {
|
|
200
|
+
output += diffResult.stdout;
|
|
201
|
+
if (!output.endsWith("\n")) {
|
|
202
|
+
output += "\n";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { output, hasUntracked: true };
|
|
207
|
+
}
|
|
208
|
+
function publishGitRpcResponse(args) {
|
|
209
|
+
args.nc.publish(args.responseSubject, gitRpcCodec.encode(JSON.stringify(args.payload)));
|
|
210
|
+
}
|
|
211
|
+
export async function handleGitRpcMessage(args) {
|
|
212
|
+
let requestId = "unknown";
|
|
213
|
+
let responseSubject = "";
|
|
214
|
+
try {
|
|
215
|
+
const payload = JSON.parse(gitRpcCodec.decode(args.msg.data));
|
|
216
|
+
const request = normalizeGitRpcRequest({ request: payload, agentId: args.agentId });
|
|
217
|
+
requestId = request.requestId;
|
|
218
|
+
responseSubject = request.responseSubject;
|
|
219
|
+
if (!request.targetPath.startsWith("/")) {
|
|
220
|
+
throw new Error("agent source requires an absolute directory path");
|
|
221
|
+
}
|
|
222
|
+
const topLevelResult = await runLocalCommand("git", ["-C", request.targetPath, "rev-parse", "--show-toplevel"], request.targetPath);
|
|
223
|
+
if (topLevelResult.code !== 0) {
|
|
224
|
+
publishGitRpcResponse({
|
|
225
|
+
nc: args.nc,
|
|
226
|
+
responseSubject,
|
|
227
|
+
payload: {
|
|
228
|
+
requestId,
|
|
229
|
+
ok: true,
|
|
230
|
+
payload: {
|
|
231
|
+
isGitRepo: false,
|
|
232
|
+
mode: "git_diff",
|
|
233
|
+
source: "agent",
|
|
234
|
+
agent: { id: args.agentId, name: null },
|
|
235
|
+
currentPath: request.targetPath,
|
|
236
|
+
repoRoot: null,
|
|
237
|
+
repoRelativePath: null,
|
|
238
|
+
branch: null,
|
|
239
|
+
gitDiff: {
|
|
240
|
+
command: "git diff --no-color",
|
|
241
|
+
format: "patch",
|
|
242
|
+
output: "",
|
|
243
|
+
outputTruncated: false,
|
|
244
|
+
},
|
|
245
|
+
message: "현재 경로가 Git 저장소가 아닙니다.",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const repoRootAbs = topLevelResult.stdout.trim();
|
|
252
|
+
const prefixResult = await runLocalCommand("git", ["-C", request.targetPath, "rev-parse", "--show-prefix"], request.targetPath);
|
|
253
|
+
const repoRelativePath = prefixResult.code === 0 ? (prefixResult.stdout.trim().replace(/\/$/, "") || ".") : ".";
|
|
254
|
+
const branchResult = await runLocalCommand("git", ["-C", repoRootAbs, "symbolic-ref", "--quiet", "--short", "HEAD"], repoRootAbs);
|
|
255
|
+
const detachedResult = branchResult.code === 0 ? null : await runLocalCommand("git", ["-C", repoRootAbs, "rev-parse", "--short", "HEAD"], repoRootAbs);
|
|
256
|
+
const branch = branchResult.code === 0 ? branchResult.stdout.trim() || null : detachedResult && detachedResult.code === 0 ? detachedResult.stdout.trim() || null : null;
|
|
257
|
+
const gitDiffArgs = buildAgentGitDiffArgs(repoRootAbs, request);
|
|
258
|
+
const gitDiffResult = await runLocalCommand("git", gitDiffArgs.args, repoRootAbs);
|
|
259
|
+
if (gitDiffResult.code !== 0) {
|
|
260
|
+
throw new Error(gitDiffResult.stderr.trim() || "Failed to run agent git diff");
|
|
261
|
+
}
|
|
262
|
+
const withUntracked = await appendAgentLocalUntrackedDiff(repoRootAbs, request, gitDiffResult.stdout);
|
|
263
|
+
publishGitRpcResponse({
|
|
264
|
+
nc: args.nc,
|
|
265
|
+
responseSubject,
|
|
266
|
+
payload: {
|
|
267
|
+
requestId,
|
|
268
|
+
ok: true,
|
|
269
|
+
payload: {
|
|
270
|
+
isGitRepo: true,
|
|
271
|
+
mode: "git_diff",
|
|
272
|
+
source: "agent",
|
|
273
|
+
agent: { id: args.agentId, name: null },
|
|
274
|
+
currentPath: request.targetPath,
|
|
275
|
+
repoRoot: repoRootAbs,
|
|
276
|
+
repoRelativePath,
|
|
277
|
+
branch,
|
|
278
|
+
gitDiff: {
|
|
279
|
+
command: withUntracked.hasUntracked ? `${gitDiffArgs.display} (+ untracked)` : gitDiffArgs.display,
|
|
280
|
+
format: request.format,
|
|
281
|
+
output: withUntracked.output,
|
|
282
|
+
outputTruncated: false,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
290
|
+
if (responseSubject) {
|
|
291
|
+
publishGitRpcResponse({
|
|
292
|
+
nc: args.nc,
|
|
293
|
+
responseSubject,
|
|
294
|
+
payload: { requestId, ok: false, error: message },
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
args.onError(`git rpc failed requestId=${requestId} error=${message}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { AckPolicy, connect, DeliverPolicy, JSONCodec, RetentionPolicy, StorageType, } from "nats";
|
|
2
|
+
export function normalizeNatsServers(value) {
|
|
3
|
+
if (!Array.isArray(value)) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
return value.filter((item) => typeof item === "string").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
7
|
+
}
|
|
8
|
+
export function normalizeNatsToken(value) {
|
|
9
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const auth = value;
|
|
13
|
+
const token = typeof auth.token === "string" ? auth.token.trim() : "";
|
|
14
|
+
return token.length > 0 ? token : null;
|
|
15
|
+
}
|
|
16
|
+
function formatNatsStatusData(value) {
|
|
17
|
+
if (value === null || value === undefined) {
|
|
18
|
+
return "null";
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return String(value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function ensureJetStreamInfra(args) {
|
|
31
|
+
const streamInfo = await args.jsm.streams.info(args.stream).catch(() => null);
|
|
32
|
+
if (!streamInfo) {
|
|
33
|
+
await args.jsm.streams.add({
|
|
34
|
+
name: args.stream,
|
|
35
|
+
subjects: [args.subject],
|
|
36
|
+
storage: StorageType.File,
|
|
37
|
+
retention: RetentionPolicy.Limits,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (args.durable) {
|
|
41
|
+
const consumerInfo = await args.jsm.consumers.info(args.stream, args.durable).catch(() => null);
|
|
42
|
+
if (!consumerInfo) {
|
|
43
|
+
await args.jsm.consumers.add(args.stream, {
|
|
44
|
+
durable_name: args.durable,
|
|
45
|
+
ack_policy: AckPolicy.Explicit,
|
|
46
|
+
deliver_policy: DeliverPolicy.All,
|
|
47
|
+
filter_subject: args.subject,
|
|
48
|
+
ack_wait: 30_000_000_000,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function initJetStreamContext(args) {
|
|
54
|
+
const sanitized = args.sanitizeUserId(args.userId);
|
|
55
|
+
const stream = `DOER_AGENT_EVENTS_${sanitized}`;
|
|
56
|
+
const subject = `doer.agent.events.${sanitized}`;
|
|
57
|
+
const durable = `doer-agent-uploader-${sanitized}`;
|
|
58
|
+
const nc = await connect(args.token ? { servers: args.servers, token: args.token } : { servers: args.servers });
|
|
59
|
+
const jsm = await nc.jetstreamManager();
|
|
60
|
+
await ensureJetStreamInfra({ jsm, stream, subject, durable });
|
|
61
|
+
void (async () => {
|
|
62
|
+
try {
|
|
63
|
+
for await (const status of nc.status()) {
|
|
64
|
+
const statusType = typeof status.type === "string" ? status.type : "unknown";
|
|
65
|
+
if (statusType === "pingTimer") {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const statusData = formatNatsStatusData(status.data);
|
|
69
|
+
args.onInfraError("nats status type=" + statusType + " data=" + statusData);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
args.onInfraError(`nats status loop ended: ${message}`);
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
return {
|
|
78
|
+
nc,
|
|
79
|
+
js: nc.jetstream(),
|
|
80
|
+
jsm,
|
|
81
|
+
codec: JSONCodec(),
|
|
82
|
+
subject,
|
|
83
|
+
stream,
|
|
84
|
+
durable,
|
|
85
|
+
servers: args.servers,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function connectBootstrapWithRetry(args) {
|
|
89
|
+
let attempt = 0;
|
|
90
|
+
while (true) {
|
|
91
|
+
attempt += 1;
|
|
92
|
+
try {
|
|
93
|
+
const natsBootstrap = await args.postJson(`${args.serverBaseUrl}/api/agent/nats`, {
|
|
94
|
+
userId: args.userId,
|
|
95
|
+
agentToken: args.agentToken,
|
|
96
|
+
});
|
|
97
|
+
const bootstrapRecord = natsBootstrap;
|
|
98
|
+
const natsServers = normalizeNatsServers(bootstrapRecord.servers);
|
|
99
|
+
if (natsServers.length === 0) {
|
|
100
|
+
throw new Error("No NATS servers configured by server");
|
|
101
|
+
}
|
|
102
|
+
const natsToken = normalizeNatsToken(bootstrapRecord.auth);
|
|
103
|
+
const jetstream = await initJetStreamContext({
|
|
104
|
+
userId: args.userId,
|
|
105
|
+
servers: natsServers,
|
|
106
|
+
token: natsToken,
|
|
107
|
+
sanitizeUserId: args.sanitizeUserId,
|
|
108
|
+
onInfraError: args.onInfraError,
|
|
109
|
+
});
|
|
110
|
+
args.onInfraError(`bootstrap ok servers=${natsServers.length} eventStream=${jetstream.stream} eventSubject=${jetstream.subject}`);
|
|
111
|
+
return { natsBootstrap, jetstream };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
const retryMs = Math.min(30_000, 1000 * Math.max(1, attempt));
|
|
116
|
+
args.onError(`bootstrap failed: ${message} (retry in ${Math.floor(retryMs / 1000)}s, attempt=${attempt})`);
|
|
117
|
+
await args.sleep(retryMs);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
export async function prepareCommandExecution(args) {
|
|
4
|
+
const shellPath = args.resolveShellPath();
|
|
5
|
+
const taskWorkspace = args.resolveTaskWorkspace(args.cwd);
|
|
6
|
+
const codexHome = args.resolveCodexHomePath();
|
|
7
|
+
await mkdir(codexHome, { recursive: true });
|
|
8
|
+
const codexAuth = await args.prepareCodexAuthBundle(args.codexAuthBundle);
|
|
9
|
+
const localAgentSettings = await args.readAgentSettingsConfig({ workspaceRoot: args.resolveWorkspaceRoot() });
|
|
10
|
+
const baseTaskEnvPatch = {
|
|
11
|
+
CODEX_HOME: codexHome,
|
|
12
|
+
DOER_USER_ID: args.userId,
|
|
13
|
+
DOER_AGENT_TASK_ID: args.taskId,
|
|
14
|
+
...args.buildAgentSettingsEnvPatch(localAgentSettings),
|
|
15
|
+
...args.runtimeEnvPatch,
|
|
16
|
+
...(codexAuth?.envPatch ?? {}),
|
|
17
|
+
WORKSPACE: taskWorkspace,
|
|
18
|
+
};
|
|
19
|
+
const taskGitEnv = await args.prepareTaskGitEnv({
|
|
20
|
+
cwd: taskWorkspace,
|
|
21
|
+
baseEnvPatch: baseTaskEnvPatch,
|
|
22
|
+
});
|
|
23
|
+
const runtimeBinPath = path.join(args.agentProjectDir, "runtime/bin");
|
|
24
|
+
const taskPath = [runtimeBinPath, process.env.PATH || ""].filter(Boolean).join(path.delimiter);
|
|
25
|
+
return {
|
|
26
|
+
shellPath,
|
|
27
|
+
taskWorkspace,
|
|
28
|
+
taskPath,
|
|
29
|
+
env: {
|
|
30
|
+
...process.env,
|
|
31
|
+
...baseTaskEnvPatch,
|
|
32
|
+
...taskGitEnv.envPatch,
|
|
33
|
+
PATH: taskPath,
|
|
34
|
+
},
|
|
35
|
+
taskGitMeta: taskGitEnv.meta ?? {},
|
|
36
|
+
codexAuthMeta: codexAuth?.meta ?? { codexAuthSynced: false },
|
|
37
|
+
codexAuthCleanup: codexAuth?.cleanup ?? (async () => { }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function createPendingRunSessionTracker(args) {
|
|
2
|
+
let closed = false;
|
|
3
|
+
let timer = null;
|
|
4
|
+
const stop = () => {
|
|
5
|
+
closed = true;
|
|
6
|
+
if (timer) {
|
|
7
|
+
clearInterval(timer);
|
|
8
|
+
timer = null;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const poll = async () => {
|
|
12
|
+
if (closed || args.task.sessionId) {
|
|
13
|
+
stop();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const detected = await args.detectPendingRunSession().catch(() => null);
|
|
17
|
+
if (!detected) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
await args.updateRunSessionMetadata(detected).catch(() => undefined);
|
|
21
|
+
if (args.task.sessionId) {
|
|
22
|
+
stop();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const start = () => {
|
|
26
|
+
if (closed || args.task.sessionId || timer) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
timer = setInterval(() => {
|
|
30
|
+
void poll();
|
|
31
|
+
}, args.pollIntervalMs ?? 1000);
|
|
32
|
+
};
|
|
33
|
+
return { poll, start, stop };
|
|
34
|
+
}
|
|
35
|
+
export function attachManagedRunProcessLifecycle(args) {
|
|
36
|
+
args.child.once("error", (error) => {
|
|
37
|
+
args.stopPendingSessionPoll();
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
args.task.status = "failed";
|
|
40
|
+
args.task.error = message;
|
|
41
|
+
args.task.finishedAt = args.formatTimestamp();
|
|
42
|
+
args.publishImmediateRunEvent({ nc: args.nc, userId: args.task.userId, task: args.task });
|
|
43
|
+
args.publishImmediateRunEvent({ nc: args.nc, userId: args.task.userId, task: args.task, type: "run.finished" });
|
|
44
|
+
void args.removeRunTask(args.task.id).catch(() => undefined);
|
|
45
|
+
void args.releaseRunStartSlot({ runId: args.task.id, sessionId: args.task.sessionId }).catch(() => undefined);
|
|
46
|
+
void args.codexAuthCleanup().catch(() => undefined);
|
|
47
|
+
args.writeRunStatus(args.task.id, `failed error=${message}`);
|
|
48
|
+
});
|
|
49
|
+
args.child.once("close", async (code, signal) => {
|
|
50
|
+
args.stopPendingSessionPoll();
|
|
51
|
+
const latest = await args.getStoredRun(args.task.id).catch(() => null);
|
|
52
|
+
if (latest?.cancelRequested) {
|
|
53
|
+
args.task.cancelRequested = true;
|
|
54
|
+
}
|
|
55
|
+
args.task.resultExitCode = typeof code === "number" ? code : null;
|
|
56
|
+
args.task.resultSignal = signal;
|
|
57
|
+
args.task.finishedAt = args.formatTimestamp();
|
|
58
|
+
args.task.status = args.task.cancelRequested ? "canceled" : (args.task.resultExitCode ?? 1) === 0 ? "completed" : "failed";
|
|
59
|
+
args.task.error = args.task.status === "failed" ? `Command exited with code ${args.task.resultExitCode ?? "null"}` : null;
|
|
60
|
+
args.publishImmediateRunEvent({ nc: args.nc, userId: args.task.userId, task: args.task });
|
|
61
|
+
args.publishImmediateRunEvent({ nc: args.nc, userId: args.task.userId, task: args.task, type: "run.finished" });
|
|
62
|
+
void args.removeRunTask(args.task.id).catch(() => undefined);
|
|
63
|
+
void args.releaseRunStartSlot({ runId: args.task.id, sessionId: args.task.sessionId }).catch(() => undefined);
|
|
64
|
+
void args.codexAuthCleanup().catch(() => undefined);
|
|
65
|
+
args.writeRunStatus(args.task.id, `completed status=${args.task.status} exitCode=${args.task.resultExitCode ?? "null"} signal=${args.task.resultSignal ?? "null"}`);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { StringCodec } from "nats";
|
|
2
|
+
const runRpcCodec = StringCodec();
|
|
3
|
+
export function normalizeRunRpcRequest(args) {
|
|
4
|
+
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
5
|
+
if (!requestId) {
|
|
6
|
+
throw new Error("missing requestId");
|
|
7
|
+
}
|
|
8
|
+
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
9
|
+
if (!requestAgentId || requestAgentId !== args.agentId) {
|
|
10
|
+
throw new Error("agent id mismatch");
|
|
11
|
+
}
|
|
12
|
+
const actionRaw = typeof args.request.action === "string" ? args.request.action.trim() : "";
|
|
13
|
+
const action = actionRaw === "cancel" || actionRaw === "get" || actionRaw === "list" ? actionRaw : "start";
|
|
14
|
+
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
15
|
+
if (!responseSubject) {
|
|
16
|
+
throw new Error("missing responseSubject");
|
|
17
|
+
}
|
|
18
|
+
const runId = typeof args.request.runId === "string" && args.request.runId.trim() ? args.request.runId.trim() : null;
|
|
19
|
+
const prompt = typeof args.request.prompt === "string" && args.request.prompt.trim() ? args.request.prompt.trim() : null;
|
|
20
|
+
const imagePaths = args.normalizeImagePaths(args.request.imagePaths);
|
|
21
|
+
const sessionId = typeof args.request.sessionId === "string" && args.request.sessionId.trim() ? args.request.sessionId.trim() : null;
|
|
22
|
+
const model = args.normalizeModel(args.request.model);
|
|
23
|
+
if (action === "start" && !prompt) {
|
|
24
|
+
throw new Error("missing prompt");
|
|
25
|
+
}
|
|
26
|
+
if ((action === "get" || action === "cancel") && !runId) {
|
|
27
|
+
throw new Error("missing runId");
|
|
28
|
+
}
|
|
29
|
+
const cwd = typeof args.request.cwd === "string" && args.request.cwd.trim() ? args.request.cwd.trim() : null;
|
|
30
|
+
const sinceSeqRaw = Number(args.request.sinceSeq);
|
|
31
|
+
const sinceSeq = Number.isInteger(sinceSeqRaw) && sinceSeqRaw >= 0 ? sinceSeqRaw : null;
|
|
32
|
+
const limitRaw = Number(args.request.limit);
|
|
33
|
+
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(Math.floor(limitRaw), 200)) : 50;
|
|
34
|
+
return {
|
|
35
|
+
requestId,
|
|
36
|
+
action,
|
|
37
|
+
runId,
|
|
38
|
+
prompt,
|
|
39
|
+
imagePaths,
|
|
40
|
+
sessionId,
|
|
41
|
+
model,
|
|
42
|
+
cwd,
|
|
43
|
+
responseSubject,
|
|
44
|
+
sinceSeq,
|
|
45
|
+
limit,
|
|
46
|
+
runtimeEnvPatch: args.normalizeEnvPatch(args.request.runtimeEnvPatch),
|
|
47
|
+
codexAuthBundle: args.normalizeCodexAuthBundle(args.request.codexAuth),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function publishRunRpcResponse(args) {
|
|
51
|
+
args.nc.publish(args.responseSubject, runRpcCodec.encode(JSON.stringify(args.payload)));
|
|
52
|
+
}
|
|
53
|
+
export async function handleNonStartRunRpc(args) {
|
|
54
|
+
const { request } = args;
|
|
55
|
+
if (request.action === "list") {
|
|
56
|
+
const merged = (await args.listPersistedRunTasks())
|
|
57
|
+
.map((task) => args.cloneRunTask(task))
|
|
58
|
+
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
|
|
59
|
+
.slice(0, request.limit);
|
|
60
|
+
publishRunRpcResponse({
|
|
61
|
+
nc: args.nc,
|
|
62
|
+
responseSubject: request.responseSubject,
|
|
63
|
+
payload: { requestId: request.requestId, ok: true, tasks: merged },
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const stored = request.runId ? await args.getStoredRun(request.runId) : null;
|
|
68
|
+
if (!stored || stored.agentId !== args.agentId || stored.userId !== args.userId) {
|
|
69
|
+
throw new Error("Run not found");
|
|
70
|
+
}
|
|
71
|
+
if (request.action === "cancel") {
|
|
72
|
+
if (stored.processPid === null) {
|
|
73
|
+
throw new Error("Run pid not found");
|
|
74
|
+
}
|
|
75
|
+
stored.cancelRequested = true;
|
|
76
|
+
stored.updatedAt = args.formatTimestamp();
|
|
77
|
+
await args.persistRunTask(stored);
|
|
78
|
+
args.publishImmediateRunEvent(stored);
|
|
79
|
+
args.writeRunStatus(stored.id, `cancel requested pid=${stored.processPid}`);
|
|
80
|
+
args.sendSignalToPid(stored.processPid, "SIGINT");
|
|
81
|
+
publishRunRpcResponse({
|
|
82
|
+
nc: args.nc,
|
|
83
|
+
responseSubject: request.responseSubject,
|
|
84
|
+
payload: { requestId: request.requestId, ok: true, task: args.cloneRunTask(stored) },
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
publishRunRpcResponse({
|
|
89
|
+
nc: args.nc,
|
|
90
|
+
responseSubject: request.responseSubject,
|
|
91
|
+
payload: { requestId: request.requestId, ok: true, task: args.cloneRunTask(stored, request.sinceSeq) },
|
|
92
|
+
});
|
|
93
|
+
}
|