@adecore/agents 0.23.0 → 0.24.0
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 +3 -3
- package/dist/chat/attachment-store.js +4 -5
- package/dist/chat/backend.d.ts +6 -1
- package/dist/chat/background-work.d.ts +1 -1
- package/dist/chat/background-work.js +2 -1
- package/dist/chat/chat-core.d.ts +53 -161
- package/dist/chat/chat-core.js +146 -112
- package/dist/chat/chat-log.js +3 -3
- package/dist/chat/chat-process.d.ts +21 -0
- package/dist/chat/chat-process.js +109 -9
- package/dist/chat/chat-session.d.ts +10 -17
- package/dist/chat/chat-session.js +141 -182
- package/dist/chat/chat-store.d.ts +1 -0
- package/dist/chat/chat-store.js +105 -1
- package/dist/chat/claude-backend.d.ts +4 -0
- package/dist/chat/claude-backend.js +51 -27
- package/dist/chat/claude-protocol.d.ts +12 -0
- package/dist/chat/claude-protocol.js +137 -7
- package/dist/chat/claude-title.d.ts +1 -3
- package/dist/chat/claude-title.js +11 -39
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +36 -17
- package/dist/chat/codex-backend.d.ts +21 -1
- package/dist/chat/codex-backend.js +168 -10
- package/dist/chat/codex-protocol.d.ts +2 -0
- package/dist/chat/codex-protocol.js +37 -3
- package/dist/chat/codex-thread.js +1 -3
- package/dist/chat/codex-transport.d.ts +3 -0
- package/dist/chat/codex-transport.js +38 -35
- package/dist/chat/context-breakdown.js +2 -2
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.js +2 -2
- package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/dist/chat/index.d.ts +29 -0
- package/dist/chat/index.js +23 -0
- package/dist/chat/projector.d.ts +2 -1
- package/dist/chat/projector.js +12 -3
- package/dist/chat/request-summary.js +1 -4
- package/dist/chat/subagent-settlement.js +1 -3
- package/dist/chat/thread.d.ts +3 -0
- package/dist/chat/thread.js +20 -3
- package/dist/chat/ui-queries.d.ts +2 -0
- package/dist/chat/ui-queries.js +40 -5
- package/dist/chat/ui-replies.d.ts +33 -0
- package/dist/chat/ui-replies.js +152 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +3 -0
- package/dist/coordination/index.d.ts +15 -0
- package/dist/coordination/index.js +16 -0
- package/dist/host/handlers.js +8 -0
- package/dist/host/index.d.ts +7 -0
- package/dist/host/index.js +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/claude.js +3 -1
- package/dist/providers/codex.js +3 -1
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.js +12 -0
- package/dist/run-process.js +54 -14
- package/dist/skills.d.ts +1 -0
- package/dist/skills.js +4 -0
- package/dist/tasks/task-coordinator.d.ts +4 -0
- package/dist/tasks/task-coordinator.js +16 -2
- package/dist/tasks/wiring.js +7 -1
- package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
- package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
- package/dist/testing/bin/fake-claude.d.ts +1 -0
- package/dist/testing/bin/fake-claude.js +6 -0
- package/dist/testing/bin/fake-codex.d.ts +1 -0
- package/dist/testing/bin/fake-codex.js +6 -0
- package/dist/{chat → testing}/fake-claude.d.ts +1 -1
- package/dist/{chat → testing}/fake-claude.js +0 -4
- package/dist/{chat → testing}/fake-cli.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.js +70 -8
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +7 -0
- package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
- package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
- package/dist/usage/exchange.d.ts +5 -3
- package/dist/usage/exchange.js +25 -16
- package/dist/usage/index-file.d.ts +1 -0
- package/dist/usage/index-file.js +20 -2
- package/dist/usage/index.d.ts +9 -0
- package/dist/usage/index.js +6 -0
- package/dist/usage/limits/normalize.js +1 -3
- package/dist/usage/pricing.d.ts +5 -3
- package/dist/usage/pricing.js +28 -19
- package/dist/usage/scanner.js +39 -11
- package/dist/usage/usage-service.js +8 -3
- package/package.json +72 -11
- package/src/chat/attachment-store.ts +4 -6
- package/src/chat/backend.ts +8 -0
- package/src/chat/background-work.ts +2 -2
- package/src/chat/chat-core.ts +221 -143
- package/src/chat/chat-log.ts +3 -3
- package/src/chat/chat-process.ts +128 -8
- package/src/chat/chat-session.ts +153 -204
- package/src/chat/chat-store.ts +104 -1
- package/src/chat/claude-backend.ts +58 -26
- package/src/chat/claude-protocol.ts +147 -8
- package/src/chat/claude-title.ts +11 -39
- package/src/chat/claude-transcript.ts +38 -18
- package/src/chat/codex-backend.ts +182 -20
- package/src/chat/codex-protocol.ts +40 -5
- package/src/chat/codex-thread.ts +1 -4
- package/src/chat/codex-transport.ts +45 -35
- package/src/chat/context-breakdown.ts +2 -2
- package/src/chat/errors.ts +1 -0
- package/src/chat/external-sessions.ts +2 -2
- package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/src/chat/index.ts +49 -0
- package/src/chat/projector.ts +13 -4
- package/src/chat/request-summary.ts +8 -5
- package/src/chat/subagent-settlement.ts +1 -4
- package/src/chat/thread.ts +22 -3
- package/src/chat/ui-queries.ts +46 -5
- package/src/chat/ui-replies.ts +187 -0
- package/src/context/index.ts +26 -0
- package/src/coordination/index.ts +67 -0
- package/src/host/handlers.ts +8 -0
- package/src/host/index.ts +8 -0
- package/src/index.ts +20 -0
- package/src/providers/accounts/status.ts +1 -4
- package/src/providers/claude.ts +3 -1
- package/src/providers/codex.ts +3 -1
- package/src/providers/index.ts +15 -0
- package/src/run-process.ts +56 -16
- package/src/skills.ts +5 -0
- package/src/tasks/task-coordinator.ts +22 -2
- package/src/tasks/wiring.ts +7 -1
- package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
- package/src/testing/bin/fake-claude.ts +7 -0
- package/src/testing/bin/fake-codex.ts +7 -0
- package/src/{chat → testing}/fake-claude.ts +1 -5
- package/src/{chat → testing}/fake-cli.ts +1 -1
- package/src/{chat → testing}/fake-codex.ts +75 -6
- package/src/testing/index.ts +7 -0
- package/src/{outbox → testing}/manual-clock.ts +1 -1
- package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
- package/src/usage/exchange.ts +31 -20
- package/src/usage/index-file.ts +23 -2
- package/src/usage/index.ts +10 -0
- package/src/usage/limits/normalize.ts +1 -4
- package/src/usage/pricing.ts +34 -23
- package/src/usage/scanner.ts +48 -11
- package/src/usage/usage-service.ts +8 -3
- /package/dist/{chat → testing}/fake-cli.js +0 -0
- /package/dist/{outbox → testing}/manual-clock.js +0 -0
- /package/dist/{watch-test-helpers.js → testing/watch.js} +0 -0
|
@@ -1,16 +1,14 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { accessSync, constants } from 'node:fs';
|
|
2
|
-
import { attachmentImageMime } from '@adecore/agent-contracts';
|
|
3
|
+
import { isRecord, attachmentImageMime } from '@adecore/agent-contracts';
|
|
3
4
|
import { errorText } from '../error-text.js';
|
|
4
5
|
import { codexServiceTier, codexThreadOptions } from '../providers/codex.js';
|
|
5
|
-
import { CodexProtocol } from './codex-protocol.js';
|
|
6
|
+
import { codexGoal, CodexProtocol } from './codex-protocol.js';
|
|
6
7
|
import { CodexTransport, DEFAULT_CODEX_CLIENT } from './codex-transport.js';
|
|
7
8
|
import { attachmentNote } from './input.js';
|
|
8
9
|
function textInput(text) {
|
|
9
10
|
return [{ type: 'text', text, text_elements: [] }];
|
|
10
11
|
}
|
|
11
|
-
function isRecord(value) {
|
|
12
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
-
}
|
|
14
12
|
/* The enabled skills of a `skills/list` answer; Codex reports one entry per folder it was asked about. */
|
|
15
13
|
export function parseSkillsList(result) {
|
|
16
14
|
const data = isRecord(result) && Array.isArray(result.data) ? result.data : [];
|
|
@@ -29,6 +27,30 @@ export function parseSkillsList(result) {
|
|
|
29
27
|
}
|
|
30
28
|
return [...skills.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
31
29
|
}
|
|
30
|
+
// The words Claude Code 2.1.296 takes for clearing a goal; Codex reads the same, so `/goal` means one thing in every chat.
|
|
31
|
+
const CLEAR_WORDS = new Set(['clear', 'stop', 'off', 'reset', 'none', 'cancel']);
|
|
32
|
+
/* A `/goal` message the way the CLIs read one: a bare `/goal` shows the goal, a control word acts on it, and anything else is a new objective. Null for any other message. */
|
|
33
|
+
export function parseGoalCommand(text) {
|
|
34
|
+
const match = /^\/goal(?:\s+([\s\S]*))?$/.exec(text.trim());
|
|
35
|
+
if (!match) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const argument = (match[1] ?? '').trim();
|
|
39
|
+
const word = argument.toLowerCase();
|
|
40
|
+
if (word === '') {
|
|
41
|
+
return { kind: 'show' };
|
|
42
|
+
}
|
|
43
|
+
if (CLEAR_WORDS.has(word)) {
|
|
44
|
+
return { kind: 'clear' };
|
|
45
|
+
}
|
|
46
|
+
if (word === 'pause' || word === 'resume') {
|
|
47
|
+
return { kind: word };
|
|
48
|
+
}
|
|
49
|
+
return { kind: 'set', objective: argument };
|
|
50
|
+
}
|
|
51
|
+
function goalLine(goal) {
|
|
52
|
+
return `Goal ${goal.status.replace('-', ' ')}: ${goal.objective}`;
|
|
53
|
+
}
|
|
32
54
|
/*
|
|
33
55
|
* One `codex app-server` process on JSON-RPC over stdio. `start` handshakes (initialize, then
|
|
34
56
|
* thread/start or thread/resume) before the first turn goes out, so every later request knows the
|
|
@@ -53,6 +75,8 @@ export class CodexBackend {
|
|
|
53
75
|
turnUnnamed = false;
|
|
54
76
|
/* A stop that came in while the turn was unnamed, sent once `turn/started` names it. */
|
|
55
77
|
interruptOwed = false;
|
|
78
|
+
// The thread's goal as Codex last reported it, which a stop pauses first so Codex opens no turn after it.
|
|
79
|
+
goalNow = null;
|
|
56
80
|
constructor(launch, host, options = {}) {
|
|
57
81
|
this.launch = launch;
|
|
58
82
|
this.host = host;
|
|
@@ -80,6 +104,19 @@ export class CodexBackend {
|
|
|
80
104
|
onExit: (exitCode, stderr) => this.handleExit(transport, exitCode, stderr)
|
|
81
105
|
});
|
|
82
106
|
this.transport = transport;
|
|
107
|
+
try {
|
|
108
|
+
await this.handshake(transport);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
// A chat that failed to start drops this backend, so nothing else would end an app-server stuck in its handshake.
|
|
112
|
+
if (this.transport === transport) {
|
|
113
|
+
this.transport = null;
|
|
114
|
+
void transport.dispose();
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async handshake(transport) {
|
|
83
120
|
await transport.request('initialize', { clientInfo: this.client, capabilities: { experimentalApi: true, requestAttestation: false } });
|
|
84
121
|
transport.notify('initialized', {});
|
|
85
122
|
const tier = codexServiceTier(this.launch.selection);
|
|
@@ -97,7 +134,10 @@ export class CodexBackend {
|
|
|
97
134
|
this.contextPending = this.launch.resumeNote;
|
|
98
135
|
}
|
|
99
136
|
catch (error) {
|
|
100
|
-
//
|
|
137
|
+
// A refusal means the thread is gone from Codex's store and a fresh one keeps the chat usable; a silence or an exit says nothing about the thread.
|
|
138
|
+
if (!(error instanceof Error) || error.name !== 'CodexRpcError') {
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
101
141
|
this.emit({ type: 'note', level: 'warning', text: `Codex could not resume its thread (${errorText(error)}). Started a new one.` });
|
|
102
142
|
result = await transport.request('thread/start', { ...params, ...instructions });
|
|
103
143
|
}
|
|
@@ -112,6 +152,17 @@ export class CodexBackend {
|
|
|
112
152
|
}
|
|
113
153
|
this.emit(event);
|
|
114
154
|
}
|
|
155
|
+
await this.readGoal(transport).catch(() => {
|
|
156
|
+
// An app-server without goals, or with the feature turned off, has none to report.
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
async readGoal(transport) {
|
|
160
|
+
const result = await transport.request('thread/goal/get', { threadId: this.threadId });
|
|
161
|
+
const goal = codexGoal(isRecord(result) ? result.goal : null);
|
|
162
|
+
if (this.transport === transport) {
|
|
163
|
+
this.emit({ type: 'goal', goal });
|
|
164
|
+
}
|
|
165
|
+
return goal;
|
|
115
166
|
}
|
|
116
167
|
turnInput(input) {
|
|
117
168
|
const images = input.attachments.filter((attachment) => attachmentImageMime(attachment) !== null);
|
|
@@ -142,18 +193,118 @@ export class CodexBackend {
|
|
|
142
193
|
return [...textInput(parts.join('')), ...images.map((attachment) => ({ type: 'localImage', path: attachment.path }))];
|
|
143
194
|
}
|
|
144
195
|
sendTurn(input) {
|
|
196
|
+
const command = input.attachments.length === 0 ? parseGoalCommand(input.text) : null;
|
|
197
|
+
if (command !== null) {
|
|
198
|
+
void this.runGoalCommand(command, input);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
145
201
|
const nativeInput = this.turnInput(input);
|
|
146
|
-
const effort = this.launch.selection.options.effort;
|
|
147
|
-
const tier = codexServiceTier(this.launch.selection);
|
|
148
202
|
this.turnUnnamed = true;
|
|
149
203
|
this.promptId = input.promptId ?? null;
|
|
150
|
-
this.request('turn/start',
|
|
204
|
+
this.request('turn/start', this.turnStartParams(nativeInput), () => this.acceptPrompt(input.promptId));
|
|
205
|
+
}
|
|
206
|
+
turnStartParams(nativeInput) {
|
|
207
|
+
const effort = this.launch.selection.options.effort;
|
|
208
|
+
const tier = codexServiceTier(this.launch.selection);
|
|
209
|
+
return {
|
|
151
210
|
threadId: this.threadId,
|
|
152
211
|
input: nativeInput,
|
|
153
212
|
model: this.launch.selection.model,
|
|
154
213
|
...(typeof effort === 'string' ? { effort } : {}),
|
|
155
214
|
...(tier === null ? {} : { serviceTier: tier })
|
|
156
|
-
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/*
|
|
218
|
+
* `/goal` through the app-server's goal calls, as Codex's own TUI does. A new objective or a resume
|
|
219
|
+
* starts the first goal turn itself, with this chat's settings, and only then makes the goal active:
|
|
220
|
+
* Codex opens every later turn on its own, and one it opened before this turn would take the old
|
|
221
|
+
* settings. Every other form settles at once with a reply, no turn of the model's.
|
|
222
|
+
*/
|
|
223
|
+
async runGoalCommand(command, input) {
|
|
224
|
+
const transport = this.transport;
|
|
225
|
+
if (!transport) {
|
|
226
|
+
this.emit({ type: 'failed', message: 'thread/goal failed: Codex is not running' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const threadId = this.threadId;
|
|
230
|
+
const setGoal = async (params) => {
|
|
231
|
+
const result = await transport.request('thread/goal/set', { threadId, origin: 'user', ...params });
|
|
232
|
+
if (this.transport === transport) {
|
|
233
|
+
this.emit({ type: 'goal', goal: codexGoal(isRecord(result) ? result.goal : null) });
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
try {
|
|
237
|
+
const current = await this.readGoal(transport);
|
|
238
|
+
if (command.kind === 'show') {
|
|
239
|
+
this.reply(input.promptId, current === null ? 'No goal is set.' : goalLine(current));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (command.kind === 'clear') {
|
|
243
|
+
const result = await transport.request('thread/goal/clear', { threadId, origin: 'user' });
|
|
244
|
+
this.emit({ type: 'goal', goal: null });
|
|
245
|
+
this.reply(input.promptId, isRecord(result) && result.cleared === true ? 'Goal cleared.' : 'No goal to clear.');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (current === null && command.kind !== 'set') {
|
|
249
|
+
this.reply(input.promptId, 'No goal is set. Set one with /goal and the objective.');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (command.kind === 'pause') {
|
|
253
|
+
await setGoal({ status: 'paused' });
|
|
254
|
+
this.reply(input.promptId, 'Goal paused. /goal resume takes it up again.');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (command.kind === 'set') {
|
|
258
|
+
// A new objective starts over, its tokens and time included.
|
|
259
|
+
if (current !== null) {
|
|
260
|
+
await transport.request('thread/goal/clear', { threadId, origin: 'user' });
|
|
261
|
+
}
|
|
262
|
+
await setGoal({ objective: command.objective, status: 'paused' });
|
|
263
|
+
}
|
|
264
|
+
const nativeInput = command.kind === 'set' ? this.turnInput({ ...input, text: command.objective }) : [];
|
|
265
|
+
this.turnUnnamed = true;
|
|
266
|
+
this.promptId = input.promptId ?? null;
|
|
267
|
+
await transport.request('turn/start', this.turnStartParams(nativeInput));
|
|
268
|
+
if (this.transport !== transport) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.acceptPrompt(input.promptId);
|
|
272
|
+
await setGoal({ status: 'active' });
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (this.transport === transport) {
|
|
276
|
+
this.emit({ type: 'failed', message: errorText(error) });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/* A turn that ends with what the host said, for a command no model answers. */
|
|
281
|
+
reply(promptId, text) {
|
|
282
|
+
if (promptId !== undefined) {
|
|
283
|
+
this.emit({ type: 'turn.accepted', promptId });
|
|
284
|
+
}
|
|
285
|
+
this.emit({ type: 'text.done', ref: `goal-${randomUUID()}`, text });
|
|
286
|
+
this.emit({ type: 'turn.done', ...(promptId === undefined ? {} : { promptId }), state: 'done', costUsd: 0 });
|
|
287
|
+
}
|
|
288
|
+
async goal(action) {
|
|
289
|
+
const transport = this.transport;
|
|
290
|
+
if (!transport) {
|
|
291
|
+
throw new Error('thread/goal failed: Codex is not running');
|
|
292
|
+
}
|
|
293
|
+
if (action === 'clear') {
|
|
294
|
+
await transport.request('thread/goal/clear', { threadId: this.threadId, origin: 'user' });
|
|
295
|
+
if (this.transport === transport) {
|
|
296
|
+
this.emit({ type: 'goal', goal: null });
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const result = await transport.request('thread/goal/set', {
|
|
301
|
+
threadId: this.threadId,
|
|
302
|
+
status: action === 'pause' ? 'paused' : 'active',
|
|
303
|
+
origin: 'user'
|
|
304
|
+
});
|
|
305
|
+
if (this.transport === transport) {
|
|
306
|
+
this.emit({ type: 'goal', goal: codexGoal(isRecord(result) ? result.goal : null) });
|
|
307
|
+
}
|
|
157
308
|
}
|
|
158
309
|
async steerTurn(input) {
|
|
159
310
|
const transport = this.transport;
|
|
@@ -254,6 +405,10 @@ export class CodexBackend {
|
|
|
254
405
|
if (!this.transport) {
|
|
255
406
|
return;
|
|
256
407
|
}
|
|
408
|
+
// As Codex's own TUI does: paused first, so no goal turn opens after the one this stops.
|
|
409
|
+
if (this.goalNow?.status === 'active') {
|
|
410
|
+
void this.transport.request('thread/goal/set', { threadId: this.threadId, status: 'paused', origin: 'user' }).catch(() => undefined);
|
|
411
|
+
}
|
|
257
412
|
if (!turnId) {
|
|
258
413
|
this.interruptOwed = this.turnUnnamed;
|
|
259
414
|
// A child request can open a host turn after the root's native turn already ended.
|
|
@@ -385,6 +540,9 @@ export class CodexBackend {
|
|
|
385
540
|
this.emit({ type: 'exit', exitCode, ...(stderr === null ? {} : { stderr }) });
|
|
386
541
|
}
|
|
387
542
|
emit(event) {
|
|
543
|
+
if (event.type === 'goal') {
|
|
544
|
+
this.goalNow = event.goal;
|
|
545
|
+
}
|
|
388
546
|
// An older autonomous turn can finish while the next prompt still waits for its own turn/started.
|
|
389
547
|
if ((event.type === 'turn.done' && (!this.turnUnnamed || event.promptId !== undefined)) || event.type === 'failed' || event.type === 'exit') {
|
|
390
548
|
this.turnUnnamed = false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChatGoal } from '@adecore/agent-contracts';
|
|
1
2
|
import type { ApprovalDecision, BackendEvent } from './backend.ts';
|
|
2
3
|
import type { CodexFrame } from './codex-transport.ts';
|
|
3
4
|
type CodexRpcId = number | string;
|
|
@@ -9,6 +10,7 @@ type CodexAnswer = {
|
|
|
9
10
|
kind: 'steer';
|
|
10
11
|
text: string;
|
|
11
12
|
};
|
|
13
|
+
export declare function codexGoal(raw: unknown): ChatGoal | null;
|
|
12
14
|
export declare function unwrapCommand(command: string): string;
|
|
13
15
|
export declare class CodexProtocol {
|
|
14
16
|
private readonly generation;
|
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
import { generatedImage } from './generated-image.js';
|
|
2
|
+
import { isRecord } from '@adecore/agent-contracts';
|
|
2
3
|
import { readCodexLimits } from '../usage/limits/normalize.js';
|
|
3
4
|
import { codexRuntimeMode } from '../providers/codex.js';
|
|
4
5
|
import { cleanTitle } from '../title-file.js';
|
|
5
|
-
function isRecord(value) {
|
|
6
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
|
-
}
|
|
8
6
|
function str(value) {
|
|
9
7
|
return typeof value === 'string' ? value : null;
|
|
10
8
|
}
|
|
11
9
|
function num(value) {
|
|
12
10
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
13
11
|
}
|
|
12
|
+
const GOAL_STATUSES = {
|
|
13
|
+
active: 'active',
|
|
14
|
+
paused: 'paused',
|
|
15
|
+
blocked: 'blocked',
|
|
16
|
+
usageLimited: 'usage-limited',
|
|
17
|
+
budgetLimited: 'budget-limited',
|
|
18
|
+
complete: 'complete'
|
|
19
|
+
};
|
|
20
|
+
/* A `ThreadGoal` of the app-server (Codex 0.162.1) as a chat says it; null for a goal it cannot read, or none. */
|
|
21
|
+
export function codexGoal(raw) {
|
|
22
|
+
if (!isRecord(raw)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const objective = str(raw.objective)?.trim() ?? '';
|
|
26
|
+
const status = GOAL_STATUSES[str(raw.status) ?? ''];
|
|
27
|
+
if (objective === '' || status === undefined) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const count = (value) => (typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined);
|
|
31
|
+
const tokensUsed = count(raw.tokensUsed);
|
|
32
|
+
const tokenBudget = count(raw.tokenBudget);
|
|
33
|
+
const secondsUsed = count(raw.timeUsedSeconds);
|
|
34
|
+
return {
|
|
35
|
+
objective,
|
|
36
|
+
status,
|
|
37
|
+
...(tokensUsed === undefined ? {} : { tokensUsed }),
|
|
38
|
+
...(tokenBudget === undefined || tokenBudget === 0 ? {} : { tokenBudget }),
|
|
39
|
+
...(secondsUsed === undefined ? {} : { secondsUsed })
|
|
40
|
+
};
|
|
41
|
+
}
|
|
14
42
|
// Codex wraps every command in a login shell; the person wants to read the command, not the wrapper.
|
|
15
43
|
const SHELL_WRAPPER = /^(?:\/bin\/|\/usr\/bin\/)?(?:zsh|bash|sh) -lc (.*)$/s;
|
|
16
44
|
export function unwrapCommand(command) {
|
|
@@ -234,6 +262,12 @@ export class CodexProtocol {
|
|
|
234
262
|
case 'turn/completed':
|
|
235
263
|
this.handleTurnCompleted(params, events);
|
|
236
264
|
break;
|
|
265
|
+
case 'thread/goal/updated':
|
|
266
|
+
events.push({ type: 'goal', goal: codexGoal(params.goal) });
|
|
267
|
+
break;
|
|
268
|
+
case 'thread/goal/cleared':
|
|
269
|
+
events.push({ type: 'goal', goal: null });
|
|
270
|
+
break;
|
|
237
271
|
case 'thread/closed':
|
|
238
272
|
this.withdrawOwner(threadId ?? this.ownThreadId, null, events);
|
|
239
273
|
break;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import { isRecord } from '@adecore/agent-contracts';
|
|
1
2
|
import { errorText } from '../error-text.js';
|
|
2
3
|
import { CodexProtocol } from './codex-protocol.js';
|
|
3
4
|
import { CodexTransport, DEFAULT_CODEX_CLIENT } from './codex-transport.js';
|
|
4
5
|
import { ChatError } from './errors.js';
|
|
5
6
|
import { readingThread, settledReading } from './subagent-projection.js';
|
|
6
|
-
function isRecord(value) {
|
|
7
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
-
}
|
|
9
7
|
/* One `thread/items/list` answer as far as a reader needs it: the entries newest first, and the cursor to older ones. */
|
|
10
8
|
export function parseThreadItemsPage(result) {
|
|
11
9
|
const data = isRecord(result) && Array.isArray(result.data) ? result.data.filter(isRecord) : [];
|
|
@@ -21,6 +21,7 @@ export declare class CodexTransport {
|
|
|
21
21
|
private readonly pending;
|
|
22
22
|
private nextId;
|
|
23
23
|
private closed;
|
|
24
|
+
private inputClosed;
|
|
24
25
|
constructor(options: CodexTransportOptions);
|
|
25
26
|
get pid(): number;
|
|
26
27
|
get alive(): boolean;
|
|
@@ -33,10 +34,12 @@ export declare class CodexTransport {
|
|
|
33
34
|
stop(): void;
|
|
34
35
|
dispose(): Promise<void>;
|
|
35
36
|
private write;
|
|
37
|
+
private broken;
|
|
36
38
|
private failed;
|
|
37
39
|
private readLine;
|
|
38
40
|
private readLines;
|
|
39
41
|
private handleLine;
|
|
40
42
|
private handleExit;
|
|
43
|
+
private rejectPending;
|
|
41
44
|
}
|
|
42
45
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errorText } from '../error-text.js';
|
|
2
|
-
import { ChatChild } from './chat-process.js';
|
|
2
|
+
import { ChatChild, MAX_LINE_LENGTH, readStreamLines } from './chat-process.js';
|
|
3
3
|
export const DEFAULT_CODEX_CLIENT = { name: 'agents', title: 'Agents', version: '0.1.0' };
|
|
4
4
|
class CodexRpcError extends Error {
|
|
5
5
|
code;
|
|
@@ -10,6 +10,8 @@ class CodexRpcError extends Error {
|
|
|
10
10
|
this.code = typeof detail.code === 'number' ? detail.code : null;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
// The app-server answers every request it takes at once and does the work in notifications, so a silent one is stuck.
|
|
14
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
13
15
|
/*
|
|
14
16
|
* One `codex app-server` process and the JSON-RPC framing on its stdio: newline-delimited JSON,
|
|
15
17
|
* a request carries an id and gets a result or an error back under the same id, a notification
|
|
@@ -21,6 +23,8 @@ export class CodexTransport {
|
|
|
21
23
|
pending = new Map();
|
|
22
24
|
nextId = 1;
|
|
23
25
|
closed = false;
|
|
26
|
+
// Set once stdin ended or broke; a write after an EPIPE only raises the next one.
|
|
27
|
+
inputClosed = false;
|
|
24
28
|
constructor(options) {
|
|
25
29
|
this.options = options;
|
|
26
30
|
this.child = new ChatChild({
|
|
@@ -28,7 +32,7 @@ export class CodexTransport {
|
|
|
28
32
|
cwd: options.cwd,
|
|
29
33
|
env: options.env,
|
|
30
34
|
...(options.spawn ? { spawn: options.spawn } : {}),
|
|
31
|
-
onError: (error) => this.
|
|
35
|
+
onError: (error) => this.broken(error),
|
|
32
36
|
onExit: (exitCode, stderr) => this.handleExit(exitCode, stderr)
|
|
33
37
|
});
|
|
34
38
|
void this.readLines();
|
|
@@ -46,11 +50,17 @@ export class CodexTransport {
|
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
52
|
const id = this.nextId++;
|
|
49
|
-
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
this.pending.delete(id);
|
|
55
|
+
reject(new Error(`${method} failed: Codex did not answer in ${REQUEST_TIMEOUT_MS / 1000} seconds`));
|
|
56
|
+
}, REQUEST_TIMEOUT_MS);
|
|
57
|
+
timer.unref?.();
|
|
58
|
+
this.pending.set(id, { resolve, reject, method, timer });
|
|
50
59
|
try {
|
|
51
60
|
this.write({ id, method, params });
|
|
52
61
|
}
|
|
53
62
|
catch (error) {
|
|
63
|
+
clearTimeout(timer);
|
|
54
64
|
this.pending.delete(id);
|
|
55
65
|
reject(error);
|
|
56
66
|
}
|
|
@@ -67,6 +77,7 @@ export class CodexTransport {
|
|
|
67
77
|
}
|
|
68
78
|
/* Closes stdin; the app-server ends on its own once it has nothing more to say. */
|
|
69
79
|
end() {
|
|
80
|
+
this.inputClosed = true;
|
|
70
81
|
try {
|
|
71
82
|
this.child.process.stdin.end();
|
|
72
83
|
}
|
|
@@ -85,18 +96,26 @@ export class CodexTransport {
|
|
|
85
96
|
return this.child.end();
|
|
86
97
|
}
|
|
87
98
|
write(frame) {
|
|
88
|
-
if (this.closed) {
|
|
99
|
+
if (this.closed || this.inputClosed) {
|
|
89
100
|
throw new Error('Codex stdin is closed');
|
|
90
101
|
}
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
try {
|
|
103
|
+
this.child.process.stdin.write(`${JSON.stringify(frame)}\n`);
|
|
104
|
+
this.child.process.stdin.flush();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
this.inputClosed = true;
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/* A pipe that failed takes no more writes; a frame a consumer choked on leaves it as it was. */
|
|
112
|
+
broken(error) {
|
|
113
|
+
this.inputClosed = true;
|
|
114
|
+
this.failed(error);
|
|
93
115
|
}
|
|
94
116
|
failed(error) {
|
|
95
117
|
const failure = new Error(`Codex transport failed: ${errorText(error)}`);
|
|
96
|
-
|
|
97
|
-
settle.reject(failure);
|
|
98
|
-
}
|
|
99
|
-
this.pending.clear();
|
|
118
|
+
this.rejectPending(() => failure);
|
|
100
119
|
try {
|
|
101
120
|
this.options.onError?.(failure);
|
|
102
121
|
}
|
|
@@ -113,35 +132,14 @@ export class CodexTransport {
|
|
|
113
132
|
}
|
|
114
133
|
}
|
|
115
134
|
async readLines() {
|
|
116
|
-
const reader = this.child.process.stdout.getReader();
|
|
117
|
-
const decoder = new TextDecoder();
|
|
118
|
-
let buffered = '';
|
|
119
135
|
try {
|
|
120
|
-
|
|
121
|
-
const { value, done } = await reader.read();
|
|
122
|
-
if (done) {
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
buffered += decoder.decode(value, { stream: true });
|
|
126
|
-
let newline = buffered.indexOf('\n');
|
|
127
|
-
while (newline >= 0) {
|
|
128
|
-
this.readLine(buffered.slice(0, newline));
|
|
129
|
-
buffered = buffered.slice(newline + 1);
|
|
130
|
-
newline = buffered.indexOf('\n');
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (buffered.trim() !== '') {
|
|
134
|
-
this.readLine(buffered);
|
|
135
|
-
}
|
|
136
|
+
await readStreamLines(this.child.process.stdout, (line) => this.readLine(line), () => this.failed(new Error(`Codex wrote a frame of more than ${MAX_LINE_LENGTH / 1_000_000} million characters, which was dropped`)));
|
|
136
137
|
}
|
|
137
138
|
catch (error) {
|
|
138
139
|
if (!this.closed) {
|
|
139
|
-
this.
|
|
140
|
+
this.broken(error);
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
|
-
finally {
|
|
143
|
-
reader.releaseLock();
|
|
144
|
-
}
|
|
145
143
|
}
|
|
146
144
|
handleLine(line) {
|
|
147
145
|
if (this.closed || line.trim() === '') {
|
|
@@ -167,6 +165,7 @@ export class CodexTransport {
|
|
|
167
165
|
return;
|
|
168
166
|
}
|
|
169
167
|
this.pending.delete(message.id);
|
|
168
|
+
clearTimeout(settle.timer);
|
|
170
169
|
if (message.error !== undefined) {
|
|
171
170
|
settle.reject(new CodexRpcError(settle.method, message.error));
|
|
172
171
|
}
|
|
@@ -179,10 +178,14 @@ export class CodexTransport {
|
|
|
179
178
|
return;
|
|
180
179
|
}
|
|
181
180
|
this.closed = true;
|
|
181
|
+
this.rejectPending((method) => new Error(`${method} failed: Codex exited`));
|
|
182
|
+
this.options.onExit(exitCode, stderr);
|
|
183
|
+
}
|
|
184
|
+
rejectPending(reason) {
|
|
182
185
|
for (const settle of this.pending.values()) {
|
|
183
|
-
|
|
186
|
+
clearTimeout(settle.timer);
|
|
187
|
+
settle.reject(reason(settle.method));
|
|
184
188
|
}
|
|
185
189
|
this.pending.clear();
|
|
186
|
-
this.options.onExit(exitCode, stderr);
|
|
187
190
|
}
|
|
188
191
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attachmentImageMime } from '@adecore/agent-contracts';
|
|
1
|
+
import { attachmentImageMime, HOST_TASK_ORIGIN } from '@adecore/agent-contracts';
|
|
2
2
|
// A rough rate for prose and code alike; scaling to the CLI's own total absorbs the error.
|
|
3
3
|
const CHARS_PER_TOKEN = 4;
|
|
4
4
|
// What a model is charged for a picture at the size a CLI sends it, whatever its bytes on disk.
|
|
@@ -54,7 +54,7 @@ export function estimateContextBreakdown(items, contextTokens) {
|
|
|
54
54
|
}
|
|
55
55
|
case 'subagent':
|
|
56
56
|
// A row the host drew for work it delegated stands for that work; the call that made it is a tool row of its own.
|
|
57
|
-
if (item.origin !==
|
|
57
|
+
if (item.origin !== HOST_TASK_ORIGIN) {
|
|
58
58
|
toolOutput += textTokens(item.prompt ?? '') + textTokens(item.result ?? '');
|
|
59
59
|
}
|
|
60
60
|
break;
|
package/dist/chat/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { CodedError } from '../coded-error.ts';
|
|
2
|
-
export type ChatErrorCode = 'refused-query' | 'stale-ui-query' | 'history-expired' | 'chat-not-found' | 'chat-unreadable' | 'chat-busy' | 'request-not-found' | 'chat-unsupported' | 'invalid-attachments' | 'subagent-not-found' | 'subagent-stale' | 'task-not-found' | 'transcript-missing' | 'transcript-format' | 'fork-failed' | 'turn-not-found' | 'item-not-found' | 'bookmark-not-found' | 'too-many-bookmarks' | 'visual-invalid' | 'visual-too-large';
|
|
2
|
+
export type ChatErrorCode = 'refused-query' | 'stale-ui-query' | 'history-expired' | 'chat-not-found' | 'chat-unreadable' | 'chat-busy' | 'request-not-found' | 'chat-unsupported' | 'invalid-attachments' | 'subagent-not-found' | 'subagent-stale' | 'task-not-found' | 'goal-not-found' | 'transcript-missing' | 'transcript-format' | 'fork-failed' | 'turn-not-found' | 'item-not-found' | 'bookmark-not-found' | 'too-many-bookmarks' | 'visual-invalid' | 'visual-too-large';
|
|
3
3
|
export declare class ChatError<TCode extends string = ChatErrorCode> extends CodedError<TCode> {
|
|
4
4
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { clipText } from '@adecore/agent-contracts';
|
|
2
|
+
import { clipText, isRecord } from '@adecore/agent-contracts';
|
|
3
3
|
import { isForkOrSubagent } from '../usage/readers/codex.js';
|
|
4
4
|
import { claudeProjectSlug, spokenText } from './claude-transcript.js';
|
|
5
5
|
const TITLE_LENGTH = 120;
|
|
@@ -18,7 +18,7 @@ function records(head) {
|
|
|
18
18
|
}
|
|
19
19
|
try {
|
|
20
20
|
const parsed = JSON.parse(line);
|
|
21
|
-
return
|
|
21
|
+
return isRecord(parsed) ? [parsed] : [];
|
|
22
22
|
}
|
|
23
23
|
catch {
|
|
24
24
|
return [];
|