@posthog/agent 2.0.0 → 2.0.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.
Files changed (131) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +221 -219
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
  4. package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
  5. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
  6. package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
  7. package/dist/adapters/claude/permissions/permission-options.js +117 -0
  8. package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
  9. package/dist/adapters/claude/questions/utils.d.ts +132 -0
  10. package/dist/adapters/claude/questions/utils.js +63 -0
  11. package/dist/adapters/claude/questions/utils.js.map +1 -0
  12. package/dist/adapters/claude/tools.d.ts +18 -0
  13. package/dist/adapters/claude/tools.js +95 -0
  14. package/dist/adapters/claude/tools.js.map +1 -0
  15. package/dist/agent-DBQY1BfC.d.ts +123 -0
  16. package/dist/agent.d.ts +5 -0
  17. package/dist/agent.js +3656 -0
  18. package/dist/agent.js.map +1 -0
  19. package/dist/claude-cli/cli.js +3695 -2746
  20. package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
  21. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
  22. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
  23. package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
  24. package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
  25. package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
  26. package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
  27. package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
  28. package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
  29. package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
  30. package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
  31. package/dist/gateway-models.d.ts +24 -0
  32. package/dist/gateway-models.js +93 -0
  33. package/dist/gateway-models.js.map +1 -0
  34. package/dist/index.d.ts +170 -1157
  35. package/dist/index.js +9373 -5135
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger-DDBiMOOD.d.ts +24 -0
  38. package/dist/posthog-api.d.ts +40 -0
  39. package/dist/posthog-api.js +175 -0
  40. package/dist/posthog-api.js.map +1 -0
  41. package/dist/server/agent-server.d.ts +41 -0
  42. package/dist/server/agent-server.js +10503 -0
  43. package/dist/server/agent-server.js.map +1 -0
  44. package/dist/server/bin.d.ts +1 -0
  45. package/dist/server/bin.js +10558 -0
  46. package/dist/server/bin.js.map +1 -0
  47. package/dist/types.d.ts +129 -0
  48. package/dist/types.js +1 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +65 -13
  51. package/src/acp-extensions.ts +98 -16
  52. package/src/adapters/acp-connection.ts +494 -0
  53. package/src/adapters/base-acp-agent.ts +150 -0
  54. package/src/adapters/claude/claude-agent.ts +596 -0
  55. package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
  56. package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
  57. package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
  58. package/src/adapters/claude/hooks.ts +64 -0
  59. package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
  60. package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
  61. package/src/adapters/claude/permissions/permission-options.ts +103 -0
  62. package/src/adapters/claude/plan/utils.ts +56 -0
  63. package/src/adapters/claude/questions/utils.ts +92 -0
  64. package/src/adapters/claude/session/commands.ts +38 -0
  65. package/src/adapters/claude/session/mcp-config.ts +37 -0
  66. package/src/adapters/claude/session/models.ts +12 -0
  67. package/src/adapters/claude/session/options.ts +236 -0
  68. package/src/adapters/claude/tool-meta.ts +143 -0
  69. package/src/adapters/claude/tools.ts +53 -688
  70. package/src/adapters/claude/types.ts +61 -0
  71. package/src/adapters/codex/spawn.ts +130 -0
  72. package/src/agent.ts +96 -587
  73. package/src/execution-mode.ts +43 -0
  74. package/src/gateway-models.ts +135 -0
  75. package/src/index.ts +79 -0
  76. package/src/otel-log-writer.test.ts +105 -0
  77. package/src/otel-log-writer.ts +94 -0
  78. package/src/posthog-api.ts +75 -235
  79. package/src/resume.ts +115 -0
  80. package/src/sagas/apply-snapshot-saga.test.ts +690 -0
  81. package/src/sagas/apply-snapshot-saga.ts +88 -0
  82. package/src/sagas/capture-tree-saga.test.ts +892 -0
  83. package/src/sagas/capture-tree-saga.ts +141 -0
  84. package/src/sagas/resume-saga.test.ts +558 -0
  85. package/src/sagas/resume-saga.ts +332 -0
  86. package/src/sagas/test-fixtures.ts +250 -0
  87. package/src/server/agent-server.test.ts +220 -0
  88. package/src/server/agent-server.ts +748 -0
  89. package/src/server/bin.ts +88 -0
  90. package/src/server/jwt.ts +65 -0
  91. package/src/server/schemas.ts +47 -0
  92. package/src/server/types.ts +13 -0
  93. package/src/server/utils/retry.test.ts +122 -0
  94. package/src/server/utils/retry.ts +61 -0
  95. package/src/server/utils/sse-parser.test.ts +93 -0
  96. package/src/server/utils/sse-parser.ts +46 -0
  97. package/src/session-log-writer.test.ts +140 -0
  98. package/src/session-log-writer.ts +137 -0
  99. package/src/test/assertions.ts +114 -0
  100. package/src/test/controllers/sse-controller.ts +107 -0
  101. package/src/test/fixtures/api.ts +111 -0
  102. package/src/test/fixtures/config.ts +33 -0
  103. package/src/test/fixtures/notifications.ts +92 -0
  104. package/src/test/mocks/claude-sdk.ts +251 -0
  105. package/src/test/mocks/msw-handlers.ts +48 -0
  106. package/src/test/setup.ts +114 -0
  107. package/src/test/wait.ts +41 -0
  108. package/src/tree-tracker.ts +173 -0
  109. package/src/types.ts +54 -137
  110. package/src/utils/acp-content.ts +58 -0
  111. package/src/utils/async-mutex.test.ts +104 -0
  112. package/src/utils/async-mutex.ts +31 -0
  113. package/src/utils/common.ts +15 -0
  114. package/src/utils/gateway.ts +9 -6
  115. package/src/utils/logger.ts +0 -30
  116. package/src/utils/streams.ts +220 -0
  117. package/CLAUDE.md +0 -331
  118. package/src/adapters/claude/claude.ts +0 -1947
  119. package/src/adapters/claude/mcp-server.ts +0 -810
  120. package/src/adapters/claude/utils.ts +0 -267
  121. package/src/adapters/connection.ts +0 -95
  122. package/src/file-manager.ts +0 -273
  123. package/src/git-manager.ts +0 -577
  124. package/src/schemas.ts +0 -241
  125. package/src/session-store.ts +0 -259
  126. package/src/task-manager.ts +0 -163
  127. package/src/todo-manager.ts +0 -180
  128. package/src/tools/registry.ts +0 -134
  129. package/src/tools/types.ts +0 -133
  130. package/src/utils/tapped-stream.ts +0 -60
  131. package/src/worktree-manager.ts +0 -974
