@ai-sdk/harness 1.0.92 → 1.0.94

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.
@@ -1,8 +1,16 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import path from 'node:path';
2
- import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
3
+ import {
4
+ safeParseJSON,
5
+ type Experimental_SandboxSession,
6
+ } from '@ai-sdk/provider-utils';
3
7
  import type { HarnessV1Skill } from '../v1';
4
8
  import { shellQuote } from './shell-quote';
5
9
 
10
+ const SKILLS_MANIFEST_FILENAME = '.ai-sdk-harness-skills.json';
11
+ const SKILLS_MANIFEST_VERSION = 1;
12
+ const SAFE_MANIFEST_SKILL_NAME = /^[A-Za-z0-9._-]+$/;
13
+
6
14
  export type SkillFilePathMode = 'relative' | 'strip-leading-slashes';
7
15
 
8
16
  export type WriteSkillsOptions = {
@@ -20,6 +28,33 @@ export type WriteSkillsOptions = {
20
28
  trailingNewline?: boolean;
21
29
  };
22
30
 
31
+ export type WriteSkillsResult = {
32
+ changed: boolean;
33
+ written: string[];
34
+ removed: string[];
35
+ unchanged: string[];
36
+ };
37
+
38
+ type ProjectedSkill = {
39
+ readonly name: string;
40
+ readonly hash: string;
41
+ readonly files: ReadonlyArray<{
42
+ readonly path: string;
43
+ readonly content: string;
44
+ }>;
45
+ };
46
+
47
+ type SkillsManifestEntry = {
48
+ readonly name: string;
49
+ readonly hash: string;
50
+ };
51
+
52
+ type SkillsManifest = {
53
+ readonly version: typeof SKILLS_MANIFEST_VERSION;
54
+ readonly state: 'complete' | 'pending';
55
+ readonly skills: ReadonlyArray<SkillsManifestEntry>;
56
+ };
57
+
23
58
  export async function writeSkills({
24
59
  sandbox,
25
60
  rootDir,
@@ -31,50 +66,365 @@ export async function writeSkills({
31
66
  invalidSkillFilePathMessage = ({ skillName, filePath }) =>
32
67
  `Invalid skill file path for ${skillName}: ${filePath}`,
33
68
  trailingNewline = false,
34
- }: WriteSkillsOptions): Promise<void> {
35
- for (const skill of skills) {
36
- validateSkillName({
37
- name: skill.name,
38
- pattern: skillNamePattern,
39
- message: invalidSkillNameMessage,
69
+ }: WriteSkillsOptions): Promise<WriteSkillsResult> {
70
+ const projectedSkills = skills
71
+ .map(skill =>
72
+ projectSkill({
73
+ skill,
74
+ skillNamePattern,
75
+ invalidSkillNameMessage,
76
+ filePathMode,
77
+ invalidSkillFilePathMessage,
78
+ trailingNewline,
79
+ }),
80
+ )
81
+ .sort((a, b) => a.name.localeCompare(b.name));
82
+ assertUniqueSkillNames(projectedSkills);
83
+
84
+ const manifestPath = path.posix.join(rootDir, SKILLS_MANIFEST_FILENAME);
85
+ const existingManifest = await readSkillsManifest({
86
+ sandbox,
87
+ manifestPath,
88
+ abortSignal,
89
+ });
90
+ const nextEntries = projectedSkills.map(({ name, hash }) => ({ name, hash }));
91
+ const nextByName = new Map(nextEntries.map(entry => [entry.name, entry]));
92
+
93
+ /*
94
+ * Delete pending-manifest directories before rewriting the requested skills;
95
+ * any listed directory may contain only part of its intended files.
96
+ */
97
+ if (existingManifest?.state === 'pending') {
98
+ await ensureSkillsDirectory({ sandbox, rootDir, abortSignal });
99
+ const recoveryNames = existingManifest.skills.map(skill => skill.name);
100
+ await removeSkillDirectories({
101
+ sandbox,
102
+ rootDir,
103
+ skillNames: recoveryNames,
104
+ abortSignal,
105
+ });
106
+ const removed = recoveryNames
107
+ .filter(name => !nextByName.has(name))
108
+ .sort((a, b) => a.localeCompare(b));
109
+ await writeProjectedSkills({
110
+ sandbox,
111
+ rootDir,
112
+ skills: projectedSkills,
113
+ abortSignal,
114
+ });
115
+ await writeSkillsManifest({
116
+ sandbox,
117
+ manifestPath,
118
+ manifest: {
119
+ version: SKILLS_MANIFEST_VERSION,
120
+ state: 'complete',
121
+ skills: nextEntries,
122
+ },
123
+ abortSignal,
40
124
  });
41
- for (const file of skill.files ?? []) {
125
+ return {
126
+ changed: recoveryNames.length > 0 || projectedSkills.length > 0,
127
+ written: projectedSkills.map(skill => skill.name),
128
+ removed,
129
+ unchanged: [],
130
+ };
131
+ }
132
+
133
+ const previousEntries = existingManifest?.skills ?? [];
134
+ const previousByName = new Map(
135
+ previousEntries.map(entry => [entry.name, entry]),
136
+ );
137
+ const removed = previousEntries
138
+ .filter(entry => !nextByName.has(entry.name))
139
+ .map(entry => entry.name)
140
+ .sort((a, b) => a.localeCompare(b));
141
+ const written = projectedSkills
142
+ .filter(skill => previousByName.get(skill.name)?.hash !== skill.hash)
143
+ .map(skill => skill.name)
144
+ .sort((a, b) => a.localeCompare(b));
145
+ const unchanged = projectedSkills
146
+ .filter(skill => previousByName.get(skill.name)?.hash === skill.hash)
147
+ .map(skill => skill.name)
148
+ .sort((a, b) => a.localeCompare(b));
149
+ const changed = removed.length > 0 || written.length > 0;
150
+
151
+ if (!changed && existingManifest != null) {
152
+ return { changed: false, written, removed, unchanged };
153
+ }
154
+
155
+ const previouslyOwned = new Set(previousEntries.map(entry => entry.name));
156
+ for (const skillName of written) {
157
+ if (previouslyOwned.has(skillName)) continue;
158
+ await assertSkillDirectoryAvailable({
159
+ sandbox,
160
+ rootDir,
161
+ skillName,
162
+ abortSignal,
163
+ });
164
+ }
165
+
166
+ await ensureSkillsDirectory({ sandbox, rootDir, abortSignal });
167
+
168
+ const pendingNames = Array.from(
169
+ new Set([
170
+ ...previousEntries.map(entry => entry.name),
171
+ ...nextEntries.map(entry => entry.name),
172
+ ]),
173
+ ).sort((a, b) => a.localeCompare(b));
174
+ await writeSkillsManifest({
175
+ sandbox,
176
+ manifestPath,
177
+ manifest: {
178
+ version: SKILLS_MANIFEST_VERSION,
179
+ state: 'pending',
180
+ skills: pendingNames.map(name => ({
181
+ name,
182
+ hash: nextByName.get(name)?.hash ?? previousByName.get(name)!.hash,
183
+ })),
184
+ },
185
+ abortSignal,
186
+ });
187
+
188
+ await removeSkillDirectories({
189
+ sandbox,
190
+ rootDir,
191
+ skillNames: [
192
+ ...removed,
193
+ ...written.filter(name => previouslyOwned.has(name)),
194
+ ],
195
+ abortSignal,
196
+ });
197
+ const writtenSet = new Set(written);
198
+ await writeProjectedSkills({
199
+ sandbox,
200
+ rootDir,
201
+ skills: projectedSkills.filter(skill => writtenSet.has(skill.name)),
202
+ abortSignal,
203
+ });
204
+ await writeSkillsManifest({
205
+ sandbox,
206
+ manifestPath,
207
+ manifest: {
208
+ version: SKILLS_MANIFEST_VERSION,
209
+ state: 'complete',
210
+ skills: nextEntries,
211
+ },
212
+ abortSignal,
213
+ });
214
+
215
+ return { changed, written, removed, unchanged };
216
+ }
217
+
218
+ async function ensureSkillsDirectory({
219
+ sandbox,
220
+ rootDir,
221
+ abortSignal,
222
+ }: {
223
+ sandbox: Experimental_SandboxSession;
224
+ rootDir: string;
225
+ abortSignal?: AbortSignal;
226
+ }): Promise<void> {
227
+ await runSandboxCommand({
228
+ sandbox,
229
+ command: `mkdir -p ${shellQuote(rootDir)}`,
230
+ abortSignal,
231
+ errorMessage: `Failed to create skills directory: ${rootDir}`,
232
+ });
233
+ }
234
+
235
+ function projectSkill({
236
+ skill,
237
+ skillNamePattern,
238
+ invalidSkillNameMessage,
239
+ filePathMode,
240
+ invalidSkillFilePathMessage,
241
+ trailingNewline,
242
+ }: {
243
+ skill: HarnessV1Skill;
244
+ skillNamePattern: RegExp;
245
+ invalidSkillNameMessage: (input: { name: string }) => string;
246
+ filePathMode: SkillFilePathMode;
247
+ invalidSkillFilePathMessage: (input: {
248
+ skillName: string;
249
+ filePath: string;
250
+ }) => string;
251
+ trailingNewline: boolean;
252
+ }): ProjectedSkill {
253
+ const name = validateSkillName({
254
+ name: skill.name,
255
+ pattern: skillNamePattern,
256
+ message: invalidSkillNameMessage,
257
+ });
258
+ const files = new Map<string, string>();
259
+ files.set('SKILL.md', renderSkillFile({ skill, trailingNewline }));
260
+ for (const file of skill.files ?? []) {
261
+ files.set(
42
262
  normalizeSkillFilePath({
43
263
  skillName: skill.name,
44
264
  filePath: file.path,
45
265
  mode: filePathMode,
46
266
  message: invalidSkillFilePathMessage,
47
- });
267
+ }),
268
+ file.content,
269
+ );
270
+ }
271
+ const projectedFiles = Array.from(files, ([filePath, content]) => ({
272
+ path: filePath,
273
+ content,
274
+ })).sort((a, b) => a.path.localeCompare(b.path));
275
+ const hash = createHash('sha256');
276
+ for (const file of projectedFiles) {
277
+ hash.update(String(Buffer.byteLength(file.path)));
278
+ hash.update(':');
279
+ hash.update(file.path);
280
+ hash.update(String(Buffer.byteLength(file.content)));
281
+ hash.update(':');
282
+ hash.update(file.content);
283
+ }
284
+ return { name, hash: hash.digest('hex'), files: projectedFiles };
285
+ }
286
+
287
+ async function readSkillsManifest({
288
+ sandbox,
289
+ manifestPath,
290
+ abortSignal,
291
+ }: {
292
+ sandbox: Experimental_SandboxSession;
293
+ manifestPath: string;
294
+ abortSignal?: AbortSignal;
295
+ }): Promise<SkillsManifest | undefined> {
296
+ const content = await sandbox.readTextFile({
297
+ path: manifestPath,
298
+ abortSignal,
299
+ });
300
+ if (content == null) return undefined;
301
+ const parsed = await safeParseJSON({ text: content });
302
+ if (!parsed.success || !isSkillsManifest(parsed.value)) {
303
+ throw new Error(`Invalid AI SDK harness skills manifest: ${manifestPath}`);
304
+ }
305
+ return parsed.value;
306
+ }
307
+
308
+ function isSkillsManifest(value: unknown): value is SkillsManifest {
309
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
310
+ return false;
311
+ }
312
+ const manifest = value as Record<string, unknown>;
313
+ if (
314
+ manifest.version !== SKILLS_MANIFEST_VERSION ||
315
+ (manifest.state !== 'complete' && manifest.state !== 'pending') ||
316
+ !Array.isArray(manifest.skills)
317
+ ) {
318
+ return false;
319
+ }
320
+ const names = new Set<string>();
321
+ for (const entry of manifest.skills) {
322
+ if (entry == null || typeof entry !== 'object' || Array.isArray(entry)) {
323
+ return false;
48
324
  }
325
+ const skill = entry as Record<string, unknown>;
326
+ if (
327
+ typeof skill.name !== 'string' ||
328
+ !isSafeManifestSkillName(skill.name) ||
329
+ typeof skill.hash !== 'string' ||
330
+ !/^[a-f0-9]{64}$/.test(skill.hash) ||
331
+ names.has(skill.name)
332
+ ) {
333
+ return false;
334
+ }
335
+ names.add(skill.name);
49
336
  }
337
+ return true;
338
+ }
50
339
 
51
- await sandbox.run({
52
- command: `mkdir -p ${shellQuote(rootDir)}`,
340
+ async function writeSkillsManifest({
341
+ sandbox,
342
+ manifestPath,
343
+ manifest,
344
+ abortSignal,
345
+ }: {
346
+ sandbox: Experimental_SandboxSession;
347
+ manifestPath: string;
348
+ manifest: SkillsManifest;
349
+ abortSignal?: AbortSignal;
350
+ }): Promise<void> {
351
+ const temporaryPath = `${manifestPath}.tmp`;
352
+ await sandbox.writeTextFile({
353
+ path: temporaryPath,
354
+ content: `${JSON.stringify(manifest, null, 2)}\n`,
355
+ abortSignal,
356
+ });
357
+ await runSandboxCommand({
358
+ sandbox,
359
+ command: `mv -f ${shellQuote(temporaryPath)} ${shellQuote(manifestPath)}`,
53
360
  abortSignal,
361
+ errorMessage: `Failed to update skills manifest: ${manifestPath}`,
54
362
  });
363
+ }
55
364
 
56
- for (const skill of skills) {
57
- const name = validateSkillName({
58
- name: skill.name,
59
- pattern: skillNamePattern,
60
- message: invalidSkillNameMessage,
61
- });
62
- const skillDir = path.posix.join(rootDir, name);
63
- await sandbox.writeTextFile({
64
- path: path.posix.join(skillDir, 'SKILL.md'),
65
- content: renderSkillFile({ skill, trailingNewline }),
66
- abortSignal,
67
- });
365
+ async function assertSkillDirectoryAvailable({
366
+ sandbox,
367
+ rootDir,
368
+ skillName,
369
+ abortSignal,
370
+ }: {
371
+ sandbox: Experimental_SandboxSession;
372
+ rootDir: string;
373
+ skillName: string;
374
+ abortSignal?: AbortSignal;
375
+ }): Promise<void> {
376
+ const skillDir = path.posix.join(rootDir, skillName);
377
+ const result = await sandbox.run({
378
+ command: `test ! -e ${shellQuote(skillDir)}`,
379
+ abortSignal,
380
+ });
381
+ if (result.exitCode !== 0) {
382
+ throw new Error(
383
+ `Cannot write harness skill '${skillName}': ${skillDir} already exists and is not owned by the AI SDK harness.`,
384
+ );
385
+ }
386
+ }
68
387
 
69
- for (const file of skill.files ?? []) {
70
- const filePath = normalizeSkillFilePath({
71
- skillName: skill.name,
72
- filePath: file.path,
73
- mode: filePathMode,
74
- message: invalidSkillFilePathMessage,
75
- });
388
+ async function removeSkillDirectories({
389
+ sandbox,
390
+ rootDir,
391
+ skillNames,
392
+ abortSignal,
393
+ }: {
394
+ sandbox: Experimental_SandboxSession;
395
+ rootDir: string;
396
+ skillNames: ReadonlyArray<string>;
397
+ abortSignal?: AbortSignal;
398
+ }): Promise<void> {
399
+ if (skillNames.length === 0) return;
400
+ const directories = Array.from(new Set(skillNames))
401
+ .sort((a, b) => a.localeCompare(b))
402
+ .map(name => shellQuote(path.posix.join(rootDir, name)))
403
+ .join(' ');
404
+ await runSandboxCommand({
405
+ sandbox,
406
+ command: `rm -rf -- ${directories}`,
407
+ abortSignal,
408
+ errorMessage: `Failed to replace harness skills in: ${rootDir}`,
409
+ });
410
+ }
411
+
412
+ async function writeProjectedSkills({
413
+ sandbox,
414
+ rootDir,
415
+ skills,
416
+ abortSignal,
417
+ }: {
418
+ sandbox: Experimental_SandboxSession;
419
+ rootDir: string;
420
+ skills: ReadonlyArray<ProjectedSkill>;
421
+ abortSignal?: AbortSignal;
422
+ }): Promise<void> {
423
+ for (const skill of skills) {
424
+ const skillDir = path.posix.join(rootDir, skill.name);
425
+ for (const file of skill.files) {
76
426
  await sandbox.writeTextFile({
77
- path: path.posix.join(skillDir, filePath),
427
+ path: path.posix.join(skillDir, file.path),
78
428
  content: file.content,
79
429
  abortSignal,
80
430
  });
@@ -82,6 +432,40 @@ export async function writeSkills({
82
432
  }
83
433
  }
84
434
 
435
+ async function runSandboxCommand({
436
+ sandbox,
437
+ command,
438
+ abortSignal,
439
+ errorMessage,
440
+ }: {
441
+ sandbox: Experimental_SandboxSession;
442
+ command: string;
443
+ abortSignal?: AbortSignal;
444
+ errorMessage: string;
445
+ }): Promise<void> {
446
+ const result = await sandbox.run({ command, abortSignal });
447
+ if (result.exitCode !== 0) {
448
+ throw new Error(
449
+ `${errorMessage} (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ''}`,
450
+ );
451
+ }
452
+ }
453
+
454
+ function assertUniqueSkillNames(skills: ReadonlyArray<ProjectedSkill>): void {
455
+ for (let index = 1; index < skills.length; index++) {
456
+ if (skills[index - 1]!.name === skills[index]!.name) {
457
+ throw new Error(`Duplicate skill name: ${skills[index]!.name}`);
458
+ }
459
+ }
460
+ }
461
+
462
+ function isSafeManifestSkillName(name: string): boolean {
463
+ SAFE_MANIFEST_SKILL_NAME.lastIndex = 0;
464
+ const matches = SAFE_MANIFEST_SKILL_NAME.test(name);
465
+ SAFE_MANIFEST_SKILL_NAME.lastIndex = 0;
466
+ return matches && name !== '.' && name !== '..' && !name.includes('/');
467
+ }
468
+
85
469
  function validateSkillName({
86
470
  name,
87
471
  pattern,
@@ -91,7 +475,10 @@ function validateSkillName({
91
475
  pattern: RegExp;
92
476
  message?: (input: { name: string }) => string;
93
477
  }): string {
94
- if (!pattern.test(name) || name === '.' || name === '..') {
478
+ pattern.lastIndex = 0;
479
+ const matches = pattern.test(name);
480
+ pattern.lastIndex = 0;
481
+ if (!matches || name === '.' || name === '..') {
95
482
  throw new Error(message?.({ name }) ?? `Invalid skill name: ${name}`);
96
483
  }
97
484
  return name;
@@ -21,6 +21,9 @@ import {
21
21
  harnessV1TextStartPartSchema,
22
22
  harnessV1ToolApprovalRequestPartSchema,
23
23
  harnessV1ToolCallPartSchema,
24
+ harnessV1ToolInputDeltaPartSchema,
25
+ harnessV1ToolInputEndPartSchema,
26
+ harnessV1ToolInputStartPartSchema,
24
27
  harnessV1ToolResultPartSchema,
25
28
  } from './harness-v1-stream-part';
26
29
 
@@ -213,6 +216,9 @@ export const harnessV1BridgeOutboundMessageSchema = z.discriminatedUnion(
213
216
  harnessV1ReasoningStartPartSchema,
214
217
  harnessV1ReasoningDeltaPartSchema,
215
218
  harnessV1ReasoningEndPartSchema,
219
+ harnessV1ToolInputStartPartSchema,
220
+ harnessV1ToolInputDeltaPartSchema,
221
+ harnessV1ToolInputEndPartSchema,
216
222
  harnessV1ToolCallPartSchema,
217
223
  harnessV1ToolApprovalRequestPartSchema,
218
224
  harnessV1ToolResultPartSchema,
@@ -1,4 +1,6 @@
1
1
  import type { JSONValue } from '@ai-sdk/provider';
2
+ import type { HarnessV1Skill } from './harness-v1-skill';
3
+ import type { HarnessV1ToolSpec } from './harness-v1-tool-spec';
2
4
 
3
5
  export type HarnessV1PendingToolApproval = {
4
6
  readonly approvalId: string;
@@ -16,6 +18,42 @@ export type HarnessV1PendingToolResult = {
16
18
  readonly input: string;
17
19
  };
18
20
 
21
+ /**
22
+ * Framework-owned settings captured when a turn begins. The same settings are
23
+ * passed to fresh and continued turns and persisted with unfinished-turn state
24
+ * so a resumed continuation cannot pick up configuration from a later turn.
25
+ */
26
+ export type HarnessV1TurnSettings = {
27
+ /**
28
+ * Model identifier selected for this turn. Adapters interpret this value
29
+ * according to the underlying harness runtime. Rerun-based continuations
30
+ * reuse it when reconstructing the turn.
31
+ */
32
+ readonly model?: string;
33
+
34
+ /**
35
+ * Skills made available to the underlying runtime for this turn. Adapters
36
+ * must replace skills from the preceding completed turn before starting a
37
+ * fresh turn. Rerun-based continuations use them to reconstruct the turn.
38
+ */
39
+ readonly skills: ReadonlyArray<HarnessV1Skill>;
40
+
41
+ /**
42
+ * Free-form instructions for this turn. Adapters should apply them through
43
+ * the runtime's native system or developer instruction mechanism when
44
+ * supported. Rerun-based continuations use them to reconstruct the turn.
45
+ */
46
+ readonly instructions?: string;
47
+
48
+ /**
49
+ * Host-defined tools made available to the underlying runtime for this turn.
50
+ * The harness emits `tool-call` events when the runtime calls one and waits
51
+ * for `submitToolResult`. Rerun-based continuations use them to reconstruct
52
+ * the turn.
53
+ */
54
+ readonly tools: ReadonlyArray<HarnessV1ToolSpec>;
55
+ };
56
+
19
57
  type HarnessV1LifecycleStateBase = {
20
58
  /**
21
59
  * Identifier of the harness that produced this state. Used by adapters to
@@ -70,6 +108,13 @@ export type HarnessV1ContinueTurnState = HarnessV1LifecycleStateBase & {
70
108
  * result before the underlying turn can continue.
71
109
  */
72
110
  readonly pendingToolResults?: readonly HarnessV1PendingToolResult[];
111
+
112
+ /**
113
+ * Framework-owned settings captured when the unfinished turn began. They
114
+ * are persisted outside adapter data so a resumed continuation cannot pick
115
+ * up settings prepared for a later turn.
116
+ */
117
+ readonly turnSettings?: HarnessV1TurnSettings;
73
118
  };
74
119
 
75
120
  export type HarnessV1LifecycleState =
@@ -8,10 +8,9 @@ import type { HarnessV1ResponseFormat } from './harness-v1-response-format';
8
8
  import type {
9
9
  HarnessV1ContinueTurnState,
10
10
  HarnessV1ResumeSessionState,
11
+ HarnessV1TurnSettings,
11
12
  } from './harness-v1-lifecycle-state';
12
- import type { HarnessV1Skill } from './harness-v1-skill';
13
13
  import type { HarnessV1StreamPart } from './harness-v1-stream-part';
14
- import type { HarnessV1ToolSpec } from './harness-v1-tool-spec';
15
14
  import type { HarnessV1BuiltinToolFiltering } from './harness-v1-tool-filtering';
16
15
 
17
16
  /**
@@ -29,12 +28,6 @@ export type HarnessV1StartOptions = {
29
28
  */
30
29
  readonly sessionId: string;
31
30
 
32
- /**
33
- * Skills made available to the underlying runtime for the lifetime of
34
- * the session. Adapters decide how to surface them.
35
- */
36
- readonly skills?: ReadonlyArray<HarnessV1Skill>;
37
-
38
31
  /**
39
32
  * Optional resume payload returned by a prior session lifecycle method. When
40
33
  * provided, the adapter should resume the existing session before accepting a
@@ -95,7 +88,7 @@ export type HarnessV1StartOptions = {
95
88
  /**
96
89
  * Options passed to `HarnessV1Session.doPromptTurn`.
97
90
  */
98
- export type HarnessV1PromptTurnOptions = {
91
+ export type HarnessV1PromptTurnOptions = HarnessV1TurnSettings & {
99
92
  /**
100
93
  * Fresh input for this turn — either a plain string or a single
101
94
  * `ModelMessage`. The harness session owns its own conversation history,
@@ -109,22 +102,6 @@ export type HarnessV1PromptTurnOptions = {
109
102
  */
110
103
  readonly responseFormat?: HarnessV1ResponseFormat;
111
104
 
112
- /**
113
- * Host-defined tools to make available to the underlying runtime for this
114
- * turn. The harness emits `tool-call` events when the runtime calls one
115
- * and waits for `submitToolResult`.
116
- */
117
- readonly tools?: ReadonlyArray<HarnessV1ToolSpec>;
118
-
119
- /**
120
- * Free-form instructions for the session. The framework supplies the same
121
- * value on every turn. Adapters should append it to the runtime's native
122
- * system or developer prompt when supported. Otherwise, they should prepend
123
- * it to the first user message of a fresh session and rely on the runtime's
124
- * persisted history when resuming.
125
- */
126
- readonly instructions?: string;
127
-
128
105
  /**
129
106
  * Signal that aborts the in-flight turn. The adapter must cancel any
130
107
  * underlying work and resolve `done` (with an error if appropriate).
@@ -147,27 +124,13 @@ export type HarnessV1PromptTurnOptions = {
147
124
  * in-flight turn rather than starting a new one. It is used to continue a turn
148
125
  * that was previously suspended temporarily, e.g. by the workflow slice loop.
149
126
  */
150
- export type HarnessV1ContinueTurnOptions = {
127
+ export type HarnessV1ContinueTurnOptions = HarnessV1TurnSettings & {
151
128
  /**
152
129
  * Response format of the in-flight turn. Rerun-based adapters use this when
153
130
  * reconstructing the turn; attach-based adapters may ignore it.
154
131
  */
155
132
  readonly responseFormat?: HarnessV1ResponseFormat;
156
133
 
157
- /**
158
- * Host-defined tools to make available for the continued turn. Same shape
159
- * as `doPromptTurn`'s `tools`. An adapter that purely attaches to a live turn
160
- * may ignore them; an adapter that re-drives the turn (rerun) needs them.
161
- */
162
- readonly tools?: ReadonlyArray<HarnessV1ToolSpec>;
163
-
164
- /**
165
- * Free-form session instructions. An adapter that re-drives the runtime may
166
- * need these to reconstruct its native system or developer prompt. An
167
- * adapter that attaches to a live turn may ignore them.
168
- */
169
- readonly instructions?: string;
170
-
171
134
  /**
172
135
  * Signal that aborts the continued turn. The adapter must cancel any
173
136
  * underlying work and resolve `done` (with an error if appropriate).
@@ -202,14 +165,6 @@ export type HarnessV1Session = {
202
165
  */
203
166
  readonly isResume: boolean;
204
167
 
205
- /**
206
- * The model id the underlying runtime is configured to use, if the adapter
207
- * knows it (e.g. from its settings). Surfaced into telemetry as
208
- * `gen_ai.request.model` and the trace span labels. Omitted when the adapter
209
- * defers to the runtime's own default and has no concrete id.
210
- */
211
- readonly modelId?: string;
212
-
213
168
  /**
214
169
  * Run one prompt turn. Returns a control handle the host uses to feed
215
170
  * tool results, approvals, and user messages back into the turn while it