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.
@@ -0,0 +1,275 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ export function sendSignalToTaskProcess(child, signal) {
5
+ if (process.platform !== "win32" && typeof child.pid === "number") {
6
+ try {
7
+ process.kill(-child.pid, signal);
8
+ return;
9
+ }
10
+ catch {
11
+ // Fall back to direct child signaling.
12
+ }
13
+ }
14
+ try {
15
+ child.kill(signal);
16
+ }
17
+ catch {
18
+ // noop
19
+ }
20
+ }
21
+ export function sendSignalToPid(pid, signal) {
22
+ if (process.platform !== "win32") {
23
+ try {
24
+ process.kill(-pid, signal);
25
+ return;
26
+ }
27
+ catch {
28
+ // Fall back to direct pid signaling.
29
+ }
30
+ }
31
+ process.kill(pid, signal);
32
+ }
33
+ async function checkCancelRequested(args) {
34
+ const query = new URLSearchParams({
35
+ userId: args.userId,
36
+ agentToken: args.agentToken,
37
+ });
38
+ const response = await args.getJson(`${args.serverBaseUrl}/api/agent/tasks/${encodeURIComponent(args.taskId)}/events?${query.toString()}`);
39
+ return Boolean(response.task?.cancelRequested);
40
+ }
41
+ async function syncCodexAuthState(args) {
42
+ const envPatch = {};
43
+ const synced = false;
44
+ if (args.authMode === "api_key" && args.apiKey) {
45
+ envPatch.OPENAI_API_KEY = args.apiKey;
46
+ }
47
+ return {
48
+ envPatch,
49
+ cleanup: async () => { },
50
+ meta: {
51
+ codexAuthSource: args.source,
52
+ codexAuthMode: args.authMode ?? null,
53
+ codexAuthHasApiKey: Boolean(args.apiKey),
54
+ codexAuthHasAuthJson: Boolean(args.authJson),
55
+ codexAuthIssuedAt: args.issuedAt ?? null,
56
+ codexAuthExpiresAt: args.expiresAt ?? null,
57
+ codexAuthSynced: synced,
58
+ },
59
+ };
60
+ }
61
+ async function prepareTaskCodexAuth(args) {
62
+ void args;
63
+ return await syncCodexAuthState({
64
+ source: "agent_local",
65
+ authJson: null,
66
+ issuedAt: null,
67
+ expiresAt: null,
68
+ });
69
+ }
70
+ export async function prepareCodexAuthBundle(bundle) {
71
+ if (!bundle) {
72
+ return null;
73
+ }
74
+ return await syncCodexAuthState({
75
+ source: "server_bundle",
76
+ authMode: bundle.authMode,
77
+ apiKey: bundle.apiKey,
78
+ authJson: bundle.authJson ?? null,
79
+ issuedAt: bundle.issuedAt ?? null,
80
+ expiresAt: bundle.expiresAt ?? null,
81
+ });
82
+ }
83
+ export async function runTask(args) {
84
+ args.setActiveTaskLogContext({
85
+ jetstream: args.jetstream,
86
+ serverBaseUrl: args.serverBaseUrl,
87
+ taskId: args.taskId,
88
+ userId: args.userId,
89
+ });
90
+ const shellPath = args.resolveShellPath();
91
+ const taskWorkspace = args.resolveTaskWorkspace(args.cwd);
92
+ const codexHome = args.resolveCodexHomePath();
93
+ await mkdir(codexHome, { recursive: true });
94
+ const runtimeConfig = await args.prepareTaskRuntimeConfig({
95
+ serverBaseUrl: args.serverBaseUrl,
96
+ taskId: args.taskId,
97
+ userId: args.userId,
98
+ agentToken: args.agentToken,
99
+ });
100
+ const codexAuth = await prepareTaskCodexAuth({
101
+ serverBaseUrl: args.serverBaseUrl,
102
+ taskId: args.taskId,
103
+ userId: args.userId,
104
+ agentToken: args.agentToken,
105
+ });
106
+ const localAgentSettings = await args.readAgentSettingsConfig({ workspaceRoot: args.resolveWorkspaceRoot() });
107
+ const baseTaskEnvPatch = {
108
+ CODEX_HOME: codexHome,
109
+ ...args.buildAgentSettingsEnvPatch(localAgentSettings),
110
+ ...(runtimeConfig?.envPatch ?? {}),
111
+ ...(codexAuth?.envPatch ?? {}),
112
+ WORKSPACE: taskWorkspace,
113
+ };
114
+ const taskGitEnv = await args.prepareTaskGitEnv({
115
+ cwd: taskWorkspace,
116
+ baseEnvPatch: baseTaskEnvPatch,
117
+ });
118
+ await args.recordAgentEvent({
119
+ jetstream: args.jetstream,
120
+ serverBaseUrl: args.serverBaseUrl,
121
+ taskId: args.taskId,
122
+ userId: args.userId,
123
+ type: "meta",
124
+ seq: args.reserveNextEventSeq(args.taskId),
125
+ payload: {
126
+ host: process.platform,
127
+ pid: process.pid,
128
+ startedAt: args.formatLocalTimestamp(),
129
+ command: args.command,
130
+ cwd: taskWorkspace,
131
+ requestedCwd: args.cwd,
132
+ shell: shellPath,
133
+ ...(runtimeConfig?.meta ?? { runtimeConfigSynced: false }),
134
+ ...(codexAuth?.meta ?? { codexAuthSynced: false }),
135
+ ...(taskGitEnv.meta ?? {}),
136
+ },
137
+ });
138
+ try {
139
+ let terminationReason = null;
140
+ let cancelStage1Timer = null;
141
+ let cancelStage2Timer = null;
142
+ let stopCancelPolling = false;
143
+ let cancelSignalSent = false;
144
+ const runtimeBinPath = path.join(args.agentProjectDir, "runtime/bin");
145
+ const taskPath = [runtimeBinPath, process.env.PATH || ""].filter(Boolean).join(path.delimiter);
146
+ const child = spawn(args.command, {
147
+ cwd: taskWorkspace,
148
+ shell: shellPath,
149
+ detached: process.platform !== "win32",
150
+ env: {
151
+ ...process.env,
152
+ ...baseTaskEnvPatch,
153
+ ...taskGitEnv.envPatch,
154
+ PATH: taskPath,
155
+ DOER_AGENT_TOKEN: args.agentToken,
156
+ },
157
+ stdio: ["ignore", "pipe", "pipe"],
158
+ });
159
+ child.stdout?.setEncoding("utf8");
160
+ child.stderr?.setEncoding("utf8");
161
+ const requestCancel = () => {
162
+ if (cancelSignalSent || terminationReason === "cancel") {
163
+ return;
164
+ }
165
+ cancelSignalSent = true;
166
+ terminationReason = "cancel";
167
+ sendSignalToTaskProcess(child, "SIGINT");
168
+ cancelStage1Timer = setTimeout(() => {
169
+ sendSignalToTaskProcess(child, "SIGTERM");
170
+ }, 1200);
171
+ cancelStage1Timer.unref?.();
172
+ cancelStage2Timer = setTimeout(() => {
173
+ sendSignalToTaskProcess(child, "SIGKILL");
174
+ }, 3500);
175
+ cancelStage2Timer.unref?.();
176
+ };
177
+ child.stdout?.on("data", (chunk) => {
178
+ args.writeTaskStream(args.taskId, "stdout", chunk);
179
+ const seq = args.reserveNextEventSeq(args.taskId);
180
+ args.persistEventOrFatal({
181
+ jetstream: args.jetstream,
182
+ serverBaseUrl: args.serverBaseUrl,
183
+ taskId: args.taskId,
184
+ userId: args.userId,
185
+ type: "stdout",
186
+ seq,
187
+ payload: { chunk, at: args.formatLocalTimestamp() },
188
+ context: "stdout persist failed",
189
+ });
190
+ });
191
+ child.stderr?.on("data", (chunk) => {
192
+ args.writeTaskStream(args.taskId, "stderr", chunk);
193
+ const seq = args.reserveNextEventSeq(args.taskId);
194
+ args.persistEventOrFatal({
195
+ jetstream: args.jetstream,
196
+ serverBaseUrl: args.serverBaseUrl,
197
+ taskId: args.taskId,
198
+ userId: args.userId,
199
+ type: "stderr",
200
+ seq,
201
+ payload: { chunk, at: args.formatLocalTimestamp() },
202
+ context: "stderr persist failed",
203
+ });
204
+ });
205
+ const cancelPoller = (async () => {
206
+ while (!stopCancelPolling) {
207
+ await args.sleep(5000);
208
+ if (stopCancelPolling || terminationReason === "cancel") {
209
+ continue;
210
+ }
211
+ const cancelRequested = await checkCancelRequested({
212
+ serverBaseUrl: args.serverBaseUrl,
213
+ taskId: args.taskId,
214
+ userId: args.userId,
215
+ agentToken: args.agentToken,
216
+ getJson: args.getJson,
217
+ }).catch(() => false);
218
+ if (!cancelRequested) {
219
+ continue;
220
+ }
221
+ requestCancel();
222
+ }
223
+ })();
224
+ const result = await new Promise((resolve, reject) => {
225
+ child.once("error", reject);
226
+ child.once("close", (code, signal) => {
227
+ resolve({ code, signal });
228
+ });
229
+ }).finally(() => {
230
+ stopCancelPolling = true;
231
+ if (cancelStage1Timer) {
232
+ clearTimeout(cancelStage1Timer);
233
+ }
234
+ if (cancelStage2Timer) {
235
+ clearTimeout(cancelStage2Timer);
236
+ }
237
+ });
238
+ await cancelPoller.catch(() => undefined);
239
+ const canceled = await checkCancelRequested({
240
+ serverBaseUrl: args.serverBaseUrl,
241
+ taskId: args.taskId,
242
+ userId: args.userId,
243
+ agentToken: args.agentToken,
244
+ getJson: args.getJson,
245
+ }).catch(() => false);
246
+ const status = canceled || terminationReason === "cancel"
247
+ ? "canceled"
248
+ : (result.code ?? 1) === 0
249
+ ? "completed"
250
+ : "failed";
251
+ const statusPayload = {
252
+ status,
253
+ exitCode: typeof result.code === "number" ? result.code : null,
254
+ signal: result.signal,
255
+ finishedAt: args.formatLocalTimestamp(),
256
+ error: status === "failed"
257
+ ? `Command exited with code ${result.code ?? "null"}`
258
+ : null,
259
+ };
260
+ await args.recordAgentEvent({
261
+ jetstream: args.jetstream,
262
+ serverBaseUrl: args.serverBaseUrl,
263
+ taskId: args.taskId,
264
+ userId: args.userId,
265
+ type: "status",
266
+ seq: args.reserveNextEventSeq(args.taskId),
267
+ payload: statusPayload,
268
+ });
269
+ args.writeAgentInfo(`task=${args.taskId} status=${status} exitCode=${typeof result.code === "number" ? result.code : "null"} signal=${result.signal ?? "null"}`);
270
+ }
271
+ finally {
272
+ args.clearActiveTaskLogContext(args.taskId);
273
+ await codexAuth?.cleanup().catch(() => undefined);
274
+ }
275
+ }