@ai-sdk/harness 1.0.99 → 1.0.101

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
  }
@@ -11,7 +11,38 @@ export function isSandboxCredentialPlaceholder(value: string): boolean {
11
11
  return /^aisdkhc_[A-Za-z0-9_-]{43}$/.test(value);
12
12
  }
13
13
 
14
- export function warnCredentialBrokeringUnavailable(): void {
14
+ /**
15
+ * Warns when credential brokering is unavailable, but only if real credentials
16
+ * remain among the credentials forwarded into the sandbox.
17
+ */
18
+ export function warnCredentialBrokeringUnavailable(options: {
19
+ environment: Readonly<Record<string, string>>;
20
+ forwardedEnvironment: Readonly<Record<string, string>>;
21
+ credentialEnvironmentVariables: ReadonlyArray<string>;
22
+ }): void {
23
+ const credentialEnvironmentVariables = [
24
+ ...new Set(options.credentialEnvironmentVariables),
25
+ ];
26
+ const credentials = credentialEnvironmentVariables
27
+ .map(name => options.environment[name])
28
+ .filter(
29
+ (credential): credential is string =>
30
+ credential != null && credential.length > 0,
31
+ );
32
+ const forwardedCredentials = credentialEnvironmentVariables
33
+ .map(name => options.forwardedEnvironment[name])
34
+ .filter((credential): credential is string => credential != null);
35
+
36
+ if (
37
+ !credentials.some(credential =>
38
+ forwardedCredentials.some(forwardedCredential =>
39
+ forwardedCredential.includes(credential),
40
+ ),
41
+ )
42
+ ) {
43
+ return;
44
+ }
45
+
15
46
  console.warn(
16
47
  'The sandbox implementation does not support configuring request transformations, so credential brokering does not work. Falling back to less secure credential forwarding.',
17
48
  );
@@ -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
  /**
@@ -0,0 +1,72 @@
1
+ import type { FunctionTool } from '@ai-sdk/provider-utils';
2
+ import { z } from 'zod/v4';
3
+
4
+ const harnessV1QuestionSchema = z.object({
5
+ id: z.string().min(1),
6
+ question: z.string().min(1),
7
+ header: z.string().optional(),
8
+ options: z
9
+ .array(
10
+ z.object({
11
+ id: z.string().min(1),
12
+ label: z.string().min(1),
13
+ description: z.string().optional(),
14
+ preview: z.string().optional(),
15
+ }),
16
+ )
17
+ .optional(),
18
+ allowMultiple: z.boolean().optional(),
19
+ allowFreeForm: z
20
+ .union([
21
+ z.boolean(),
22
+ z.object({
23
+ secret: z.boolean(),
24
+ }),
25
+ ])
26
+ .optional(),
27
+ });
28
+
29
+ export const harnessV1QuestionsToolInputSchema = z.object({
30
+ allowPartialAnswers: z.boolean(),
31
+ questions: z.array(harnessV1QuestionSchema).min(1),
32
+ });
33
+
34
+ const harnessV1QuestionAnswerSchema = z.object({
35
+ optionIds: z.array(z.string()),
36
+ freeform: z.string().optional(),
37
+ });
38
+
39
+ export const harnessV1QuestionsToolOutputSchema = z.discriminatedUnion(
40
+ 'action',
41
+ [
42
+ z.object({
43
+ action: z.literal('answered'),
44
+ answers: z.record(z.string(), harnessV1QuestionAnswerSchema),
45
+ }),
46
+ z.object({
47
+ action: z.literal('partially-answered'),
48
+ answers: z.record(z.string(), harnessV1QuestionAnswerSchema),
49
+ }),
50
+ z.object({ action: z.literal('declined') }),
51
+ z.object({ action: z.literal('cancelled') }),
52
+ ],
53
+ );
54
+
55
+ export type HarnessV1QuestionsToolInput = z.infer<
56
+ typeof harnessV1QuestionsToolInputSchema
57
+ >;
58
+
59
+ export type HarnessV1QuestionsToolOutput = z.infer<
60
+ typeof harnessV1QuestionsToolOutputSchema
61
+ >;
62
+
63
+ export const harnessV1QuestionsTool: FunctionTool<
64
+ HarnessV1QuestionsToolInput,
65
+ HarnessV1QuestionsToolOutput
66
+ > = {
67
+ description: 'Ask the user one or more questions',
68
+ inputSchema: harnessV1QuestionsToolInputSchema,
69
+ outputSchema: harnessV1QuestionsToolOutputSchema,
70
+ };
71
+
72
+ export type HarnessV1QuestionsTool = typeof harnessV1QuestionsTool;
package/src/v1/index.ts CHANGED
@@ -28,6 +28,15 @@ export {
28
28
  HARNESS_V1_BUILTIN_TOOLS,
29
29
  commonTool,
30
30
  } from './harness-v1-builtin-tool';
31
+ export type {
32
+ HarnessV1QuestionsTool,
33
+ HarnessV1QuestionsToolInput,
34
+ HarnessV1QuestionsToolOutput,
35
+ } from './harness-v1-questions-tool';
36
+ export {
37
+ harnessV1QuestionsToolInputSchema,
38
+ harnessV1QuestionsToolOutputSchema,
39
+ } from './harness-v1-questions-tool';
31
40
  export type { HarnessV1Metadata } from './harness-v1-metadata';
32
41
  export type { HarnessV1Prompt } from './harness-v1-prompt';
33
42
  export type {