@posthog/agent 1.30.0 → 2.0.1
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/LICENSE +1 -1
- package/README.md +221 -219
- package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
- package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
- package/dist/adapters/claude/permissions/permission-options.js +117 -0
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
- package/dist/adapters/claude/questions/utils.d.ts +132 -0
- package/dist/adapters/claude/questions/utils.js +63 -0
- package/dist/adapters/claude/questions/utils.js.map +1 -0
- package/dist/adapters/claude/tools.d.ts +18 -0
- package/dist/adapters/claude/tools.js +95 -0
- package/dist/adapters/claude/tools.js.map +1 -0
- package/dist/agent-DBQY1BfC.d.ts +123 -0
- package/dist/agent.d.ts +5 -0
- package/dist/agent.js +3656 -0
- package/dist/agent.js.map +1 -0
- package/dist/claude-cli/cli.js +3695 -2746
- package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
- package/dist/gateway-models.d.ts +24 -0
- package/dist/gateway-models.js +93 -0
- package/dist/gateway-models.js.map +1 -0
- package/dist/index.d.ts +172 -1203
- package/dist/index.js +3704 -6826
- package/dist/index.js.map +1 -1
- package/dist/logger-DDBiMOOD.d.ts +24 -0
- package/dist/posthog-api.d.ts +40 -0
- package/dist/posthog-api.js +175 -0
- package/dist/posthog-api.js.map +1 -0
- package/dist/server/agent-server.d.ts +41 -0
- package/dist/server/agent-server.js +4451 -0
- package/dist/server/agent-server.js.map +1 -0
- package/dist/server/bin.d.ts +1 -0
- package/dist/server/bin.js +4507 -0
- package/dist/server/bin.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -14
- package/src/acp-extensions.ts +93 -61
- package/src/adapters/acp-connection.ts +494 -0
- package/src/adapters/base-acp-agent.ts +150 -0
- package/src/adapters/claude/claude-agent.ts +596 -0
- package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
- package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
- package/src/adapters/claude/hooks.ts +64 -0
- package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
- package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
- package/src/adapters/claude/permissions/permission-options.ts +103 -0
- package/src/adapters/claude/plan/utils.ts +56 -0
- package/src/adapters/claude/questions/utils.ts +92 -0
- package/src/adapters/claude/session/commands.ts +38 -0
- package/src/adapters/claude/session/mcp-config.ts +37 -0
- package/src/adapters/claude/session/models.ts +12 -0
- package/src/adapters/claude/session/options.ts +236 -0
- package/src/adapters/claude/tool-meta.ts +143 -0
- package/src/adapters/claude/tools.ts +53 -611
- package/src/adapters/claude/types.ts +61 -0
- package/src/adapters/codex/spawn.ts +130 -0
- package/src/agent.ts +97 -734
- package/src/execution-mode.ts +43 -0
- package/src/gateway-models.ts +135 -0
- package/src/index.ts +79 -0
- package/src/otel-log-writer.test.ts +105 -0
- package/src/otel-log-writer.ts +94 -0
- package/src/posthog-api.ts +75 -235
- package/src/resume.ts +115 -0
- package/src/sagas/apply-snapshot-saga.test.ts +690 -0
- package/src/sagas/apply-snapshot-saga.ts +88 -0
- package/src/sagas/capture-tree-saga.test.ts +892 -0
- package/src/sagas/capture-tree-saga.ts +141 -0
- package/src/sagas/resume-saga.test.ts +558 -0
- package/src/sagas/resume-saga.ts +332 -0
- package/src/sagas/test-fixtures.ts +250 -0
- package/src/server/agent-server.test.ts +220 -0
- package/src/server/agent-server.ts +748 -0
- package/src/server/bin.ts +88 -0
- package/src/server/jwt.ts +65 -0
- package/src/server/schemas.ts +47 -0
- package/src/server/types.ts +13 -0
- package/src/server/utils/retry.test.ts +122 -0
- package/src/server/utils/retry.ts +61 -0
- package/src/server/utils/sse-parser.test.ts +93 -0
- package/src/server/utils/sse-parser.ts +46 -0
- package/src/session-log-writer.test.ts +140 -0
- package/src/session-log-writer.ts +137 -0
- package/src/test/assertions.ts +114 -0
- package/src/test/controllers/sse-controller.ts +107 -0
- package/src/test/fixtures/api.ts +111 -0
- package/src/test/fixtures/config.ts +33 -0
- package/src/test/fixtures/notifications.ts +92 -0
- package/src/test/mocks/claude-sdk.ts +251 -0
- package/src/test/mocks/msw-handlers.ts +48 -0
- package/src/test/setup.ts +114 -0
- package/src/test/wait.ts +41 -0
- package/src/tree-tracker.ts +173 -0
- package/src/types.ts +51 -154
- package/src/utils/acp-content.ts +58 -0
- package/src/utils/async-mutex.test.ts +104 -0
- package/src/utils/async-mutex.ts +31 -0
- package/src/utils/common.ts +15 -0
- package/src/utils/gateway.ts +9 -6
- package/src/utils/logger.ts +0 -30
- package/src/utils/streams.ts +220 -0
- package/CLAUDE.md +0 -331
- package/dist/templates/plan-template.md +0 -41
- package/src/adapters/claude/claude.ts +0 -1543
- package/src/adapters/claude/mcp-server.ts +0 -810
- package/src/adapters/claude/utils.ts +0 -267
- package/src/agents/execution.ts +0 -37
- package/src/agents/planning.ts +0 -60
- package/src/agents/research.ts +0 -160
- package/src/file-manager.ts +0 -306
- package/src/git-manager.ts +0 -577
- package/src/prompt-builder.ts +0 -499
- package/src/schemas.ts +0 -241
- package/src/session-store.ts +0 -259
- package/src/task-manager.ts +0 -163
- package/src/template-manager.ts +0 -236
- package/src/templates/plan-template.md +0 -41
- package/src/todo-manager.ts +0 -180
- package/src/tools/registry.ts +0 -129
- package/src/tools/types.ts +0 -127
- package/src/utils/tapped-stream.ts +0 -60
- package/src/workflow/config.ts +0 -53
- package/src/workflow/steps/build.ts +0 -135
- package/src/workflow/steps/finalize.ts +0 -241
- package/src/workflow/steps/plan.ts +0 -167
- package/src/workflow/steps/research.ts +0 -223
- package/src/workflow/types.ts +0 -62
- package/src/workflow/utils.ts +0 -53
- package/src/worktree-manager.ts +0 -928
|
@@ -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
|
+
}
|