@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.
- package/CHANGELOG.md +29 -0
- package/README.md +5 -1
- package/dist/agent/index.d.ts +166 -118
- package/dist/agent/index.js +399 -102
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.d.ts +20 -1
- package/dist/bridge/index.js +29 -5
- package/dist/bridge/index.js.map +1 -1
- package/dist/index.d.ts +142 -102
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +8 -2
- package/dist/utils/index.js +339 -33
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -5
- package/src/agent/harness-agent-session.ts +115 -7
- package/src/agent/harness-agent-settings.ts +87 -7
- package/src/agent/harness-agent.ts +328 -112
- package/src/agent/internal/lifecycle-state-validation.ts +3 -0
- package/src/agent/internal/run-prompt.ts +49 -11
- package/src/agent/internal/strip-work-dir.ts +102 -0
- package/src/agent/internal/translate-stream-part.ts +5 -0
- package/src/bridge/index.ts +73 -6
- package/src/utils/index.ts +1 -0
- package/src/utils/write-skills.ts +419 -32
- package/src/v1/harness-v1-bridge-protocol.ts +6 -0
- package/src/v1/harness-v1-lifecycle-state.ts +45 -0
- package/src/v1/harness-v1-session.ts +3 -48
- package/src/v1/harness-v1-stream-part.ts +31 -1
- package/src/v1/index.ts +1 -0
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import path from 'node:path';
|
|
2
|
-
import
|
|
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<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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,
|
|
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
|
-
|
|
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
|