@zvada/agent-server 0.2.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/package.json +87 -0
  4. package/src/client/client.ts +589 -0
  5. package/src/client/index.ts +18 -0
  6. package/src/client/transports.ts +84 -0
  7. package/src/core/agents/acp/acp-agent.ts +322 -0
  8. package/src/core/agents/acp/adapter.ts +260 -0
  9. package/src/core/agents/acp/client.ts +212 -0
  10. package/src/core/agents/acp/known-agents.ts +29 -0
  11. package/src/core/agents/acp/mappings.ts +136 -0
  12. package/src/core/agents/base.ts +145 -0
  13. package/src/core/agents/claude-code/adapter.ts +451 -0
  14. package/src/core/agents/claude-code/claude-agent.ts +235 -0
  15. package/src/core/agents/claude-code/generator-session.ts +344 -0
  16. package/src/core/agents/claude-code/options.ts +161 -0
  17. package/src/core/agents/claude-code/session-manager.ts +159 -0
  18. package/src/core/agents/codex-app-server/adapter.ts +214 -0
  19. package/src/core/agents/codex-app-server/client.ts +221 -0
  20. package/src/core/agents/codex-app-server/codex-app-server-agent.ts +385 -0
  21. package/src/core/agents/codex-items.ts +122 -0
  22. package/src/core/agents/codex-sdk/adapter.ts +204 -0
  23. package/src/core/agents/codex-sdk/codex-sdk-agent.ts +236 -0
  24. package/src/core/agents/config-fingerprint.ts +19 -0
  25. package/src/core/agents/error-classifier.ts +68 -0
  26. package/src/core/agents/registry.ts +40 -0
  27. package/src/core/agents/session-store.ts +72 -0
  28. package/src/core/agents/tool-meta.ts +68 -0
  29. package/src/core/agents/types.ts +54 -0
  30. package/src/core/index.ts +114 -0
  31. package/src/core/presets.ts +78 -0
  32. package/src/core/provision/extract.ts +31 -0
  33. package/src/core/provision/index.ts +10 -0
  34. package/src/core/provision/npm.ts +114 -0
  35. package/src/core/provision/pins.ts +51 -0
  36. package/src/core/provision/platform.ts +73 -0
  37. package/src/core/provision/provisioner.ts +478 -0
  38. package/src/core/proxy/anthropic-proxy.ts +69 -0
  39. package/src/core/proxy/api-key-store.ts +34 -0
  40. package/src/core/proxy/index.ts +7 -0
  41. package/src/core/runtime/agent-runtime.ts +363 -0
  42. package/src/core/runtime/event-processor.ts +218 -0
  43. package/src/core/runtime/event-sink.ts +37 -0
  44. package/src/core/utils/errors.ts +41 -0
  45. package/src/index.ts +4 -0
  46. package/src/protocol/async-queue.ts +68 -0
  47. package/src/protocol/config.ts +100 -0
  48. package/src/protocol/factories.ts +125 -0
  49. package/src/protocol/harness.ts +50 -0
  50. package/src/protocol/ids.ts +53 -0
  51. package/src/protocol/index.ts +16 -0
  52. package/src/protocol/lifecycle.ts +309 -0
  53. package/src/protocol/models.ts +45 -0
  54. package/src/protocol/part-input.ts +58 -0
  55. package/src/protocol/parts.ts +60 -0
  56. package/src/protocol/thinking.ts +32 -0
  57. package/src/protocol/tokens.ts +39 -0
  58. package/src/protocol/tool-state.ts +89 -0
  59. package/src/protocol/wire.ts +313 -0
  60. package/src/server/acp/binding.ts +163 -0
  61. package/src/server/acp/translate.ts +160 -0
  62. package/src/server/agent-server.ts +357 -0
  63. package/src/server/bin.ts +174 -0
  64. package/src/server/index.ts +24 -0
  65. package/src/server/install.ts +51 -0
  66. package/src/server/session-log.ts +66 -0
  67. package/src/server/transports.ts +149 -0
