@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,640 @@
1
+ import type {
2
+ ActiveSessionMessage,
3
+ ToolCallUpdate,
4
+ } from '@agentclientprotocol/sdk';
5
+
6
+ type SessionUpdateMessage = Extract<
7
+ ActiveSessionMessage,
8
+ { readonly kind: 'session_update' }
9
+ >;
10
+
11
+ type BufferedUpdate = {
12
+ readonly message: SessionUpdateMessage;
13
+ readonly rawUpdate: unknown;
14
+ };
15
+
16
+ type CorrelationInvocation = {
17
+ readonly token: string;
18
+ readonly serverName: string;
19
+ readonly toolName: string;
20
+ readonly inputFingerprint: string;
21
+ readonly order: number;
22
+ expiryTimer?: ReturnType<typeof setTimeout>;
23
+ toolCallId?: string;
24
+ };
25
+
26
+ type CorrelationCandidate = {
27
+ readonly toolCallId: string;
28
+ readonly order: number;
29
+ readonly evidence: unknown[];
30
+ };
31
+
32
+ const CORRELATION_WINDOW_MS = 1000;
33
+ const MAX_BUFFERED_UPDATES = 128;
34
+
35
+ export function createHostToolCorrelation({
36
+ emitSemanticUpdate,
37
+ emitRawUpdate,
38
+ hostToolServerName,
39
+ hostTools,
40
+ }: {
41
+ emitSemanticUpdate: (options: {
42
+ message: SessionUpdateMessage;
43
+ rawUpdate: unknown;
44
+ }) => void;
45
+ emitRawUpdate: (options: { rawUpdate: unknown }) => void;
46
+ hostToolServerName: string;
47
+ hostTools: ReadonlyArray<{ readonly name: string }>;
48
+ }): {
49
+ update(options: { message: SessionUpdateMessage; rawUpdate?: unknown }): void;
50
+ registerInvocation(options: {
51
+ token: string;
52
+ serverName: string;
53
+ toolName: string;
54
+ input: Readonly<Record<string, unknown>>;
55
+ order: number;
56
+ }): void;
57
+ claimHostToolPermission(options: { toolCall: ToolCallUpdate }): boolean;
58
+ flush(): void;
59
+ removeInvocation(options: { token: string }): void;
60
+ close(): void;
61
+ } {
62
+ const invocations = new Map<string, CorrelationInvocation>();
63
+ const candidates = new Map<string, CorrelationCandidate>();
64
+ const suppressedToolCallIds = new Set<string>();
65
+ const releasedToolCallIds = new Set<string>();
66
+ let nextCandidateOrder = 0;
67
+ let buffered: BufferedUpdate[] = [];
68
+ let flushTimer: ReturnType<typeof setTimeout> | undefined;
69
+
70
+ const flush = () => {
71
+ if (flushTimer != null) {
72
+ clearTimeout(flushTimer);
73
+ flushTimer = undefined;
74
+ }
75
+ if (buffered.length === 0) return;
76
+ const updates = buffered;
77
+ buffered = [];
78
+ for (const update of updates) {
79
+ if (
80
+ isToolUpdate(update.message) &&
81
+ suppressedToolCallIds.has(update.message.update.toolCallId)
82
+ ) {
83
+ continue;
84
+ }
85
+ if (isToolUpdate(update.message)) {
86
+ releasedToolCallIds.add(update.message.update.toolCallId);
87
+ }
88
+ emitSemanticUpdate(update);
89
+ }
90
+ for (const toolCallId of candidates.keys()) {
91
+ if (!suppressedToolCallIds.has(toolCallId)) {
92
+ releasedToolCallIds.add(toolCallId);
93
+ }
94
+ }
95
+ candidates.clear();
96
+ };
97
+
98
+ const scheduleFlush = () => {
99
+ if (flushTimer != null) return;
100
+ flushTimer = setTimeout(flush, CORRELATION_WINDOW_MS);
101
+ flushTimer.unref?.();
102
+ };
103
+
104
+ const suppressCandidate = ({
105
+ candidate,
106
+ invocation,
107
+ }: {
108
+ candidate: CorrelationCandidate;
109
+ invocation: CorrelationInvocation;
110
+ }) => {
111
+ invocation.toolCallId = candidate.toolCallId;
112
+ suppressedToolCallIds.add(candidate.toolCallId);
113
+ candidates.delete(candidate.toolCallId);
114
+ deleteInvocation({ invocations, token: invocation.token });
115
+ };
116
+
117
+ const suppressToolCallUpdate = ({ toolCallId }: { toolCallId: string }) => {
118
+ suppressedToolCallIds.add(toolCallId);
119
+ candidates.delete(toolCallId);
120
+ if (candidates.size === 0) flush();
121
+ };
122
+
123
+ const reconcile = () => {
124
+ const orderedInvocations = [...invocations.values()].sort(
125
+ (left, right) => left.order - right.order,
126
+ );
127
+ const orderedCandidates = [...candidates.values()].sort(
128
+ (left, right) => left.order - right.order,
129
+ );
130
+ for (const invocation of orderedInvocations) {
131
+ if (invocation.toolCallId != null) continue;
132
+ const candidate = orderedCandidates.find(
133
+ item =>
134
+ candidates.has(item.toolCallId) &&
135
+ hasPortableEvidence({ candidate: item, invocation }),
136
+ );
137
+ if (candidate != null) suppressCandidate({ candidate, invocation });
138
+ }
139
+ if (candidates.size === 0) flush();
140
+ };
141
+
142
+ return {
143
+ update: ({ message, rawUpdate }) => {
144
+ const preservedUpdate =
145
+ rawUpdate === undefined ? message.update : rawUpdate;
146
+ emitRawUpdate({ rawUpdate: preservedUpdate });
147
+ if (!isToolUpdate(message)) {
148
+ if (buffered.length === 0) {
149
+ emitSemanticUpdate({ message, rawUpdate: preservedUpdate });
150
+ } else {
151
+ buffered.push({ message, rawUpdate: preservedUpdate });
152
+ if (buffered.length >= MAX_BUFFERED_UPDATES) flush();
153
+ }
154
+ return;
155
+ }
156
+
157
+ const toolCallId = message.update.toolCallId;
158
+ if (suppressedToolCallIds.has(toolCallId)) {
159
+ if (isTerminalToolUpdate(message)) {
160
+ suppressedToolCallIds.delete(toolCallId);
161
+ removeInvocationForToolCall({ invocations, toolCallId });
162
+ }
163
+ return;
164
+ }
165
+ if (releasedToolCallIds.has(toolCallId)) {
166
+ emitSemanticUpdate({ message, rawUpdate: preservedUpdate });
167
+ if (isTerminalToolUpdate(message))
168
+ releasedToolCallIds.delete(toolCallId);
169
+ return;
170
+ }
171
+
172
+ buffered.push({ message, rawUpdate: preservedUpdate });
173
+ const candidate =
174
+ candidates.get(toolCallId) ??
175
+ ({
176
+ toolCallId,
177
+ order: ++nextCandidateOrder,
178
+ evidence: [],
179
+ } satisfies CorrelationCandidate);
180
+ candidate.evidence.push(message.update, preservedUpdate);
181
+ candidates.set(toolCallId, candidate);
182
+
183
+ const tokenMatch = findTokenInvocation({
184
+ value: preservedUpdate,
185
+ invocations,
186
+ });
187
+ if (tokenMatch != null) {
188
+ suppressCandidate({ candidate, invocation: tokenMatch });
189
+ }
190
+ reconcile();
191
+ if (buffered.length >= MAX_BUFFERED_UPDATES) flush();
192
+ else if (buffered.length > 0) scheduleFlush();
193
+ },
194
+ registerInvocation: ({ token, serverName, toolName, input, order }) => {
195
+ const invocation: CorrelationInvocation = {
196
+ token,
197
+ serverName,
198
+ toolName,
199
+ inputFingerprint: canonicalFingerprint({ value: input }),
200
+ order,
201
+ };
202
+ invocations.set(token, invocation);
203
+ invocation.expiryTimer = setTimeout(
204
+ () => deleteInvocation({ invocations, token }),
205
+ CORRELATION_WINDOW_MS,
206
+ );
207
+ invocation.expiryTimer.unref?.();
208
+ const tokenCandidate = [...candidates.values()].find(candidate =>
209
+ candidate.evidence.some(value =>
210
+ containsExactValue({ value, target: token }),
211
+ ),
212
+ );
213
+ if (tokenCandidate != null) {
214
+ suppressCandidate({ candidate: tokenCandidate, invocation });
215
+ }
216
+ reconcile();
217
+ },
218
+ claimHostToolPermission: ({ toolCall }) => {
219
+ const candidate = candidates.get(toolCall.toolCallId);
220
+ const matches = hostTools.filter(({ name }) => {
221
+ const permission = resolvePermissionHostTool({
222
+ toolCall,
223
+ serverName: hostToolServerName,
224
+ toolName: name,
225
+ });
226
+ if (permission == null) return false;
227
+ if (candidate == null) return permission.hasRequestIdentity;
228
+ return hasPortableEvidence({
229
+ candidate,
230
+ invocation: {
231
+ token: '',
232
+ serverName: hostToolServerName,
233
+ toolName: name,
234
+ inputFingerprint: canonicalFingerprint({
235
+ value: permission.input,
236
+ }),
237
+ order: 0,
238
+ },
239
+ });
240
+ });
241
+ if (matches.length !== 1) return false;
242
+ suppressToolCallUpdate({ toolCallId: toolCall.toolCallId });
243
+ return true;
244
+ },
245
+ flush,
246
+ removeInvocation: ({ token }) => {
247
+ deleteInvocation({ invocations, token });
248
+ },
249
+ close: () => {
250
+ flush();
251
+ for (const invocation of invocations.values()) {
252
+ if (invocation.expiryTimer != null) {
253
+ clearTimeout(invocation.expiryTimer);
254
+ }
255
+ }
256
+ invocations.clear();
257
+ candidates.clear();
258
+ suppressedToolCallIds.clear();
259
+ releasedToolCallIds.clear();
260
+ },
261
+ };
262
+ }
263
+
264
+ function hasPortableEvidence({
265
+ candidate,
266
+ invocation,
267
+ }: {
268
+ candidate: CorrelationCandidate;
269
+ invocation: CorrelationInvocation;
270
+ }): boolean {
271
+ let hasServerIdentity = false;
272
+ let hasToolName = false;
273
+ let hasInput = false;
274
+ for (const evidence of candidate.evidence) {
275
+ const metadata = getProperty({ value: evidence, property: '_meta' });
276
+ const rawInput = getProperty({ value: evidence, property: 'rawInput' });
277
+ const programmaticName = getProperty({
278
+ value: evidence,
279
+ property: 'name',
280
+ });
281
+ const deferredToolName = getProperty({
282
+ value: rawInput,
283
+ property: 'tool_name',
284
+ });
285
+ const deferredToolInput = getProperty({
286
+ value: rawInput,
287
+ property: 'tool_input',
288
+ });
289
+ if (
290
+ containsCombinedIdentity({
291
+ value: [metadata, programmaticName],
292
+ serverName: invocation.serverName,
293
+ toolName: invocation.toolName,
294
+ })
295
+ ) {
296
+ hasServerIdentity = true;
297
+ hasToolName = true;
298
+ }
299
+ if (
300
+ typeof deferredToolName === 'string' &&
301
+ isRecord(deferredToolInput) &&
302
+ hasDelimitedPair({
303
+ value: deferredToolName,
304
+ serverName: invocation.serverName,
305
+ toolName: invocation.toolName,
306
+ })
307
+ ) {
308
+ hasServerIdentity = true;
309
+ hasToolName = true;
310
+ if (
311
+ canonicalFingerprint({ value: deferredToolInput }) ===
312
+ invocation.inputFingerprint
313
+ ) {
314
+ hasInput = true;
315
+ }
316
+ }
317
+ if (
318
+ containsExactValue({
319
+ value: [metadata, rawInput],
320
+ target: invocation.serverName,
321
+ })
322
+ ) {
323
+ hasServerIdentity = true;
324
+ }
325
+ if (
326
+ programmaticName === invocation.toolName ||
327
+ containsExactValue({
328
+ value: [metadata, rawInput],
329
+ target: invocation.toolName,
330
+ })
331
+ ) {
332
+ hasToolName = true;
333
+ }
334
+ if (
335
+ containsFingerprint({
336
+ value: [metadata, rawInput],
337
+ target: invocation.inputFingerprint,
338
+ })
339
+ ) {
340
+ hasInput = true;
341
+ }
342
+ }
343
+ return hasServerIdentity && hasToolName && hasInput;
344
+ }
345
+
346
+ function resolvePermissionHostTool({
347
+ toolCall,
348
+ serverName,
349
+ toolName,
350
+ }: {
351
+ toolCall: ToolCallUpdate;
352
+ serverName: string;
353
+ toolName: string;
354
+ }):
355
+ | {
356
+ readonly input: Readonly<Record<string, unknown>>;
357
+ readonly hasRequestIdentity: boolean;
358
+ }
359
+ | undefined {
360
+ if (!isRecord(toolCall.rawInput)) return undefined;
361
+ const deferredToolName = getProperty({
362
+ value: toolCall.rawInput,
363
+ property: 'tool_name',
364
+ });
365
+ const deferredToolInput = getProperty({
366
+ value: toolCall.rawInput,
367
+ property: 'tool_input',
368
+ });
369
+ if (
370
+ hasOwnProperty({ value: toolCall.rawInput, property: 'tool_name' }) &&
371
+ hasOwnProperty({ value: toolCall.rawInput, property: 'tool_input' })
372
+ ) {
373
+ if (
374
+ typeof deferredToolName !== 'string' ||
375
+ !isRecord(deferredToolInput) ||
376
+ !hasDelimitedPair({
377
+ value: deferredToolName,
378
+ serverName,
379
+ toolName,
380
+ })
381
+ ) {
382
+ return undefined;
383
+ }
384
+ return {
385
+ input: deferredToolInput,
386
+ hasRequestIdentity: true,
387
+ };
388
+ }
389
+ return {
390
+ input: toolCall.rawInput,
391
+ hasRequestIdentity: containsCombinedIdentity({
392
+ value: [
393
+ getProperty({ value: toolCall, property: 'title' }),
394
+ getProperty({ value: toolCall, property: 'name' }),
395
+ getProperty({ value: toolCall, property: '_meta' }),
396
+ ],
397
+ serverName,
398
+ toolName,
399
+ }),
400
+ };
401
+ }
402
+
403
+ function containsCombinedIdentity({
404
+ value,
405
+ serverName,
406
+ toolName,
407
+ }: {
408
+ value: unknown;
409
+ serverName: string;
410
+ toolName: string;
411
+ }): boolean {
412
+ const seen = new Set<object>();
413
+ const visit = (candidate: unknown): boolean => {
414
+ if (typeof candidate === 'string') {
415
+ return hasDelimitedPair({ value: candidate, serverName, toolName });
416
+ }
417
+ if (candidate == null || typeof candidate !== 'object') return false;
418
+ if (seen.has(candidate)) return false;
419
+ seen.add(candidate);
420
+ const values = Array.isArray(candidate)
421
+ ? candidate
422
+ : Object.values(candidate);
423
+ for (const item of values) {
424
+ if (visit(item)) return true;
425
+ }
426
+ return false;
427
+ };
428
+ return visit(value);
429
+ }
430
+
431
+ function hasDelimitedPair({
432
+ value,
433
+ serverName,
434
+ toolName,
435
+ }: {
436
+ value: string;
437
+ serverName: string;
438
+ toolName: string;
439
+ }): boolean {
440
+ if (serverName.length === 0 || toolName.length === 0) return false;
441
+ let serverIndex = value.indexOf(serverName);
442
+ while (serverIndex !== -1) {
443
+ const beforeServer = serverIndex === 0 ? undefined : value[serverIndex - 1];
444
+ const serverHasLeadingBoundary =
445
+ beforeServer == null ||
446
+ value.slice(0, serverIndex).endsWith('__') ||
447
+ !/[A-Za-z0-9_-]/.test(beforeServer);
448
+ const delimiterStart = serverIndex + serverName.length;
449
+ let toolIndex = delimiterStart;
450
+ while (toolIndex < value.length && !/[A-Za-z0-9-]/.test(value[toolIndex])) {
451
+ toolIndex += 1;
452
+ }
453
+ const toolEnd = toolIndex + toolName.length;
454
+ const afterTool = toolEnd === value.length ? undefined : value[toolEnd];
455
+ const toolHasTrailingBoundary =
456
+ afterTool == null ||
457
+ value.startsWith('__', toolEnd) ||
458
+ !/[A-Za-z0-9_-]/.test(afterTool);
459
+ if (
460
+ serverHasLeadingBoundary &&
461
+ toolIndex > delimiterStart &&
462
+ value.startsWith(toolName, toolIndex) &&
463
+ toolHasTrailingBoundary
464
+ ) {
465
+ return true;
466
+ }
467
+ serverIndex = value.indexOf(serverName, serverIndex + 1);
468
+ }
469
+ return false;
470
+ }
471
+
472
+ function findTokenInvocation({
473
+ value,
474
+ invocations,
475
+ }: {
476
+ value: unknown;
477
+ invocations: ReadonlyMap<string, CorrelationInvocation>;
478
+ }): CorrelationInvocation | undefined {
479
+ for (const invocation of invocations.values()) {
480
+ if (containsExactValue({ value, target: invocation.token })) {
481
+ return invocation;
482
+ }
483
+ }
484
+ return undefined;
485
+ }
486
+
487
+ function removeInvocationForToolCall({
488
+ invocations,
489
+ toolCallId,
490
+ }: {
491
+ invocations: Map<string, CorrelationInvocation>;
492
+ toolCallId: string;
493
+ }): void {
494
+ for (const [token, invocation] of invocations) {
495
+ if (invocation.toolCallId === toolCallId) {
496
+ deleteInvocation({ invocations, token });
497
+ }
498
+ }
499
+ }
500
+
501
+ function deleteInvocation({
502
+ invocations,
503
+ token,
504
+ }: {
505
+ invocations: Map<string, CorrelationInvocation>;
506
+ token: string;
507
+ }): void {
508
+ const invocation = invocations.get(token);
509
+ if (invocation?.expiryTimer != null) clearTimeout(invocation.expiryTimer);
510
+ invocations.delete(token);
511
+ }
512
+
513
+ function isToolUpdate(
514
+ message: SessionUpdateMessage,
515
+ ): message is SessionUpdateMessage & {
516
+ readonly update: {
517
+ readonly sessionUpdate: 'tool_call' | 'tool_call_update';
518
+ readonly toolCallId: string;
519
+ readonly status?: string | null;
520
+ };
521
+ } {
522
+ return (
523
+ message.update.sessionUpdate === 'tool_call' ||
524
+ message.update.sessionUpdate === 'tool_call_update'
525
+ );
526
+ }
527
+
528
+ function containsExactValue({
529
+ value,
530
+ target,
531
+ }: {
532
+ value: unknown;
533
+ target: string;
534
+ }): boolean {
535
+ const seen = new Set<object>();
536
+ const visit = (candidate: unknown): boolean => {
537
+ if (candidate === target) return true;
538
+ if (candidate == null || typeof candidate !== 'object') return false;
539
+ if (seen.has(candidate)) return false;
540
+ seen.add(candidate);
541
+ const values = Array.isArray(candidate)
542
+ ? candidate
543
+ : Object.values(candidate);
544
+ for (const item of values) {
545
+ if (visit(item)) return true;
546
+ }
547
+ return false;
548
+ };
549
+ return visit(value);
550
+ }
551
+
552
+ function containsFingerprint({
553
+ value,
554
+ target,
555
+ }: {
556
+ value: unknown;
557
+ target: string;
558
+ }): boolean {
559
+ const seen = new Set<object>();
560
+ const visit = (candidate: unknown): boolean => {
561
+ if (candidate != null && typeof candidate === 'object') {
562
+ if (canonicalFingerprint({ value: candidate }) === target) return true;
563
+ if (seen.has(candidate)) return false;
564
+ seen.add(candidate);
565
+ const values = Array.isArray(candidate)
566
+ ? candidate
567
+ : Object.values(candidate);
568
+ for (const item of values) {
569
+ if (visit(item)) return true;
570
+ }
571
+ }
572
+ return false;
573
+ };
574
+ return visit(value);
575
+ }
576
+
577
+ function canonicalFingerprint({ value }: { value: unknown }): string {
578
+ return JSON.stringify(canonicalizeJSON({ value }));
579
+ }
580
+
581
+ function canonicalizeJSON({ value }: { value: unknown }): unknown {
582
+ if (Array.isArray(value)) {
583
+ return value.map(item => canonicalizeJSON({ value: item }));
584
+ }
585
+ if (value == null || typeof value !== 'object') return value;
586
+ return Object.fromEntries(
587
+ Object.keys(value as Record<string, unknown>)
588
+ .sort()
589
+ .filter(key => (value as Record<string, unknown>)[key] !== undefined)
590
+ .map(key => [
591
+ key,
592
+ canonicalizeJSON({
593
+ value: (value as Record<string, unknown>)[key],
594
+ }),
595
+ ]),
596
+ );
597
+ }
598
+
599
+ function getProperty({
600
+ value,
601
+ property,
602
+ }: {
603
+ value: unknown;
604
+ property: string;
605
+ }): unknown {
606
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
607
+ return undefined;
608
+ }
609
+ return Reflect.get(value, property);
610
+ }
611
+
612
+ function hasOwnProperty({
613
+ value,
614
+ property,
615
+ }: {
616
+ value: Readonly<Record<string, unknown>>;
617
+ property: string;
618
+ }): boolean {
619
+ return Object.prototype.hasOwnProperty.call(value, property);
620
+ }
621
+
622
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
623
+ return value != null && typeof value === 'object' && !Array.isArray(value);
624
+ }
625
+
626
+ function isTerminalToolUpdate(
627
+ message: SessionUpdateMessage,
628
+ ): message is SessionUpdateMessage & {
629
+ readonly update: {
630
+ readonly sessionUpdate: 'tool_call' | 'tool_call_update';
631
+ readonly toolCallId: string;
632
+ readonly status: 'completed' | 'failed';
633
+ };
634
+ } {
635
+ return (
636
+ isToolUpdate(message) &&
637
+ (message.update.status === 'completed' ||
638
+ message.update.status === 'failed')
639
+ );
640
+ }