@moxxy/plugin-provider-codex-agent 0.0.0-stage → 0.45.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/dist/account-limit.d.ts +11 -0
  3. package/dist/account-limit.d.ts.map +1 -0
  4. package/dist/account-limit.js +24 -0
  5. package/dist/account-limit.js.map +1 -0
  6. package/dist/agent-provider.d.ts +62 -0
  7. package/dist/agent-provider.d.ts.map +1 -0
  8. package/dist/agent-provider.js +209 -0
  9. package/dist/agent-provider.js.map +1 -0
  10. package/dist/codex-session.d.ts +220 -0
  11. package/dist/codex-session.d.ts.map +1 -0
  12. package/dist/codex-session.js +1113 -0
  13. package/dist/codex-session.js.map +1 -0
  14. package/dist/constants.d.ts +15 -0
  15. package/dist/constants.d.ts.map +1 -0
  16. package/dist/constants.js +15 -0
  17. package/dist/constants.js.map +1 -0
  18. package/dist/executable.d.ts +16 -0
  19. package/dist/executable.d.ts.map +1 -0
  20. package/dist/executable.js +43 -0
  21. package/dist/executable.js.map +1 -0
  22. package/dist/index.d.ts +9 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +79 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/login.d.ts +37 -0
  27. package/dist/login.d.ts.map +1 -0
  28. package/dist/login.js +127 -0
  29. package/dist/login.js.map +1 -0
  30. package/dist/offered-models.d.ts +12 -0
  31. package/dist/offered-models.d.ts.map +1 -0
  32. package/dist/offered-models.js +53 -0
  33. package/dist/offered-models.js.map +1 -0
  34. package/dist/own-settings.d.ts +9 -0
  35. package/dist/own-settings.d.ts.map +1 -0
  36. package/dist/own-settings.js +34 -0
  37. package/dist/own-settings.js.map +1 -0
  38. package/dist/protocol.d.ts +279 -0
  39. package/dist/protocol.d.ts.map +1 -0
  40. package/dist/protocol.js +30 -0
  41. package/dist/protocol.js.map +1 -0
  42. package/dist/subagents.d.ts +20 -0
  43. package/dist/subagents.d.ts.map +1 -0
  44. package/dist/subagents.js +90 -0
  45. package/dist/subagents.js.map +1 -0
  46. package/dist/translate.d.ts +87 -0
  47. package/dist/translate.d.ts.map +1 -0
  48. package/dist/translate.js +334 -0
  49. package/dist/translate.js.map +1 -0
  50. package/dist/view-image.d.ts +4 -0
  51. package/dist/view-image.d.ts.map +1 -0
  52. package/dist/view-image.js +47 -0
  53. package/dist/view-image.js.map +1 -0
  54. package/package.json +82 -3
  55. package/src/account-limit.ts +25 -0
  56. package/src/agent-cli.test.ts +76 -0
  57. package/src/agent-provider.test.ts +1556 -0
  58. package/src/agent-provider.ts +234 -0
  59. package/src/codex-session.ts +1258 -0
  60. package/src/constants.ts +18 -0
  61. package/src/event-coverage.test.ts +200 -0
  62. package/src/executable.test.ts +53 -0
  63. package/src/executable.ts +49 -0
  64. package/src/fake-codex.testkit.ts +576 -0
  65. package/src/fixtures/codex-app-server-0.155.1.json +122 -0
  66. package/src/fixtures/codex-app-server-0.163.0.json +125 -0
  67. package/src/fixtures/codex-model-list-0.163.0.json +436 -0
  68. package/src/index.test.ts +45 -0
  69. package/src/index.ts +99 -0
  70. package/src/login.test.ts +93 -0
  71. package/src/login.ts +167 -0
  72. package/src/offered-models.test.ts +87 -0
  73. package/src/offered-models.ts +65 -0
  74. package/src/own-settings.test.ts +45 -0
  75. package/src/own-settings.ts +39 -0
  76. package/src/protocol.ts +256 -0
  77. package/src/subagents.test.ts +47 -0
  78. package/src/subagents.ts +98 -0
  79. package/src/translate.test.ts +160 -0
  80. package/src/translate.ts +362 -0
  81. package/src/view-image.test.ts +29 -0
  82. package/src/view-image.ts +47 -0
  83. package/README.md +0 -3
