@ai-sdk/harness 0.0.0-6b196531-20260710185421

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 (77) hide show
  1. package/CHANGELOG.md +414 -0
  2. package/LICENSE +13 -0
  3. package/README.md +176 -0
  4. package/agent/index.ts +56 -0
  5. package/bridge/index.ts +10 -0
  6. package/dist/agent/index.d.ts +1631 -0
  7. package/dist/agent/index.js +3491 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/bridge/index.d.ts +129 -0
  10. package/dist/bridge/index.js +482 -0
  11. package/dist/bridge/index.js.map +1 -0
  12. package/dist/index.d.ts +1587 -0
  13. package/dist/index.js +517 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/index.d.ts +329 -0
  16. package/dist/utils/index.js +1241 -0
  17. package/dist/utils/index.js.map +1 -0
  18. package/package.json +100 -0
  19. package/src/agent/harness-agent-session.ts +518 -0
  20. package/src/agent/harness-agent-settings.ts +187 -0
  21. package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
  22. package/src/agent/harness-agent-tool-types.ts +15 -0
  23. package/src/agent/harness-agent-types.ts +50 -0
  24. package/src/agent/harness-agent.ts +865 -0
  25. package/src/agent/internal/bootstrap-recipe.ts +124 -0
  26. package/src/agent/internal/bridge-port-registry.ts +52 -0
  27. package/src/agent/internal/harness-stream-text-result.ts +731 -0
  28. package/src/agent/internal/lifecycle-state-validation.ts +95 -0
  29. package/src/agent/internal/permission-mode.ts +50 -0
  30. package/src/agent/internal/resolve-observability.ts +128 -0
  31. package/src/agent/internal/run-prompt.ts +901 -0
  32. package/src/agent/internal/sandbox-bootstrap.ts +266 -0
  33. package/src/agent/internal/strip-work-dir.ts +68 -0
  34. package/src/agent/internal/to-harness-stream.ts +75 -0
  35. package/src/agent/internal/tool-filtering.ts +114 -0
  36. package/src/agent/internal/translate-stream-part.ts +221 -0
  37. package/src/agent/internal/turn-telemetry.ts +361 -0
  38. package/src/agent/observability/file-reporter.ts +206 -0
  39. package/src/agent/observability/index.ts +15 -0
  40. package/src/agent/observability/trace-tree-reporter.ts +122 -0
  41. package/src/agent/observability/types.ts +86 -0
  42. package/src/agent/prepare-harness-sandbox-template.ts +68 -0
  43. package/src/agent/prepare-sandbox-for-harness.ts +165 -0
  44. package/src/bridge/index.ts +797 -0
  45. package/src/errors/harness-capability-unsupported-error.ts +41 -0
  46. package/src/errors/harness-error.ts +22 -0
  47. package/src/index.ts +3 -0
  48. package/src/utils/ai-gateway-auth.ts +15 -0
  49. package/src/utils/bridge-diagnostics.ts +213 -0
  50. package/src/utils/bridge-ready.ts +277 -0
  51. package/src/utils/classify-disk-log.ts +43 -0
  52. package/src/utils/index.ts +31 -0
  53. package/src/utils/sandbox-channel.ts +525 -0
  54. package/src/utils/sandbox-home-dir.ts +22 -0
  55. package/src/utils/shell-quote.ts +3 -0
  56. package/src/utils/write-skills.ts +141 -0
  57. package/src/v1/harness-v1-bootstrap.ts +46 -0
  58. package/src/v1/harness-v1-bridge-protocol.ts +342 -0
  59. package/src/v1/harness-v1-builtin-tool.ts +138 -0
  60. package/src/v1/harness-v1-call-warning.ts +22 -0
  61. package/src/v1/harness-v1-diagnostic.ts +66 -0
  62. package/src/v1/harness-v1-lifecycle-state.ts +65 -0
  63. package/src/v1/harness-v1-metadata.ts +13 -0
  64. package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
  65. package/src/v1/harness-v1-observability.ts +20 -0
  66. package/src/v1/harness-v1-permission-mode.ts +11 -0
  67. package/src/v1/harness-v1-prompt-control.ts +41 -0
  68. package/src/v1/harness-v1-prompt.ts +11 -0
  69. package/src/v1/harness-v1-sandbox-provider.ts +76 -0
  70. package/src/v1/harness-v1-session.ts +280 -0
  71. package/src/v1/harness-v1-skill.ts +36 -0
  72. package/src/v1/harness-v1-stream-part.ts +363 -0
  73. package/src/v1/harness-v1-tool-filtering.ts +25 -0
  74. package/src/v1/harness-v1-tool-spec.ts +31 -0
  75. package/src/v1/harness-v1.ts +94 -0
  76. package/src/v1/index.ts +99 -0
  77. package/utils/index.ts +1 -0
