doer-agent 0.8.1 → 0.8.2
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-notes-ai-rpc.js +286 -0
- package/dist/agent-runtime-utils.js +3 -0
- package/dist/agent.js +18 -1
- package/dist/codex-app-server-manager.js +13 -1
- package/package.json +1 -1
|
@@ -0,0 +1,286 @@
|
|
|
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
|
+
"",
|
|
33
|
+
`<instruction>\n${instruction}\n</instruction>`,
|
|
34
|
+
`<document>\n${document}\n</document>`,
|
|
35
|
+
];
|
|
36
|
+
if (selection) {
|
|
37
|
+
parts.push(`<selection>\n${selection}\n</selection>`);
|
|
38
|
+
}
|
|
39
|
+
return parts.join("\n\n");
|
|
40
|
+
}
|
|
41
|
+
function isTerminalTurnMethod(method) {
|
|
42
|
+
return method === "turn/completed" ||
|
|
43
|
+
method === "turn/failed" ||
|
|
44
|
+
method === "turn/error" ||
|
|
45
|
+
method === "turn/cancelled" ||
|
|
46
|
+
method === "turn/canceled" ||
|
|
47
|
+
method === "turn/interrupted" ||
|
|
48
|
+
method === "turn/aborted";
|
|
49
|
+
}
|
|
50
|
+
function threadIdFromParams(params) {
|
|
51
|
+
const record = recordValue(params);
|
|
52
|
+
const thread = recordValue(record?.thread);
|
|
53
|
+
return stringValue(record?.threadId) || stringValue(thread?.id);
|
|
54
|
+
}
|
|
55
|
+
function turnIdFromParams(params) {
|
|
56
|
+
const record = recordValue(params);
|
|
57
|
+
const turn = recordValue(record?.turn);
|
|
58
|
+
return stringValue(record?.turnId) || stringValue(turn?.id);
|
|
59
|
+
}
|
|
60
|
+
function agentMessageDeltaFromParams(params) {
|
|
61
|
+
const record = recordValue(params);
|
|
62
|
+
if (!record) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
return stringValue(record.delta) || stringValue(record.text);
|
|
66
|
+
}
|
|
67
|
+
function terminalErrorFromParams(params) {
|
|
68
|
+
const record = recordValue(params);
|
|
69
|
+
const error = recordValue(record?.error);
|
|
70
|
+
return stringValue(record?.message) ||
|
|
71
|
+
stringValue(error?.message) ||
|
|
72
|
+
stringValue(record?.reason);
|
|
73
|
+
}
|
|
74
|
+
async function archiveThread(args) {
|
|
75
|
+
try {
|
|
76
|
+
await args.manager.request("thread/archive", { threadId: args.threadId }, 30_000);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
args.onError(`notes ai thread archive failed threadId=${args.threadId} error=${message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function runNotesAiSession(args) {
|
|
84
|
+
const instruction = stringValue(args.request.instruction);
|
|
85
|
+
if (!instruction) {
|
|
86
|
+
throw new Error("instruction is required");
|
|
87
|
+
}
|
|
88
|
+
if (args.abortController.signal.aborted) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let threadId = "";
|
|
92
|
+
let turnId = "";
|
|
93
|
+
let settled = false;
|
|
94
|
+
let cleanupNotification = () => { };
|
|
95
|
+
let settleCompleted = (_callback) => { };
|
|
96
|
+
const completed = new Promise((resolve, reject) => {
|
|
97
|
+
const timeout = setTimeout(() => {
|
|
98
|
+
if (!settled) {
|
|
99
|
+
reject(new Error("Timed out while waiting for Codex notes AI result"));
|
|
100
|
+
}
|
|
101
|
+
}, SESSION_TIMEOUT_MS);
|
|
102
|
+
settleCompleted = (callback) => {
|
|
103
|
+
if (settled) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
settled = true;
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
cleanupNotification();
|
|
109
|
+
callback();
|
|
110
|
+
};
|
|
111
|
+
args.abortController.signal.addEventListener("abort", () => {
|
|
112
|
+
settleCompleted(() => resolve());
|
|
113
|
+
}, { once: true });
|
|
114
|
+
cleanupNotification = args.manager.onNotification((method, params) => {
|
|
115
|
+
const eventThreadId = threadIdFromParams(params);
|
|
116
|
+
const eventTurnId = turnIdFromParams(params);
|
|
117
|
+
const isSessionEvent = (threadId && eventThreadId === threadId) ||
|
|
118
|
+
(turnId && eventTurnId === turnId);
|
|
119
|
+
if (!isSessionEvent) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (turnId && eventTurnId && eventTurnId !== turnId) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (method === "item/agentMessage/delta") {
|
|
126
|
+
const text = agentMessageDeltaFromParams(params);
|
|
127
|
+
if (!text) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
publishEvent({
|
|
131
|
+
nc: args.nc,
|
|
132
|
+
subject: args.eventsSubject,
|
|
133
|
+
payload: { type: "delta", sessionId: args.sessionId, text },
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!isTerminalTurnMethod(method)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (method === "turn/completed") {
|
|
141
|
+
settleCompleted(() => {
|
|
142
|
+
publishEvent({
|
|
143
|
+
nc: args.nc,
|
|
144
|
+
subject: args.eventsSubject,
|
|
145
|
+
payload: { type: "done", sessionId: args.sessionId },
|
|
146
|
+
});
|
|
147
|
+
resolve();
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const message = terminalErrorFromParams(params) || `Codex notes AI turn ended with ${method}`;
|
|
152
|
+
settleCompleted(() => reject(new Error(message)));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
const threadResult = recordValue(await args.manager.request("thread/start", {
|
|
157
|
+
cwd: null,
|
|
158
|
+
sessionStartSource: "clear",
|
|
159
|
+
}, 30_000));
|
|
160
|
+
const thread = recordValue(threadResult?.thread);
|
|
161
|
+
threadId = stringValue(thread?.id);
|
|
162
|
+
if (!threadId) {
|
|
163
|
+
throw new Error("Codex app-server did not return a thread id");
|
|
164
|
+
}
|
|
165
|
+
if (args.abortController.signal.aborted) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const turnResult = recordValue(await args.manager.request("turn/start", {
|
|
169
|
+
threadId,
|
|
170
|
+
input: buildCodexUserInput(buildNotesAiPrompt(args.request)),
|
|
171
|
+
}, 30_000));
|
|
172
|
+
const turn = recordValue(turnResult?.turn);
|
|
173
|
+
turnId = stringValue(turn?.id);
|
|
174
|
+
if (!turnId) {
|
|
175
|
+
throw new Error("Codex app-server did not return a turn id");
|
|
176
|
+
}
|
|
177
|
+
if (args.abortController.signal.aborted) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await completed.finally(() => cleanupNotification());
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
settleCompleted(() => { });
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
if (threadId) {
|
|
188
|
+
await archiveThread({
|
|
189
|
+
manager: args.manager,
|
|
190
|
+
threadId,
|
|
191
|
+
onError: args.onError,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function handleStart(args) {
|
|
197
|
+
const sessionId = stringValue(args.request.sessionId);
|
|
198
|
+
const eventsSubject = stringValue(args.request.eventsSubject);
|
|
199
|
+
if (!sessionId || !eventsSubject) {
|
|
200
|
+
throw new Error("sessionId and eventsSubject are required");
|
|
201
|
+
}
|
|
202
|
+
if (stringValue(args.request.agentId) && stringValue(args.request.agentId) !== args.agentId) {
|
|
203
|
+
throw new Error("agent id mismatch");
|
|
204
|
+
}
|
|
205
|
+
activeSessions.get(sessionId)?.abortController.abort();
|
|
206
|
+
const abortController = new AbortController();
|
|
207
|
+
activeSessions.set(sessionId, { abortController });
|
|
208
|
+
respond(args.msg, { requestId: args.request.requestId, ok: true, action: "start", sessionId });
|
|
209
|
+
void runNotesAiSession({
|
|
210
|
+
nc: args.nc,
|
|
211
|
+
manager: args.manager,
|
|
212
|
+
request: args.request,
|
|
213
|
+
sessionId,
|
|
214
|
+
eventsSubject,
|
|
215
|
+
abortController,
|
|
216
|
+
onError: args.onError,
|
|
217
|
+
}).catch((error) => {
|
|
218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
219
|
+
if (!abortController.signal.aborted) {
|
|
220
|
+
publishEvent({
|
|
221
|
+
nc: args.nc,
|
|
222
|
+
subject: eventsSubject,
|
|
223
|
+
payload: { type: "error", sessionId, error: message },
|
|
224
|
+
});
|
|
225
|
+
args.onError(`notes ai session failed sessionId=${sessionId} error=${message}`);
|
|
226
|
+
}
|
|
227
|
+
}).finally(() => {
|
|
228
|
+
if (activeSessions.get(sessionId)?.abortController === abortController) {
|
|
229
|
+
activeSessions.delete(sessionId);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
export async function handleNotesAiRpcMessage(args) {
|
|
234
|
+
let payload = {};
|
|
235
|
+
try {
|
|
236
|
+
payload = JSON.parse(codec.decode(args.msg.data));
|
|
237
|
+
const action = stringValue(payload.action);
|
|
238
|
+
if (action === "cancel") {
|
|
239
|
+
const sessionId = stringValue(payload.sessionId);
|
|
240
|
+
activeSessions.get(sessionId)?.abortController.abort();
|
|
241
|
+
activeSessions.delete(sessionId);
|
|
242
|
+
respond(args.msg, { requestId: payload.requestId, ok: true, action, sessionId });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (action !== "start") {
|
|
246
|
+
throw new Error("unsupported notes ai action");
|
|
247
|
+
}
|
|
248
|
+
await handleStart({
|
|
249
|
+
msg: args.msg,
|
|
250
|
+
nc: args.nc,
|
|
251
|
+
manager: args.manager,
|
|
252
|
+
request: payload,
|
|
253
|
+
agentId: args.agentId,
|
|
254
|
+
onError: args.onError,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
259
|
+
respond(args.msg, {
|
|
260
|
+
requestId: payload.requestId,
|
|
261
|
+
ok: false,
|
|
262
|
+
action: stringValue(payload.action),
|
|
263
|
+
error: message,
|
|
264
|
+
});
|
|
265
|
+
args.onError(`notes ai rpc failed action=${stringValue(payload.action) || "unknown"} error=${message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
export function subscribeToNotesAiRpc(args) {
|
|
269
|
+
args.nc.subscribe(args.subject, {
|
|
270
|
+
callback: (error, msg) => {
|
|
271
|
+
if (error) {
|
|
272
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
273
|
+
args.onError(`notes ai rpc subscription error: ${message}`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
void handleNotesAiRpcMessage({
|
|
277
|
+
msg,
|
|
278
|
+
nc: args.nc,
|
|
279
|
+
manager: args.manager,
|
|
280
|
+
agentId: args.agentId,
|
|
281
|
+
onError: args.onError,
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
args.onInfo(`notes ai rpc subscribed subject=${args.subject}`);
|
|
286
|
+
}
|
|
@@ -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:
|
|
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);
|