@@ -0,0 +1,234 @@
1
+ import {
2
+ estimateTextTokens,
3
+ type AgentActivity,
4
+ type AgentStepEvent,
5
+ type AgentStepRequest,
6
+ type AgentTurnStart,
7
+ type LLMProvider,
8
+ type ModelDescriptor,
9
+ type ProviderEvent,
10
+ type ProviderRequest,
11
+ type TokenUsage,
12
+ } from '@moxxy/sdk';
13
+ import { AgentSessionPool, Inbox, JsonRpcLineClient, oneOffPrompt, type AgentSessionLease } from '@moxxy/agent-connection-kit';
14
+ import { CODEX_AGENT_NAME, CODEX_AGENT_PROVIDER_ID } from './constants.js';
15
+ import { CodexAgentSession, type StartCodexAppServer } from './codex-session.js';
16
+ import { spawnCodexAppServer } from './executable.js';
17
+ import type { CodexThreadResponse, CodexTokenUsage, CodexTurn } from './protocol.js';
18
+ import { toTokenUsage } from './translate.js';
19
+
20
+ export const CODEX_AGENT_DEFAULT_MODEL = 'gpt-6-astra';
21
+ /** What a new conversation starts on while the person chose no default: light, so it spares their plan. */
22
+ export const CODEX_AGENT_STARTER_MODEL = 'gpt-6-luna';
23
+ const NON_TEXT_BLOCK_TOKENS = 256;
24
+ const MAX_LIVE_CODEX_SESSIONS = 8;
25
+
26
+ const agentCapabilities = {
27
+ supportsTools: true,
28
+ supportsStreaming: true,
29
+ supportsImages: true,
30
+ supportsDocuments: false,
31
+ supportsAudio: false,
32
+ supportsReasoning: true,
33
+ supportsFast: true,
34
+ hostedTools: ['web_search'],
35
+ } as const;
36
+
37
+ /** Models Codex offers on a ChatGPT sign-in, used until Codex's own `model/list` answered; Codex decides what the account may use. */
38
+ export const codexAgentModels: ReadonlyArray<ModelDescriptor> = [
39
+ { id: CODEX_AGENT_DEFAULT_MODEL, contextWindow: 272_000, maxOutputTokens: 128_000, ...agentCapabilities },
40
+ // Codex 0.163's own catalog: 500k for 6.1 Sol, 272k for the rest (the API models have 1.05M).
41
+ { id: 'gpt-6.1-sol', contextWindow: 500_000, maxOutputTokens: 128_000, ...agentCapabilities },
42
+ { id: 'gpt-6-sol', contextWindow: 272_000, maxOutputTokens: 128_000, ...agentCapabilities },
43
+ { id: 'gpt-6-luna', contextWindow: 272_000, maxOutputTokens: 128_000, ...agentCapabilities },
44
+ { id: 'gpt-5.6-sol', contextWindow: 272_000, maxOutputTokens: 128_000, ...agentCapabilities },
45
+ { id: 'gpt-5.6-terra', contextWindow: 272_000, maxOutputTokens: 128_000, ...agentCapabilities },
46
+ { id: 'gpt-5.6-luna', contextWindow: 272_000, maxOutputTokens: 128_000, ...agentCapabilities },
47
+ ];
48
+
49
+ export interface CodexAgentProviderConfig {
50
+ /** The person's Codex CLI; resolved from PATH when left out. */
51
+ readonly executable?: string;
52
+ /** Persisted provider-item model (`plugins.provider.items.codex-agent.model`). */
53
+ readonly model?: string;
54
+ readonly defaultModel?: string;
55
+ /** Runner workspace, supplied on activation. */
56
+ readonly cwd?: string;
57
+ /** How `codex app-server` is started; spawns the person's Codex by default. */
58
+ readonly startAppServer?: StartCodexAppServer;
59
+ /** The live Codex sessions; shared across the process by default. */
60
+ readonly sessions?: AgentSessionPool<CodexAgentSession>;
61
+ }
62
+
63
+ let sharedSessions: AgentSessionPool<CodexAgentSession> | undefined;
64
+
65
+ /** The live Codex sessions of this process, one per Moxxy conversation. */
66
+ export function codexSessions(): AgentSessionPool<CodexAgentSession> {
67
+ sharedSessions ??= new AgentSessionPool<CodexAgentSession>({ limit: MAX_LIVE_CODEX_SESSIONS });
68
+ return sharedSessions;
69
+ }
70
+
71
+ /**
72
+ * The Codex agent connection: Codex does each turn itself through
73
+ * `codex app-server`, unmodified (its instructions, config, tools, skills and
74
+ * MCP servers), on the person's ChatGPT sign-in. Moxxy adds only its own MCP
75
+ * server to the thread, and nothing to the prompt.
76
+ */
77
+ export class CodexAgentProvider implements LLMProvider {
78
+ readonly name = CODEX_AGENT_PROVIDER_ID;
79
+ readonly agentName = CODEX_AGENT_NAME;
80
+ /** Its own plan mode and goal: Moxxy shows the plan and ends the goal when the agent says so. */
81
+ readonly agentOwns = { plan: true, goal: true } as const;
82
+ readonly models = codexAgentModels;
83
+ readonly starterModel = CODEX_AGENT_STARTER_MODEL;
84
+ private readonly defaultModel: string;
85
+ private readonly start: StartCodexAppServer;
86
+
87
+ constructor(private readonly config: CodexAgentProviderConfig = {}) {
88
+ this.defaultModel = config.model ?? config.defaultModel ?? CODEX_AGENT_DEFAULT_MODEL;
89
+ this.start = config.startAppServer ?? spawnCodexAppServer(config.executable ?? 'codex');
90
+ }
91
+
92
+ async *agentStep(req: AgentStepRequest): AsyncIterable<AgentStepEvent> {
93
+ let lease: AgentSessionLease<CodexAgentSession>;
94
+ try {
95
+ lease = await this.lease(req);
96
+ } catch (err) {
97
+ if (!req.signal.aborted) yield { type: 'error', message: messageOf(err), retryable: false };
98
+ return;
99
+ }
100
+ try {
101
+ yield* lease.session.turn(req);
102
+ } finally {
103
+ if (lease.session.isAlive) lease.release();
104
+ else await lease.discard();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Work Codex left running (a subagent, a command in the background) ended
110
+ * between turns. Codex starts no turn then, so Moxxy opens one to show it.
111
+ */
112
+ onAgentTurn(listener: (start: AgentTurnStart) => void): () => void {
113
+ return this.sessions().onAgentTurn(listener);
114
+ }
115
+
116
+ /** What Codex's background work does between turns, recorded at once. */
117
+ onAgentActivity(listener: (activity: AgentActivity) => void): () => void {
118
+ return this.sessions().onActivity(listener);
119
+ }
120
+
121
+ /** Stop one work Codex runs in the background in the conversation. */
122
+ async stopBackgroundWork(sessionId: string, workId: string): Promise<boolean> {
123
+ const live = this.sessions().running(sessionId);
124
+ return live ? live.stopWork(workId) : false;
125
+ }
126
+
127
+ private sessions(): AgentSessionPool<CodexAgentSession> {
128
+ return this.config.sessions ?? codexSessions();
129
+ }
130
+
131
+ /** One-off text (summaries, skill drafts): an ephemeral read-only Codex thread, kept nowhere. */
132
+ async *stream(req: ProviderRequest): AsyncIterable<ProviderEvent> {
133
+ const model = req.model || this.defaultModel;
134
+ yield { type: 'message_start', model };
135
+ const cwd = this.config.cwd ?? process.cwd();
136
+ let usage: TokenUsage | undefined;
137
+ let connection: Awaited<ReturnType<StartCodexAppServer>> | undefined;
138
+ try {
139
+ connection = await this.start({ cwd });
140
+ const client = new JsonRpcLineClient({ readable: connection.readable, writable: connection.writable });
141
+ const notes = new Inbox<{ readonly method: string; readonly params: Record<string, unknown> } | null>();
142
+ client.onNotification((method, params) => notes.push({ method, params: (params ?? {}) as Record<string, unknown> }));
143
+ connection.readable.once('close', () => notes.push(null));
144
+ await client.request('initialize', { clientInfo: { name: 'moxxy', title: 'Moxxy', version: '1' }, capabilities: { experimentalApi: true } });
145
+ client.notify('initialized');
146
+ const { system, prompt } = oneOffPrompt(req);
147
+ const thread = await client.request<CodexThreadResponse>('thread/start', {
148
+ cwd,
149
+ ephemeral: true,
150
+ approvalPolicy: 'never',
151
+ sandbox: 'read-only',
152
+ model,
153
+ ...(system ? { developerInstructions: system } : {}),
154
+ });
155
+ const started = await client.request<{ turn: CodexTurn }>(
156
+ 'turn/start',
157
+ { threadId: thread.thread.id, input: [{ type: 'text', text: prompt, text_elements: [] }] },
158
+ req.signal,
159
+ );
160
+ const stop = () => void client.request('turn/interrupt', { threadId: thread.thread.id, turnId: started.turn.id }).catch(() => undefined);
161
+ req.signal?.addEventListener('abort', stop, { once: true });
162
+ try {
163
+ for (;;) {
164
+ const note = await notes.next();
165
+ if (note === null) throw new Error('Codex stopped.');
166
+ const { method, params } = note;
167
+ if (params.turnId !== undefined && params.turnId !== started.turn.id) continue;
168
+ if (method === 'item/agentMessage/delta' && typeof params.delta === 'string') yield { type: 'text_delta', delta: params.delta };
169
+ else if (method === 'thread/tokenUsage/updated') {
170
+ const last = (params.tokenUsage as { last?: CodexTokenUsage } | undefined)?.last;
171
+ if (last) usage = toTokenUsage(last);
172
+ } else if (method === 'turn/completed') {
173
+ const turn = params.turn as CodexTurn | undefined;
174
+ if (!turn || turn.id !== started.turn.id) continue;
175
+ if (turn.status === 'failed') {
176
+ yield { type: 'error', message: `Codex: ${turn.error?.message ?? 'the turn failed'}`, retryable: false };
177
+ return;
178
+ }
179
+ break;
180
+ }
181
+ }
182
+ } finally {
183
+ req.signal?.removeEventListener('abort', stop);
184
+ }
185
+ } catch (err) {
186
+ if (!req.signal?.aborted) yield { type: 'error', message: messageOf(err), retryable: false };
187
+ return;
188
+ } finally {
189
+ connection?.close();
190
+ }
191
+ yield { type: 'message_end', stopReason: 'end_turn', ...(usage ? { usage } : {}) };
192
+ }
193
+
194
+ async countTokens(req: Pick<ProviderRequest, 'model' | 'messages' | 'system' | 'tools'>): Promise<number> {
195
+ let text = req.system ?? '';
196
+ let nonTextBlocks = 0;
197
+ for (const message of req.messages) {
198
+ for (const block of message.content) {
199
+ if (block.type === 'text') text += block.text;
200
+ else nonTextBlocks += 1;
201
+ }
202
+ }
203
+ for (const tool of req.tools ?? []) text += tool.name + tool.description;
204
+ return estimateTextTokens(text) + nonTextBlocks * NON_TEXT_BLOCK_TOKENS;
205
+ }
206
+
207
+ /** The conversation's running Codex, or a new one (resuming its thread) when there is none that fits. */
208
+ private async lease(req: AgentStepRequest): Promise<AgentSessionLease<CodexAgentSession>> {
209
+ const settings = {
210
+ start: this.start,
211
+ cwd: req.cwd,
212
+ ...(req.model ? { model: req.model } : {}),
213
+ tools: req.allTools ?? req.tools,
214
+ ...(req.skills ? { skills: req.skills } : {}),
215
+ ...(req.resumeSessionId ? { resume: req.resumeSessionId } : {}),
216
+ onAgentTurn: (name: string, workId?: string) => this.sessions().agentTurn({ sessionId: req.sessionId, name, ...(workId ? { workId } : {}) }),
217
+ onActivity: (events: ReadonlyArray<AgentStepEvent>) => this.sessions().activity({ sessionId: req.sessionId, events }),
218
+ };
219
+ const sessions = this.sessions();
220
+ const create = () => CodexAgentSession.start(settings);
221
+ const lease = await sessions.acquire(req.sessionId, { signal: req.signal, create });
222
+ if (lease.session.isAlive && lease.session.fits(settings)) return lease;
223
+ await lease.discard();
224
+ return sessions.acquire(req.sessionId, { signal: req.signal, create });
225
+ }
226
+ }
227
+
228
+ export function createCodexAgentClient(config: CodexAgentProviderConfig = {}): LLMProvider {
229
+ return new CodexAgentProvider(config);
230
+ }
231
+
232
+ function messageOf(err: unknown): string {
233
+ return err instanceof Error ? err.message : String(err);
234
+ }