@@ -0,0 +1,494 @@
1
+ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
2
+ import { POSTHOG_NOTIFICATIONS } from "../acp-extensions.js";
3
+ import type { SessionLogWriter } from "../session-log-writer.js";
4
+ import type { ProcessSpawnedCallback } from "../types.js";
5
+ import { Logger } from "../utils/logger.js";
6
+ import {
7
+ createBidirectionalStreams,
8
+ createTappedWritableStream,
9
+ nodeReadableToWebReadable,
10
+ nodeWritableToWebWritable,
11
+ type StreamPair,
12
+ } from "../utils/streams.js";
13
+ import { ClaudeAcpAgent } from "./claude/claude-agent.js";
14
+ import { type CodexProcessOptions, spawnCodexProcess } from "./codex/spawn.js";
15
+
16
+ export type AgentAdapter = "claude" | "codex";
17
+
18
+ export type AcpConnectionConfig = {
19
+ adapter?: AgentAdapter;
20
+ logWriter?: SessionLogWriter;
21
+ taskRunId?: string;
22
+ taskId?: string;
23
+ /** Deployment environment - "local" for desktop, "cloud" for cloud sandbox */
24
+ deviceType?: "local" | "cloud";
25
+ logger?: Logger;
26
+ processCallbacks?: ProcessSpawnedCallback;
27
+ codexOptions?: CodexProcessOptions;
28
+ allowedModelIds?: Set<string>;
29
+ };
30
+
31
+ export type AcpConnection = {
32
+ agentConnection?: AgentSideConnection;
33
+ clientStreams: StreamPair;
34
+ cleanup: () => Promise<void>;
35
+ };
36
+
37
+ export type InProcessAcpConnection = AcpConnection;
38
+
39
+ type ConfigOption = {
40
+ id?: string;
41
+ category?: string | null;
42
+ currentValue?: string;
43
+ options?: Array<
44
+ { value?: string } | { group?: string; options?: Array<{ value?: string }> }
45
+ >;
46
+ };
47
+
48
+ function isGroupedOptions(
49
+ options: NonNullable<ConfigOption["options"]>,
50
+ ): options is Array<{ group?: string; options?: Array<{ value?: string }> }> {
51
+ return options.length > 0 && "group" in options[0];
52
+ }
53
+
54
+ function filterModelConfigOptions(
55
+ msg: Record<string, unknown>,
56
+ allowedModelIds: Set<string>,
57
+ ): Record<string, unknown> | null {
58
+ const payload = msg as {
59
+ method?: string;
60
+ result?: { configOptions?: ConfigOption[] };
61
+ params?: {
62
+ update?: { sessionUpdate?: string; configOptions?: ConfigOption[] };
63
+ };
64
+ };
65
+
66
+ const configOptions =
67
+ payload.result?.configOptions ?? payload.params?.update?.configOptions;
68
+ if (!configOptions) return null;
69
+
70
+ const filtered = configOptions.map((opt) => {
71
+ if (opt.category !== "model" || !opt.options) return opt;
72
+
73
+ const options = opt.options;
74
+ if (isGroupedOptions(options)) {
75
+ const filteredOptions = options.map((group) => ({
76
+ ...group,
77
+ options: (group.options ?? []).filter(
78
+ (o) => o?.value && allowedModelIds.has(o.value),
79
+ ),
80
+ }));
81
+ const flat = filteredOptions.flatMap((g) => g.options ?? []);
82
+ const currentAllowed =
83
+ opt.currentValue && allowedModelIds.has(opt.currentValue);
84
+ const nextCurrent =
85
+ currentAllowed || flat.length === 0 ? opt.currentValue : flat[0]?.value;
86
+
87
+ return {
88
+ ...opt,
89
+ currentValue: nextCurrent,
90
+ options: filteredOptions,
91
+ };
92
+ }
93
+
94
+ const valueOptions = options as Array<{ value?: string }>;
95
+ const filteredOptions = valueOptions.filter(
96
+ (o) => o?.value && allowedModelIds.has(o.value),
97
+ );
98
+ const currentAllowed =
99
+ opt.currentValue && allowedModelIds.has(opt.currentValue);
100
+ const nextCurrent =
101
+ currentAllowed || filteredOptions.length === 0
102
+ ? opt.currentValue
103
+ : filteredOptions[0]?.value;
104
+
105
+ return {
106
+ ...opt,
107
+ currentValue: nextCurrent,
108
+ options: filteredOptions,
109
+ };
110
+ });
111
+
112
+ if (payload.result?.configOptions) {
113
+ return { ...msg, result: { ...payload.result, configOptions: filtered } };
114
+ }
115
+ if (payload.params?.update?.configOptions) {
116
+ return {
117
+ ...msg,
118
+ params: {
119
+ ...payload.params,
120
+ update: { ...payload.params.update, configOptions: filtered },
121
+ },
122
+ };
123
+ }
124
+ return null;
125
+ }
126
+
127
+ function extractReasoningEffort(
128
+ configOptions: ConfigOption[] | undefined,
129
+ ): string | undefined {
130
+ if (!configOptions) return undefined;
131
+ const option = configOptions.find((opt) => opt.id === "reasoning_effort");
132
+ return option?.currentValue ?? undefined;
133
+ }
134
+
135
+ /**
136
+ * Creates an ACP connection with the specified agent framework.
137
+ *
138
+ * @param config - Configuration including framework selection
139
+ * @returns Connection with agent and client streams
140
+ */
141
+ export function createAcpConnection(
142
+ config: AcpConnectionConfig = {},
143
+ ): AcpConnection {
144
+ const adapterType = config.adapter ?? "claude";
145
+
146
+ if (adapterType === "codex") {
147
+ return createCodexConnection(config);
148
+ }
149
+
150
+ return createClaudeConnection(config);
151
+ }
152
+
153
+ function createClaudeConnection(config: AcpConnectionConfig): AcpConnection {
154
+ const logger =
155
+ config.logger?.child("AcpConnection") ??
156
+ new Logger({ debug: true, prefix: "[AcpConnection]" });
157
+ const streams = createBidirectionalStreams();
158
+
159
+ const { logWriter } = config;
160
+
161
+ let agentWritable = streams.agent.writable;
162
+ let clientWritable = streams.client.writable;
163
+
164
+ if (config.taskRunId && logWriter) {
165
+ if (!logWriter.isRegistered(config.taskRunId)) {
166
+ logWriter.register(config.taskRunId, {
167
+ taskId: config.taskId ?? config.taskRunId,
168
+ runId: config.taskRunId,
169
+ deviceType: config.deviceType,
170
+ });
171
+ }
172
+
173
+ agentWritable = createTappedWritableStream(streams.agent.writable, {
174
+ onMessage: (line) => {
175
+ logWriter.appendRawLine(config.taskRunId!, line);
176
+ },
177
+ logger,
178
+ });
179
+
180
+ clientWritable = createTappedWritableStream(streams.client.writable, {
181
+ onMessage: (line) => {
182
+ logWriter.appendRawLine(config.taskRunId!, line);
183
+ },
184
+ logger,
185
+ });
186
+ } else {
187
+ logger.info("Tapped streams NOT enabled", {
188
+ hasTaskRunId: !!config.taskRunId,
189
+ hasLogWriter: !!logWriter,
190
+ });
191
+ }
192
+
193
+ const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
194
+
195
+ let agent: ClaudeAcpAgent | null = null;
196
+ const agentConnection = new AgentSideConnection((client) => {
197
+ agent = new ClaudeAcpAgent(client, logWriter, config.processCallbacks);
198
+ logger.info(`Created ${agent.adapterName} agent`);
199
+ return agent;
200
+ }, agentStream);
201
+
202
+ return {
203
+ agentConnection,
204
+ clientStreams: {
205
+ readable: streams.client.readable,
206
+ writable: clientWritable,
207
+ },
208
+ cleanup: async () => {
209
+ logger.info("Cleaning up ACP connection");
210
+
211
+ if (agent) {
212
+ await agent.closeSession();
213
+ }
214
+
215
+ try {
216
+ await streams.client.writable.close();
217
+ } catch {
218
+ // Stream may already be closed
219
+ }
220
+ try {
221
+ await streams.agent.writable.close();
222
+ } catch {
223
+ // Stream may already be closed
224
+ }
225
+ },
226
+ };
227
+ }
228
+
229
+ function createCodexConnection(config: AcpConnectionConfig): AcpConnection {
230
+ const logger =
231
+ config.logger?.child("CodexConnection") ??
232
+ new Logger({ debug: true, prefix: "[CodexConnection]" });
233
+
234
+ const { logWriter } = config;
235
+ const allowedModelIds = config.allowedModelIds;
236
+
237
+ const codexProcess = spawnCodexProcess({
238
+ ...config.codexOptions,
239
+ logger,
240
+ processCallbacks: config.processCallbacks,
241
+ });
242
+
243
+ let clientReadable = nodeReadableToWebReadable(codexProcess.stdout);
244
+ let clientWritable = nodeWritableToWebWritable(codexProcess.stdin);
245
+
246
+ let isLoadingSession = false;
247
+ let loadRequestId: string | number | null = null;
248
+ let newSessionRequestId: string | number | null = null;
249
+ let sdkSessionEmitted = false;
250
+ const reasoningEffortBySessionId = new Map<string, string>();
251
+ let injectedConfigId = 0;
252
+
253
+ const decoder = new TextDecoder();
254
+ const encoder = new TextEncoder();
255
+ let readBuffer = "";
256
+
257
+ const taskRunId = config.taskRunId;
258
+
259
+ const filteringReadable = clientReadable.pipeThrough(
260
+ new TransformStream<Uint8Array, Uint8Array>({
261
+ transform(chunk, controller) {
262
+ readBuffer += decoder.decode(chunk, { stream: true });
263
+ const lines = readBuffer.split("\n");
264
+ readBuffer = lines.pop() ?? "";
265
+
266
+ const outputLines: string[] = [];
267
+
268
+ for (const line of lines) {
269
+ const trimmed = line.trim();
270
+ if (!trimmed) {
271
+ outputLines.push(line);
272
+ continue;
273
+ }
274
+
275
+ let shouldFilter = false;
276
+
277
+ try {
278
+ const msg = JSON.parse(trimmed);
279
+ const sessionId =
280
+ msg?.params?.sessionId ?? msg?.result?.sessionId ?? null;
281
+ const configOptions =
282
+ msg?.result?.configOptions ?? msg?.params?.update?.configOptions;
283
+ if (sessionId && configOptions) {
284
+ const effort = extractReasoningEffort(configOptions);
285
+ if (effort) {
286
+ reasoningEffortBySessionId.set(sessionId, effort);
287
+ }
288
+ }
289
+
290
+ if (
291
+ !sdkSessionEmitted &&
292
+ newSessionRequestId !== null &&
293
+ msg.id === newSessionRequestId &&
294
+ "result" in msg
295
+ ) {
296
+ const sessionId = msg.result?.sessionId;
297
+ if (sessionId && taskRunId) {
298
+ const sdkSessionNotification = {
299
+ jsonrpc: "2.0",
300
+ method: POSTHOG_NOTIFICATIONS.SDK_SESSION,
301
+ params: {
302
+ taskRunId,
303
+ sessionId,
304
+ adapter: "codex",
305
+ },
306
+ };
307
+ outputLines.push(JSON.stringify(sdkSessionNotification));
308
+ sdkSessionEmitted = true;
309
+ }
310
+ newSessionRequestId = null;
311
+ }
312
+
313
+ if (isLoadingSession) {
314
+ if (msg.id === loadRequestId && "result" in msg) {
315
+ logger.debug("session/load complete, resuming stream");
316
+ isLoadingSession = false;
317
+ loadRequestId = null;
318
+ } else if (msg.method === "session/update") {
319
+ shouldFilter = true;
320
+ }
321
+ }
322
+
323
+ if (!shouldFilter && allowedModelIds && allowedModelIds.size > 0) {
324
+ const updated = filterModelConfigOptions(msg, allowedModelIds);
325
+ if (updated) {
326
+ outputLines.push(JSON.stringify(updated));
327
+ continue;
328
+ }
329
+ }
330
+ } catch {
331
+ // Not valid JSON, pass through
332
+ }
333
+
334
+ if (!shouldFilter) {
335
+ outputLines.push(line);
336
+ const isChunkNoise =
337
+ trimmed.includes('"sessionUpdate":"agent_message_chunk"') ||
338
+ trimmed.includes('"sessionUpdate":"agent_thought_chunk"');
339
+ if (!isChunkNoise) {
340
+ logger.debug("codex-acp stdout:", trimmed);
341
+ }
342
+ }
343
+ }
344
+
345
+ if (outputLines.length > 0) {
346
+ const output = `${outputLines.join("\n")}\n`;
347
+ controller.enqueue(encoder.encode(output));
348
+ }
349
+ },
350
+ flush(controller) {
351
+ if (readBuffer.trim()) {
352
+ controller.enqueue(encoder.encode(readBuffer));
353
+ }
354
+ },
355
+ }),
356
+ );
357
+ clientReadable = filteringReadable;
358
+
359
+ const originalWritable = clientWritable;
360
+ clientWritable = new WritableStream({
361
+ write(chunk) {
362
+ const text = decoder.decode(chunk, { stream: true });
363
+ const trimmed = text.trim();
364
+ logger.debug("codex-acp stdin:", trimmed);
365
+
366
+ try {
367
+ const msg = JSON.parse(trimmed);
368
+ if (
369
+ msg.method === "session/set_config_option" &&
370
+ msg.params?.configId === "reasoning_effort" &&
371
+ msg.params?.sessionId &&
372
+ msg.params?.value
373
+ ) {
374
+ reasoningEffortBySessionId.set(
375
+ msg.params.sessionId,
376
+ msg.params.value,
377
+ );
378
+ }
379
+ if (msg.method === "session/prompt" && msg.params?.sessionId) {
380
+ const effort = reasoningEffortBySessionId.get(msg.params.sessionId);
381
+ if (effort) {
382
+ const injection = {
383
+ jsonrpc: "2.0",
384
+ id: `reasoning_effort_${Date.now()}_${injectedConfigId++}`,
385
+ method: "session/set_config_option",
386
+ params: {
387
+ sessionId: msg.params.sessionId,
388
+ configId: "reasoning_effort",
389
+ value: effort,
390
+ },
391
+ };
392
+ const injectionLine = `${JSON.stringify(injection)}\n`;
393
+ const writer = originalWritable.getWriter();
394
+ return writer
395
+ .write(encoder.encode(injectionLine))
396
+ .then(() => writer.releaseLock())
397
+ .then(() => {
398
+ const nextWriter = originalWritable.getWriter();
399
+ return nextWriter
400
+ .write(chunk)
401
+ .finally(() => nextWriter.releaseLock());
402
+ });
403
+ }
404
+ }
405
+ if (msg.method === "session/new" && msg.id) {
406
+ logger.debug("session/new detected, tracking request ID");
407
+ newSessionRequestId = msg.id;
408
+ } else if (msg.method === "session/load" && msg.id) {
409
+ logger.debug("session/load detected, pausing stream updates");
410
+ isLoadingSession = true;
411
+ loadRequestId = msg.id;
412
+ }
413
+ } catch {
414
+ // Not valid JSON
415
+ }
416
+
417
+ const writer = originalWritable.getWriter();
418
+ return writer.write(chunk).finally(() => writer.releaseLock());
419
+ },
420
+ close() {
421
+ const writer = originalWritable.getWriter();
422
+ return writer.close().finally(() => writer.releaseLock());
423
+ },
424
+ });
425
+
426
+ const shouldTapLogs = config.taskRunId && logWriter;
427
+
428
+ if (shouldTapLogs) {
429
+ const taskRunId = config.taskRunId!;
430
+ if (!logWriter.isRegistered(taskRunId)) {
431
+ logWriter.register(taskRunId, {
432
+ taskId: config.taskId ?? taskRunId,
433
+ runId: taskRunId,
434
+ });
435
+ }
436
+
437
+ clientWritable = createTappedWritableStream(clientWritable, {
438
+ onMessage: (line) => {
439
+ logWriter.appendRawLine(taskRunId, line);
440
+ },
441
+ logger,
442
+ });
443
+
444
+ const originalReadable = clientReadable;
445
+ const logDecoder = new TextDecoder();
446
+ let logBuffer = "";
447
+
448
+ clientReadable = originalReadable.pipeThrough(
449
+ new TransformStream<Uint8Array, Uint8Array>({
450
+ transform(chunk, controller) {
451
+ logBuffer += logDecoder.decode(chunk, { stream: true });
452
+ const lines = logBuffer.split("\n");
453
+ logBuffer = lines.pop() ?? "";
454
+
455
+ for (const line of lines) {
456
+ if (line.trim()) {
457
+ logWriter.appendRawLine(taskRunId, line);
458
+ }
459
+ }
460
+
461
+ controller.enqueue(chunk);
462
+ },
463
+ flush() {
464
+ if (logBuffer.trim()) {
465
+ logWriter.appendRawLine(taskRunId, logBuffer);
466
+ }
467
+ },
468
+ }),
469
+ );
470
+ } else {
471
+ logger.info("Tapped streams NOT enabled for Codex", {
472
+ hasTaskRunId: !!config.taskRunId,
473
+ hasLogWriter: !!logWriter,
474
+ });
475
+ }
476
+
477
+ return {
478
+ agentConnection: undefined,
479
+ clientStreams: {
480
+ readable: clientReadable,
481
+ writable: clientWritable,
482
+ },
483
+ cleanup: async () => {
484
+ logger.info("Cleaning up Codex connection");
485
+ codexProcess.kill();
486
+
487
+ try {
488
+ await clientWritable.close();
489
+ } catch {
490
+ // Stream may already be closed
491
+ }
492
+ },
493
+ };
494
+ }
@@ -0,0 +1,150 @@
1
+ import type {
2
+ Agent,
3
+ AgentSideConnection,
4
+ AuthenticateRequest,
5
+ CancelNotification,
6
+ InitializeRequest,
7
+ InitializeResponse,
8
+ NewSessionRequest,
9
+ NewSessionResponse,
10
+ PromptRequest,
11
+ PromptResponse,
12
+ ReadTextFileRequest,
13
+ ReadTextFileResponse,
14
+ SessionConfigSelectOption,
15
+ SessionNotification,
16
+ WriteTextFileRequest,
17
+ WriteTextFileResponse,
18
+ } from "@agentclientprotocol/sdk";
19
+ import {
20
+ DEFAULT_GATEWAY_MODEL,
21
+ fetchGatewayModels,
22
+ formatGatewayModelName,
23
+ isAnthropicModel,
24
+ } from "../gateway-models.js";
25
+ import { Logger } from "../utils/logger.js";
26
+
27
+ export interface BaseSession {
28
+ notificationHistory: SessionNotification[];
29
+ cancelled: boolean;
30
+ interruptReason?: string;
31
+ abortController: AbortController;
32
+ }
33
+
34
+ export abstract class BaseAcpAgent implements Agent {
35
+ abstract readonly adapterName: string;
36
+ protected session!: BaseSession;
37
+ protected sessionId!: string;
38
+ client: AgentSideConnection;
39
+ logger: Logger;
40
+ fileContentCache: { [key: string]: string } = {};
41
+
42
+ constructor(client: AgentSideConnection) {
43
+ this.client = client;
44
+ this.logger = new Logger({ debug: true, prefix: "[BaseAcpAgent]" });
45
+ }
46
+
47
+ abstract initialize(request: InitializeRequest): Promise<InitializeResponse>;
48
+ abstract newSession(params: NewSessionRequest): Promise<NewSessionResponse>;
49
+ abstract prompt(params: PromptRequest): Promise<PromptResponse>;
50
+ protected abstract interruptSession(): Promise<void>;
51
+
52
+ async cancel(params: CancelNotification): Promise<void> {
53
+ if (this.sessionId !== params.sessionId) {
54
+ throw new Error("Session not found");
55
+ }
56
+ this.session.cancelled = true;
57
+ const meta = params._meta as { interruptReason?: string } | undefined;
58
+ if (meta?.interruptReason) {
59
+ this.session.interruptReason = meta.interruptReason;
60
+ }
61
+ await this.interruptSession();
62
+ }
63
+
64
+ async closeSession(): Promise<void> {
65
+ try {
66
+ // Abort first so in-flight HTTP requests are cancelled,
67
+ // otherwise interrupt() deadlocks waiting for the query to stop
68
+ // while the query waits on an API call that will never abort.
69
+ this.session.abortController.abort();
70
+ await this.cancel({ sessionId: this.sessionId });
71
+ this.logger.info("Closed session", { sessionId: this.sessionId });
72
+ } catch (err) {
73
+ this.logger.warn("Failed to close session", {
74
+ sessionId: this.sessionId,
75
+ error: err,
76
+ });
77
+ }
78
+ }
79
+
80
+ hasSession(sessionId: string): boolean {
81
+ return this.sessionId === sessionId;
82
+ }
83
+
84
+ appendNotification(
85
+ sessionId: string,
86
+ notification: SessionNotification,
87
+ ): void {
88
+ if (this.sessionId === sessionId) {
89
+ this.session.notificationHistory.push(notification);
90
+ }
91
+ }
92
+
93
+ async readTextFile(
94
+ params: ReadTextFileRequest,
95
+ ): Promise<ReadTextFileResponse> {
96
+ const response = await this.client.readTextFile(params);
97
+ if (!params.limit && !params.line) {
98
+ this.fileContentCache[params.path] = response.content;
99
+ }
100
+ return response;
101
+ }
102
+
103
+ async writeTextFile(
104
+ params: WriteTextFileRequest,
105
+ ): Promise<WriteTextFileResponse> {
106
+ const response = await this.client.writeTextFile(params);
107
+ this.fileContentCache[params.path] = params.content;
108
+ return response;
109
+ }
110
+
111
+ async authenticate(_params: AuthenticateRequest): Promise<void> {
112
+ throw new Error("Method not implemented.");
113
+ }
114
+
115
+ async getModelConfigOptions(currentModelOverride?: string): Promise<{
116
+ currentModelId: string;
117
+ options: SessionConfigSelectOption[];
118
+ }> {
119
+ const gatewayModels = await fetchGatewayModels();
120
+
121
+ const options = gatewayModels
122
+ .filter((model) => isAnthropicModel(model))
123
+ .map((model) => ({
124
+ value: model.id,
125
+ name: formatGatewayModelName(model),
126
+ description: `Context: ${model.context_window.toLocaleString()} tokens`,
127
+ }));
128
+
129
+ const isAnthropicModelId = (modelId: string): boolean =>
130
+ modelId.startsWith("claude-") || modelId.startsWith("anthropic/");
131
+
132
+ let currentModelId = currentModelOverride ?? DEFAULT_GATEWAY_MODEL;
133
+
134
+ if (!options.some((opt) => opt.value === currentModelId)) {
135
+ if (!isAnthropicModelId(currentModelId)) {
136
+ currentModelId = DEFAULT_GATEWAY_MODEL;
137
+ }
138
+ }
139
+
140
+ if (!options.some((opt) => opt.value === currentModelId)) {
141
+ options.unshift({
142
+ value: currentModelId,
143
+ name: currentModelId,
144
+ description: "Custom model",
145
+ });
146
+ }
147
+
148
+ return { currentModelId, options };
149
+ }
150
+ }