@@ -0,0 +1,266 @@
1
+ import { posix } from 'node:path';
2
+ import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
3
+ import type { HarnessV1Bootstrap } from '../../v1';
4
+ import type { HarnessAgentSandboxConfig } from '../harness-agent-settings';
5
+ import { applyBootstrapRecipe, hashHarnessBootstrap } from './bootstrap-recipe';
6
+
7
+ const SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
8
+
9
+ type SandboxBootstrapSettings = Omit<HarnessAgentSandboxConfig, 'onSession'>;
10
+
11
+ export type SandboxBootstrapPlan = {
12
+ readonly recipe?: HarnessV1Bootstrap;
13
+ readonly recipeIdentity?: string;
14
+ readonly identity?: string;
15
+ readonly workDir?: string;
16
+ readonly onFirstCreate?: (
17
+ session: SandboxSession,
18
+ opts: { abortSignal?: AbortSignal },
19
+ ) => Promise<void>;
20
+ };
21
+
22
+ export function validateSandboxBootstrapSettings(
23
+ settings: SandboxBootstrapSettings,
24
+ ): void {
25
+ if ((settings.onBootstrap == null) !== (settings.bootstrapHash == null)) {
26
+ throw new Error(
27
+ 'HarnessAgent: `sandboxConfig.onBootstrap` and `sandboxConfig.bootstrapHash` must be provided together.',
28
+ );
29
+ }
30
+
31
+ if (settings.workDir != null) {
32
+ normalizeSandboxWorkDir(settings.workDir);
33
+ }
34
+ }
35
+
36
+ export function normalizeSandboxWorkDir(workDir: string): string {
37
+ if (workDir.length === 0) {
38
+ throw new Error('HarnessAgent: `sandboxConfig.workDir` must not be empty.');
39
+ }
40
+ if (workDir.includes('\0')) {
41
+ throw new Error(
42
+ 'HarnessAgent: `sandboxConfig.workDir` must not contain NUL.',
43
+ );
44
+ }
45
+ if (workDir.includes('\\')) {
46
+ throw new Error(
47
+ 'HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators.',
48
+ );
49
+ }
50
+ if (posix.isAbsolute(workDir)) {
51
+ throw new Error('HarnessAgent: `sandboxConfig.workDir` must be relative.');
52
+ }
53
+
54
+ const normalized = posix.normalize(workDir);
55
+ if (
56
+ normalized === '.' ||
57
+ normalized === '..' ||
58
+ normalized.startsWith('../')
59
+ ) {
60
+ throw new Error(
61
+ 'HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory.',
62
+ );
63
+ }
64
+ return normalized;
65
+ }
66
+
67
+ export function resolveSessionWorkDir({
68
+ defaultWorkingDirectory,
69
+ harnessId,
70
+ sessionId,
71
+ workDir,
72
+ }: {
73
+ readonly defaultWorkingDirectory: string;
74
+ readonly harnessId: string;
75
+ readonly sessionId: string;
76
+ readonly workDir?: string;
77
+ }): string {
78
+ return joinSandboxPath({
79
+ base: defaultWorkingDirectory,
80
+ path: workDir ?? `${harnessId}-${sessionId}`,
81
+ });
82
+ }
83
+
84
+ export async function createSandboxBootstrapPlan({
85
+ recipe,
86
+ settings,
87
+ }: {
88
+ readonly recipe?: HarnessV1Bootstrap;
89
+ readonly settings: SandboxBootstrapSettings;
90
+ }): Promise<SandboxBootstrapPlan> {
91
+ const workDir =
92
+ settings.workDir == null
93
+ ? undefined
94
+ : normalizeSandboxWorkDir(settings.workDir);
95
+ const recipeIdentity =
96
+ recipe == null ? undefined : await hashHarnessBootstrap(recipe);
97
+ const hasCallerBootstrap = settings.onBootstrap != null;
98
+ const needsCombinedIdentity =
99
+ hasCallerBootstrap || (recipeIdentity != null && workDir != null);
100
+ const identity =
101
+ needsCombinedIdentity && (recipeIdentity != null || hasCallerBootstrap)
102
+ ? await hashSandboxBootstrapIdentity({
103
+ recipeIdentity,
104
+ bootstrapHash: settings.bootstrapHash,
105
+ workDir,
106
+ })
107
+ : recipeIdentity;
108
+
109
+ return {
110
+ ...(recipe != null ? { recipe } : {}),
111
+ ...(recipeIdentity != null ? { recipeIdentity } : {}),
112
+ ...(identity != null ? { identity } : {}),
113
+ ...(workDir != null ? { workDir } : {}),
114
+ ...(recipe != null || settings.onBootstrap != null
115
+ ? {
116
+ onFirstCreate: (session, opts) =>
117
+ runSandboxBootstrap({
118
+ session,
119
+ recipe,
120
+ recipeIdentity,
121
+ workDir,
122
+ onBootstrap: settings.onBootstrap,
123
+ abortSignal: opts.abortSignal,
124
+ }),
125
+ }
126
+ : {}),
127
+ };
128
+ }
129
+
130
+ export async function runSandboxBootstrap({
131
+ session,
132
+ recipe,
133
+ recipeIdentity,
134
+ workDir,
135
+ onBootstrap,
136
+ abortSignal,
137
+ }: {
138
+ readonly session: SandboxSession;
139
+ readonly recipe?: HarnessV1Bootstrap;
140
+ readonly recipeIdentity?: string;
141
+ readonly workDir?: string;
142
+ readonly onBootstrap?: SandboxBootstrapSettings['onBootstrap'];
143
+ readonly abortSignal?: AbortSignal;
144
+ }): Promise<void> {
145
+ if (recipe != null && recipeIdentity != null) {
146
+ await applyBootstrapRecipe(session, recipe, recipeIdentity, {
147
+ abortSignal,
148
+ });
149
+ }
150
+
151
+ if (onBootstrap == null) return;
152
+
153
+ const defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
154
+ session,
155
+ abortSignal,
156
+ });
157
+ const bootstrapWorkDir =
158
+ workDir == null
159
+ ? defaultWorkingDirectory
160
+ : joinSandboxPath({
161
+ base: defaultWorkingDirectory,
162
+ path: workDir,
163
+ });
164
+
165
+ await ensureSandboxDirectory({
166
+ session,
167
+ workDir: bootstrapWorkDir,
168
+ abortSignal,
169
+ });
170
+ await onBootstrap({ session, workDir: bootstrapWorkDir, abortSignal });
171
+ }
172
+
173
+ export async function resolveDefaultWorkingDirectory({
174
+ session,
175
+ abortSignal,
176
+ }: {
177
+ readonly session: SandboxSession;
178
+ readonly abortSignal?: AbortSignal;
179
+ }): Promise<string> {
180
+ const result = await session.run({
181
+ command: 'pwd',
182
+ abortSignal,
183
+ });
184
+ if (result.exitCode !== 0) {
185
+ throw new Error(
186
+ `Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
187
+ );
188
+ }
189
+
190
+ const cwd = result.stdout.trim();
191
+ if (!posix.isAbsolute(cwd)) {
192
+ throw new Error(
193
+ `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`,
194
+ );
195
+ }
196
+ return cwd === '/' ? cwd : cwd.replace(/\/+$/, '');
197
+ }
198
+
199
+ export async function ensureSandboxDirectory({
200
+ session,
201
+ workDir,
202
+ abortSignal,
203
+ }: {
204
+ readonly session: SandboxSession;
205
+ readonly workDir: string;
206
+ readonly abortSignal?: AbortSignal;
207
+ }): Promise<void> {
208
+ const result = await session.run({
209
+ command: 'mkdir -p "$WORK_DIR"',
210
+ env: { WORK_DIR: workDir },
211
+ abortSignal,
212
+ });
213
+ if (result.exitCode !== 0) {
214
+ throw new Error(
215
+ `Failed to create sandbox work directory ${workDir} (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
216
+ );
217
+ }
218
+ }
219
+
220
+ async function hashSandboxBootstrapIdentity({
221
+ recipeIdentity,
222
+ bootstrapHash,
223
+ workDir,
224
+ }: {
225
+ readonly recipeIdentity?: string;
226
+ readonly bootstrapHash?: string;
227
+ readonly workDir?: string;
228
+ }): Promise<string> {
229
+ const encoder = new TextEncoder();
230
+ const chunks: Uint8Array[] = [];
231
+ const pushString = (value: string) => {
232
+ chunks.push(encoder.encode(value));
233
+ chunks.push(encoder.encode('\0'));
234
+ };
235
+
236
+ pushString(String(SANDBOX_BOOTSTRAP_IDENTITY_VERSION));
237
+ pushString(recipeIdentity ?? '');
238
+ pushString(bootstrapHash ?? '');
239
+ pushString(workDir ?? '');
240
+
241
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
242
+ const buffer = new Uint8Array(totalLength);
243
+ let offset = 0;
244
+ for (const chunk of chunks) {
245
+ buffer.set(chunk, offset);
246
+ offset += chunk.length;
247
+ }
248
+
249
+ const digest = await crypto.subtle.digest('SHA-256', buffer);
250
+ const bytes = new Uint8Array(digest);
251
+ let hex = '';
252
+ for (let i = 0; i < 8; i++) {
253
+ hex += bytes[i].toString(16).padStart(2, '0');
254
+ }
255
+ return hex;
256
+ }
257
+
258
+ function joinSandboxPath({
259
+ base,
260
+ path,
261
+ }: {
262
+ readonly base: string;
263
+ readonly path: string;
264
+ }): string {
265
+ return posix.join(base, path);
266
+ }
@@ -0,0 +1,68 @@
1
+ import type { HarnessV1StreamPart } from '../../v1';
2
+
3
+ /**
4
+ * Remove the session working-directory prefix from path-bearing fields of a
5
+ * stream event, returning a new event for display to consumers.
6
+ *
7
+ * Harness adapters run the agent in a per-session working directory that is a
8
+ * subdirectory of the sandbox root, and the agent's tools use absolute paths so
9
+ * they resolve against the root regardless of where the runtime process
10
+ * operates. The absolute paths are correct but noisy in a UI, so this strips
11
+ * the prefix for the consumer-facing projection only.
12
+ *
13
+ * Blanket prefix replacement (rather than rewriting known path fields) is used
14
+ * deliberately: `tool-result` results are free-form text — command stdout, grep
15
+ * output — where paths can appear anywhere and field-aware rewriting is
16
+ * impossible. The prefix is long and contains the session id, so it is unique
17
+ * enough that replacing every occurrence is safe.
18
+ */
19
+ export function stripWorkDir(
20
+ part: HarnessV1StreamPart,
21
+ sessionWorkDir: string,
22
+ ): HarnessV1StreamPart {
23
+ if (sessionWorkDir.length === 0) return part;
24
+
25
+ switch (part.type) {
26
+ case 'tool-call':
27
+ return { ...part, input: stripString(part.input, sessionWorkDir) };
28
+ case 'tool-result':
29
+ return {
30
+ ...part,
31
+ result: stripDeep(part.result, sessionWorkDir) as Extract<
32
+ HarnessV1StreamPart,
33
+ { type: 'tool-result' }
34
+ >['result'],
35
+ };
36
+ case 'file-change':
37
+ return { ...part, path: stripString(part.path, sessionWorkDir) };
38
+ default:
39
+ return part;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Replace occurrences of the working directory in a string. A reference to the
45
+ * directory followed by a separator becomes workspace-relative
46
+ * (`/work/dir/src/a.ts` → `src/a.ts`); a bare reference to the directory itself
47
+ * becomes `.`.
48
+ */
49
+ function stripString(value: string, workDir: string): string {
50
+ return value.split(`${workDir}/`).join('').split(workDir).join('.');
51
+ }
52
+
53
+ /**
54
+ * Recursively strip the working directory from every string nested in an
55
+ * arbitrary JSON-like value. Non-string leaves are returned unchanged.
56
+ */
57
+ function stripDeep(value: unknown, workDir: string): unknown {
58
+ if (typeof value === 'string') return stripString(value, workDir);
59
+ if (Array.isArray(value)) return value.map(item => stripDeep(item, workDir));
60
+ if (value !== null && typeof value === 'object') {
61
+ const out: Record<string, unknown> = {};
62
+ for (const [key, val] of Object.entries(value)) {
63
+ out[key] = stripDeep(val, workDir);
64
+ }
65
+ return out;
66
+ }
67
+ return value;
68
+ }
@@ -0,0 +1,75 @@
1
+ import type { HarnessV1PromptControl } from '../../v1/harness-v1-prompt-control';
2
+ import type { HarnessV1StreamPart } from '../../v1/harness-v1-stream-part';
3
+
4
+ /**
5
+ * Bridge an adapter's emit-based event surface into a pull-based
6
+ * `ReadableStream<HarnessV1StreamPart>`.
7
+ *
8
+ * Adapters implement `doPromptTurn` / `doContinueTurn` against an `emit` callback
9
+ * because that is the natural shape when wrapping an SDK that itself produces
10
+ * events. Consumers (notably `HarnessAgent`) prefer a stream because that is
11
+ * the idiomatic AI SDK shape. This helper converts the former into the latter
12
+ * and is agnostic to which turn entry point produced the control surface — the
13
+ * caller supplies an `invoke` thunk that wires `emit` into either method.
14
+ *
15
+ * Lifetime:
16
+ * 1. Calls `invoke(emit)` immediately (which runs `doPromptTurn`/`doContinueTurn`).
17
+ * 2. Every `emit(part)` becomes a stream chunk.
18
+ * 3. When `control.done` resolves, the stream closes — this includes a
19
+ * graceful `doSuspendTurn`, which resolves `done` cleanly after draining.
20
+ * 4. When `control.done` rejects, an `{ type: 'error', error }` part is
21
+ * enqueued and the stream is then closed normally. The rejection is
22
+ * surfaced to consumers as a discriminated-union event rather than as
23
+ * a stream error so iteration code does not need a separate try/catch
24
+ * around the consumer loop.
25
+ * 5. The supplied `abortSignal` (if any) aborts the underlying turn and
26
+ * closes the stream.
27
+ *
28
+ * The returned `control` is the same object the adapter produced and is
29
+ * intended for use by the consumer to submit tool results / approvals /
30
+ * user messages back into the in-flight turn.
31
+ */
32
+ export async function toHarnessStream(options: {
33
+ invoke: (
34
+ emit: (event: HarnessV1StreamPart) => void,
35
+ ) => PromiseLike<HarnessV1PromptControl>;
36
+ }): Promise<{
37
+ stream: ReadableStream<HarnessV1StreamPart>;
38
+ control: HarnessV1PromptControl;
39
+ }> {
40
+ let controller!: ReadableStreamDefaultController<HarnessV1StreamPart>;
41
+ let closed = false;
42
+
43
+ const stream = new ReadableStream<HarnessV1StreamPart>({
44
+ start(c) {
45
+ controller = c;
46
+ },
47
+ });
48
+
49
+ const safeEnqueue = (part: HarnessV1StreamPart) => {
50
+ if (closed) return;
51
+ controller.enqueue(part);
52
+ };
53
+
54
+ const safeClose = () => {
55
+ if (closed) return;
56
+ closed = true;
57
+ controller.close();
58
+ };
59
+
60
+ const control = await options.invoke(safeEnqueue);
61
+
62
+ Promise.resolve(control.done)
63
+ .then(
64
+ () => safeClose(),
65
+ (err: unknown) => {
66
+ safeEnqueue({ type: 'error', error: err });
67
+ safeClose();
68
+ },
69
+ )
70
+ // Belt-and-suspenders: any throw inside the handlers themselves should
71
+ // not become an unhandled rejection.
72
+ .catch(() => {});
73
+
74
+ return { stream, control };
75
+ }
@@ -0,0 +1,114 @@
1
+ import { HarnessCapabilityUnsupportedError } from '../../errors/harness-capability-unsupported-error';
2
+ import type { HarnessV1, HarnessV1BuiltinToolFiltering } from '../../v1';
3
+ import { NoSuchToolError, type ActiveTools } from 'ai';
4
+ import type { ToolSet } from '@ai-sdk/provider-utils';
5
+
6
+ export type ResolvedHarnessAgentToolFiltering<TUserTools extends ToolSet> = {
7
+ readonly activeUserTools: TUserTools;
8
+ readonly builtinToolFiltering?: HarnessV1BuiltinToolFiltering;
9
+ };
10
+
11
+ export function resolveHarnessAgentToolFiltering<
12
+ TAllTools extends ToolSet,
13
+ TUserTools extends ToolSet,
14
+ >(input: {
15
+ harness: HarnessV1;
16
+ userTools: TUserTools;
17
+ allTools: TAllTools;
18
+ activeTools: ActiveTools<TAllTools>;
19
+ inactiveTools: ActiveTools<TAllTools>;
20
+ }): ResolvedHarnessAgentToolFiltering<TUserTools> {
21
+ if (input.activeTools !== undefined && input.inactiveTools !== undefined) {
22
+ throw new Error(
23
+ 'HarnessAgent: pass either `activeTools` or `inactiveTools`, not both.',
24
+ );
25
+ }
26
+
27
+ const allToolNames = Object.keys(input.allTools);
28
+ const activeTools = dedupeToolNames({ toolNames: input.activeTools });
29
+ const inactiveTools = dedupeToolNames({ toolNames: input.inactiveTools });
30
+ validateToolNames({
31
+ requestedToolNames: activeTools ?? inactiveTools,
32
+ availableToolNames: allToolNames,
33
+ });
34
+
35
+ const userToolNames = Object.keys(input.userTools);
36
+ const activeUserToolNames =
37
+ activeTools != null
38
+ ? userToolNames.filter(name => activeTools.includes(name))
39
+ : inactiveTools != null
40
+ ? userToolNames.filter(name => !inactiveTools.includes(name))
41
+ : userToolNames;
42
+
43
+ const builtinToolNames = Object.keys(input.harness.builtinTools);
44
+ const disabledBuiltinToolNames =
45
+ activeTools != null
46
+ ? builtinToolNames.filter(name => !activeTools.includes(name))
47
+ : inactiveTools != null
48
+ ? builtinToolNames.filter(name => inactiveTools.includes(name))
49
+ : [];
50
+
51
+ const builtinToolFiltering =
52
+ disabledBuiltinToolNames.length > 0
53
+ ? activeTools != null
54
+ ? {
55
+ mode: 'allow' as const,
56
+ toolNames: builtinToolNames.filter(name =>
57
+ activeTools.includes(name),
58
+ ),
59
+ }
60
+ : { mode: 'deny' as const, toolNames: disabledBuiltinToolNames }
61
+ : undefined;
62
+
63
+ if (
64
+ builtinToolFiltering != null &&
65
+ input.harness.supportsBuiltinToolFiltering !== true &&
66
+ input.harness.supportsBuiltinToolApprovals !== true
67
+ ) {
68
+ throw new HarnessCapabilityUnsupportedError({
69
+ message: `Harness '${input.harness.harnessId}' does not support built-in tool filtering controls.`,
70
+ harnessId: input.harness.harnessId,
71
+ });
72
+ }
73
+
74
+ return {
75
+ activeUserTools: filterToolSet({
76
+ tools: input.userTools,
77
+ toolNames: activeUserToolNames,
78
+ }),
79
+ ...(builtinToolFiltering != null ? { builtinToolFiltering } : {}),
80
+ };
81
+ }
82
+
83
+ function dedupeToolNames(input: {
84
+ toolNames: ReadonlyArray<string> | undefined;
85
+ }): ReadonlyArray<string> | undefined {
86
+ return input.toolNames == null
87
+ ? undefined
88
+ : Array.from(new Set(input.toolNames));
89
+ }
90
+
91
+ function validateToolNames(input: {
92
+ requestedToolNames: ReadonlyArray<string> | undefined;
93
+ availableToolNames: ReadonlyArray<string>;
94
+ }): void {
95
+ if (input.requestedToolNames == null) return;
96
+ for (const toolName of input.requestedToolNames) {
97
+ if (!input.availableToolNames.includes(toolName)) {
98
+ throw new NoSuchToolError({
99
+ toolName,
100
+ availableTools: [...input.availableToolNames],
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ function filterToolSet<TUserTools extends ToolSet>(input: {
107
+ tools: TUserTools;
108
+ toolNames: ReadonlyArray<string>;
109
+ }): TUserTools {
110
+ const allowed = new Set(input.toolNames);
111
+ return Object.fromEntries(
112
+ Object.entries(input.tools).filter(([name]) => allowed.has(name)),
113
+ ) as TUserTools;
114
+ }