@@ -0,0 +1,214 @@
1
+ import {
2
+ DEFAULT_TOKEN_USAGE,
3
+ type Part,
4
+ type ReasoningPart,
5
+ type StopReason,
6
+ type StreamContext,
7
+ type TextPart,
8
+ type TokenUsage,
9
+ type ToolPart,
10
+ completeToolPart,
11
+ createReasoningPart,
12
+ createTextPart,
13
+ createToolPart,
14
+ } from "../../../protocol/index.ts";
15
+ import {
16
+ type CodexToolDescriptor,
17
+ type CodexToolItem,
18
+ codexToolDescriptor,
19
+ } from "../codex-items.ts";
20
+ import type { AdapterEvent, EventTransformer, TransformResult } from "../types.ts";
21
+
22
+ /**
23
+ * Parses `codex app-server` v2 notifications into AdapterEvents. This harness
24
+ * streams token-level deltas (item/agentMessage/delta, item/reasoning/*Delta),
25
+ * so unlike the Codex SDK adapter no diffing is needed. Items carry camelCase
26
+ * fields. Tool-like items (commandExecution/fileChange/mcpToolCall/webSearch)
27
+ * become tool parts that complete on item/completed.
28
+ */
29
+
30
+ /** One app-server item: the shared codex tool vocabulary plus streaming fields. */
31
+ interface CodexItem extends CodexToolItem {
32
+ type: string;
33
+ text?: string;
34
+ }
35
+ interface Notification {
36
+ method: string;
37
+ params: {
38
+ item?: CodexItem;
39
+ itemId?: string;
40
+ delta?: string;
41
+ tokenUsage?: {
42
+ total?: Record<string, number>;
43
+ last?: Record<string, number>;
44
+ modelContextWindow?: number;
45
+ };
46
+ turn?: { error?: { message?: string } | null };
47
+ error?: { message?: string; additionalDetails?: string | null };
48
+ willRetry?: boolean;
49
+ message?: string;
50
+ };
51
+ }
52
+
53
+ export class CodexAppServerTransformer implements EventTransformer<unknown> {
54
+ private readonly ctx: StreamContext;
55
+ private readonly partByItem = new Map<string, Part>();
56
+
57
+ private usage: TokenUsage = { ...DEFAULT_TOKEN_USAGE };
58
+ private stopReason?: StopReason;
59
+ private finishReason?: string;
60
+ private error?: string;
61
+
62
+ constructor(ctx: { sessionId: string }) {
63
+ this.ctx = { sessionId: ctx.sessionId, messageId: "" };
64
+ }
65
+
66
+ process(raw: unknown): AdapterEvent[] {
67
+ const { method, params } = raw as Notification;
68
+ switch (method) {
69
+ case "item/started":
70
+ return params.item ? this.openItem(params.item) : [];
71
+ case "item/completed":
72
+ return params.item ? this.completeItem(params.item) : [];
73
+ case "item/agentMessage/delta":
74
+ return this.textDelta(params.itemId, params.delta, "text");
75
+ case "item/reasoning/textDelta":
76
+ case "item/reasoning/summaryTextDelta":
77
+ return this.textDelta(params.itemId, params.delta, "reasoning");
78
+ case "thread/tokenUsage/updated": {
79
+ this.captureUsage(params.tokenUsage?.total);
80
+ const total = params.tokenUsage?.total;
81
+ if (!total) return [];
82
+ const used =
83
+ total.totalTokens ??
84
+ (total.inputTokens ?? 0) + (total.cachedInputTokens ?? 0) + (total.outputTokens ?? 0);
85
+ const size = params.tokenUsage?.modelContextWindow;
86
+ return [{ kind: "usage", used, ...(size !== undefined && { size }) }];
87
+ }
88
+ case "turn/completed":
89
+ // (A pre-completion non-retry `error` cannot reach here in practice:
90
+ // the agent's queue ends the turn on that notification.)
91
+ if (params.turn?.error) this.error = params.turn.error.message ?? "codex turn error";
92
+ this.finishReason = this.error ? "error" : "completed";
93
+ if (!this.error) this.stopReason = "end_turn";
94
+ return [];
95
+ case "error":
96
+ // v2 errors carry a nested TurnError and may be transient. A retrying
97
+ // notification is diagnostic only; turn/completed remains authoritative.
98
+ if (params.willRetry) return [];
99
+ this.error = params.error?.message ?? params.message ?? "codex app-server error";
100
+ this.finishReason = "error";
101
+ return [];
102
+ default:
103
+ return [];
104
+ }
105
+ }
106
+
107
+ finish(): TransformResult {
108
+ return {
109
+ usage: this.usage,
110
+ stopReason: this.stopReason,
111
+ finishReason: this.finishReason,
112
+ error: this.error,
113
+ };
114
+ }
115
+
116
+ private openItem(item: CodexItem): AdapterEvent[] {
117
+ switch (item.type) {
118
+ case "agentMessage": {
119
+ const part = createTextPart(this.ctx, item.text ?? "", true);
120
+ this.partByItem.set(item.id, part);
121
+ return [{ kind: "part-open", part }];
122
+ }
123
+ case "reasoning": {
124
+ const part = createReasoningPart(this.ctx, item.text ?? "", true);
125
+ this.partByItem.set(item.id, part);
126
+ return [{ kind: "part-open", part }];
127
+ }
128
+ default: {
129
+ // commandExecution / fileChange / mcpToolCall / webSearch share one
130
+ // descriptor vocabulary; userMessage and unknown item types produce
131
+ // no assistant output.
132
+ const d = codexToolDescriptor(item.type);
133
+ return d ? this.openTool(item, d) : [];
134
+ }
135
+ }
136
+ }
137
+
138
+ private completeItem(item: CodexItem): AdapterEvent[] {
139
+ const part = this.partByItem.get(item.id);
140
+ switch (item.type) {
141
+ case "agentMessage":
142
+ case "reasoning": {
143
+ const p = (part ??
144
+ (item.type === "agentMessage"
145
+ ? createTextPart(this.ctx, item.text ?? "", true)
146
+ : createReasoningPart(this.ctx, item.text ?? "", true))) as TextPart | ReasoningPart;
147
+ if (item.text !== undefined) p.text = item.text;
148
+ p.state = "done";
149
+ if (!part) this.partByItem.set(item.id, p);
150
+ return [{ kind: "part-update", part: p }];
151
+ }
152
+ default: {
153
+ const d = codexToolDescriptor(item.type);
154
+ if (!d) return [];
155
+ const tool = this.ensureTool(item, d);
156
+ completeToolPart(tool, d.result(item));
157
+ return [{ kind: "part-update", part: tool }];
158
+ }
159
+ }
160
+ }
161
+
162
+ private textDelta(
163
+ itemId: string | undefined,
164
+ delta: string | undefined,
165
+ kind: "text" | "reasoning",
166
+ ): AdapterEvent[] {
167
+ if (!itemId || !delta) return [];
168
+ const events: AdapterEvent[] = [];
169
+ let part = this.partByItem.get(itemId) as TextPart | ReasoningPart | undefined;
170
+ if (!part) {
171
+ part =
172
+ kind === "text"
173
+ ? createTextPart(this.ctx, "", true)
174
+ : createReasoningPart(this.ctx, "", true);
175
+ this.partByItem.set(itemId, part);
176
+ events.push({ kind: "part-open", part });
177
+ }
178
+ part.text += delta;
179
+ events.push(
180
+ kind === "text"
181
+ ? { kind: "text-delta", partId: part.id, text: delta }
182
+ : { kind: "reasoning-delta", partId: part.id, text: delta },
183
+ );
184
+ return events;
185
+ }
186
+
187
+ private openTool(item: CodexItem, d: CodexToolDescriptor): AdapterEvent[] {
188
+ const part = createToolPart(this.ctx, item.id, d.toolName(item), d.input(item), d.meta(item));
189
+ this.partByItem.set(item.id, part);
190
+ return [{ kind: "part-open", part }];
191
+ }
192
+
193
+ private ensureTool(item: CodexItem, d: CodexToolDescriptor): ToolPart {
194
+ const existing = this.partByItem.get(item.id) as ToolPart | undefined;
195
+ if (existing) return existing;
196
+ const part = createToolPart(this.ctx, item.id, d.toolName(item), d.input(item), d.meta(item));
197
+ this.partByItem.set(item.id, part);
198
+ return part;
199
+ }
200
+
201
+ private captureUsage(total: Record<string, number> | undefined): void {
202
+ if (!total) return;
203
+ this.usage = {
204
+ input: total.inputTokens ?? 0,
205
+ output: total.outputTokens ?? 0,
206
+ reasoning: total.reasoningOutputTokens ?? total.reasoningTokens ?? 0,
207
+ cache: { read: total.cachedInputTokens ?? 0, write: 0 },
208
+ };
209
+ }
210
+ }
211
+
212
+ export function createCodexAppServerTransformer(ctx: { sessionId: string }): EventTransformer {
213
+ return new CodexAppServerTransformer(ctx);
214
+ }
@@ -0,0 +1,221 @@
1
+ import { type ChildProcess, spawn as nodeSpawn } from "node:child_process";
2
+ import { StringDecoder } from "node:string_decoder";
3
+
4
+ /** A JSON-RPC notification (server → client, no id). */
5
+ export interface CodexNotification {
6
+ method: string;
7
+ params: Record<string, unknown> & { threadId?: string; turnId?: string };
8
+ }
9
+ type NotificationHandler = (n: CodexNotification) => void;
10
+
11
+ /** Answers a server→client request (approvals). Throwing rejects the request. */
12
+ export type ServerRequestHandler = (
13
+ method: string,
14
+ params: Record<string, unknown>,
15
+ ) => Promise<unknown>;
16
+
17
+ interface PendingRequest {
18
+ method: string;
19
+ resolve: (value: unknown) => void;
20
+ reject: (error: Error) => void;
21
+ timer?: ReturnType<typeof setTimeout>;
22
+ }
23
+
24
+ interface RpcResponse {
25
+ id: number;
26
+ result?: unknown;
27
+ error?: { code?: number; message?: string };
28
+ }
29
+
30
+ export interface CodexAppServerClientOptions {
31
+ codexPath?: string;
32
+ cwd?: string;
33
+ env?: NodeJS.ProcessEnv;
34
+ startupTimeoutMs?: number;
35
+ /** Default deadline for every JSON-RPC request. */
36
+ requestTimeoutMs?: number;
37
+ /** Injectable process boundary for deterministic tests. */
38
+ spawnProcess?: typeof nodeSpawn;
39
+ }
40
+
41
+ /**
42
+ * Newline-delimited (JSONL) JSON-RPC client over a `codex app-server` subprocess.
43
+ * Requests get monotonic ids resolved against a pending map; notifications fan
44
+ * out to handlers. Inbound server→client requests (approvals) go to the
45
+ * registered request handler; without one they are declined (-32601).
46
+ */
47
+ export class CodexAppServerClient {
48
+ private proc?: ChildProcess;
49
+ private nextId = 1;
50
+ private buffer = "";
51
+ private readonly decoder = new StringDecoder("utf8");
52
+ private readonly pending = new Map<number, PendingRequest>();
53
+ private readonly handlers = new Set<NotificationHandler>();
54
+ private readonly closeHandlers = new Set<(error?: Error) => void>();
55
+ private requestHandler?: ServerRequestHandler;
56
+ private exited = false;
57
+
58
+ constructor(private readonly opts: CodexAppServerClientOptions = {}) {}
59
+
60
+ onNotification(handler: NotificationHandler): () => void {
61
+ this.handlers.add(handler);
62
+ return () => this.handlers.delete(handler);
63
+ }
64
+
65
+ /** Install the answerer for server→client requests (approval round-trips). */
66
+ setRequestHandler(handler: ServerRequestHandler | undefined): void {
67
+ this.requestHandler = handler;
68
+ }
69
+
70
+ /** Fires when the subprocess exits or the client is closed — lets callers
71
+ * unblock any in-flight turn instead of waiting on notifications that will
72
+ * never arrive. */
73
+ onClose(handler: (error?: Error) => void): () => void {
74
+ this.closeHandlers.add(handler);
75
+ return () => this.closeHandlers.delete(handler);
76
+ }
77
+
78
+ get closed(): boolean {
79
+ return this.exited;
80
+ }
81
+
82
+ async initialize(): Promise<void> {
83
+ this.start();
84
+ await this.request(
85
+ "initialize",
86
+ {
87
+ clientInfo: { name: "agent-server", title: null, version: "0.1.0" },
88
+ capabilities: null,
89
+ },
90
+ this.opts.startupTimeoutMs ?? 15_000,
91
+ );
92
+ }
93
+
94
+ request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {
95
+ if (!this.proc || this.exited) return Promise.reject(new Error("codex app-server not running"));
96
+ const id = this.nextId++;
97
+ return new Promise<unknown>((resolve, reject) => {
98
+ const deadline = timeoutMs ?? this.opts.requestTimeoutMs ?? 30_000;
99
+ const timer = setTimeout(() => {
100
+ this.pending.delete(id);
101
+ reject(new Error(`codex app-server '${method}' timed out`));
102
+ }, deadline);
103
+ this.pending.set(id, { method, resolve, reject, timer });
104
+ try {
105
+ this.write({ id, method, params });
106
+ } catch (error) {
107
+ clearTimeout(timer);
108
+ this.pending.delete(id);
109
+ reject(error);
110
+ }
111
+ });
112
+ }
113
+
114
+ close(): void {
115
+ const proc = this.proc;
116
+ const wasOpen = !this.exited;
117
+ this.proc = undefined;
118
+ this.exited = true;
119
+ for (const [id, p] of this.pending) {
120
+ if (p.timer) clearTimeout(p.timer);
121
+ p.reject(new Error(`codex app-server closed before '${p.method}'`));
122
+ this.pending.delete(id);
123
+ }
124
+ if (proc && !proc.killed) proc.kill("SIGTERM");
125
+ if (wasOpen) this.notifyClosed();
126
+ }
127
+
128
+ private notifyClosed(error?: Error): void {
129
+ for (const handler of this.closeHandlers) handler(error);
130
+ this.closeHandlers.clear();
131
+ }
132
+
133
+ private start(): void {
134
+ if (this.proc && !this.exited) return;
135
+ this.exited = false;
136
+ const spawnProcess = this.opts.spawnProcess ?? nodeSpawn;
137
+ const proc = spawnProcess(this.opts.codexPath ?? "codex", ["app-server"], {
138
+ cwd: this.opts.cwd,
139
+ env: this.opts.env ?? process.env,
140
+ stdio: ["pipe", "pipe", "pipe"],
141
+ });
142
+ this.proc = proc;
143
+ proc.stdout?.on("data", (chunk: Buffer) => this.onStdout(chunk));
144
+ proc.stdin?.on("error", (err) => this.onExit(err));
145
+ proc.stdout?.on("error", (err) => this.onExit(err));
146
+ proc.stderr?.on("data", () => {});
147
+ proc.on("error", (err) => this.onExit(err));
148
+ proc.on("exit", (code, signal) =>
149
+ this.onExit(new Error(`codex app-server exited (code=${code} signal=${signal})`)),
150
+ );
151
+ }
152
+
153
+ private onStdout(chunk: Buffer): void {
154
+ this.buffer += this.decoder.write(chunk);
155
+ let nl = this.buffer.indexOf("\n");
156
+ while (nl !== -1) {
157
+ const line = this.buffer.slice(0, nl).trim();
158
+ this.buffer = this.buffer.slice(nl + 1);
159
+ if (line) {
160
+ try {
161
+ this.handleMessage(JSON.parse(line));
162
+ } catch {
163
+ // ignore non-JSON lines
164
+ }
165
+ }
166
+ nl = this.buffer.indexOf("\n");
167
+ }
168
+ }
169
+
170
+ private handleMessage(msg: unknown): void {
171
+ if (!msg || typeof msg !== "object") return;
172
+ const m = msg as { id?: number; method?: string; params?: unknown } & RpcResponse;
173
+ if (typeof m.id === "number" && m.method === undefined) {
174
+ const pending = this.pending.get(m.id);
175
+ if (!pending) return;
176
+ this.pending.delete(m.id);
177
+ if (pending.timer) clearTimeout(pending.timer);
178
+ if (m.error) pending.reject(new Error(m.error.message ?? `error from '${pending.method}'`));
179
+ else pending.resolve(m.result);
180
+ return;
181
+ }
182
+ if (typeof m.method === "string") {
183
+ if (typeof m.id === "number") {
184
+ // Server→client request (approvals). Route to the handler; decline
185
+ // when none is installed (approvalPolicy "never" turns).
186
+ const handler = this.requestHandler;
187
+ const id = m.id;
188
+ if (!handler) {
189
+ this.write({ id, error: { code: -32601, message: `unsupported: ${m.method}` } });
190
+ return;
191
+ }
192
+ void handler(m.method, (m.params ?? {}) as Record<string, unknown>)
193
+ .then((result) => this.write({ id, result }))
194
+ .catch((err: unknown) =>
195
+ this.write({ id, error: { code: -32603, message: String(err) } }),
196
+ );
197
+ return;
198
+ }
199
+ const notification: CodexNotification = {
200
+ method: m.method,
201
+ params: (m.params ?? {}) as CodexNotification["params"],
202
+ };
203
+ for (const handler of this.handlers) handler(notification);
204
+ }
205
+ }
206
+
207
+ private onExit(error: Error): void {
208
+ if (this.exited) return;
209
+ this.exited = true;
210
+ for (const [id, p] of this.pending) {
211
+ if (p.timer) clearTimeout(p.timer);
212
+ p.reject(error);
213
+ this.pending.delete(id);
214
+ }
215
+ this.notifyClosed(error);
216
+ }
217
+
218
+ private write(payload: unknown): void {
219
+ this.proc?.stdin?.write(`${JSON.stringify(payload)}\n`);
220
+ }
221
+ }