@ai-sdk/harness-acp 0.0.0 → 1.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/CHANGELOG.md +24 -0
- package/LICENSE +13 -0
- package/README.md +69 -3
- package/dist/bridge/host-tool-mcp.mjs +302 -0
- package/dist/bridge/host-tool-mcp.mjs.map +1 -0
- package/dist/bridge/index.mjs +3317 -0
- package/dist/bridge/index.mjs.map +1 -0
- package/dist/bridge/package.json +12 -0
- package/dist/bridge/pnpm-lock.yaml +816 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +2089 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -10
- package/src/acp-auth.ts +254 -0
- package/src/acp-bridge-protocol.ts +13 -0
- package/src/acp-harness.ts +107 -0
- package/src/index.ts +19 -0
- package/src/v1/acp-v1-bridge-protocol.ts +151 -0
- package/src/v1/acp-v1-harness.ts +1461 -0
- package/src/v1/acp-v1-lifecycle.ts +115 -0
- package/src/v1/acp-v1-prompt.ts +122 -0
- package/src/v1/acp-v1-settings.ts +97 -0
- package/src/v1/acp-v1-skills.ts +228 -0
- package/src/v1/acp-v1-turn-start-config.ts +93 -0
- package/src/v1/bridge/acp-diagnostics.ts +83 -0
- package/src/v1/bridge/acp-stream-capture.ts +152 -0
- package/src/v1/bridge/acp-v1-bridge-environment.ts +95 -0
- package/src/v1/bridge/agent-stderr-monitor.ts +52 -0
- package/src/v1/bridge/create-emit-stream-event.ts +97 -0
- package/src/v1/bridge/host-tool-correlation.ts +640 -0
- package/src/v1/bridge/host-tool-mcp-server.ts +180 -0
- package/src/v1/bridge/host-tool-mcp.ts +185 -0
- package/src/v1/bridge/host-tool-relay-client.ts +53 -0
- package/src/v1/bridge/host-tool-relay.ts +510 -0
- package/src/v1/bridge/index.ts +714 -0
- package/src/v1/bridge/package.json +12 -0
- package/src/v1/bridge/permission-controller.ts +152 -0
- package/src/v1/bridge/permission-mode.ts +163 -0
- package/src/v1/bridge/pnpm-lock.yaml +816 -0
- package/src/v1/bridge/profile-values.ts +97 -0
- package/src/v1/bridge/protocol-configuration.ts +146 -0
- package/src/v1/bridge/recovered-session.ts +145 -0
- package/src/v1/bridge/refresh-host-tool-catalog.ts +33 -0
- package/src/v1/bridge/session-lifecycle.ts +83 -0
- package/src/v1/bridge/stream-translator.ts +901 -0
- package/src/v1/implementation.ts +308 -0
- package/src/v1/index.ts +16 -0
- package/src/version.ts +6 -0
- package/index.js +0 -1
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
import type { HarnessV1StreamPart } from '@ai-sdk/harness';
|
|
2
|
+
import type { PromptResponse, SessionUpdate } from '@agentclientprotocol/sdk';
|
|
3
|
+
import type { ACPBuiltinToolMapping } from '../acp-v1-bridge-protocol';
|
|
4
|
+
|
|
5
|
+
type HarnessUsage = Extract<
|
|
6
|
+
HarnessV1StreamPart,
|
|
7
|
+
{ readonly type: 'finish' }
|
|
8
|
+
>['totalUsage'];
|
|
9
|
+
type HarnessToolResult = Extract<
|
|
10
|
+
HarnessV1StreamPart,
|
|
11
|
+
{ readonly type: 'tool-result' }
|
|
12
|
+
>['result'];
|
|
13
|
+
type HarnessJSONValue = HarnessToolResult | null;
|
|
14
|
+
|
|
15
|
+
type ToolState = {
|
|
16
|
+
readonly toolCallId: string;
|
|
17
|
+
readonly values: Record<string, unknown>;
|
|
18
|
+
readonly emittedFilePaths: Set<string>;
|
|
19
|
+
emittedCall: boolean;
|
|
20
|
+
emittedResult: boolean;
|
|
21
|
+
toolName?: string;
|
|
22
|
+
nativeName?: string;
|
|
23
|
+
dynamic?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ACPSessionUpdate = SessionUpdate;
|
|
27
|
+
export type ACPPromptResponse = PromptResponse;
|
|
28
|
+
|
|
29
|
+
export function createACPStreamTranslator({
|
|
30
|
+
emit,
|
|
31
|
+
builtinTools = [],
|
|
32
|
+
}: {
|
|
33
|
+
emit: (event: HarnessV1StreamPart) => void;
|
|
34
|
+
builtinTools?: ReadonlyArray<ACPBuiltinToolMapping>;
|
|
35
|
+
}): {
|
|
36
|
+
update: (
|
|
37
|
+
options:
|
|
38
|
+
| ACPSessionUpdate
|
|
39
|
+
| {
|
|
40
|
+
update: ACPSessionUpdate;
|
|
41
|
+
rawUpdate?: unknown;
|
|
42
|
+
preserveRaw?: boolean;
|
|
43
|
+
},
|
|
44
|
+
) => void;
|
|
45
|
+
permissionToolCall: (options: {
|
|
46
|
+
toolCall: Extract<
|
|
47
|
+
ACPSessionUpdate,
|
|
48
|
+
{ sessionUpdate: 'tool_call' | 'tool_call_update' }
|
|
49
|
+
>;
|
|
50
|
+
}) => void;
|
|
51
|
+
raw: (options: { rawValue: unknown }) => void;
|
|
52
|
+
close: () => void;
|
|
53
|
+
finish: (response: ACPPromptResponse) => void;
|
|
54
|
+
hostToolCall: (options: {
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
toolName: string;
|
|
57
|
+
input: Readonly<Record<string, unknown>>;
|
|
58
|
+
}) => void;
|
|
59
|
+
hostToolResult: (options: {
|
|
60
|
+
toolCallId: string;
|
|
61
|
+
toolName: string;
|
|
62
|
+
output: unknown;
|
|
63
|
+
isError?: boolean;
|
|
64
|
+
}) => void;
|
|
65
|
+
} {
|
|
66
|
+
let openBlock:
|
|
67
|
+
| {
|
|
68
|
+
readonly type: 'text' | 'reasoning';
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly messageId: string | null;
|
|
71
|
+
}
|
|
72
|
+
| undefined;
|
|
73
|
+
let blockCounter = 0;
|
|
74
|
+
let stepOpen = false;
|
|
75
|
+
let completedToolStepEligible = false;
|
|
76
|
+
let finished = false;
|
|
77
|
+
const blockIdCounts = new Map<string, number>();
|
|
78
|
+
const toolStates = new Map<string, ToolState>();
|
|
79
|
+
const pendingToolCallIds = new Set<string>();
|
|
80
|
+
const builtinToolsByName = indexBuiltinTools({ builtinTools });
|
|
81
|
+
|
|
82
|
+
const closeBlock = () => {
|
|
83
|
+
if (openBlock == null) return;
|
|
84
|
+
emit({
|
|
85
|
+
type: openBlock.type === 'text' ? 'text-end' : 'reasoning-end',
|
|
86
|
+
id: openBlock.id,
|
|
87
|
+
});
|
|
88
|
+
openBlock = undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const createBlockId = ({
|
|
92
|
+
type,
|
|
93
|
+
messageId,
|
|
94
|
+
}: {
|
|
95
|
+
type: 'text' | 'reasoning';
|
|
96
|
+
messageId: string | null;
|
|
97
|
+
}): string => {
|
|
98
|
+
const base = messageId ?? `acp-${type}-${++blockCounter}`;
|
|
99
|
+
const count = (blockIdCounts.get(base) ?? 0) + 1;
|
|
100
|
+
blockIdCounts.set(base, count);
|
|
101
|
+
return count === 1 ? base : `${base}-${count}`;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const emitContent = ({
|
|
105
|
+
type,
|
|
106
|
+
text,
|
|
107
|
+
messageId,
|
|
108
|
+
}: {
|
|
109
|
+
type: 'text' | 'reasoning';
|
|
110
|
+
text: string;
|
|
111
|
+
messageId?: string | null;
|
|
112
|
+
}) => {
|
|
113
|
+
const normalizedMessageId = messageId ?? null;
|
|
114
|
+
if (
|
|
115
|
+
openBlock == null ||
|
|
116
|
+
openBlock.type !== type ||
|
|
117
|
+
openBlock.messageId !== normalizedMessageId
|
|
118
|
+
) {
|
|
119
|
+
closeBlock();
|
|
120
|
+
const id = createBlockId({ type, messageId: normalizedMessageId });
|
|
121
|
+
emit({
|
|
122
|
+
type: type === 'text' ? 'text-start' : 'reasoning-start',
|
|
123
|
+
id,
|
|
124
|
+
});
|
|
125
|
+
openBlock = { type, id, messageId: normalizedMessageId };
|
|
126
|
+
}
|
|
127
|
+
emit({
|
|
128
|
+
type: type === 'text' ? 'text-delta' : 'reasoning-delta',
|
|
129
|
+
id: openBlock.id,
|
|
130
|
+
delta: text,
|
|
131
|
+
});
|
|
132
|
+
stepOpen = true;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const emitInferredStep = ({
|
|
136
|
+
finishReason,
|
|
137
|
+
}: {
|
|
138
|
+
finishReason: Extract<
|
|
139
|
+
HarnessV1StreamPart,
|
|
140
|
+
{ readonly type: 'finish-step' }
|
|
141
|
+
>['finishReason'];
|
|
142
|
+
}) => {
|
|
143
|
+
closeBlock();
|
|
144
|
+
emit({
|
|
145
|
+
type: 'finish-step',
|
|
146
|
+
finishReason,
|
|
147
|
+
usage: unknownUsage(),
|
|
148
|
+
harnessMetadata: { acp: { inferredStep: true } },
|
|
149
|
+
});
|
|
150
|
+
stepOpen = false;
|
|
151
|
+
completedToolStepEligible = false;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const flushCompletedToolStep = () => {
|
|
155
|
+
if (
|
|
156
|
+
!stepOpen ||
|
|
157
|
+
!completedToolStepEligible ||
|
|
158
|
+
pendingToolCallIds.size > 0
|
|
159
|
+
) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
emitInferredStep({
|
|
163
|
+
finishReason: { unified: 'tool-calls', raw: 'tool-calls' },
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const emitToolUpdate = ({
|
|
168
|
+
update,
|
|
169
|
+
rawUpdate,
|
|
170
|
+
forceEmit = false,
|
|
171
|
+
}: {
|
|
172
|
+
update: Extract<
|
|
173
|
+
ACPSessionUpdate,
|
|
174
|
+
{ sessionUpdate: 'tool_call' | 'tool_call_update' }
|
|
175
|
+
>;
|
|
176
|
+
rawUpdate: unknown;
|
|
177
|
+
forceEmit?: boolean;
|
|
178
|
+
}) => {
|
|
179
|
+
closeBlock();
|
|
180
|
+
const state =
|
|
181
|
+
toolStates.get(update.toolCallId) ??
|
|
182
|
+
({
|
|
183
|
+
toolCallId: update.toolCallId,
|
|
184
|
+
values: {},
|
|
185
|
+
emittedFilePaths: new Set(),
|
|
186
|
+
emittedCall: false,
|
|
187
|
+
emittedResult: false,
|
|
188
|
+
} satisfies ToolState);
|
|
189
|
+
toolStates.set(update.toolCallId, state);
|
|
190
|
+
mergeToolUpdate({ state, update, rawUpdate });
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
!forceEmit &&
|
|
194
|
+
!state.emittedCall &&
|
|
195
|
+
update.sessionUpdate === 'tool_call' &&
|
|
196
|
+
update.status === 'pending'
|
|
197
|
+
) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!state.emittedCall) {
|
|
202
|
+
const programmaticName = getStringProperty({
|
|
203
|
+
value: state.values,
|
|
204
|
+
property: 'name',
|
|
205
|
+
});
|
|
206
|
+
const builtin = resolveBuiltinTool({
|
|
207
|
+
programmaticName,
|
|
208
|
+
metadata: state.values._meta,
|
|
209
|
+
rawInput: state.values.rawInput,
|
|
210
|
+
builtinTools,
|
|
211
|
+
builtinToolsByName,
|
|
212
|
+
});
|
|
213
|
+
if (
|
|
214
|
+
!forceEmit &&
|
|
215
|
+
builtin?.inputSchema != null &&
|
|
216
|
+
state.values.status !== 'completed' &&
|
|
217
|
+
state.values.status !== 'failed' &&
|
|
218
|
+
!hasRequiredBuiltinToolInput({
|
|
219
|
+
rawInput: state.values.rawInput,
|
|
220
|
+
inputSchema: builtin.inputSchema,
|
|
221
|
+
})
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (builtin != null) {
|
|
226
|
+
state.toolName = builtin.toolName;
|
|
227
|
+
state.nativeName =
|
|
228
|
+
builtin.nativeName != null && builtin.toolName !== builtin.nativeName
|
|
229
|
+
? builtin.nativeName
|
|
230
|
+
: undefined;
|
|
231
|
+
state.dynamic = false;
|
|
232
|
+
} else {
|
|
233
|
+
state.toolName = createDynamicToolName({
|
|
234
|
+
programmaticName,
|
|
235
|
+
toolCallId: state.toolCallId,
|
|
236
|
+
});
|
|
237
|
+
state.dynamic = true;
|
|
238
|
+
}
|
|
239
|
+
emit({
|
|
240
|
+
type: 'tool-call',
|
|
241
|
+
toolCallId: state.toolCallId,
|
|
242
|
+
toolName: state.toolName,
|
|
243
|
+
input: stringifyToolInput({ input: state.values.rawInput }),
|
|
244
|
+
providerExecuted: true,
|
|
245
|
+
...(state.dynamic ? { dynamic: true } : {}),
|
|
246
|
+
...(state.nativeName == null ? {} : { nativeName: state.nativeName }),
|
|
247
|
+
});
|
|
248
|
+
state.emittedCall = true;
|
|
249
|
+
stepOpen = true;
|
|
250
|
+
pendingToolCallIds.add(state.toolCallId);
|
|
251
|
+
completedToolStepEligible = false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
emitFileChanges({ state, emit });
|
|
255
|
+
|
|
256
|
+
if (
|
|
257
|
+
!state.emittedResult &&
|
|
258
|
+
(state.values.status === 'completed' || state.values.status === 'failed')
|
|
259
|
+
) {
|
|
260
|
+
emit({
|
|
261
|
+
type: 'tool-result',
|
|
262
|
+
toolCallId: state.toolCallId,
|
|
263
|
+
toolName: state.toolName!,
|
|
264
|
+
result: createToolResult({ state }),
|
|
265
|
+
...(state.values.status === 'failed' ? { isError: true } : {}),
|
|
266
|
+
...(state.dynamic ? { dynamic: true } : {}),
|
|
267
|
+
});
|
|
268
|
+
state.emittedResult = true;
|
|
269
|
+
pendingToolCallIds.delete(state.toolCallId);
|
|
270
|
+
completedToolStepEligible = pendingToolCallIds.size === 0;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const hostToolCall = ({
|
|
275
|
+
toolCallId,
|
|
276
|
+
toolName,
|
|
277
|
+
input,
|
|
278
|
+
}: {
|
|
279
|
+
toolCallId: string;
|
|
280
|
+
toolName: string;
|
|
281
|
+
input: Readonly<Record<string, unknown>>;
|
|
282
|
+
}) => {
|
|
283
|
+
closeBlock();
|
|
284
|
+
emit({
|
|
285
|
+
type: 'tool-call',
|
|
286
|
+
toolCallId,
|
|
287
|
+
toolName,
|
|
288
|
+
input: stringifyToolInput({ input }),
|
|
289
|
+
providerExecuted: false,
|
|
290
|
+
});
|
|
291
|
+
stepOpen = true;
|
|
292
|
+
pendingToolCallIds.add(toolCallId);
|
|
293
|
+
completedToolStepEligible = false;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const hostToolResult = ({
|
|
297
|
+
toolCallId,
|
|
298
|
+
toolName,
|
|
299
|
+
output,
|
|
300
|
+
isError,
|
|
301
|
+
}: {
|
|
302
|
+
toolCallId: string;
|
|
303
|
+
toolName: string;
|
|
304
|
+
output: unknown;
|
|
305
|
+
isError?: boolean;
|
|
306
|
+
}) => {
|
|
307
|
+
closeBlock();
|
|
308
|
+
emit({
|
|
309
|
+
type: 'tool-result',
|
|
310
|
+
toolCallId,
|
|
311
|
+
toolName,
|
|
312
|
+
result: toSafeJSONValue({ value: output, fallback: {} }) ?? {},
|
|
313
|
+
...(isError ? { isError: true } : {}),
|
|
314
|
+
});
|
|
315
|
+
pendingToolCallIds.delete(toolCallId);
|
|
316
|
+
completedToolStepEligible = pendingToolCallIds.size === 0;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
update: options => {
|
|
321
|
+
const { update, rawUpdate, preserveRaw } =
|
|
322
|
+
'update' in options
|
|
323
|
+
? {
|
|
324
|
+
update: options.update,
|
|
325
|
+
rawUpdate: options.rawUpdate,
|
|
326
|
+
preserveRaw: options.preserveRaw,
|
|
327
|
+
}
|
|
328
|
+
: { update: options, rawUpdate: options, preserveRaw: true };
|
|
329
|
+
const preservedUpdate = rawUpdate === undefined ? update : rawUpdate;
|
|
330
|
+
if (preserveRaw !== false) {
|
|
331
|
+
emit({ type: 'raw', rawValue: preservedUpdate });
|
|
332
|
+
}
|
|
333
|
+
if (
|
|
334
|
+
update.sessionUpdate === 'agent_message_chunk' &&
|
|
335
|
+
update.content?.type === 'text' &&
|
|
336
|
+
typeof update.content.text === 'string'
|
|
337
|
+
) {
|
|
338
|
+
flushCompletedToolStep();
|
|
339
|
+
emitContent({
|
|
340
|
+
type: 'text',
|
|
341
|
+
text: update.content.text,
|
|
342
|
+
messageId: update.messageId,
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (
|
|
347
|
+
update.sessionUpdate === 'agent_thought_chunk' &&
|
|
348
|
+
update.content?.type === 'text' &&
|
|
349
|
+
typeof update.content.text === 'string'
|
|
350
|
+
) {
|
|
351
|
+
flushCompletedToolStep();
|
|
352
|
+
emitContent({
|
|
353
|
+
type: 'reasoning',
|
|
354
|
+
text: update.content.text,
|
|
355
|
+
messageId: update.messageId,
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (
|
|
360
|
+
update.sessionUpdate === 'agent_message_chunk' ||
|
|
361
|
+
update.sessionUpdate === 'agent_thought_chunk'
|
|
362
|
+
) {
|
|
363
|
+
closeBlock();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (
|
|
367
|
+
update.sessionUpdate === 'tool_call' ||
|
|
368
|
+
update.sessionUpdate === 'tool_call_update'
|
|
369
|
+
) {
|
|
370
|
+
emitToolUpdate({ update, rawUpdate: preservedUpdate });
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
permissionToolCall: ({ toolCall }) => {
|
|
374
|
+
emitToolUpdate({
|
|
375
|
+
update: toolCall,
|
|
376
|
+
rawUpdate: toolCall,
|
|
377
|
+
forceEmit: true,
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
raw: ({ rawValue }) => {
|
|
381
|
+
emit({ type: 'raw', rawValue });
|
|
382
|
+
},
|
|
383
|
+
close: closeBlock,
|
|
384
|
+
hostToolCall,
|
|
385
|
+
hostToolResult,
|
|
386
|
+
finish: response => {
|
|
387
|
+
if (finished) return;
|
|
388
|
+
finished = true;
|
|
389
|
+
emit({ type: 'raw', rawValue: response });
|
|
390
|
+
const finishReason = mapACPFinishReason({
|
|
391
|
+
stopReason: response.stopReason,
|
|
392
|
+
});
|
|
393
|
+
const usageMetadata =
|
|
394
|
+
response.usage == null
|
|
395
|
+
? undefined
|
|
396
|
+
: mapACPUsageMetadata({ usage: response.usage });
|
|
397
|
+
if (stepOpen) {
|
|
398
|
+
emitInferredStep({ finishReason });
|
|
399
|
+
} else {
|
|
400
|
+
closeBlock();
|
|
401
|
+
}
|
|
402
|
+
emit({
|
|
403
|
+
type: 'finish',
|
|
404
|
+
finishReason,
|
|
405
|
+
totalUsage: mapACPUsage({ usage: response.usage }),
|
|
406
|
+
harnessMetadata: {
|
|
407
|
+
acp: {
|
|
408
|
+
stopReason: response.stopReason,
|
|
409
|
+
...(usageMetadata == null ? {} : { usage: usageMetadata }),
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function mapACPFinishReason({ stopReason }: { stopReason: string }): {
|
|
418
|
+
unified:
|
|
419
|
+
| 'stop'
|
|
420
|
+
| 'length'
|
|
421
|
+
| 'content-filter'
|
|
422
|
+
| 'tool-calls'
|
|
423
|
+
| 'error'
|
|
424
|
+
| 'other';
|
|
425
|
+
raw: string;
|
|
426
|
+
} {
|
|
427
|
+
switch (stopReason) {
|
|
428
|
+
case 'end_turn':
|
|
429
|
+
return { unified: 'stop', raw: stopReason };
|
|
430
|
+
case 'max_tokens':
|
|
431
|
+
case 'max_turn_requests':
|
|
432
|
+
return { unified: 'length', raw: stopReason };
|
|
433
|
+
case 'refusal':
|
|
434
|
+
return { unified: 'content-filter', raw: stopReason };
|
|
435
|
+
default:
|
|
436
|
+
return { unified: 'other', raw: stopReason };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function indexBuiltinTools({
|
|
441
|
+
builtinTools,
|
|
442
|
+
}: {
|
|
443
|
+
builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
|
|
444
|
+
}): ReadonlyMap<string, ACPBuiltinToolMapping> {
|
|
445
|
+
const matches = new Map<string, ACPBuiltinToolMapping | null>();
|
|
446
|
+
for (const builtinTool of builtinTools) {
|
|
447
|
+
addBuiltinToolMatch({
|
|
448
|
+
matches,
|
|
449
|
+
name: builtinTool.toolName,
|
|
450
|
+
builtinTool,
|
|
451
|
+
});
|
|
452
|
+
if (builtinTool.nativeName != null) {
|
|
453
|
+
addBuiltinToolMatch({
|
|
454
|
+
matches,
|
|
455
|
+
name: builtinTool.nativeName,
|
|
456
|
+
builtinTool,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return new Map(
|
|
461
|
+
[...matches].filter(
|
|
462
|
+
(entry): entry is [string, ACPBuiltinToolMapping] => entry[1] != null,
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function addBuiltinToolMatch({
|
|
468
|
+
matches,
|
|
469
|
+
name,
|
|
470
|
+
builtinTool,
|
|
471
|
+
}: {
|
|
472
|
+
matches: Map<string, ACPBuiltinToolMapping | null>;
|
|
473
|
+
name: string;
|
|
474
|
+
builtinTool: ACPBuiltinToolMapping;
|
|
475
|
+
}): void {
|
|
476
|
+
if (!matches.has(name)) {
|
|
477
|
+
matches.set(name, builtinTool);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (matches.get(name)?.toolName !== builtinTool.toolName) {
|
|
481
|
+
matches.set(name, null);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function resolveBuiltinTool({
|
|
486
|
+
programmaticName,
|
|
487
|
+
metadata,
|
|
488
|
+
rawInput,
|
|
489
|
+
builtinTools,
|
|
490
|
+
builtinToolsByName,
|
|
491
|
+
}: {
|
|
492
|
+
programmaticName: string | undefined;
|
|
493
|
+
metadata: unknown;
|
|
494
|
+
rawInput: unknown;
|
|
495
|
+
builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
|
|
496
|
+
builtinToolsByName: ReadonlyMap<string, ACPBuiltinToolMapping>;
|
|
497
|
+
}): ACPBuiltinToolMapping | undefined {
|
|
498
|
+
if (programmaticName != null) {
|
|
499
|
+
return builtinToolsByName.get(programmaticName);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const metadataMatch = findBuiltinToolsInMetadata({
|
|
503
|
+
metadata,
|
|
504
|
+
builtinToolsByName,
|
|
505
|
+
});
|
|
506
|
+
if (metadataMatch.hasProgrammaticName) {
|
|
507
|
+
return metadataMatch.matches.length === 1
|
|
508
|
+
? metadataMatch.matches[0]
|
|
509
|
+
: undefined;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const schemaMatches = builtinTools.filter(tool =>
|
|
513
|
+
matchesBuiltinToolInput({ rawInput, inputSchema: tool.inputSchema }),
|
|
514
|
+
);
|
|
515
|
+
return schemaMatches.length === 1 ? schemaMatches[0] : undefined;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function findBuiltinToolsInMetadata({
|
|
519
|
+
metadata,
|
|
520
|
+
builtinToolsByName,
|
|
521
|
+
}: {
|
|
522
|
+
metadata: unknown;
|
|
523
|
+
builtinToolsByName: ReadonlyMap<string, ACPBuiltinToolMapping>;
|
|
524
|
+
}): {
|
|
525
|
+
hasProgrammaticName: boolean;
|
|
526
|
+
matches: ReadonlyArray<ACPBuiltinToolMapping>;
|
|
527
|
+
} {
|
|
528
|
+
const matches = new Map<string, ACPBuiltinToolMapping>();
|
|
529
|
+
let hasProgrammaticName = false;
|
|
530
|
+
const seen = new Set<object>();
|
|
531
|
+
const visit = (value: unknown): void => {
|
|
532
|
+
if (value == null || typeof value !== 'object' || seen.has(value)) return;
|
|
533
|
+
seen.add(value);
|
|
534
|
+
for (const [property, child] of Array.isArray(value)
|
|
535
|
+
? value.entries()
|
|
536
|
+
: Object.entries(value as Record<string, unknown>)) {
|
|
537
|
+
if (
|
|
538
|
+
(property === 'name' || property === 'toolName') &&
|
|
539
|
+
typeof child === 'string'
|
|
540
|
+
) {
|
|
541
|
+
hasProgrammaticName = true;
|
|
542
|
+
const builtin = builtinToolsByName.get(child);
|
|
543
|
+
if (builtin != null) matches.set(builtin.toolName, builtin);
|
|
544
|
+
}
|
|
545
|
+
visit(child);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
visit(metadata);
|
|
549
|
+
return { hasProgrammaticName, matches: [...matches.values()] };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function matchesBuiltinToolInput({
|
|
553
|
+
rawInput,
|
|
554
|
+
inputSchema,
|
|
555
|
+
}: {
|
|
556
|
+
rawInput: unknown;
|
|
557
|
+
inputSchema: ACPBuiltinToolMapping['inputSchema'];
|
|
558
|
+
}): boolean {
|
|
559
|
+
if (!isRecord(rawInput) || !isRecord(inputSchema)) return false;
|
|
560
|
+
if (inputSchema.type !== 'object') return false;
|
|
561
|
+
const required = inputSchema.required;
|
|
562
|
+
if (
|
|
563
|
+
!Array.isArray(required) ||
|
|
564
|
+
required.length === 0 ||
|
|
565
|
+
!required.every(property => typeof property === 'string')
|
|
566
|
+
) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
if (!hasRequiredBuiltinToolInput({ rawInput, inputSchema })) return false;
|
|
570
|
+
|
|
571
|
+
const properties = isRecord(inputSchema.properties)
|
|
572
|
+
? inputSchema.properties
|
|
573
|
+
: {};
|
|
574
|
+
return Object.entries(properties)
|
|
575
|
+
.filter(([, schema]) => isJSONSchemaDiscriminator({ schema }))
|
|
576
|
+
.every(([property, schema]) => {
|
|
577
|
+
if (!Object.prototype.hasOwnProperty.call(rawInput, property)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
return matchesJSONSchemaValue({ value: rawInput[property], schema });
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function hasRequiredBuiltinToolInput({
|
|
585
|
+
rawInput,
|
|
586
|
+
inputSchema,
|
|
587
|
+
}: {
|
|
588
|
+
rawInput: unknown;
|
|
589
|
+
inputSchema: ACPBuiltinToolMapping['inputSchema'];
|
|
590
|
+
}): boolean {
|
|
591
|
+
if (!isRecord(inputSchema) || inputSchema.type !== 'object') return true;
|
|
592
|
+
if (!isRecord(rawInput)) return false;
|
|
593
|
+
const required = inputSchema.required;
|
|
594
|
+
if (!Array.isArray(required) || required.length === 0) return true;
|
|
595
|
+
if (!required.every(property => typeof property === 'string')) return true;
|
|
596
|
+
const properties = isRecord(inputSchema.properties)
|
|
597
|
+
? inputSchema.properties
|
|
598
|
+
: {};
|
|
599
|
+
return required.every(property => {
|
|
600
|
+
if (!Object.prototype.hasOwnProperty.call(rawInput, property)) return false;
|
|
601
|
+
return matchesJSONSchemaValue({
|
|
602
|
+
value: rawInput[property],
|
|
603
|
+
schema: properties[property],
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function isJSONSchemaDiscriminator({ schema }: { schema: unknown }): boolean {
|
|
609
|
+
return (
|
|
610
|
+
isRecord(schema) &&
|
|
611
|
+
('const' in schema ||
|
|
612
|
+
(Array.isArray(schema.enum) && schema.enum.length === 1))
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function matchesJSONSchemaValue({
|
|
617
|
+
value,
|
|
618
|
+
schema,
|
|
619
|
+
}: {
|
|
620
|
+
value: unknown;
|
|
621
|
+
schema: unknown;
|
|
622
|
+
}): boolean {
|
|
623
|
+
if (schema === true) return true;
|
|
624
|
+
if (schema === false || !isRecord(schema)) return false;
|
|
625
|
+
if ('const' in schema && !Object.is(value, schema.const)) return false;
|
|
626
|
+
if (
|
|
627
|
+
Array.isArray(schema.enum) &&
|
|
628
|
+
!schema.enum.some(candidate => Object.is(value, candidate))
|
|
629
|
+
) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
for (const alternatives of [schema.anyOf, schema.oneOf]) {
|
|
633
|
+
if (
|
|
634
|
+
Array.isArray(alternatives) &&
|
|
635
|
+
!alternatives.some(alternative =>
|
|
636
|
+
matchesJSONSchemaValue({ value, schema: alternative }),
|
|
637
|
+
)
|
|
638
|
+
) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (typeof schema.type !== 'string') return true;
|
|
643
|
+
switch (schema.type) {
|
|
644
|
+
case 'null':
|
|
645
|
+
return value === null;
|
|
646
|
+
case 'boolean':
|
|
647
|
+
return typeof value === 'boolean';
|
|
648
|
+
case 'integer':
|
|
649
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
650
|
+
case 'number':
|
|
651
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
652
|
+
case 'string':
|
|
653
|
+
return typeof value === 'string';
|
|
654
|
+
case 'array':
|
|
655
|
+
return Array.isArray(value);
|
|
656
|
+
case 'object':
|
|
657
|
+
return isRecord(value);
|
|
658
|
+
default:
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function mergeToolUpdate({
|
|
664
|
+
state,
|
|
665
|
+
update,
|
|
666
|
+
rawUpdate,
|
|
667
|
+
}: {
|
|
668
|
+
state: ToolState;
|
|
669
|
+
update: Extract<
|
|
670
|
+
ACPSessionUpdate,
|
|
671
|
+
{ sessionUpdate: 'tool_call' | 'tool_call_update' }
|
|
672
|
+
>;
|
|
673
|
+
rawUpdate: unknown;
|
|
674
|
+
}): void {
|
|
675
|
+
const parsed = update as unknown as Record<string, unknown>;
|
|
676
|
+
for (const property of [
|
|
677
|
+
'title',
|
|
678
|
+
'kind',
|
|
679
|
+
'status',
|
|
680
|
+
'content',
|
|
681
|
+
'locations',
|
|
682
|
+
'rawInput',
|
|
683
|
+
'rawOutput',
|
|
684
|
+
'_meta',
|
|
685
|
+
]) {
|
|
686
|
+
if (
|
|
687
|
+
Object.prototype.hasOwnProperty.call(parsed, property) &&
|
|
688
|
+
parsed[property] !== undefined
|
|
689
|
+
) {
|
|
690
|
+
state.values[property] = parsed[property];
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const programmaticName = getStringProperty({
|
|
694
|
+
value: rawUpdate,
|
|
695
|
+
property: 'name',
|
|
696
|
+
});
|
|
697
|
+
if (programmaticName != null) state.values.name = programmaticName;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function emitFileChanges({
|
|
701
|
+
state,
|
|
702
|
+
emit,
|
|
703
|
+
}: {
|
|
704
|
+
state: ToolState;
|
|
705
|
+
emit: (event: HarnessV1StreamPart) => void;
|
|
706
|
+
}): void {
|
|
707
|
+
const content = Array.isArray(state.values.content)
|
|
708
|
+
? state.values.content
|
|
709
|
+
: [];
|
|
710
|
+
for (const item of content) {
|
|
711
|
+
if (!isRecord(item) || item.type !== 'diff') continue;
|
|
712
|
+
const path = getStringProperty({ value: item, property: 'path' });
|
|
713
|
+
const newText = getStringProperty({ value: item, property: 'newText' });
|
|
714
|
+
if (path == null || newText == null) continue;
|
|
715
|
+
const oldText = item.oldText;
|
|
716
|
+
const event = oldText == null ? 'create' : 'modify';
|
|
717
|
+
emitFileChange({ state, emit, path, event });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (state.values.kind !== 'edit') return;
|
|
721
|
+
const locations = Array.isArray(state.values.locations)
|
|
722
|
+
? state.values.locations
|
|
723
|
+
: [];
|
|
724
|
+
for (const location of locations) {
|
|
725
|
+
const path = getStringProperty({ value: location, property: 'path' });
|
|
726
|
+
if (path != null) {
|
|
727
|
+
emitFileChange({ state, emit, path, event: 'modify' });
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function emitFileChange({
|
|
733
|
+
state,
|
|
734
|
+
emit,
|
|
735
|
+
path,
|
|
736
|
+
event,
|
|
737
|
+
}: {
|
|
738
|
+
state: ToolState;
|
|
739
|
+
emit: (event: HarnessV1StreamPart) => void;
|
|
740
|
+
path: string;
|
|
741
|
+
event: 'create' | 'modify' | 'delete';
|
|
742
|
+
}): void {
|
|
743
|
+
if (state.emittedFilePaths.has(path)) return;
|
|
744
|
+
state.emittedFilePaths.add(path);
|
|
745
|
+
emit({
|
|
746
|
+
type: 'file-change',
|
|
747
|
+
path,
|
|
748
|
+
event,
|
|
749
|
+
harnessMetadata: {
|
|
750
|
+
acp: {
|
|
751
|
+
toolCallId: state.toolCallId,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function createDynamicToolName({
|
|
758
|
+
programmaticName,
|
|
759
|
+
toolCallId,
|
|
760
|
+
}: {
|
|
761
|
+
programmaticName: string | undefined;
|
|
762
|
+
toolCallId: string;
|
|
763
|
+
}): string {
|
|
764
|
+
const source = programmaticName ?? `acp_tool_${toolCallId}`;
|
|
765
|
+
const normalized = source
|
|
766
|
+
.replace(/[^A-Za-z0-9_-]+/g, '_')
|
|
767
|
+
.replace(/^_+|_+$/g, '')
|
|
768
|
+
.slice(0, 128);
|
|
769
|
+
if (normalized.length === 0) return 'acp_tool';
|
|
770
|
+
return /^[A-Za-z_]/.test(normalized) ? normalized : `acp_${normalized}`;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function stringifyToolInput({ input }: { input: unknown }): string {
|
|
774
|
+
return JSON.stringify(toSafeJSONValue({ value: input, fallback: {} }));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function createToolResult({ state }: { state: ToolState }): HarnessToolResult {
|
|
778
|
+
const value =
|
|
779
|
+
state.values.rawOutput != null
|
|
780
|
+
? state.values.rawOutput
|
|
781
|
+
: state.values.content != null
|
|
782
|
+
? state.values.content
|
|
783
|
+
: {};
|
|
784
|
+
return toSafeJSONValue({ value, fallback: {} }) ?? {};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function toSafeJSONValue({
|
|
788
|
+
value,
|
|
789
|
+
fallback,
|
|
790
|
+
seen = new Set(),
|
|
791
|
+
}: {
|
|
792
|
+
value: unknown;
|
|
793
|
+
fallback: HarnessJSONValue;
|
|
794
|
+
seen?: Set<object>;
|
|
795
|
+
}): HarnessJSONValue {
|
|
796
|
+
if (value == null) return fallback;
|
|
797
|
+
if (typeof value === 'string' || typeof value === 'boolean') return value;
|
|
798
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
799
|
+
if (typeof value === 'bigint') return value.toString();
|
|
800
|
+
if (typeof value !== 'object') return fallback;
|
|
801
|
+
if (seen.has(value)) return '[Circular]';
|
|
802
|
+
seen.add(value);
|
|
803
|
+
if (Array.isArray(value)) {
|
|
804
|
+
const result = value.map(item =>
|
|
805
|
+
toSafeJSONValue({ value: item, fallback: null, seen }),
|
|
806
|
+
);
|
|
807
|
+
seen.delete(value);
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
const result = Object.create(null) as Record<string, HarnessJSONValue>;
|
|
811
|
+
try {
|
|
812
|
+
for (const [key, item] of Object.entries(value)) {
|
|
813
|
+
result[key] = toSafeJSONValue({
|
|
814
|
+
value: item,
|
|
815
|
+
fallback: null,
|
|
816
|
+
seen,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
seen.delete(value);
|
|
821
|
+
return fallback;
|
|
822
|
+
}
|
|
823
|
+
seen.delete(value);
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function getStringProperty({
|
|
828
|
+
value,
|
|
829
|
+
property,
|
|
830
|
+
}: {
|
|
831
|
+
value: unknown;
|
|
832
|
+
property: string;
|
|
833
|
+
}): string | undefined {
|
|
834
|
+
if (!isRecord(value)) return undefined;
|
|
835
|
+
const propertyValue = value[property];
|
|
836
|
+
return typeof propertyValue === 'string' ? propertyValue : undefined;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
840
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function mapACPUsage({
|
|
844
|
+
usage,
|
|
845
|
+
}: {
|
|
846
|
+
usage: ACPPromptResponse['usage'];
|
|
847
|
+
}): HarnessUsage {
|
|
848
|
+
if (usage == null) return unknownUsage();
|
|
849
|
+
const raw = mapACPUsageMetadata({ usage });
|
|
850
|
+
return {
|
|
851
|
+
inputTokens: {
|
|
852
|
+
total: usage.inputTokens,
|
|
853
|
+
noCache: undefined,
|
|
854
|
+
cacheRead: usage.cachedReadTokens ?? undefined,
|
|
855
|
+
cacheWrite: usage.cachedWriteTokens ?? undefined,
|
|
856
|
+
},
|
|
857
|
+
outputTokens: {
|
|
858
|
+
total: usage.outputTokens,
|
|
859
|
+
text: undefined,
|
|
860
|
+
reasoning: usage.thoughtTokens ?? undefined,
|
|
861
|
+
},
|
|
862
|
+
raw,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function mapACPUsageMetadata({
|
|
867
|
+
usage,
|
|
868
|
+
}: {
|
|
869
|
+
usage: NonNullable<ACPPromptResponse['usage']>;
|
|
870
|
+
}) {
|
|
871
|
+
return {
|
|
872
|
+
totalTokens: usage.totalTokens,
|
|
873
|
+
inputTokens: usage.inputTokens,
|
|
874
|
+
outputTokens: usage.outputTokens,
|
|
875
|
+
...(usage.thoughtTokens == null
|
|
876
|
+
? {}
|
|
877
|
+
: { thoughtTokens: usage.thoughtTokens }),
|
|
878
|
+
...(usage.cachedReadTokens == null
|
|
879
|
+
? {}
|
|
880
|
+
: { cachedReadTokens: usage.cachedReadTokens }),
|
|
881
|
+
...(usage.cachedWriteTokens == null
|
|
882
|
+
? {}
|
|
883
|
+
: { cachedWriteTokens: usage.cachedWriteTokens }),
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function unknownUsage(): HarnessUsage {
|
|
888
|
+
return {
|
|
889
|
+
inputTokens: {
|
|
890
|
+
total: undefined,
|
|
891
|
+
noCache: undefined,
|
|
892
|
+
cacheRead: undefined,
|
|
893
|
+
cacheWrite: undefined,
|
|
894
|
+
},
|
|
895
|
+
outputTokens: {
|
|
896
|
+
total: undefined,
|
|
897
|
+
text: undefined,
|
|
898
|
+
reasoning: undefined,
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
}
|