@ai-sdk/harness-acp 0.0.0 → 1.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +13 -0
  3. package/README.md +70 -3
  4. package/dist/bridge/host-tool-mcp.mjs +302 -0
  5. package/dist/bridge/host-tool-mcp.mjs.map +1 -0
  6. package/dist/bridge/index.mjs +3278 -0
  7. package/dist/bridge/index.mjs.map +1 -0
  8. package/dist/bridge/package.json +12 -0
  9. package/dist/bridge/pnpm-lock.yaml +816 -0
  10. package/dist/index.d.ts +98 -0
  11. package/dist/index.js +2094 -0
  12. package/dist/index.js.map +1 -0
  13. package/package.json +64 -10
  14. package/src/acp-auth.ts +254 -0
  15. package/src/acp-bridge-protocol.ts +13 -0
  16. package/src/acp-harness.ts +160 -0
  17. package/src/index.ts +19 -0
  18. package/src/v1/acp-v1-bridge-protocol.ts +151 -0
  19. package/src/v1/acp-v1-harness.ts +1447 -0
  20. package/src/v1/acp-v1-lifecycle.ts +115 -0
  21. package/src/v1/acp-v1-prompt.ts +122 -0
  22. package/src/v1/acp-v1-settings.ts +98 -0
  23. package/src/v1/acp-v1-skills.ts +228 -0
  24. package/src/v1/acp-v1-turn-start-config.ts +93 -0
  25. package/src/v1/bridge/acp-diagnostics.ts +57 -0
  26. package/src/v1/bridge/acp-stream-capture.ts +152 -0
  27. package/src/v1/bridge/acp-v1-bridge-environment.ts +95 -0
  28. package/src/v1/bridge/create-emit-stream-event.ts +97 -0
  29. package/src/v1/bridge/host-tool-correlation.ts +640 -0
  30. package/src/v1/bridge/host-tool-mcp-server.ts +180 -0
  31. package/src/v1/bridge/host-tool-mcp.ts +185 -0
  32. package/src/v1/bridge/host-tool-relay-client.ts +53 -0
  33. package/src/v1/bridge/host-tool-relay.ts +510 -0
  34. package/src/v1/bridge/index.ts +729 -0
  35. package/src/v1/bridge/package.json +12 -0
  36. package/src/v1/bridge/permission-controller.ts +152 -0
  37. package/src/v1/bridge/permission-mode.ts +163 -0
  38. package/src/v1/bridge/pnpm-lock.yaml +816 -0
  39. package/src/v1/bridge/profile-values.ts +97 -0
  40. package/src/v1/bridge/protocol-configuration.ts +146 -0
  41. package/src/v1/bridge/recovered-session.ts +145 -0
  42. package/src/v1/bridge/refresh-host-tool-catalog.ts +33 -0
  43. package/src/v1/bridge/session-lifecycle.ts +83 -0
  44. package/src/v1/bridge/stream-translator.ts +901 -0
  45. package/src/v1/implementation.ts +282 -0
  46. package/src/v1/index.ts +16 -0
  47. package/src/version.ts +6 -0
  48. 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
+ }