doer-agent 0.9.10 → 0.9.11
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.
|
@@ -190,20 +190,27 @@ async function handleCodexAppRpcMessage(args) {
|
|
|
190
190
|
}
|
|
191
191
|
export function subscribeToCodexAppRpc(args) {
|
|
192
192
|
const handoffJobs = new CodexThreadHandoffJobManager(async (input, onProgress) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
193
|
+
return await createCodexThreadHandoff({
|
|
194
|
+
manager: args.manager,
|
|
195
|
+
...input,
|
|
196
|
+
onProgress,
|
|
197
|
+
onLog: args.onInfo,
|
|
198
|
+
});
|
|
199
|
+
}, {
|
|
200
|
+
onStatus: (job) => {
|
|
201
|
+
const elapsedMs = Math.max(0, Date.parse(job.updatedAt) - Date.parse(job.createdAt));
|
|
202
|
+
const target = job.result?.threadId ? ` targetThreadId=${job.result.threadId}` : "";
|
|
203
|
+
const error = job.error
|
|
204
|
+
? ` error=${JSON.stringify(job.error.replace(/\s+/g, " ").slice(0, 500))}`
|
|
205
|
+
: "";
|
|
206
|
+
const message = `thread handoff job status jobId=${job.jobId} sourceThreadId=${job.sourceThreadId} phase=${job.phase} elapsedMs=${elapsedMs}${target}${error}`;
|
|
207
|
+
if (job.phase === "failed") {
|
|
208
|
+
args.onError(message);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
args.onInfo(message);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
207
214
|
});
|
|
208
215
|
args.nc.closed().finally(() => {
|
|
209
216
|
void args.manager.stop().catch(() => undefined);
|
package/dist/codex-text-task.js
CHANGED
|
@@ -37,14 +37,17 @@ function buildCodexUserInput(prompt) {
|
|
|
37
37
|
}];
|
|
38
38
|
}
|
|
39
39
|
export async function runCodexTextTask(args) {
|
|
40
|
+
const startedAt = Date.now();
|
|
40
41
|
let threadId = "";
|
|
41
42
|
let turnId = "";
|
|
42
43
|
let output = "";
|
|
44
|
+
let outputStarted = false;
|
|
43
45
|
let settled = false;
|
|
44
46
|
let cleanupNotification = () => { };
|
|
45
47
|
let settle = (_callback) => { };
|
|
46
48
|
const completed = new Promise((resolve, reject) => {
|
|
47
49
|
const timeout = setTimeout(() => {
|
|
50
|
+
args.onLog?.(`codex text task status phase=timed_out threadId=${threadId || "pending"} turnId=${turnId || "pending"} elapsedMs=${Date.now() - startedAt} outputChars=${output.length}`);
|
|
48
51
|
settle(() => reject(new Error("Timed out while waiting for Codex text task")));
|
|
49
52
|
}, args.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
50
53
|
settle = (callback) => {
|
|
@@ -69,11 +72,16 @@ export async function runCodexTextTask(args) {
|
|
|
69
72
|
if (method === "item/agentMessage/delta") {
|
|
70
73
|
const record = recordValue(params);
|
|
71
74
|
output += stringValue(record?.delta) || stringValue(record?.text);
|
|
75
|
+
if (!outputStarted && output) {
|
|
76
|
+
outputStarted = true;
|
|
77
|
+
args.onLog?.(`codex text task status phase=responding threadId=${threadId} turnId=${turnId || eventTurnId || "pending"} elapsedMs=${Date.now() - startedAt}`);
|
|
78
|
+
}
|
|
72
79
|
return;
|
|
73
80
|
}
|
|
74
81
|
if (!isTerminalTurnMethod(method)) {
|
|
75
82
|
return;
|
|
76
83
|
}
|
|
84
|
+
args.onLog?.(`codex text task status phase=terminal_notification method=${method} threadId=${threadId} turnId=${turnId || eventTurnId || "pending"} elapsedMs=${Date.now() - startedAt} outputChars=${output.length}`);
|
|
77
85
|
if (method === "turn/completed") {
|
|
78
86
|
settle(resolve);
|
|
79
87
|
return;
|
|
@@ -92,6 +100,7 @@ export async function runCodexTextTask(args) {
|
|
|
92
100
|
if (!threadId) {
|
|
93
101
|
throw new Error("Codex app-server did not return a text task thread id");
|
|
94
102
|
}
|
|
103
|
+
args.onLog?.(`codex text task status phase=thread_started threadId=${threadId} elapsedMs=${Date.now() - startedAt}`);
|
|
95
104
|
const turnResult = recordValue(await args.manager.request("turn/start", {
|
|
96
105
|
threadId,
|
|
97
106
|
input: buildCodexUserInput(args.prompt),
|
|
@@ -100,16 +109,25 @@ export async function runCodexTextTask(args) {
|
|
|
100
109
|
if (!turnId) {
|
|
101
110
|
throw new Error("Codex app-server did not return a text task turn id");
|
|
102
111
|
}
|
|
112
|
+
args.onLog?.(`codex text task status phase=turn_started threadId=${threadId} turnId=${turnId} elapsedMs=${Date.now() - startedAt}`);
|
|
103
113
|
await completed.finally(() => cleanupNotification());
|
|
114
|
+
args.onLog?.(`codex text task status phase=completed threadId=${threadId} turnId=${turnId} elapsedMs=${Date.now() - startedAt} outputChars=${output.length}`);
|
|
104
115
|
return output;
|
|
105
116
|
}
|
|
106
117
|
catch (error) {
|
|
107
118
|
settle(() => { });
|
|
119
|
+
args.onLog?.(`codex text task status phase=failed threadId=${threadId || "pending"} turnId=${turnId || "pending"} elapsedMs=${Date.now() - startedAt} error=${JSON.stringify((error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").slice(0, 500))}`);
|
|
108
120
|
throw error;
|
|
109
121
|
}
|
|
110
122
|
finally {
|
|
111
123
|
if (threadId) {
|
|
112
|
-
await args.manager.request("thread/unsubscribe", { threadId }, 30_000)
|
|
124
|
+
await args.manager.request("thread/unsubscribe", { threadId }, 30_000)
|
|
125
|
+
.then(() => {
|
|
126
|
+
args.onLog?.(`codex text task status phase=unsubscribed threadId=${threadId} turnId=${turnId || "pending"} elapsedMs=${Date.now() - startedAt}`);
|
|
127
|
+
})
|
|
128
|
+
.catch((error) => {
|
|
129
|
+
args.onLog?.(`codex text task status phase=unsubscribe_failed threadId=${threadId} turnId=${turnId || "pending"} elapsedMs=${Date.now() - startedAt} error=${JSON.stringify((error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").slice(0, 500))}`);
|
|
130
|
+
});
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
133
|
}
|
|
@@ -8,14 +8,16 @@ function copySnapshot(job) {
|
|
|
8
8
|
}
|
|
9
9
|
export class CodexThreadHandoffJobManager {
|
|
10
10
|
run;
|
|
11
|
-
terminalTtlMs;
|
|
12
|
-
maxJobs;
|
|
13
11
|
jobs = new Map();
|
|
14
12
|
activeJobBySourceThreadId = new Map();
|
|
15
|
-
|
|
13
|
+
terminalTtlMs;
|
|
14
|
+
maxJobs;
|
|
15
|
+
onStatus;
|
|
16
|
+
constructor(run, options = {}) {
|
|
16
17
|
this.run = run;
|
|
17
|
-
this.terminalTtlMs = terminalTtlMs;
|
|
18
|
-
this.maxJobs = maxJobs;
|
|
18
|
+
this.terminalTtlMs = options.terminalTtlMs ?? 30 * 60_000;
|
|
19
|
+
this.maxJobs = options.maxJobs ?? 100;
|
|
20
|
+
this.onStatus = options.onStatus;
|
|
19
21
|
}
|
|
20
22
|
start(input) {
|
|
21
23
|
this.cleanup();
|
|
@@ -37,6 +39,7 @@ export class CodexThreadHandoffJobManager {
|
|
|
37
39
|
};
|
|
38
40
|
this.jobs.set(job.jobId, job);
|
|
39
41
|
this.activeJobBySourceThreadId.set(input.sourceThreadId, job.jobId);
|
|
42
|
+
this.emitStatus(job);
|
|
40
43
|
setImmediate(() => {
|
|
41
44
|
void this.execute(job.jobId, input);
|
|
42
45
|
});
|
|
@@ -55,6 +58,7 @@ export class CodexThreadHandoffJobManager {
|
|
|
55
58
|
}
|
|
56
59
|
current.phase = phase;
|
|
57
60
|
current.updatedAt = new Date().toISOString();
|
|
61
|
+
this.emitStatus(current);
|
|
58
62
|
};
|
|
59
63
|
try {
|
|
60
64
|
const result = await this.run(input, updatePhase);
|
|
@@ -63,6 +67,7 @@ export class CodexThreadHandoffJobManager {
|
|
|
63
67
|
current.phase = "completed";
|
|
64
68
|
current.result = result;
|
|
65
69
|
current.updatedAt = new Date().toISOString();
|
|
70
|
+
this.emitStatus(current);
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
catch (error) {
|
|
@@ -71,6 +76,7 @@ export class CodexThreadHandoffJobManager {
|
|
|
71
76
|
current.phase = "failed";
|
|
72
77
|
current.error = error instanceof Error ? error.message : String(error);
|
|
73
78
|
current.updatedAt = new Date().toISOString();
|
|
79
|
+
this.emitStatus(current);
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
finally {
|
|
@@ -79,6 +85,9 @@ export class CodexThreadHandoffJobManager {
|
|
|
79
85
|
}
|
|
80
86
|
}
|
|
81
87
|
}
|
|
88
|
+
emitStatus(job) {
|
|
89
|
+
this.onStatus?.(copySnapshot(job));
|
|
90
|
+
}
|
|
82
91
|
cleanup() {
|
|
83
92
|
const cutoff = Date.now() - this.terminalTtlMs;
|
|
84
93
|
for (const [jobId, job] of this.jobs) {
|
|
@@ -17,11 +17,14 @@ async function nextTick() {
|
|
|
17
17
|
}
|
|
18
18
|
test("starts handoff work asynchronously and reports progress", async () => {
|
|
19
19
|
let finish;
|
|
20
|
+
const statuses = [];
|
|
20
21
|
const manager = new CodexThreadHandoffJobManager(async (input, onProgress) => {
|
|
21
22
|
onProgress("collecting");
|
|
22
23
|
return await new Promise((resolve) => {
|
|
23
24
|
finish = resolve;
|
|
24
25
|
});
|
|
26
|
+
}, {
|
|
27
|
+
onStatus: (job) => statuses.push(job.phase),
|
|
25
28
|
});
|
|
26
29
|
const started = manager.start({ sourceThreadId: "source-thread" });
|
|
27
30
|
assert.equal(started.job.phase, "queued");
|
|
@@ -33,6 +36,7 @@ test("starts handoff work asynchronously and reports progress", async () => {
|
|
|
33
36
|
const completed = manager.get(started.job.jobId);
|
|
34
37
|
assert.equal(completed?.phase, "completed");
|
|
35
38
|
assert.equal(completed?.result?.threadId, "target-thread");
|
|
39
|
+
assert.deepEqual(statuses, ["queued", "collecting", "completed"]);
|
|
36
40
|
});
|
|
37
41
|
test("deduplicates active jobs for the same source thread", () => {
|
|
38
42
|
const manager = new CodexThreadHandoffJobManager(async () => {
|
|
@@ -44,8 +48,11 @@ test("deduplicates active jobs for the same source thread", () => {
|
|
|
44
48
|
assert.equal(second.job.jobId, first.job.jobId);
|
|
45
49
|
});
|
|
46
50
|
test("keeps a failed job status for polling", async () => {
|
|
51
|
+
const statuses = [];
|
|
47
52
|
const manager = new CodexThreadHandoffJobManager(async () => {
|
|
48
53
|
throw new Error("summary failed");
|
|
54
|
+
}, {
|
|
55
|
+
onStatus: (job) => statuses.push({ phase: job.phase, error: job.error }),
|
|
49
56
|
});
|
|
50
57
|
const started = manager.start({ sourceThreadId: "source-thread" });
|
|
51
58
|
await nextTick();
|
|
@@ -53,4 +60,8 @@ test("keeps a failed job status for polling", async () => {
|
|
|
53
60
|
const failed = manager.get(started.job.jobId);
|
|
54
61
|
assert.equal(failed?.phase, "failed");
|
|
55
62
|
assert.equal(failed?.error, "summary failed");
|
|
63
|
+
assert.deepEqual(statuses, [
|
|
64
|
+
{ phase: "queued", error: undefined },
|
|
65
|
+
{ phase: "failed", error: "summary failed" },
|
|
66
|
+
]);
|
|
56
67
|
});
|