@alpacakit/agents 0.1.0-beta.39 → 0.2.0-beta.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/README.md +121 -1
- package/dist/agents/claude-session.d.ts +37 -0
- package/dist/agents/claude-session.d.ts.map +1 -0
- package/dist/agents/claude-session.js +419 -0
- package/dist/agents/claude-stream.d.ts +65 -0
- package/dist/agents/claude-stream.d.ts.map +1 -0
- package/dist/agents/claude-stream.js +111 -0
- package/dist/agents/claude.d.ts +2 -1
- package/dist/agents/claude.d.ts.map +1 -1
- package/dist/agents/claude.js +6 -107
- package/dist/agents/codex-session.d.ts +52 -0
- package/dist/agents/codex-session.d.ts.map +1 -0
- package/dist/agents/codex-session.js +495 -0
- package/dist/agents/codex.d.ts +2 -1
- package/dist/agents/codex.d.ts.map +1 -1
- package/dist/agents/codex.js +3 -1
- package/dist/codex/app-server.d.ts +50 -1
- package/dist/codex/app-server.d.ts.map +1 -1
- package/dist/codex/app-server.js +130 -27
- package/dist/codex/index.d.ts +3 -2
- package/dist/codex/index.d.ts.map +1 -1
- package/dist/codex/index.js +2 -1
- package/dist/codex/protocol.d.ts +101 -0
- package/dist/codex/protocol.d.ts.map +1 -1
- package/dist/codex/protocol.js +78 -0
- package/dist/core/process-close.d.ts +11 -0
- package/dist/core/process-close.d.ts.map +1 -0
- package/dist/core/process-close.js +27 -0
- package/dist/core/session.d.ts +100 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +29 -0
- package/dist/host/session-files.d.ts +22 -0
- package/dist/host/session-files.d.ts.map +1 -0
- package/dist/host/session-files.js +91 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/internal/controlled-process/supervisor-runtime.js +27 -1
- package/dist/kernel/definition.d.ts +11 -2
- package/dist/kernel/definition.d.ts.map +1 -1
- package/dist/kernel/facets.d.ts +38 -1
- package/dist/kernel/facets.d.ts.map +1 -1
- package/dist/kernel/facets.js +20 -0
- package/dist/kernel/runtime.d.ts +10 -6
- package/dist/kernel/runtime.d.ts.map +1 -1
- package/dist/kernel/runtime.js +4 -1
- package/dist/kernel/session.d.ts +97 -0
- package/dist/kernel/session.d.ts.map +1 -0
- package/dist/kernel/session.js +255 -0
- package/dist/testing/fake-exec.d.ts +6 -0
- package/dist/testing/fake-exec.d.ts.map +1 -1
- package/dist/testing/fake-exec.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex session adapter: one `codex app-server` process holding one thread.
|
|
3
|
+
*
|
|
4
|
+
* Protocol facts (codex 0.153.4, T53 / T57 / T61 lab runs):
|
|
5
|
+
* - `thread/start {cwd, sandbox, approvalPolicy, approvalsReviewer}` creates
|
|
6
|
+
* the thread and takes its writer lock; with a writable sandbox Codex also
|
|
7
|
+
* appends a `trust_level` entry for `cwd` to `~/.codex/config.toml`;
|
|
8
|
+
* - `thread/resume` must repeat the permissions — without `sandbox` the
|
|
9
|
+
* resumed turns ran as danger-full-access; it answers -32600 "already has
|
|
10
|
+
* an active writer" while another process holds the thread and -32600
|
|
11
|
+
* "… is archived" for an archived one (`thread/unarchive` first, opt-in);
|
|
12
|
+
* - `turn/start` answers the turn id at once; `turn/steer {expectedTurnId}`
|
|
13
|
+
* appends to it; `turn/interrupt` ends it with `turn/completed`
|
|
14
|
+
* status `interrupted` (the command's own `item/completed` may trail by ~36 s);
|
|
15
|
+
* - `item/commandExecution/requestApproval` is a server request answered by
|
|
16
|
+
* `{"decision": "accept" | "decline"}`; closing stdin releases the lock.
|
|
17
|
+
*/
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { CODEX_INCOMING_KIND, createCodexAppServer, } from "../codex/app-server.js";
|
|
20
|
+
import { CODEX_APP_SERVER_INCOMING, CODEX_APP_SERVER_METHOD, CODEX_SANDBOX_MODE, encodeCodexThreadPermissions, } from "../codex/protocol.js";
|
|
21
|
+
import { isAgentError } from "../core/error.js";
|
|
22
|
+
import { readArray, readBoolean, readRecord, readString, } from "../core/json.js";
|
|
23
|
+
import { APPROVAL_DECISION, SESSION_EVENT_TYPE, SESSION_OWNER_KIND, TURN_OUTCOME, } from "../core/session.js";
|
|
24
|
+
import { fileExists, readDirectoryNames, scanJsonlFirstLast, } from "../host/session-files.js";
|
|
25
|
+
import { assertHostSessionId, SessionState, sessionStateError, } from "../kernel/session.js";
|
|
26
|
+
const CODEX_ID = "codex";
|
|
27
|
+
const CODEX_COMMAND = "codex";
|
|
28
|
+
/** `approval_policy` words (codex 0.153.4 `AskForApproval`; the granular object form is not modeled). */
|
|
29
|
+
export const CODEX_APPROVAL_POLICY = {
|
|
30
|
+
untrusted: "untrusted",
|
|
31
|
+
onRequest: "on-request",
|
|
32
|
+
never: "never",
|
|
33
|
+
};
|
|
34
|
+
export const CODEX_APPROVALS_REVIEWER = {
|
|
35
|
+
user: "user",
|
|
36
|
+
autoReview: "auto_review",
|
|
37
|
+
guardianSubagent: "guardian_subagent",
|
|
38
|
+
};
|
|
39
|
+
const CODEX_SESSION = {
|
|
40
|
+
notification: {
|
|
41
|
+
threadId: "threadId",
|
|
42
|
+
turnId: "turnId",
|
|
43
|
+
turn: "turn",
|
|
44
|
+
item: "item",
|
|
45
|
+
id: "id",
|
|
46
|
+
type: "type",
|
|
47
|
+
text: "text",
|
|
48
|
+
status: "status",
|
|
49
|
+
items: "items",
|
|
50
|
+
phase: "phase",
|
|
51
|
+
finalAnswer: "final_answer",
|
|
52
|
+
agentMessage: "agentMessage",
|
|
53
|
+
userMessage: "userMessage",
|
|
54
|
+
reasoning: "reasoning",
|
|
55
|
+
command: "command",
|
|
56
|
+
reason: "reason",
|
|
57
|
+
},
|
|
58
|
+
turnStatus: {
|
|
59
|
+
completed: "completed",
|
|
60
|
+
interrupted: "interrupted",
|
|
61
|
+
failed: "failed",
|
|
62
|
+
},
|
|
63
|
+
decision: { accept: "accept", decline: "decline" },
|
|
64
|
+
error: {
|
|
65
|
+
archived: "is archived",
|
|
66
|
+
methodNotFound: -32601,
|
|
67
|
+
unsupported: "unsupported server request",
|
|
68
|
+
},
|
|
69
|
+
files: {
|
|
70
|
+
root: ".codex",
|
|
71
|
+
sessions: "sessions",
|
|
72
|
+
archivedSessions: "archived_sessions",
|
|
73
|
+
writerLocks: "thread-writer-locks",
|
|
74
|
+
lockExtension: ".lock",
|
|
75
|
+
rolloutExtension: ".jsonl",
|
|
76
|
+
type: "type",
|
|
77
|
+
payload: "payload",
|
|
78
|
+
sessionMeta: "session_meta",
|
|
79
|
+
turnContext: "turn_context",
|
|
80
|
+
cwd: "cwd",
|
|
81
|
+
approvalPolicy: "approval_policy",
|
|
82
|
+
approvalsReviewer: "approvals_reviewer",
|
|
83
|
+
sandboxPolicy: "sandbox_policy",
|
|
84
|
+
writableRoots: "writable_roots",
|
|
85
|
+
networkAccess: "network_access",
|
|
86
|
+
excludeTmpdirEnvVar: "exclude_tmpdir_env_var",
|
|
87
|
+
excludeSlashTmp: "exclude_slash_tmp",
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
const USER_INPUT_TEXT = "text";
|
|
91
|
+
const APPROVAL_METHODS = [
|
|
92
|
+
CODEX_APP_SERVER_INCOMING.commandExecutionRequestApproval,
|
|
93
|
+
CODEX_APP_SERVER_INCOMING.fileChangeRequestApproval,
|
|
94
|
+
];
|
|
95
|
+
const NON_TOOL_ITEM_TYPES = [
|
|
96
|
+
CODEX_SESSION.notification.agentMessage,
|
|
97
|
+
CODEX_SESSION.notification.userMessage,
|
|
98
|
+
CODEX_SESSION.notification.reasoning,
|
|
99
|
+
];
|
|
100
|
+
function wirePermissions(permissions) {
|
|
101
|
+
const sandbox = permissions.sandbox;
|
|
102
|
+
return encodeCodexThreadPermissions({
|
|
103
|
+
sandbox: sandbox.type,
|
|
104
|
+
approvalPolicy: permissions.approvalPolicy,
|
|
105
|
+
approvalsReviewer: permissions.approvalsReviewer,
|
|
106
|
+
workspaceWrite: sandbox.type === CODEX_SANDBOX_MODE.workspaceWrite
|
|
107
|
+
? {
|
|
108
|
+
writableRoots: sandbox.writableRoots,
|
|
109
|
+
networkAccess: sandbox.networkAccess,
|
|
110
|
+
excludeTmpdirEnvVar: sandbox.excludeTmpdirEnvVar,
|
|
111
|
+
excludeSlashTmp: sandbox.excludeSlashTmp,
|
|
112
|
+
}
|
|
113
|
+
: null,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function turnOutcomeFrom(status) {
|
|
117
|
+
if (status === CODEX_SESSION.turnStatus.interrupted) {
|
|
118
|
+
return TURN_OUTCOME.interrupted;
|
|
119
|
+
}
|
|
120
|
+
if (status === CODEX_SESSION.turnStatus.completed) {
|
|
121
|
+
return TURN_OUTCOME.completed;
|
|
122
|
+
}
|
|
123
|
+
return TURN_OUTCOME.failed;
|
|
124
|
+
}
|
|
125
|
+
/** The last `agentMessage` with phase `final_answer` among a completed turn's items. */
|
|
126
|
+
function finalAnswerOf(turn) {
|
|
127
|
+
let text = null;
|
|
128
|
+
for (const value of readArray(turn, CODEX_SESSION.notification.items) ?? []) {
|
|
129
|
+
const item = value !== null && typeof value === "object"
|
|
130
|
+
? value
|
|
131
|
+
: null;
|
|
132
|
+
if (item !== null &&
|
|
133
|
+
readString(item, CODEX_SESSION.notification.type) ===
|
|
134
|
+
CODEX_SESSION.notification.agentMessage &&
|
|
135
|
+
readString(item, CODEX_SESSION.notification.phase) ===
|
|
136
|
+
CODEX_SESSION.notification.finalAnswer) {
|
|
137
|
+
text = readString(item, CODEX_SESSION.notification.text) ?? text;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return text;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Codex accepts `decline` for every approval request even when its
|
|
144
|
+
* `availableDecisions` lists only `accept` plus amendment / `cancel`
|
|
145
|
+
* variants (T53 h2), so the caller is always offered both answers.
|
|
146
|
+
*/
|
|
147
|
+
const CODEX_APPROVAL_DECISIONS = [
|
|
148
|
+
APPROVAL_DECISION.accept,
|
|
149
|
+
APPROVAL_DECISION.decline,
|
|
150
|
+
];
|
|
151
|
+
class CodexSession {
|
|
152
|
+
id;
|
|
153
|
+
client;
|
|
154
|
+
ctx;
|
|
155
|
+
state = new SessionState(CODEX_ID);
|
|
156
|
+
pendingApprovals = new Map();
|
|
157
|
+
/** Notifications that arrived while `turn/start` was still unanswered. */
|
|
158
|
+
buffered = [];
|
|
159
|
+
reading;
|
|
160
|
+
constructor(id, client, incoming, ctx) {
|
|
161
|
+
this.id = id;
|
|
162
|
+
this.client = client;
|
|
163
|
+
this.ctx = ctx;
|
|
164
|
+
this.reading = this.read(incoming);
|
|
165
|
+
}
|
|
166
|
+
get events() {
|
|
167
|
+
return this.state.events;
|
|
168
|
+
}
|
|
169
|
+
async send(text) {
|
|
170
|
+
this.state.requireIdle();
|
|
171
|
+
// The slot is taken from here: a second send() during the round trip is
|
|
172
|
+
// refused, and notifications that beat the response are held back.
|
|
173
|
+
this.state.markStarting();
|
|
174
|
+
let turnId;
|
|
175
|
+
try {
|
|
176
|
+
({ turnId } = await this.client.request(CODEX_APP_SERVER_METHOD.turnStart, {
|
|
177
|
+
threadId: this.id,
|
|
178
|
+
input: [{ type: USER_INPUT_TEXT, text }],
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
this.state.clearStarting();
|
|
183
|
+
this.buffered.splice(0);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
const turn = this.state.beginTurn(turnId);
|
|
187
|
+
for (const held of this.buffered.splice(0)) {
|
|
188
|
+
this.handleNotification(held.method, held.params);
|
|
189
|
+
}
|
|
190
|
+
return turn;
|
|
191
|
+
}
|
|
192
|
+
async steer(text) {
|
|
193
|
+
const turnId = this.state.requireActive();
|
|
194
|
+
await this.client.request(CODEX_APP_SERVER_METHOD.turnSteer, {
|
|
195
|
+
threadId: this.id,
|
|
196
|
+
expectedTurnId: turnId,
|
|
197
|
+
input: [{ type: USER_INPUT_TEXT, text }],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async interrupt() {
|
|
201
|
+
const turnId = this.state.requireActive();
|
|
202
|
+
await this.client.request(CODEX_APP_SERVER_METHOD.turnInterrupt, {
|
|
203
|
+
threadId: this.id,
|
|
204
|
+
turnId,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async respond(requestId, decision) {
|
|
208
|
+
this.state.requireOpen();
|
|
209
|
+
const request = this.pendingApprovals.get(requestId);
|
|
210
|
+
if (request === undefined) {
|
|
211
|
+
throw sessionStateError(CODEX_ID, `no pending approval request "${requestId}"`);
|
|
212
|
+
}
|
|
213
|
+
this.pendingApprovals.delete(requestId);
|
|
214
|
+
this.client.respond(request.id, {
|
|
215
|
+
result: {
|
|
216
|
+
decision: decision === APPROVAL_DECISION.accept
|
|
217
|
+
? CODEX_SESSION.decision.accept
|
|
218
|
+
: CODEX_SESSION.decision.decline,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async close() {
|
|
223
|
+
await this.client.close();
|
|
224
|
+
await this.reading;
|
|
225
|
+
}
|
|
226
|
+
async read(incoming) {
|
|
227
|
+
try {
|
|
228
|
+
for (let step = await incoming.next(); !step.done; step = await incoming.next()) {
|
|
229
|
+
if (step.value.kind === CODEX_INCOMING_KIND.request) {
|
|
230
|
+
this.handleServerRequest(step.value);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
this.handleNotification(step.value.method, step.value.params);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
this.ctx.logger.log("error", "codex session reader failed", error);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
// Whatever happened above, the stream must end so `events` consumers
|
|
242
|
+
// and `close()` are released.
|
|
243
|
+
this.pendingApprovals.clear();
|
|
244
|
+
this.state.end(await this.client.exit, this.client.stderrTail);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
forActiveTurn(params) {
|
|
248
|
+
if (params === null ||
|
|
249
|
+
readString(params, CODEX_SESSION.notification.threadId) !== this.id) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return params;
|
|
253
|
+
}
|
|
254
|
+
handleNotification(method, raw) {
|
|
255
|
+
const params = this.forActiveTurn(raw);
|
|
256
|
+
if (params === null) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (this.state.isStarting) {
|
|
260
|
+
this.buffered.push({ method, params });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const active = this.state.activeTurnId;
|
|
264
|
+
if (method === CODEX_APP_SERVER_INCOMING.turnCompleted) {
|
|
265
|
+
const turn = readRecord(params, CODEX_SESSION.notification.turn);
|
|
266
|
+
if (turn === null ||
|
|
267
|
+
readString(turn, CODEX_SESSION.notification.id) !== active) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Approval requests belong to the turn that raised them.
|
|
271
|
+
this.pendingApprovals.clear();
|
|
272
|
+
this.state.endTurn(turnOutcomeFrom(readString(turn, CODEX_SESSION.notification.status)), finalAnswerOf(turn));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (readString(params, CODEX_SESSION.notification.turnId) !== active) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const item = readRecord(params, CODEX_SESSION.notification.item);
|
|
279
|
+
const itemType = item === null ? null : readString(item, CODEX_SESSION.notification.type);
|
|
280
|
+
if (item === null || itemType === null) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (method === CODEX_APP_SERVER_INCOMING.itemStarted &&
|
|
284
|
+
!NON_TOOL_ITEM_TYPES.includes(itemType)) {
|
|
285
|
+
this.state.emit({ type: SESSION_EVENT_TYPE.tool, name: itemType });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (method === CODEX_APP_SERVER_INCOMING.itemCompleted &&
|
|
289
|
+
itemType === CODEX_SESSION.notification.agentMessage) {
|
|
290
|
+
const text = readString(item, CODEX_SESSION.notification.text);
|
|
291
|
+
if (text !== null && text !== "") {
|
|
292
|
+
this.state.message(text);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
handleServerRequest(request) {
|
|
297
|
+
const params = this.forActiveTurn(request.params);
|
|
298
|
+
if (params === null || !APPROVAL_METHODS.includes(request.method)) {
|
|
299
|
+
// An unanswered server request would hang the turn; refusing it lets
|
|
300
|
+
// the model see the failure and go on.
|
|
301
|
+
this.ctx.logger.log("warn", `refusing unsupported codex server request "${request.method}"`);
|
|
302
|
+
this.client.respond(request.id, {
|
|
303
|
+
error: {
|
|
304
|
+
code: CODEX_SESSION.error.methodNotFound,
|
|
305
|
+
message: CODEX_SESSION.error.unsupported,
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const requestId = String(request.id);
|
|
311
|
+
this.pendingApprovals.set(requestId, request);
|
|
312
|
+
this.state.emit({
|
|
313
|
+
type: SESSION_EVENT_TYPE.approvalRequested,
|
|
314
|
+
requestId,
|
|
315
|
+
summary: readString(params, CODEX_SESSION.notification.command) ??
|
|
316
|
+
readString(params, CODEX_SESSION.notification.reason) ??
|
|
317
|
+
request.method,
|
|
318
|
+
decisions: CODEX_APPROVAL_DECISIONS,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function subscribe(client) {
|
|
323
|
+
return client.incoming[Symbol.asyncIterator]();
|
|
324
|
+
}
|
|
325
|
+
async function openCodexSession(options, ctx) {
|
|
326
|
+
const client = createCodexAppServer({
|
|
327
|
+
exec: ctx.exec,
|
|
328
|
+
clock: ctx.clock,
|
|
329
|
+
env: ctx.env,
|
|
330
|
+
cwd: options.workspace,
|
|
331
|
+
command: CODEX_COMMAND,
|
|
332
|
+
});
|
|
333
|
+
const incoming = subscribe(client);
|
|
334
|
+
try {
|
|
335
|
+
const { threadId } = await client.request(CODEX_APP_SERVER_METHOD.threadStart, {
|
|
336
|
+
cwd: options.workspace,
|
|
337
|
+
...wirePermissions(options.permissions),
|
|
338
|
+
});
|
|
339
|
+
return new CodexSession(threadId, client, incoming, ctx);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
await client.close();
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function resumeCodexSession(id, options, ctx) {
|
|
347
|
+
assertHostSessionId(id, CODEX_ID);
|
|
348
|
+
const client = createCodexAppServer({
|
|
349
|
+
exec: ctx.exec,
|
|
350
|
+
clock: ctx.clock,
|
|
351
|
+
env: ctx.env,
|
|
352
|
+
cwd: options.workspace,
|
|
353
|
+
command: CODEX_COMMAND,
|
|
354
|
+
});
|
|
355
|
+
const incoming = subscribe(client);
|
|
356
|
+
const params = {
|
|
357
|
+
threadId: id,
|
|
358
|
+
excludeTurns: true,
|
|
359
|
+
...wirePermissions(options.permissions),
|
|
360
|
+
};
|
|
361
|
+
try {
|
|
362
|
+
let resumed;
|
|
363
|
+
try {
|
|
364
|
+
resumed = await client.request(CODEX_APP_SERVER_METHOD.threadResume, params);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
// Unarchiving is a visible side effect (the thread returns to the
|
|
368
|
+
// ChatGPT app's active list), so it happens only on request.
|
|
369
|
+
if (!options.unarchive || !isArchivedError(error)) {
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
await client.request(CODEX_APP_SERVER_METHOD.threadUnarchive, {
|
|
373
|
+
threadId: id,
|
|
374
|
+
});
|
|
375
|
+
resumed = await client.request(CODEX_APP_SERVER_METHOD.threadResume, params);
|
|
376
|
+
}
|
|
377
|
+
return new CodexSession(resumed.threadId, client, incoming, ctx);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
await client.close();
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function isArchivedError(error) {
|
|
385
|
+
return (isAgentError(error, "failed") &&
|
|
386
|
+
error.message.includes(CODEX_SESSION.error.archived));
|
|
387
|
+
}
|
|
388
|
+
function codexRoot(ctx, ...segments) {
|
|
389
|
+
return path.join(ctx.location.homeDir, CODEX_SESSION.files.root, ...segments);
|
|
390
|
+
}
|
|
391
|
+
/** `rollout-<time>-<id>.jsonl` under `sessions/YYYY/MM/DD/` (live) or `archived_sessions/` (archived). */
|
|
392
|
+
async function findCodexRollout(ctx, id) {
|
|
393
|
+
const suffix = `-${id.toLowerCase()}${CODEX_SESSION.files.rolloutExtension}`;
|
|
394
|
+
const matches = (name) => name.toLowerCase().endsWith(suffix);
|
|
395
|
+
const root = codexRoot(ctx, CODEX_SESSION.files.sessions);
|
|
396
|
+
for (const year of (await readDirectoryNames(root)).sort().reverse()) {
|
|
397
|
+
for (const month of (await readDirectoryNames(path.join(root, year)))
|
|
398
|
+
.sort()
|
|
399
|
+
.reverse()) {
|
|
400
|
+
const monthDir = path.join(root, year, month);
|
|
401
|
+
for (const day of (await readDirectoryNames(monthDir)).sort().reverse()) {
|
|
402
|
+
const dayDir = path.join(monthDir, day);
|
|
403
|
+
const file = (await readDirectoryNames(dayDir)).find(matches);
|
|
404
|
+
if (file !== undefined) {
|
|
405
|
+
return { file: path.join(dayDir, file), archived: false };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const archivedRoot = codexRoot(ctx, CODEX_SESSION.files.archivedSessions);
|
|
411
|
+
const archived = (await readDirectoryNames(archivedRoot)).find(matches);
|
|
412
|
+
return archived === undefined
|
|
413
|
+
? null
|
|
414
|
+
: { file: path.join(archivedRoot, archived), archived: true };
|
|
415
|
+
}
|
|
416
|
+
function oneOf(values, value) {
|
|
417
|
+
return value !== null && values.includes(value)
|
|
418
|
+
? value
|
|
419
|
+
: null;
|
|
420
|
+
}
|
|
421
|
+
/** Only shapes seen in rollouts decode; anything unfamiliar is `null`, never a guess. */
|
|
422
|
+
function sandboxPolicyFrom(policy) {
|
|
423
|
+
const type = policy === null ? null : readString(policy, CODEX_SESSION.files.type);
|
|
424
|
+
if (type === CODEX_SANDBOX_MODE.readOnly ||
|
|
425
|
+
type === CODEX_SANDBOX_MODE.dangerFullAccess) {
|
|
426
|
+
return { type };
|
|
427
|
+
}
|
|
428
|
+
if (policy === null || type !== CODEX_SANDBOX_MODE.workspaceWrite) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const roots = readArray(policy, CODEX_SESSION.files.writableRoots) ?? [];
|
|
432
|
+
const networkAccess = readBoolean(policy, CODEX_SESSION.files.networkAccess);
|
|
433
|
+
const excludeTmpdirEnvVar = readBoolean(policy, CODEX_SESSION.files.excludeTmpdirEnvVar);
|
|
434
|
+
const excludeSlashTmp = readBoolean(policy, CODEX_SESSION.files.excludeSlashTmp);
|
|
435
|
+
if (!roots.every((root) => typeof root === "string") ||
|
|
436
|
+
networkAccess === null ||
|
|
437
|
+
excludeTmpdirEnvVar === null ||
|
|
438
|
+
excludeSlashTmp === null) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
type,
|
|
443
|
+
writableRoots: roots,
|
|
444
|
+
networkAccess,
|
|
445
|
+
excludeTmpdirEnvVar,
|
|
446
|
+
excludeSlashTmp,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function permissionsFrom(context) {
|
|
450
|
+
const approvalPolicy = oneOf(Object.values(CODEX_APPROVAL_POLICY), readString(context, CODEX_SESSION.files.approvalPolicy));
|
|
451
|
+
const approvalsReviewer = oneOf(Object.values(CODEX_APPROVALS_REVIEWER), readString(context, CODEX_SESSION.files.approvalsReviewer));
|
|
452
|
+
const sandbox = sandboxPolicyFrom(readRecord(context, CODEX_SESSION.files.sandboxPolicy));
|
|
453
|
+
if (approvalPolicy === null ||
|
|
454
|
+
approvalsReviewer === null ||
|
|
455
|
+
sandbox === null) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return { approvalPolicy, approvalsReviewer, sandbox };
|
|
459
|
+
}
|
|
460
|
+
function payloadOfType(record, type) {
|
|
461
|
+
return readString(record, CODEX_SESSION.files.type) === type
|
|
462
|
+
? readRecord(record, CODEX_SESSION.files.payload)
|
|
463
|
+
: null;
|
|
464
|
+
}
|
|
465
|
+
async function inspectCodexSession(id, ctx) {
|
|
466
|
+
assertHostSessionId(id, CODEX_ID);
|
|
467
|
+
const rollout = await findCodexRollout(ctx, id);
|
|
468
|
+
if (rollout === null) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
const cwd = await scanJsonlFirstLast(rollout.file, (record) => {
|
|
472
|
+
const meta = payloadOfType(record, CODEX_SESSION.files.sessionMeta);
|
|
473
|
+
return meta === null ? null : readString(meta, CODEX_SESSION.files.cwd);
|
|
474
|
+
});
|
|
475
|
+
if (cwd.first === null) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
const contexts = await scanJsonlFirstLast(rollout.file, (record) => payloadOfType(record, CODEX_SESSION.files.turnContext));
|
|
479
|
+
const locked = await fileExists(codexRoot(ctx, CODEX_SESSION.files.writerLocks, `${id}${CODEX_SESSION.files.lockExtension}`));
|
|
480
|
+
return {
|
|
481
|
+
workspace: cwd.first,
|
|
482
|
+
lastPermissions: contexts.last === null ? null : permissionsFrom(contexts.last),
|
|
483
|
+
owner: locked
|
|
484
|
+
? { kind: SESSION_OWNER_KIND.process, pid: null, entrypoint: null }
|
|
485
|
+
: { kind: SESSION_OWNER_KIND.none },
|
|
486
|
+
archived: rollout.archived,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
export function codexSessions() {
|
|
490
|
+
return {
|
|
491
|
+
open: openCodexSession,
|
|
492
|
+
resume: resumeCodexSession,
|
|
493
|
+
inspect: inspectCodexSession,
|
|
494
|
+
};
|
|
495
|
+
}
|
package/dist/agents/codex.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type FailureMatcher } from "../kernel/classify.js";
|
|
9
9
|
import type { AgentDefinition } from "../kernel/definition.js";
|
|
10
|
+
import { type CodexSessionPermissions } from "./codex-session.js";
|
|
10
11
|
export declare const CODEX_HOME_ENV_VAR = "CODEX_HOME";
|
|
11
12
|
export declare const CODEX_API_KEY_ENV_VAR = "OPENAI_API_KEY";
|
|
12
13
|
/**
|
|
@@ -15,5 +16,5 @@ export declare const CODEX_API_KEY_ENV_VAR = "OPENAI_API_KEY";
|
|
|
15
16
|
* surface the same provider text.
|
|
16
17
|
*/
|
|
17
18
|
export declare function capacityTextMatcher(): FailureMatcher;
|
|
18
|
-
export declare function codex(): AgentDefinition<"codex">;
|
|
19
|
+
export declare function codex(): AgentDefinition<"codex", CodexSessionPermissions>;
|
|
19
20
|
//# sourceMappingURL=codex.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/agents/codex.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyCH,OAAO,EAAE,KAAK,cAAc,EAAqB,MAAM,uBAAuB,CAAC;AAG/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/agents/codex.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyCH,OAAO,EAAE,KAAK,cAAc,EAAqB,MAAM,uBAAuB,CAAC;AAG/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EACL,KAAK,uBAAuB,EAE7B,MAAM,oBAAoB,CAAC;AAE5B,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAC/C,eAAO,MAAM,qBAAqB,mBAAmB,CAAC;AA6GtD;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,CAepD;AAiND,wBAAgB,KAAK,IAAI,eAAe,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAsFzE"}
|
package/dist/agents/codex.js
CHANGED
|
@@ -21,6 +21,7 @@ import { parseCooldownHint } from "../kernel/classify.js";
|
|
|
21
21
|
import { defineCliAgent } from "../kernel/cli-agent.js";
|
|
22
22
|
import { cliLoginCommand } from "../kernel/cli-login.js";
|
|
23
23
|
import { SANDBOX_MODE } from "../kernel/options.js";
|
|
24
|
+
import { codexSessions, } from "./codex-session.js";
|
|
24
25
|
export const CODEX_HOME_ENV_VAR = "CODEX_HOME";
|
|
25
26
|
export const CODEX_API_KEY_ENV_VAR = "OPENAI_API_KEY";
|
|
26
27
|
const CODEX = {
|
|
@@ -298,7 +299,7 @@ function codexLogin(args) {
|
|
|
298
299
|
});
|
|
299
300
|
}
|
|
300
301
|
export function codex() {
|
|
301
|
-
|
|
302
|
+
const definition = defineCliAgent({
|
|
302
303
|
id: CODEX.id,
|
|
303
304
|
command: CODEX.command,
|
|
304
305
|
args: {
|
|
@@ -374,4 +375,5 @@ export function codex() {
|
|
|
374
375
|
remove: removeCodexMcp,
|
|
375
376
|
},
|
|
376
377
|
});
|
|
378
|
+
return { ...definition, session: codexSessions() };
|
|
377
379
|
}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Codex app-server client over stdio JSON-RPC. One client is one child
|
|
3
|
+
* process, held as long as the caller wants: host-plane calls create one
|
|
4
|
+
* per call (`withCodexAppServer`), a Session keeps one for the thread's
|
|
5
|
+
* whole life. Both directions are modeled — client requests get typed,
|
|
6
|
+
* decoded results; server notifications and server-initiated requests
|
|
7
|
+
* (approval prompts) arrive on `incoming`, and `respond` answers the latter.
|
|
8
|
+
* A request timeout fails that request only; it never kills the process,
|
|
9
|
+
* because a Session's other traffic shares it.
|
|
10
|
+
*/
|
|
11
|
+
import { type UnknownRecord } from "../core/json.js";
|
|
12
|
+
import type { Clock, Exec, ExecExit } from "../core/ports.js";
|
|
2
13
|
import type { CodexAppServerMethods, CodexMcpServerStatus } from "./protocol.js";
|
|
3
14
|
/**
|
|
4
15
|
* How the client identifies itself to the codex app-server `initialize`
|
|
@@ -20,8 +31,46 @@ export type CodexAppServerOptions = {
|
|
|
20
31
|
readonly requestTimeoutMs?: number;
|
|
21
32
|
readonly clientInfo?: AppServerClientInfo;
|
|
22
33
|
};
|
|
34
|
+
export declare const CODEX_INCOMING_KIND: {
|
|
35
|
+
readonly notification: "notification";
|
|
36
|
+
readonly request: "request";
|
|
37
|
+
};
|
|
38
|
+
/** JSON-RPC ids the server chooses for its own requests. */
|
|
39
|
+
export type CodexServerRequestId = number | string;
|
|
40
|
+
/** What the server sends on its own: notifications, or requests it awaits an answer to. */
|
|
41
|
+
export type CodexIncoming = {
|
|
42
|
+
readonly kind: typeof CODEX_INCOMING_KIND.notification;
|
|
43
|
+
readonly method: string;
|
|
44
|
+
readonly params: UnknownRecord | null;
|
|
45
|
+
} | {
|
|
46
|
+
readonly kind: typeof CODEX_INCOMING_KIND.request;
|
|
47
|
+
readonly id: CodexServerRequestId;
|
|
48
|
+
readonly method: string;
|
|
49
|
+
readonly params: UnknownRecord | null;
|
|
50
|
+
};
|
|
51
|
+
export type CodexServerRequestAnswer = {
|
|
52
|
+
readonly result: unknown;
|
|
53
|
+
} | {
|
|
54
|
+
readonly error: {
|
|
55
|
+
readonly code: number;
|
|
56
|
+
readonly message: string;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
23
59
|
export interface CodexAppServer {
|
|
24
60
|
request<M extends keyof CodexAppServerMethods>(method: M, params: CodexAppServerMethods[M]["params"]): Promise<CodexAppServerMethods[M]["result"]>;
|
|
61
|
+
/**
|
|
62
|
+
* Server-originated traffic in arrival order. Each iteration is its own
|
|
63
|
+
* subscription that starts at the moment of subscribing; nothing is
|
|
64
|
+
* buffered for consumers that never subscribe.
|
|
65
|
+
*/
|
|
66
|
+
readonly incoming: AsyncIterable<CodexIncoming>;
|
|
67
|
+
/** Answer a server-initiated request by its id. */
|
|
68
|
+
respond(id: CodexServerRequestId, answer: CodexServerRequestAnswer): void;
|
|
69
|
+
/** Resolves when the child has exited (after `close()`, or on its own). */
|
|
70
|
+
readonly exit: Promise<ExecExit>;
|
|
71
|
+
/** The last ~2 KB of the server's stderr, for failure evidence. */
|
|
72
|
+
readonly stderrTail: string;
|
|
73
|
+
/** End stdin (the server exits on EOF), escalate to SIGTERM then SIGKILL after graces, wait for exit. */
|
|
25
74
|
close(): Promise<void>;
|
|
26
75
|
}
|
|
27
76
|
export declare function createCodexAppServer(options: CodexAppServerOptions): CodexAppServer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-server.d.ts","sourceRoot":"","sources":["../../src/codex/app-server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-server.d.ts","sourceRoot":"","sources":["../../src/codex/app-server.ts"],"names":[],"mappings":"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"}
|