@ai-sdk/harness 1.0.100 → 1.0.102

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.
@@ -11,6 +11,7 @@ import { appendFile, mkdir, writeFile } from 'node:fs/promises';
11
11
  import { existsSync, readFileSync } from 'node:fs';
12
12
  import { randomUUID } from 'node:crypto';
13
13
  import { env as procEnv, pid, stdout } from 'node:process';
14
+ import type { ToolResultPart } from '@ai-sdk/provider-utils';
14
15
  import { WebSocketServer, type WebSocket } from 'ws';
15
16
 
16
17
  export { HarnessBridgeCapabilityUnsupportedError } from './harness-bridge-capability-unsupported-error';
@@ -244,8 +245,21 @@ export interface BridgeTurn {
244
245
  * itself (via {@link emit}) using the same `toolCallId`.
245
246
  */
246
247
  requestToolResult(
247
- toolCallId: string,
248
- ): Promise<{ output: unknown; isError?: boolean }>;
248
+ input:
249
+ | string
250
+ | {
251
+ toolCallId: string;
252
+ matches?: (result: {
253
+ output: unknown;
254
+ isError?: boolean;
255
+ toolResult?: ToolResultPart;
256
+ }) => boolean;
257
+ },
258
+ ): Promise<{
259
+ output: unknown;
260
+ isError?: boolean;
261
+ toolResult?: ToolResultPart;
262
+ }>;
249
263
 
