@posthog/agent 1.29.0 → 2.0.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/dist/index.d.ts +57 -87
- package/dist/index.js +916 -2203
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/acp-extensions.ts +0 -37
- package/src/adapters/claude/claude.ts +515 -107
- package/src/adapters/claude/tools.ts +178 -101
- package/src/adapters/connection.ts +95 -0
- package/src/agent.ts +50 -184
- package/src/file-manager.ts +1 -34
- package/src/git-manager.ts +2 -20
- package/src/posthog-api.ts +4 -4
- package/src/tools/registry.ts +5 -0
- package/src/tools/types.ts +6 -0
- package/src/types.ts +5 -25
- package/src/utils/gateway.ts +15 -0
- package/src/worktree-manager.ts +92 -46
- package/dist/templates/plan-template.md +0 -41
- package/src/agents/execution.ts +0 -37
- package/src/agents/planning.ts +0 -60
- package/src/agents/research.ts +0 -160
- package/src/prompt-builder.ts +0 -497
- package/src/template-manager.ts +0 -240
- package/src/templates/plan-template.md +0 -41
- 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
|
@@ -38,24 +38,30 @@ interface ToolUpdate {
|
|
|
38
38
|
locations?: ToolCallLocation[];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
interface ToolUse {
|
|
42
|
+
name: string;
|
|
43
|
+
input?: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
export function toolInfoFromToolUse(
|
|
42
|
-
toolUse:
|
|
47
|
+
toolUse: ToolUse,
|
|
43
48
|
cachedFileContent: { [key: string]: string },
|
|
44
49
|
logger: Logger = new Logger({ debug: false, prefix: "[ClaudeTools]" }),
|
|
45
50
|
): ToolInfo {
|
|
46
51
|
const name = toolUse.name;
|
|
47
|
-
|
|
52
|
+
// Cast input to allow property access - each case handles its expected properties
|
|
53
|
+
const input = toolUse.input as Record<string, unknown> | undefined;
|
|
48
54
|
|
|
49
55
|
switch (name) {
|
|
50
56
|
case "Task":
|
|
51
57
|
return {
|
|
52
|
-
title: input?.description ? input.description : "Task",
|
|
58
|
+
title: input?.description ? String(input.description) : "Task",
|
|
53
59
|
kind: "think",
|
|
54
60
|
content: input?.prompt
|
|
55
61
|
? [
|
|
56
62
|
{
|
|
57
63
|
type: "content",
|
|
58
|
-
content: { type: "text", text: input.prompt },
|
|
64
|
+
content: { type: "text", text: String(input.prompt) },
|
|
59
65
|
},
|
|
60
66
|
]
|
|
61
67
|
: [],
|
|
@@ -64,42 +70,46 @@ export function toolInfoFromToolUse(
|
|
|
64
70
|
case "NotebookRead":
|
|
65
71
|
return {
|
|
66
72
|
title: input?.notebook_path
|
|
67
|
-
? `Read Notebook ${input.notebook_path}`
|
|
73
|
+
? `Read Notebook ${String(input.notebook_path)}`
|
|
68
74
|
: "Read Notebook",
|
|
69
75
|
kind: "read",
|
|
70
76
|
content: [],
|
|
71
|
-
locations: input?.notebook_path
|
|
77
|
+
locations: input?.notebook_path
|
|
78
|
+
? [{ path: String(input.notebook_path) }]
|
|
79
|
+
: [],
|
|
72
80
|
};
|
|
73
81
|
|
|
74
82
|
case "NotebookEdit":
|
|
75
83
|
return {
|
|
76
84
|
title: input?.notebook_path
|
|
77
|
-
? `Edit Notebook ${input.notebook_path}`
|
|
85
|
+
? `Edit Notebook ${String(input.notebook_path)}`
|
|
78
86
|
: "Edit Notebook",
|
|
79
87
|
kind: "edit",
|
|
80
88
|
content: input?.new_source
|
|
81
89
|
? [
|
|
82
90
|
{
|
|
83
91
|
type: "content",
|
|
84
|
-
content: { type: "text", text: input.new_source },
|
|
92
|
+
content: { type: "text", text: String(input.new_source) },
|
|
85
93
|
},
|
|
86
94
|
]
|
|
87
95
|
: [],
|
|
88
|
-
locations: input?.notebook_path
|
|
96
|
+
locations: input?.notebook_path
|
|
97
|
+
? [{ path: String(input.notebook_path) }]
|
|
98
|
+
: [],
|
|
89
99
|
};
|
|
90
100
|
|
|
91
101
|
case "Bash":
|
|
92
102
|
case toolNames.bash:
|
|
93
103
|
return {
|
|
94
104
|
title: input?.command
|
|
95
|
-
? `\`${input.command.replaceAll("`", "\\`")}\``
|
|
105
|
+
? `\`${String(input.command).replaceAll("`", "\\`")}\``
|
|
96
106
|
: "Terminal",
|
|
97
107
|
kind: "execute",
|
|
98
108
|
content: input?.description
|
|
99
109
|
? [
|
|
100
110
|
{
|
|
101
111
|
type: "content",
|
|
102
|
-
content: { type: "text", text: input.description },
|
|
112
|
+
content: { type: "text", text: String(input.description) },
|
|
103
113
|
},
|
|
104
114
|
]
|
|
105
115
|
: [],
|
|
@@ -123,24 +133,21 @@ export function toolInfoFromToolUse(
|
|
|
123
133
|
|
|
124
134
|
case toolNames.read: {
|
|
125
135
|
let limit = "";
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
")";
|
|
133
|
-
} else if (input.offset) {
|
|
134
|
-
limit = ` (from line ${input.offset + 1})`;
|
|
136
|
+
const inputLimit = input?.limit as number | undefined;
|
|
137
|
+
const inputOffset = (input?.offset as number | undefined) ?? 0;
|
|
138
|
+
if (inputLimit) {
|
|
139
|
+
limit = ` (${inputOffset + 1} - ${inputOffset + inputLimit})`;
|
|
140
|
+
} else if (inputOffset) {
|
|
141
|
+
limit = ` (from line ${inputOffset + 1})`;
|
|
135
142
|
}
|
|
136
143
|
return {
|
|
137
|
-
title: `Read ${input.file_path
|
|
144
|
+
title: `Read ${input?.file_path ? String(input.file_path) : "File"}${limit}`,
|
|
138
145
|
kind: "read",
|
|
139
|
-
locations: input
|
|
146
|
+
locations: input?.file_path
|
|
140
147
|
? [
|
|
141
148
|
{
|
|
142
|
-
path: input.file_path,
|
|
143
|
-
line:
|
|
149
|
+
path: String(input.file_path),
|
|
150
|
+
line: inputOffset,
|
|
144
151
|
},
|
|
145
152
|
]
|
|
146
153
|
: [],
|
|
@@ -153,11 +160,11 @@ export function toolInfoFromToolUse(
|
|
|
153
160
|
title: "Read File",
|
|
154
161
|
kind: "read",
|
|
155
162
|
content: [],
|
|
156
|
-
locations: input
|
|
163
|
+
locations: input?.file_path
|
|
157
164
|
? [
|
|
158
165
|
{
|
|
159
|
-
path: input.file_path,
|
|
160
|
-
line: input
|
|
166
|
+
path: String(input.file_path),
|
|
167
|
+
line: (input?.offset as number | undefined) ?? 0,
|
|
161
168
|
},
|
|
162
169
|
]
|
|
163
170
|
: [],
|
|
@@ -165,7 +172,7 @@ export function toolInfoFromToolUse(
|
|
|
165
172
|
|
|
166
173
|
case "LS":
|
|
167
174
|
return {
|
|
168
|
-
title: `List the ${input?.path ? `\`${input.path}\`` : "current"} directory's contents`,
|
|
175
|
+
title: `List the ${input?.path ? `\`${String(input.path)}\`` : "current"} directory's contents`,
|
|
169
176
|
kind: "search",
|
|
170
177
|
content: [],
|
|
171
178
|
locations: [],
|
|
@@ -173,9 +180,9 @@ export function toolInfoFromToolUse(
|
|
|
173
180
|
|
|
174
181
|
case toolNames.edit:
|
|
175
182
|
case "Edit": {
|
|
176
|
-
const path = input?.file_path
|
|
177
|
-
let oldText = input.old_string
|
|
178
|
-
let newText = input.new_string
|
|
183
|
+
const path = input?.file_path ? String(input.file_path) : undefined;
|
|
184
|
+
let oldText = input?.old_string ? String(input.old_string) : null;
|
|
185
|
+
let newText = input?.new_string ? String(input.new_string) : "";
|
|
179
186
|
let affectedLines: number[] = [];
|
|
180
187
|
|
|
181
188
|
if (path && oldText) {
|
|
@@ -218,86 +225,92 @@ export function toolInfoFromToolUse(
|
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
case toolNames.write: {
|
|
221
|
-
let
|
|
222
|
-
|
|
223
|
-
|
|
228
|
+
let contentResult: ToolCallContent[] = [];
|
|
229
|
+
const filePath = input?.file_path ? String(input.file_path) : undefined;
|
|
230
|
+
const contentStr = input?.content ? String(input.content) : undefined;
|
|
231
|
+
if (filePath) {
|
|
232
|
+
contentResult = [
|
|
224
233
|
{
|
|
225
234
|
type: "diff",
|
|
226
|
-
path:
|
|
235
|
+
path: filePath,
|
|
227
236
|
oldText: null,
|
|
228
|
-
newText:
|
|
237
|
+
newText: contentStr ?? "",
|
|
229
238
|
},
|
|
230
239
|
];
|
|
231
|
-
} else if (
|
|
232
|
-
|
|
240
|
+
} else if (contentStr) {
|
|
241
|
+
contentResult = [
|
|
233
242
|
{
|
|
234
243
|
type: "content",
|
|
235
|
-
content: { type: "text", text:
|
|
244
|
+
content: { type: "text", text: contentStr },
|
|
236
245
|
},
|
|
237
246
|
];
|
|
238
247
|
}
|
|
239
248
|
return {
|
|
240
|
-
title:
|
|
249
|
+
title: filePath ? `Write ${filePath}` : "Write",
|
|
241
250
|
kind: "edit",
|
|
242
|
-
content,
|
|
243
|
-
locations:
|
|
251
|
+
content: contentResult,
|
|
252
|
+
locations: filePath ? [{ path: filePath }] : [],
|
|
244
253
|
};
|
|
245
254
|
}
|
|
246
255
|
|
|
247
|
-
case "Write":
|
|
256
|
+
case "Write": {
|
|
257
|
+
const filePath = input?.file_path ? String(input.file_path) : undefined;
|
|
258
|
+
const contentStr = input?.content ? String(input.content) : "";
|
|
248
259
|
return {
|
|
249
|
-
title:
|
|
260
|
+
title: filePath ? `Write ${filePath}` : "Write",
|
|
250
261
|
kind: "edit",
|
|
251
|
-
content:
|
|
262
|
+
content: filePath
|
|
252
263
|
? [
|
|
253
264
|
{
|
|
254
265
|
type: "diff",
|
|
255
|
-
path:
|
|
266
|
+
path: filePath,
|
|
256
267
|
oldText: null,
|
|
257
|
-
newText:
|
|
268
|
+
newText: contentStr,
|
|
258
269
|
},
|
|
259
270
|
]
|
|
260
271
|
: [],
|
|
261
|
-
locations:
|
|
272
|
+
locations: filePath ? [{ path: filePath }] : [],
|
|
262
273
|
};
|
|
274
|
+
}
|
|
263
275
|
|
|
264
276
|
case "Glob": {
|
|
265
277
|
let label = "Find";
|
|
266
|
-
|
|
267
|
-
|
|
278
|
+
const pathStr = input?.path ? String(input.path) : undefined;
|
|
279
|
+
if (pathStr) {
|
|
280
|
+
label += ` \`${pathStr}\``;
|
|
268
281
|
}
|
|
269
|
-
if (input
|
|
270
|
-
label += ` \`${input.pattern}\``;
|
|
282
|
+
if (input?.pattern) {
|
|
283
|
+
label += ` \`${String(input.pattern)}\``;
|
|
271
284
|
}
|
|
272
285
|
return {
|
|
273
286
|
title: label,
|
|
274
287
|
kind: "search",
|
|
275
288
|
content: [],
|
|
276
|
-
locations:
|
|
289
|
+
locations: pathStr ? [{ path: pathStr }] : [],
|
|
277
290
|
};
|
|
278
291
|
}
|
|
279
292
|
|
|
280
293
|
case "Grep": {
|
|
281
294
|
let label = "grep";
|
|
282
295
|
|
|
283
|
-
if (input["-i"]) {
|
|
296
|
+
if (input?.["-i"]) {
|
|
284
297
|
label += " -i";
|
|
285
298
|
}
|
|
286
|
-
if (input["-n"]) {
|
|
299
|
+
if (input?.["-n"]) {
|
|
287
300
|
label += " -n";
|
|
288
301
|
}
|
|
289
302
|
|
|
290
|
-
if (input["-A"] !== undefined) {
|
|
303
|
+
if (input?.["-A"] !== undefined) {
|
|
291
304
|
label += ` -A ${input["-A"]}`;
|
|
292
305
|
}
|
|
293
|
-
if (input["-B"] !== undefined) {
|
|
306
|
+
if (input?.["-B"] !== undefined) {
|
|
294
307
|
label += ` -B ${input["-B"]}`;
|
|
295
308
|
}
|
|
296
|
-
if (input["-C"] !== undefined) {
|
|
309
|
+
if (input?.["-C"] !== undefined) {
|
|
297
310
|
label += ` -C ${input["-C"]}`;
|
|
298
311
|
}
|
|
299
312
|
|
|
300
|
-
if (input
|
|
313
|
+
if (input?.output_mode) {
|
|
301
314
|
switch (input.output_mode) {
|
|
302
315
|
case "FilesWithMatches":
|
|
303
316
|
label += " -l";
|
|
@@ -310,26 +323,26 @@ export function toolInfoFromToolUse(
|
|
|
310
323
|
}
|
|
311
324
|
}
|
|
312
325
|
|
|
313
|
-
if (input
|
|
326
|
+
if (input?.head_limit !== undefined) {
|
|
314
327
|
label += ` | head -${input.head_limit}`;
|
|
315
328
|
}
|
|
316
329
|
|
|
317
|
-
if (input
|
|
318
|
-
label += ` --include="${input.glob}"`;
|
|
330
|
+
if (input?.glob) {
|
|
331
|
+
label += ` --include="${String(input.glob)}"`;
|
|
319
332
|
}
|
|
320
333
|
|
|
321
|
-
if (input
|
|
322
|
-
label += ` --type=${input.type}`;
|
|
334
|
+
if (input?.type) {
|
|
335
|
+
label += ` --type=${String(input.type)}`;
|
|
323
336
|
}
|
|
324
337
|
|
|
325
|
-
if (input
|
|
338
|
+
if (input?.multiline) {
|
|
326
339
|
label += " -P";
|
|
327
340
|
}
|
|
328
341
|
|
|
329
|
-
label += ` "${input.pattern}"`;
|
|
342
|
+
label += ` "${input?.pattern ? String(input.pattern) : ""}"`;
|
|
330
343
|
|
|
331
|
-
if (input
|
|
332
|
-
label += ` ${input.path}`;
|
|
344
|
+
if (input?.path) {
|
|
345
|
+
label += ` ${String(input.path)}`;
|
|
333
346
|
}
|
|
334
347
|
|
|
335
348
|
return {
|
|
@@ -341,27 +354,29 @@ export function toolInfoFromToolUse(
|
|
|
341
354
|
|
|
342
355
|
case "WebFetch":
|
|
343
356
|
return {
|
|
344
|
-
title: input?.url ? `Fetch ${input.url}` : "Fetch",
|
|
357
|
+
title: input?.url ? `Fetch ${String(input.url)}` : "Fetch",
|
|
345
358
|
kind: "fetch",
|
|
346
359
|
content: input?.prompt
|
|
347
360
|
? [
|
|
348
361
|
{
|
|
349
362
|
type: "content",
|
|
350
|
-
content: { type: "text", text: input.prompt },
|
|
363
|
+
content: { type: "text", text: String(input.prompt) },
|
|
351
364
|
},
|
|
352
365
|
]
|
|
353
366
|
: [],
|
|
354
367
|
};
|
|
355
368
|
|
|
356
369
|
case "WebSearch": {
|
|
357
|
-
let label = `"${input.query}"`;
|
|
370
|
+
let label = `"${input?.query ? String(input.query) : ""}"`;
|
|
371
|
+
const allowedDomains = input?.allowed_domains as string[] | undefined;
|
|
372
|
+
const blockedDomains = input?.blocked_domains as string[] | undefined;
|
|
358
373
|
|
|
359
|
-
if (
|
|
360
|
-
label += ` (allowed: ${
|
|
374
|
+
if (allowedDomains && allowedDomains.length > 0) {
|
|
375
|
+
label += ` (allowed: ${allowedDomains.join(", ")})`;
|
|
361
376
|
}
|
|
362
377
|
|
|
363
|
-
if (
|
|
364
|
-
label += ` (blocked: ${
|
|
378
|
+
if (blockedDomains && blockedDomains.length > 0) {
|
|
379
|
+
label += ` (blocked: ${blockedDomains.join(", ")})`;
|
|
365
380
|
}
|
|
366
381
|
|
|
367
382
|
return {
|
|
@@ -374,7 +389,7 @@ export function toolInfoFromToolUse(
|
|
|
374
389
|
case "TodoWrite":
|
|
375
390
|
return {
|
|
376
391
|
title: Array.isArray(input?.todos)
|
|
377
|
-
? `Update TODOs: ${input.todos.map((todo:
|
|
392
|
+
? `Update TODOs: ${input.todos.map((todo: { content?: string }) => todo.content).join(", ")}`
|
|
378
393
|
: "Update TODOs",
|
|
379
394
|
kind: "think",
|
|
380
395
|
content: [],
|
|
@@ -385,9 +400,35 @@ export function toolInfoFromToolUse(
|
|
|
385
400
|
title: "Ready to code?",
|
|
386
401
|
kind: "switch_mode",
|
|
387
402
|
content: input?.plan
|
|
388
|
-
? [
|
|
403
|
+
? [
|
|
404
|
+
{
|
|
405
|
+
type: "content",
|
|
406
|
+
content: { type: "text", text: String(input.plan) },
|
|
407
|
+
},
|
|
408
|
+
]
|
|
409
|
+
: [],
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
case "AskUserQuestion": {
|
|
413
|
+
const questions = input?.questions as
|
|
414
|
+
| Array<{ question?: string }>
|
|
415
|
+
| undefined;
|
|
416
|
+
return {
|
|
417
|
+
title: questions?.[0]?.question || "Question",
|
|
418
|
+
kind: "ask" as ToolKind,
|
|
419
|
+
content: questions
|
|
420
|
+
? [
|
|
421
|
+
{
|
|
422
|
+
type: "content",
|
|
423
|
+
content: {
|
|
424
|
+
type: "text",
|
|
425
|
+
text: JSON.stringify(questions, null, 2),
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
]
|
|
389
429
|
: [],
|
|
390
430
|
};
|
|
431
|
+
}
|
|
391
432
|
|
|
392
433
|
case "Other": {
|
|
393
434
|
let output: string;
|
|
@@ -431,25 +472,32 @@ export function toolUpdateFromToolResult(
|
|
|
431
472
|
| BetaTextEditorCodeExecutionToolResultBlockParam
|
|
432
473
|
| BetaRequestMCPToolResultBlockParam
|
|
433
474
|
| BetaToolSearchToolResultBlockParam,
|
|
434
|
-
toolUse:
|
|
475
|
+
toolUse: ToolUse | undefined,
|
|
435
476
|
): ToolUpdate {
|
|
436
477
|
switch (toolUse?.name) {
|
|
437
478
|
case "Read":
|
|
438
479
|
case toolNames.read:
|
|
439
480
|
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
|
440
481
|
return {
|
|
441
|
-
content: toolResult.content.map((
|
|
442
|
-
type
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
),
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
482
|
+
content: toolResult.content.map((item) => {
|
|
483
|
+
const itemObj = item as { type?: string; text?: string };
|
|
484
|
+
if (itemObj.type === "text") {
|
|
485
|
+
return {
|
|
486
|
+
type: "content" as const,
|
|
487
|
+
content: {
|
|
488
|
+
type: "text" as const,
|
|
489
|
+
text: markdownEscape(
|
|
490
|
+
(itemObj.text ?? "").replace(SYSTEM_REMINDER, ""),
|
|
491
|
+
),
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// For non-text content, return as-is with proper typing
|
|
496
|
+
return {
|
|
497
|
+
type: "content" as const,
|
|
498
|
+
content: item as { type: "text"; text: string },
|
|
499
|
+
};
|
|
500
|
+
}),
|
|
453
501
|
};
|
|
454
502
|
} else if (
|
|
455
503
|
typeof toolResult.content === "string" &&
|
|
@@ -492,6 +540,29 @@ export function toolUpdateFromToolResult(
|
|
|
492
540
|
case "ExitPlanMode": {
|
|
493
541
|
return { title: "Exited Plan Mode" };
|
|
494
542
|
}
|
|
543
|
+
case "AskUserQuestion": {
|
|
544
|
+
// The answer is returned in the tool result
|
|
545
|
+
const content = toolResult.content;
|
|
546
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
547
|
+
const firstItem = content[0];
|
|
548
|
+
if (
|
|
549
|
+
typeof firstItem === "object" &&
|
|
550
|
+
firstItem !== null &&
|
|
551
|
+
"text" in firstItem
|
|
552
|
+
) {
|
|
553
|
+
return {
|
|
554
|
+
title: "Answer received",
|
|
555
|
+
content: [
|
|
556
|
+
{
|
|
557
|
+
type: "content",
|
|
558
|
+
content: { type: "text", text: String(firstItem.text) },
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return { title: "Question answered" };
|
|
565
|
+
}
|
|
495
566
|
default: {
|
|
496
567
|
return toAcpContentUpdate(
|
|
497
568
|
toolResult.content,
|
|
@@ -502,21 +573,27 @@ export function toolUpdateFromToolResult(
|
|
|
502
573
|
}
|
|
503
574
|
|
|
504
575
|
function toAcpContentUpdate(
|
|
505
|
-
content:
|
|
576
|
+
content: unknown,
|
|
506
577
|
isError: boolean = false,
|
|
507
578
|
): { content?: ToolCallContent[] } {
|
|
508
579
|
if (Array.isArray(content) && content.length > 0) {
|
|
509
580
|
return {
|
|
510
|
-
content: content.map((
|
|
511
|
-
type
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
581
|
+
content: content.map((item) => {
|
|
582
|
+
const itemObj = item as { type?: string; text?: string };
|
|
583
|
+
if (isError && itemObj.type === "text") {
|
|
584
|
+
return {
|
|
585
|
+
type: "content" as const,
|
|
586
|
+
content: {
|
|
587
|
+
type: "text" as const,
|
|
588
|
+
text: `\`\`\`\n${itemObj.text ?? ""}\n\`\`\``,
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
type: "content" as const,
|
|
594
|
+
content: item as { type: "text"; text: string },
|
|
595
|
+
};
|
|
596
|
+
}),
|
|
520
597
|
};
|
|
521
598
|
} else if (typeof content === "string" && content.length > 0) {
|
|
522
599
|
return {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ACP connection factory.
|
|
3
|
+
*
|
|
4
|
+
* Creates ACP connections for the Claude Code agent.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
8
|
+
import type { SessionStore } from "@/session-store.js";
|
|
9
|
+
import { Logger } from "@/utils/logger.js";
|
|
10
|
+
import { createTappedWritableStream } from "@/utils/tapped-stream.js";
|
|
11
|
+
import { ClaudeAcpAgent } from "./claude/claude.js";
|
|
12
|
+
import { createBidirectionalStreams, type StreamPair } from "./claude/utils.js";
|
|
13
|
+
|
|
14
|
+
export type AgentFramework = "claude";
|
|
15
|
+
|
|
16
|
+
export type AcpConnectionConfig = {
|
|
17
|
+
framework?: AgentFramework;
|
|
18
|
+
sessionStore?: SessionStore;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
taskId?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type InProcessAcpConnection = {
|
|
24
|
+
agentConnection: AgentSideConnection;
|
|
25
|
+
clientStreams: StreamPair;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates an ACP connection with the specified agent framework.
|
|
30
|
+
*
|
|
31
|
+
* @param config - Configuration including framework selection
|
|
32
|
+
* @returns Connection with agent and client streams
|
|
33
|
+
*/
|
|
34
|
+
export function createAcpConnection(
|
|
35
|
+
config: AcpConnectionConfig = {},
|
|
36
|
+
): InProcessAcpConnection {
|
|
37
|
+
const logger = new Logger({ debug: true, prefix: "[AcpConnection]" });
|
|
38
|
+
const streams = createBidirectionalStreams();
|
|
39
|
+
|
|
40
|
+
const { sessionStore, framework = "claude" } = config;
|
|
41
|
+
|
|
42
|
+
// Tap both streams for automatic persistence
|
|
43
|
+
// All messages (bidirectional) will be persisted as they flow through
|
|
44
|
+
let agentWritable = streams.agent.writable;
|
|
45
|
+
let clientWritable = streams.client.writable;
|
|
46
|
+
|
|
47
|
+
if (config.sessionId && sessionStore) {
|
|
48
|
+
// Register session for persistence BEFORE tapping streams
|
|
49
|
+
// This ensures all messages from the start get persisted
|
|
50
|
+
if (!sessionStore.isRegistered(config.sessionId)) {
|
|
51
|
+
sessionStore.register(config.sessionId, {
|
|
52
|
+
taskId: config.taskId ?? config.sessionId,
|
|
53
|
+
runId: config.sessionId,
|
|
54
|
+
logUrl: "", // Will be updated when we get the real logUrl
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Tap agent→client stream
|
|
59
|
+
agentWritable = createTappedWritableStream(streams.agent.writable, {
|
|
60
|
+
onMessage: (line) => {
|
|
61
|
+
sessionStore.appendRawLine(config.sessionId!, line);
|
|
62
|
+
},
|
|
63
|
+
logger,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Tap client→agent stream
|
|
67
|
+
clientWritable = createTappedWritableStream(streams.client.writable, {
|
|
68
|
+
onMessage: (line) => {
|
|
69
|
+
sessionStore.appendRawLine(config.sessionId!, line);
|
|
70
|
+
},
|
|
71
|
+
logger,
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
logger.info("Tapped streams NOT enabled", {
|
|
75
|
+
hasSessionId: !!config.sessionId,
|
|
76
|
+
hasSessionStore: !!sessionStore,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
|
|
81
|
+
|
|
82
|
+
// Create the Claude agent
|
|
83
|
+
const agentConnection = new AgentSideConnection((client) => {
|
|
84
|
+
logger.info("Creating Claude agent");
|
|
85
|
+
return new ClaudeAcpAgent(client, sessionStore);
|
|
86
|
+
}, agentStream);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
agentConnection,
|
|
90
|
+
clientStreams: {
|
|
91
|
+
readable: streams.client.readable,
|
|
92
|
+
writable: clientWritable,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|