doer-agent 0.8.1 → 0.8.3

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,309 @@
1
+ import { StringCodec } from "nats";
2
+ const codec = StringCodec();
3
+ const SESSION_TIMEOUT_MS = 180_000;
4
+ const activeSessions = new Map();
5
+ function stringValue(value) {
6
+ return typeof value === "string" ? value : "";
7
+ }
8
+ function recordValue(value) {
9
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
10
+ }
11
+ function publishEvent(args) {
12
+ args.nc.publish(args.subject, codec.encode(JSON.stringify(args.payload)));
13
+ }
14
+ function respond(msg, payload) {
15
+ msg.respond(codec.encode(JSON.stringify(payload)));
16
+ }
17
+ function buildCodexUserInput(prompt) {
18
+ return [{
19
+ type: "text",
20
+ text: prompt,
21
+ text_elements: [],
22
+ }];
23
+ }
24
+ function buildNotesAiPrompt(request) {
25
+ const document = stringValue(request.document);
26
+ const selection = stringValue(request.selection);
27
+ const instruction = stringValue(request.instruction);
28
+ const parts = [
29
+ "You are editing a Markdown note inside Doer.",
30
+ "Return only Markdown content. Do not include explanations, preambles, or code fences unless the requested content itself needs them.",
31
+ "If a selection is provided, return only the replacement for that selection. If no selection is provided, return content to insert at the cursor.",
32
+ "When you generate an image, do not write attachment:image links. The generated image will be inserted into the note automatically.",
33
+ "",
34
+ `<instruction>\n${instruction}\n</instruction>`,
35
+ `<document>\n${document}\n</document>`,
36
+ ];
37
+ if (selection) {
38
+ parts.push(`<selection>\n${selection}\n</selection>`);
39
+ }
40
+ return parts.join("\n\n");
41
+ }
42
+ function isTerminalTurnMethod(method) {
43
+ return method === "turn/completed" ||
44
+ method === "turn/failed" ||
45
+ method === "turn/error" ||
46
+ method === "turn/cancelled" ||
47
+ method === "turn/canceled" ||
48
+ method === "turn/interrupted" ||
49
+ method === "turn/aborted";
50
+ }
51
+ function threadIdFromParams(params) {
52
+ const record = recordValue(params);
53
+ const thread = recordValue(record?.thread);
54
+ return stringValue(record?.threadId) || stringValue(thread?.id);
55
+ }
56
+ function turnIdFromParams(params) {
57
+ const record = recordValue(params);
58
+ const turn = recordValue(record?.turn);
59
+ return stringValue(record?.turnId) || stringValue(turn?.id);
60
+ }
61
+ function agentMessageDeltaFromParams(params) {
62
+ const record = recordValue(params);
63
+ if (!record) {
64
+ return "";
65
+ }
66
+ return stringValue(record.delta) || stringValue(record.text);
67
+ }
68
+ function generatedImageMarkdownFromParams(params, threadId) {
69
+ const record = recordValue(params);
70
+ const item = recordValue(record?.item);
71
+ if (!record || !item || stringValue(item.type) !== "imageGeneration") {
72
+ return "";
73
+ }
74
+ const imageId = stringValue(item.id);
75
+ if (!threadId || !imageId) {
76
+ return "";
77
+ }
78
+ return `\n\n![generated image](.codex/generated_images/${threadId}/${imageId}.png)\n\n`;
79
+ }
80
+ function terminalErrorFromParams(params) {
81
+ const record = recordValue(params);
82
+ const error = recordValue(record?.error);
83
+ return stringValue(record?.message) ||
84
+ stringValue(error?.message) ||
85
+ stringValue(record?.reason);
86
+ }
87
+ async function archiveCompletedThread(args) {
88
+ try {
89
+ await args.manager.request("thread/archive", { threadId: args.threadId }, 30_000);
90
+ }
91
+ catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ args.onError(`notes ai thread archive failed threadId=${args.threadId} error=${message}`);
94
+ }
95
+ }
96
+ async function runNotesAiSession(args) {
97
+ const instruction = stringValue(args.request.instruction);
98
+ if (!instruction) {
99
+ throw new Error("instruction is required");
100
+ }
101
+ if (args.abortController.signal.aborted) {
102
+ return;
103
+ }
104
+ let threadId = "";
105
+ let turnId = "";
106
+ let settled = false;
107
+ let cleanupNotification = () => { };
108
+ let settleCompleted = (_callback) => { };
109
+ const completed = new Promise((resolve, reject) => {
110
+ const timeout = setTimeout(() => {
111
+ if (!settled) {
112
+ reject(new Error("Timed out while waiting for Codex notes AI result"));
113
+ }
114
+ }, SESSION_TIMEOUT_MS);
115
+ settleCompleted = (callback) => {
116
+ if (settled) {
117
+ return;
118
+ }
119
+ settled = true;
120
+ clearTimeout(timeout);
121
+ cleanupNotification();
122
+ callback();
123
+ };
124
+ args.abortController.signal.addEventListener("abort", () => {
125
+ settleCompleted(() => resolve("aborted"));
126
+ }, { once: true });
127
+ cleanupNotification = args.manager.onNotification((method, params) => {
128
+ const eventThreadId = threadIdFromParams(params);
129
+ const eventTurnId = turnIdFromParams(params);
130
+ const isSessionEvent = (threadId && eventThreadId === threadId) ||
131
+ (turnId && eventTurnId === turnId);
132
+ if (!isSessionEvent) {
133
+ return;
134
+ }
135
+ if (turnId && eventTurnId && eventTurnId !== turnId) {
136
+ return;
137
+ }
138
+ if (method === "item/agentMessage/delta") {
139
+ const text = agentMessageDeltaFromParams(params);
140
+ if (!text) {
141
+ return;
142
+ }
143
+ publishEvent({
144
+ nc: args.nc,
145
+ subject: args.eventsSubject,
146
+ payload: { type: "delta", sessionId: args.sessionId, text },
147
+ });
148
+ return;
149
+ }
150
+ if (method === "item/completed") {
151
+ const markdown = generatedImageMarkdownFromParams(params, threadId);
152
+ if (!markdown) {
153
+ return;
154
+ }
155
+ publishEvent({
156
+ nc: args.nc,
157
+ subject: args.eventsSubject,
158
+ payload: { type: "delta", sessionId: args.sessionId, text: markdown },
159
+ });
160
+ return;
161
+ }
162
+ if (!isTerminalTurnMethod(method)) {
163
+ return;
164
+ }
165
+ if (method === "turn/completed") {
166
+ settleCompleted(() => {
167
+ publishEvent({
168
+ nc: args.nc,
169
+ subject: args.eventsSubject,
170
+ payload: { type: "done", sessionId: args.sessionId },
171
+ });
172
+ resolve("completed");
173
+ });
174
+ return;
175
+ }
176
+ const message = terminalErrorFromParams(params) || `Codex notes AI turn ended with ${method}`;
177
+ settleCompleted(() => reject(new Error(message)));
178
+ });
179
+ });
180
+ try {
181
+ const threadResult = recordValue(await args.manager.request("thread/start", {
182
+ cwd: null,
183
+ sessionStartSource: "clear",
184
+ }, 30_000));
185
+ const thread = recordValue(threadResult?.thread);
186
+ threadId = stringValue(thread?.id);
187
+ if (!threadId) {
188
+ throw new Error("Codex app-server did not return a thread id");
189
+ }
190
+ if (args.abortController.signal.aborted) {
191
+ return;
192
+ }
193
+ const turnResult = recordValue(await args.manager.request("turn/start", {
194
+ threadId,
195
+ input: buildCodexUserInput(buildNotesAiPrompt(args.request)),
196
+ }, 30_000));
197
+ const turn = recordValue(turnResult?.turn);
198
+ turnId = stringValue(turn?.id);
199
+ if (!turnId) {
200
+ throw new Error("Codex app-server did not return a turn id");
201
+ }
202
+ if (args.abortController.signal.aborted) {
203
+ return;
204
+ }
205
+ const completion = await completed.finally(() => cleanupNotification());
206
+ if (completion === "completed") {
207
+ await archiveCompletedThread({
208
+ manager: args.manager,
209
+ threadId,
210
+ onError: args.onError,
211
+ });
212
+ }
213
+ }
214
+ catch (error) {
215
+ settleCompleted(() => { });
216
+ throw error;
217
+ }
218
+ }
219
+ async function handleStart(args) {
220
+ const sessionId = stringValue(args.request.sessionId);
221
+ const eventsSubject = stringValue(args.request.eventsSubject);
222
+ if (!sessionId || !eventsSubject) {
223
+ throw new Error("sessionId and eventsSubject are required");
224
+ }
225
+ if (stringValue(args.request.agentId) && stringValue(args.request.agentId) !== args.agentId) {
226
+ throw new Error("agent id mismatch");
227
+ }
228
+ activeSessions.get(sessionId)?.abortController.abort();
229
+ const abortController = new AbortController();
230
+ activeSessions.set(sessionId, { abortController });
231
+ respond(args.msg, { requestId: args.request.requestId, ok: true, action: "start", sessionId });
232
+ void runNotesAiSession({
233
+ nc: args.nc,
234
+ manager: args.manager,
235
+ request: args.request,
236
+ sessionId,
237
+ eventsSubject,
238
+ abortController,
239
+ onError: args.onError,
240
+ }).catch((error) => {
241
+ const message = error instanceof Error ? error.message : String(error);
242
+ if (!abortController.signal.aborted) {
243
+ publishEvent({
244
+ nc: args.nc,
245
+ subject: eventsSubject,
246
+ payload: { type: "error", sessionId, error: message },
247
+ });
248
+ args.onError(`notes ai session failed sessionId=${sessionId} error=${message}`);
249
+ }
250
+ }).finally(() => {
251
+ if (activeSessions.get(sessionId)?.abortController === abortController) {
252
+ activeSessions.delete(sessionId);
253
+ }
254
+ });
255
+ }
256
+ export async function handleNotesAiRpcMessage(args) {
257
+ let payload = {};
258
+ try {
259
+ payload = JSON.parse(codec.decode(args.msg.data));
260
+ const action = stringValue(payload.action);
261
+ if (action === "cancel") {
262
+ const sessionId = stringValue(payload.sessionId);
263
+ activeSessions.get(sessionId)?.abortController.abort();
264
+ activeSessions.delete(sessionId);
265
+ respond(args.msg, { requestId: payload.requestId, ok: true, action, sessionId });
266
+ return;
267
+ }
268
+ if (action !== "start") {
269
+ throw new Error("unsupported notes ai action");
270
+ }
271
+ await handleStart({
272
+ msg: args.msg,
273
+ nc: args.nc,
274
+ manager: args.manager,
275
+ request: payload,
276
+ agentId: args.agentId,
277
+ onError: args.onError,
278
+ });
279
+ }
280
+ catch (error) {
281
+ const message = error instanceof Error ? error.message : "unknown error";
282
+ respond(args.msg, {
283
+ requestId: payload.requestId,
284
+ ok: false,
285
+ action: stringValue(payload.action),
286
+ error: message,
287
+ });
288
+ args.onError(`notes ai rpc failed action=${stringValue(payload.action) || "unknown"} error=${message}`);
289
+ }
290
+ }
291
+ export function subscribeToNotesAiRpc(args) {
292
+ args.nc.subscribe(args.subject, {
293
+ callback: (error, msg) => {
294
+ if (error) {
295
+ const message = error instanceof Error ? error.message : String(error);
296
+ args.onError(`notes ai rpc subscription error: ${message}`);
297
+ return;
298
+ }
299
+ void handleNotesAiRpcMessage({
300
+ msg,
301
+ nc: args.nc,
302
+ manager: args.manager,
303
+ agentId: args.agentId,
304
+ onError: args.onError,
305
+ });
306
+ },
307
+ });
308
+ args.onInfo(`notes ai rpc subscribed subject=${args.subject}`);
309
+ }
@@ -25,6 +25,9 @@ export function buildAgentFsRpcSubject(userId, agentId) {
25
25
  export function buildAgentNotesRpcSubject(userId, agentId) {
26
26
  return `doer.agent.notes.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
27
27
  }
28
+ export function buildAgentNotesAiRpcSubject(userId, agentId) {
29
+ return `doer.agent.notes.ai.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
30
+ }
28
31
  export function buildAgentDaemonRpcSubject(userId, agentId) {
29
32
  return `doer.agent.daemon.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
30
33
  }
package/dist/agent.js CHANGED
@@ -6,6 +6,7 @@ import { buildAgentSettingsEnvPatch, readAgentSettingsConfig, } from "./agent-se
6
6
  import { handleFsRpcMessage } from "./agent-fs-rpc.js";
7
7
  import { handleGitRpcMessage } from "./agent-git-rpc.js";
8
8
  import { handleNotesRpcMessage } from "./agent-notes-rpc.js";
9
+ import { subscribeToNotesAiRpc } from "./agent-notes-ai-rpc.js";
9
10
  import { ensureBundledDoerSkills } from "./agent-bundled-skills.js";
10
11
  import { subscribeToCodexAppRpc } from "./agent-codex-app-rpc.js";
11
12
  import { createCodexAppServerManager } from "./codex-app-server-manager.js";
@@ -17,7 +18,7 @@ import { subscribeToSkillRpc } from "./agent-skill-rpc.js";
17
18
  import { subscribeToMaintenanceRpc } from "./agent-maintenance-rpc.js";
18
19
  import { subscribeToHttpProxyRpc } from "./agent-http-proxy-rpc.js";
19
20
  import { sendSignalToTaskProcess } from "./agent-task-execution.js";
20
- import { buildAgentCodexAppEventsSubject, buildAgentCodexAppRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentHttpProxyRpcSubject, buildAgentMaintenanceRpcSubject, buildAgentNotesRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, } from "./agent-runtime-utils.js";
21
+ import { buildAgentCodexAppEventsSubject, buildAgentCodexAppRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentHttpProxyRpcSubject, buildAgentMaintenanceRpcSubject, buildAgentNotesRpcSubject, buildAgentNotesAiRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, } from "./agent-runtime-utils.js";
21
22
  import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
22
23
  import { createEventPersistenceHelpers, heartbeatAgentSession, postJson, } from "./agent-runtime-io.js";
23
24
  import { handleSettingsRpcMessage } from "./agent-settings-rpc.js";
@@ -131,6 +132,16 @@ function subscribeToNotesRpc(args) {
131
132
  });
132
133
  writeAgentInfo(`notes rpc subscribed subject=${subject}`);
133
134
  }
135
+ function subscribeToNotesAiRpcSession(args) {
136
+ subscribeToNotesAiRpc({
137
+ nc: args.jetstream.nc,
138
+ subject: buildAgentNotesAiRpcSubject(args.userId, args.agentId),
139
+ manager: args.codexAppServerManager,
140
+ agentId: args.agentId,
141
+ onInfo: writeAgentInfo,
142
+ onError: writeAgentError,
143
+ });
144
+ }
134
145
  function formatCodexAppNotificationParams(params) {
135
146
  if (params === undefined) {
136
147
  return "undefined";
@@ -290,6 +301,12 @@ async function main() {
290
301
  userId,
291
302
  agentId: initialAgentId,
292
303
  });
304
+ subscribeToNotesAiRpcSession({
305
+ jetstream,
306
+ userId,
307
+ agentId: initialAgentId,
308
+ codexAppServerManager,
309
+ });
293
310
  subscribeToDaemonRpc({
294
311
  nc: jetstream.nc,
295
312
  subject: buildAgentDaemonRpcSubject(userId, initialAgentId),
@@ -63,6 +63,7 @@ export function createCodexAppServerManager(args) {
63
63
  let client = null;
64
64
  let createPromise = null;
65
65
  let generation = 0;
66
+ const notificationListeners = new Set();
66
67
  const createClient = async () => {
67
68
  const settings = await args.readAgentSettingsConfig({ workspaceRoot: args.workspaceRoot });
68
69
  const appServerArgs = await buildCodexAppServerArgs({
@@ -89,7 +90,12 @@ export function createCodexAppServerManager(args) {
89
90
  args: appServerArgs,
90
91
  env,
91
92
  onLog: args.onLog,
92
- onNotification: args.onNotification,
93
+ onNotification: (method, params) => {
94
+ for (const listener of notificationListeners) {
95
+ listener(method, params);
96
+ }
97
+ args.onNotification?.(method, params);
98
+ },
93
99
  });
94
100
  };
95
101
  const getClient = async () => {
@@ -117,6 +123,12 @@ export function createCodexAppServerManager(args) {
117
123
  }
118
124
  };
119
125
  return {
126
+ onNotification(listener) {
127
+ notificationListeners.add(listener);
128
+ return () => {
129
+ notificationListeners.delete(listener);
130
+ };
131
+ },
120
132
  async request(method, params, timeoutMs) {
121
133
  const activeClient = await getClient();
122
134
  return await activeClient.request(method, params, timeoutMs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",