250
264
  /**
251
265
  * Register interest in a host approval decision and resolve when the matching
@@ -342,6 +356,7 @@ type InboundControl =
342
356
  toolCallId: string;
343
357
  output: unknown;
344
358
  isError?: boolean;
359
+ toolResult?: ToolResultPart;
345
360
  }
346
361
  | {
347
362
  type: 'tool-approval-response';
@@ -506,8 +521,27 @@ export async function runBridge<TStart extends { type: 'start' }>(
506
521
 
507
522
  const pendingToolResults = new Map<
508
523
  string,
509
- (output: { output: unknown; isError?: boolean }) => void
524
+ {
525
+ resolve: (output: {
526
+ output: unknown;
527
+ isError?: boolean;
528
+ toolResult?: ToolResultPart;
529
+ }) => void;
530
+ matches?: (output: {
531
+ output: unknown;
532
+ isError?: boolean;
533
+ toolResult?: ToolResultPart;
534
+ }) => boolean;
535
+ }
510
536
  >();
537
+ const bufferedToolResults: Array<{
538
+ toolCallId: string;
539
+ result: {
540
+ output: unknown;
541
+ isError?: boolean;
542
+ toolResult?: ToolResultPart;
543
+ };
544
+ }> = [];
511
545
  const pendingToolApprovals = new Map<
512
546
  string,
513
547
  (response: { approved: boolean; reason?: string }) => void
@@ -749,10 +783,28 @@ export async function runBridge<TStart extends { type: 'start' }>(
749
783
  const userMessages = createBridgeUserMessageQueue({ respond: emit });
750
784
  const turn: BridgeTurn = {
751
785
  emit,
752
- requestToolResult: toolCallId =>
753
- new Promise(resolve => {
754
- pendingToolResults.set(toolCallId, resolve);
755
- }),
786
+ requestToolResult: requestInput => {
787
+ const request =
788
+ typeof requestInput === 'string'
789
+ ? { toolCallId: requestInput }
790
+ : requestInput;
791
+ const bufferedIndex = bufferedToolResults.findIndex(
792
+ buffered =>
793
+ buffered.toolCallId === request.toolCallId ||
794
+ request.matches?.(buffered.result) === true,
795
+ );
796
+ if (bufferedIndex >= 0) {
797
+ return Promise.resolve(
798
+ bufferedToolResults.splice(bufferedIndex, 1)[0].result,
799
+ );
800
+ }
801
+ return new Promise(resolve => {
802
+ pendingToolResults.set(request.toolCallId, {
803
+ resolve,
804
+ matches: request.matches,
805
+ });
806
+ });
807
+ },
756
808
  requestToolApproval: approvalId =>
757
809
  new Promise(resolve => {
758
810
  pendingToolApprovals.set(approvalId, resolve);
@@ -800,10 +852,29 @@ export async function runBridge<TStart extends { type: 'start' }>(
800
852
  return;
801
853
  }
802
854
  case 'tool-result': {
803
- const resolver = pendingToolResults.get(msg.toolCallId);
804
- if (resolver) {
805
- pendingToolResults.delete(msg.toolCallId);
806
- resolver({ output: msg.output, isError: msg.isError });
855
+ const result = {
856
+ output: msg.output,
857
+ isError: msg.isError,
858
+ toolResult: msg.toolResult,
859
+ };
860
+ const exactPending = pendingToolResults.get(msg.toolCallId);
861
+ const matchingPending =
862
+ exactPending == null
863
+ ? Array.from(pendingToolResults.entries()).find(
864
+ ([, pending]) => pending.matches?.(result) === true,
865
+ )
866
+ : undefined;
867
+ const pending = exactPending ?? matchingPending?.[1];
868
+ const pendingId =
869
+ exactPending != null ? msg.toolCallId : matchingPending?.[0];
870
+ if (pending != null && pendingId != null) {
871
+ pendingToolResults.delete(pendingId);
872
+ pending.resolve(result);
873
+ } else {
874
+ bufferedToolResults.push({
875
+ toolCallId: msg.toolCallId,
876
+ result,
877
+ });
807
878
  }
808
879
  return;
809
880
  }
@@ -26,6 +26,11 @@ export {
26
26
  } from './sandbox-credential-brokering';
27
27
  export { resolveSandboxHomeDir } from './sandbox-home-dir';
28
28
  export { shellQuote } from './shell-quote';
29
+ export {
30
+ writeInstructions,
31
+ type WriteInstructionsOptions,
32
+ type WriteInstructionsResult,
33
+ } from './write-instructions';
29
34
  export {
30
35
  writeSkills,
31
36
  type SkillFilePathMode,
@@ -0,0 +1,373 @@
1
+ import path from 'node:path';
2
+ import {
3
+ safeParseJSON,
4
+ type Experimental_SandboxSession,
5
+ } from '@ai-sdk/provider-utils';
6
+ import { shellQuote } from './shell-quote';
7
+
8
+ const INSTRUCTIONS_METADATA_VERSION = 1;
9
+
10
+ export type WriteInstructionsOptions = {
11
+ sandbox: Experimental_SandboxSession;
12
+ homePath: string;
13
+ instructionsFile: string;
14
+ instructions?: string;
15
+ abortSignal?: AbortSignal;
16
+ };
17
+
18
+ export type WriteInstructionsResult = {
19
+ changed: boolean;
20
+ filePath: string;
21
+ };
22
+
23
+ type InstructionsMetadata = {
24
+ readonly version: typeof INSTRUCTIONS_METADATA_VERSION;
25
+ readonly originalContent: string | null;
26
+ readonly instructions: string;
27
+ readonly appliedContent: string;
28
+ };
29
+
30
+ export async function writeInstructions({
31
+ sandbox,
32
+ homePath,
33
+ instructionsFile,
34
+ instructions,
35
+ abortSignal,
36
+ }: WriteInstructionsOptions): Promise<WriteInstructionsResult> {
37
+ const { targetPath, metadataPath } = resolveInstructionsFilePath({
38
+ homePath,
39
+ instructionsFile,
40
+ });
41
+
42
+ const hasInstructions =
43
+ typeof instructions === 'string' && instructions.trim().length > 0;
44
+ const trimmedInstructions = hasInstructions ? instructions.trim() : '';
45
+
46
+ const currentDiskContent = await sandbox.readTextFile({
47
+ path: targetPath,
48
+ abortSignal,
49
+ });
50
+ const existingMetadata = await readInstructionsMetadata({
51
+ sandbox,
52
+ metadataPath,
53
+ abortSignal,
54
+ });
55
+
56
+ if (hasInstructions) {
57
+ const originalContent = deriveOriginalContent({
58
+ currentDiskContent,
59
+ existingMetadata,
60
+ });
61
+
62
+ const targetContent =
63
+ originalContent != null && originalContent.trim().length > 0
64
+ ? `${originalContent.replace(/\n+$/, '')}\n\n${trimmedInstructions}\n`
65
+ : `${trimmedInstructions}\n`;
66
+
67
+ if (
68
+ currentDiskContent === targetContent &&
69
+ existingMetadata != null &&
70
+ existingMetadata.instructions === trimmedInstructions &&
71
+ existingMetadata.originalContent === originalContent
72
+ ) {
73
+ return { changed: false, filePath: targetPath };
74
+ }
75
+
76
+ await sandbox.writeTextFile({
77
+ path: targetPath,
78
+ content: targetContent,
79
+ abortSignal,
80
+ });
81
+
82
+ await writeInstructionsMetadata({
83
+ sandbox,
84
+ metadataPath,
85
+ metadata: {
86
+ version: INSTRUCTIONS_METADATA_VERSION,
87
+ originalContent,
88
+ instructions: trimmedInstructions,
89
+ appliedContent: targetContent,
90
+ },
91
+ abortSignal,
92
+ });
93
+
94
+ return { changed: true, filePath: targetPath };
95
+ }
96
+
97
+ if (existingMetadata == null) {
98
+ return { changed: false, filePath: targetPath };
99
+ }
100
+
101
+ const restoredContent = deriveRestoredContent({
102
+ currentDiskContent,
103
+ existingMetadata,
104
+ });
105
+
106
+ if (restoredContent != null) {
107
+ const contentToWrite = `${restoredContent.replace(/\n+$/, '')}\n`;
108
+ await sandbox.writeTextFile({
109
+ path: targetPath,
110
+ content: contentToWrite,
111
+ abortSignal,
112
+ });
113
+ } else {
114
+ await removeTargetFile({
115
+ sandbox,
116
+ targetPath,
117
+ abortSignal,
118
+ });
119
+ }
120
+
121
+ await removeMetadataFile({
122
+ sandbox,
123
+ metadataPath,
124
+ abortSignal,
125
+ });
126
+
127
+ return { changed: true, filePath: targetPath };
128
+ }
129
+
130
+ function deriveOriginalContent({
131
+ currentDiskContent,
132
+ existingMetadata,
133
+ }: {
134
+ currentDiskContent: string | null;
135
+ existingMetadata: InstructionsMetadata | undefined;
136
+ }): string | null {
137
+ if (existingMetadata == null) {
138
+ return currentDiskContent;
139
+ }
140
+ if (currentDiskContent == null) {
141
+ return existingMetadata.originalContent;
142
+ }
143
+ if (currentDiskContent === existingMetadata.appliedContent) {
144
+ return existingMetadata.originalContent;
145
+ }
146
+ const trimmedDisk = currentDiskContent.replace(/\n+$/, '');
147
+ const expectedSuffix = `\n\n${existingMetadata.instructions}`;
148
+ if (trimmedDisk.endsWith(expectedSuffix)) {
149
+ const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
150
+ return userBase.length > 0 ? userBase : null;
151
+ }
152
+ if (trimmedDisk === existingMetadata.instructions) {
153
+ return null;
154
+ }
155
+ return existingMetadata.originalContent ?? currentDiskContent;
156
+ }
157
+
158
+ function deriveRestoredContent({
159
+ currentDiskContent,
160
+ existingMetadata,
161
+ }: {
162
+ currentDiskContent: string | null;
163
+ existingMetadata: InstructionsMetadata;
164
+ }): string | null {
165
+ if (currentDiskContent == null) {
166
+ return null;
167
+ }
168
+ if (currentDiskContent === existingMetadata.appliedContent) {
169
+ return existingMetadata.originalContent != null &&
170
+ existingMetadata.originalContent.trim().length > 0
171
+ ? existingMetadata.originalContent
172
+ : null;
173
+ }
174
+ const trimmedDisk = currentDiskContent.replace(/\n+$/, '');
175
+ const expectedSuffix = `\n\n${existingMetadata.instructions}`;
176
+ if (trimmedDisk.endsWith(expectedSuffix)) {
177
+ const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
178
+ return userBase.length > 0 ? userBase : null;
179
+ }
180
+ if (trimmedDisk === existingMetadata.instructions) {
181
+ return null;
182
+ }
183
+ return currentDiskContent;
184
+ }
185
+
186
+ function resolveInstructionsFilePath({
187
+ homePath,
188
+ instructionsFile,
189
+ }: {
190
+ homePath: string;
191
+ instructionsFile: string;
192
+ }): { targetPath: string; metadataPath: string } {
193
+ if (typeof homePath !== 'string' || homePath.trim().length === 0) {
194
+ throw new Error('Invalid homePath: expected a non-empty string.');
195
+ }
196
+ if (!path.posix.isAbsolute(homePath)) {
197
+ throw new Error(
198
+ `Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`,
199
+ );
200
+ }
201
+ if (
202
+ typeof instructionsFile !== 'string' ||
203
+ instructionsFile.trim().length === 0
204
+ ) {
205
+ throw new Error(
206
+ `Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`,
207
+ );
208
+ }
209
+ const containsTraversal = instructionsFile
210
+ .split(/[\\\/]/)
211
+ .some(segment => segment === '..');
212
+ const normalizedInstructionsFile = path.posix.normalize(
213
+ instructionsFile.trim(),
214
+ );
215
+ const normalizedNoTrailingSlash = normalizedInstructionsFile.replace(
216
+ /\/+$/,
217
+ '',
218
+ );
219
+ if (
220
+ instructionsFile.includes('\\') ||
221
+ path.posix.isAbsolute(instructionsFile) ||
222
+ path.win32.isAbsolute(instructionsFile) ||
223
+ containsTraversal ||
224
+ instructionsFile.endsWith('/') ||
225
+ instructionsFile.endsWith('\\') ||
226
+ instructionsFile.endsWith('/.') ||
227
+ instructionsFile.endsWith('/..') ||
228
+ normalizedNoTrailingSlash === '' ||
229
+ normalizedNoTrailingSlash === '.' ||
230
+ normalizedInstructionsFile.startsWith('../') ||
231
+ normalizedInstructionsFile.includes('/../') ||
232
+ normalizedInstructionsFile.endsWith('/..')
233
+ ) {
234
+ throw new Error(
235
+ `Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`,
236
+ );
237
+ }
238
+ const targetPath = path.posix.join(homePath, normalizedNoTrailingSlash);
239
+ const relative = path.posix.relative(homePath, targetPath);
240
+ if (
241
+ relative === '' ||
242
+ relative.startsWith('..') ||
243
+ path.posix.isAbsolute(relative)
244
+ ) {
245
+ throw new Error(
246
+ `Invalid instructionsFile ${JSON.stringify(instructionsFile)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`,
247
+ );
248
+ }
249
+
250
+ const dir = path.posix.dirname(targetPath);
251
+ const base = path.posix.basename(targetPath);
252
+ const metadataPath = path.posix.join(
253
+ dir,
254
+ `.${base}.ai-sdk-harness-instructions.json`,
255
+ );
256
+
257
+ return { targetPath, metadataPath };
258
+ }
259
+
260
+ async function readInstructionsMetadata({
261
+ sandbox,
262
+ metadataPath,
263
+ abortSignal,
264
+ }: {
265
+ sandbox: Experimental_SandboxSession;
266
+ metadataPath: string;
267
+ abortSignal?: AbortSignal;
268
+ }): Promise<InstructionsMetadata | undefined> {
269
+ const content = await sandbox.readTextFile({
270
+ path: metadataPath,
271
+ abortSignal,
272
+ });
273
+ if (content == null) return undefined;
274
+ const parsed = await safeParseJSON({ text: content });
275
+ if (!parsed.success || !isInstructionsMetadata(parsed.value)) {
276
+ throw new Error(
277
+ `Invalid AI SDK harness instructions metadata: ${metadataPath}`,
278
+ );
279
+ }
280
+ return parsed.value;
281
+ }
282
+
283
+ function isInstructionsMetadata(value: unknown): value is InstructionsMetadata {
284
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
285
+ return false;
286
+ }
287
+ const candidate = value as Record<string, unknown>;
288
+ return (
289
+ candidate.version === INSTRUCTIONS_METADATA_VERSION &&
290
+ (candidate.originalContent === null ||
291
+ typeof candidate.originalContent === 'string') &&
292
+ typeof candidate.instructions === 'string' &&
293
+ typeof candidate.appliedContent === 'string'
294
+ );
295
+ }
296
+
297
+ async function writeInstructionsMetadata({
298
+ sandbox,
299
+ metadataPath,
300
+ metadata,
301
+ abortSignal,
302
+ }: {
303
+ sandbox: Experimental_SandboxSession;
304
+ metadataPath: string;
305
+ metadata: InstructionsMetadata;
306
+ abortSignal?: AbortSignal;
307
+ }): Promise<void> {
308
+ const temporaryPath = `${metadataPath}.tmp`;
309
+ await sandbox.writeTextFile({
310
+ path: temporaryPath,
311
+ content: `${JSON.stringify(metadata, null, 2)}\n`,
312
+ abortSignal,
313
+ });
314
+ await runSandboxCommand({
315
+ sandbox,
316
+ command: `mv -f ${shellQuote(temporaryPath)} ${shellQuote(metadataPath)}`,
317
+ abortSignal,
318
+ errorMessage: `Failed to update instructions metadata: ${metadataPath}`,
319
+ });
320
+ }
321
+
322
+ async function removeMetadataFile({
323
+ sandbox,
324
+ metadataPath,
325
+ abortSignal,
326
+ }: {
327
+ sandbox: Experimental_SandboxSession;
328
+ metadataPath: string;
329
+ abortSignal?: AbortSignal;
330
+ }): Promise<void> {
331
+ await runSandboxCommand({
332
+ sandbox,
333
+ command: `rm -f -- ${shellQuote(metadataPath)}`,
334
+ abortSignal,
335
+ errorMessage: `Failed to remove instructions metadata: ${metadataPath}`,
336
+ });
337
+ }
338
+
339
+ async function removeTargetFile({
340
+ sandbox,
341
+ targetPath,
342
+ abortSignal,
343
+ }: {
344
+ sandbox: Experimental_SandboxSession;
345
+ targetPath: string;
346
+ abortSignal?: AbortSignal;
347
+ }): Promise<void> {
348
+ await runSandboxCommand({
349
+ sandbox,
350
+ command: `rm -f -- ${shellQuote(targetPath)}`,
351
+ abortSignal,
352
+ errorMessage: `Failed to remove instructions file: ${targetPath}`,
353
+ });
354
+ }
355
+
356
+ async function runSandboxCommand({
357
+ sandbox,
358
+ command,
359
+ abortSignal,
360
+ errorMessage,
361
+ }: {
362
+ sandbox: Experimental_SandboxSession;
363
+ command: string;
364
+ abortSignal?: AbortSignal;
365
+ errorMessage: string;
366
+ }): Promise<void> {
367
+ const result = await sandbox.run({ command, abortSignal });
368
+ if (result.exitCode !== 0) {
369
+ throw new Error(
370
+ `${errorMessage} (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ''}`,
371
+ );
372
+ }
373
+ }
@@ -15,7 +15,8 @@ export type SkillFilePathMode = 'relative' | 'strip-leading-slashes';
15
15
 
16
16
  export type WriteSkillsOptions = {
17
17
  sandbox: Experimental_SandboxSession;
18
- rootDir: string;
18
+ homePath: string;
19
+ skillsDir: string;
19
20
  skills: ReadonlyArray<HarnessV1Skill>;
20
21
  abortSignal?: AbortSignal;
21
22
  skillNamePattern?: RegExp;
@@ -57,7 +58,8 @@ type SkillsManifest = {
57
58
 
58
59
  export async function writeSkills({
59
60
  sandbox,
60
- rootDir,
61
+ homePath,
62
+ skillsDir,
61
63
  skills,
62
64
  abortSignal,
63
65
  skillNamePattern = /^[A-Za-z0-9._-]+$/,
@@ -67,6 +69,7 @@ export async function writeSkills({
67
69
  `Invalid skill file path for ${skillName}: ${filePath}`,
68
70
  trailingNewline = false,
69
71
  }: WriteSkillsOptions): Promise<WriteSkillsResult> {
72
+ const rootDir = resolveSkillsRootDir({ homePath, skillsDir });
70
73
  const projectedSkills = skills
71
74
  .map(skill =>
72
75
  projectSkill({
@@ -526,3 +529,57 @@ function renderSkillFile({
526
529
  const content = `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}`;
527
530
  return trailingNewline ? `${content}\n` : content;
528
531
  }
532
+
533
+ function resolveSkillsRootDir({
534
+ homePath,
535
+ skillsDir,
536
+ }: {
537
+ homePath: string;
538
+ skillsDir: string;
539
+ }): string {
540
+ if (typeof homePath !== 'string' || homePath.trim().length === 0) {
541
+ throw new Error('Invalid homePath: expected a non-empty string.');
542
+ }
543
+ if (!path.posix.isAbsolute(homePath)) {
544
+ throw new Error(
545
+ `Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`,
546
+ );
547
+ }
548
+ if (typeof skillsDir !== 'string' || skillsDir.trim().length === 0) {
549
+ throw new Error(
550
+ `Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`,
551
+ );
552
+ }
553
+ const containsTraversal = skillsDir
554
+ .split(/[\\\/]/)
555
+ .some(segment => segment === '..');
556
+ const normalizedSkillsDir = path.posix.normalize(skillsDir.trim());
557
+ const normalizedNoTrailingSlash = normalizedSkillsDir.replace(/\/+$/, '');
558
+ if (
559
+ skillsDir.includes('\\') ||
560
+ path.posix.isAbsolute(skillsDir) ||
561
+ path.win32.isAbsolute(skillsDir) ||
562
+ containsTraversal ||
563
+ normalizedNoTrailingSlash === '' ||
564
+ normalizedNoTrailingSlash === '.' ||
565
+ normalizedSkillsDir.startsWith('../') ||
566
+ normalizedSkillsDir.includes('/../') ||
567
+ normalizedSkillsDir.endsWith('/..')
568
+ ) {
569
+ throw new Error(
570
+ `Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`,
571
+ );
572
+ }
573
+ const rootDir = path.posix.join(homePath, normalizedNoTrailingSlash);
574
+ const relative = path.posix.relative(homePath, rootDir);
575
+ if (
576
+ relative === '' ||
577
+ relative.startsWith('..') ||
578
+ path.posix.isAbsolute(relative)
579
+ ) {
580
+ throw new Error(
581
+ `Invalid skillsDir ${JSON.stringify(skillsDir)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`,
582
+ );
583
+ }
584
+ return rootDir;
585
+ }
@@ -295,6 +295,7 @@ export const harnessV1BridgeToolResultInboundSchema = z.object({
295
295
  toolCallId: z.string(),
296
296
  output: z.unknown(),
297
297
  isError: z.boolean().optional(),
298
+ toolResult: z.unknown().optional(),
298
299
  });
299
300
 
300
301
  export const harnessV1BridgeToolApprovalResponseInboundSchema = z.object({
@@ -1,5 +1,6 @@
1
1
  import { tool, type FlexibleSchema, type Tool } from '@ai-sdk/provider-utils';
2
2
  import { z } from 'zod/v4';
3
+ import { harnessV1QuestionsTool } from './harness-v1-questions-tool';
3
4
 
4
5
  /**
5
6
  * Cross-harness vocabulary of common built-in tool names with their baseline
@@ -50,6 +51,7 @@ export const HARNESS_V1_BUILTIN_TOOLS = {
50
51
  inputSchema: z.object({ query: z.string() }),
51
52
  outputSchema: z.unknown(),
52
53
  }),
54
+ askUserQuestions: harnessV1QuestionsTool,
53
55
  } as const;
54
56
 
55
57
  export type HarnessV1BuiltinToolName = keyof typeof HARNESS_V1_BUILTIN_TOOLS;
@@ -1,4 +1,5 @@
1
1
  import type { JSONValue } from '@ai-sdk/provider';
2
+ import type { ProviderOptions } from '@ai-sdk/provider-utils';
2
3
  import type { HarnessV1Skill } from './harness-v1-skill';
3
4
  import type { HarnessV1ToolSpec } from './harness-v1-tool-spec';
4
5
 
@@ -16,6 +17,7 @@ export type HarnessV1PendingToolResult = {
16
17
  readonly toolCallId: string;
17
18
  readonly toolName: string;
18
19
  readonly input: string;
20
+ readonly providerOptions?: ProviderOptions;
19
21
  };
20
22
 
21
23
  /**
@@ -1,3 +1,5 @@
1
+ import type { ToolResultPart } from '@ai-sdk/provider-utils';
2
+
1
3
  /**
2
4
  * Bidirectional control surface returned by `doPromptTurn`.
3
5
  *
@@ -16,6 +18,7 @@ export type HarnessV1PromptControl = {
16
18
  toolCallId: string;
17
19
  output: unknown;
18
20
  isError?: boolean;
21
+ toolResult?: ToolResultPart;
19
22
  }): PromiseLike<void>;
20
23
 
21
24
  /**