@ai-sdk/harness-pi 1.0.73 → 1.0.74
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 +7 -0
- package/dist/index.js +420 -110
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/pi-session.ts +501 -100
- package/src/pi-workspace-mirror.ts +132 -41
|
@@ -54,46 +54,137 @@ async function readCommandOutput(
|
|
|
54
54
|
command: string,
|
|
55
55
|
): Promise<string> {
|
|
56
56
|
const result = await sandbox.run({ command });
|
|
57
|
-
const output = result.stdout || result.stderr;
|
|
58
57
|
if (result.exitCode != null && result.exitCode !== 0) {
|
|
59
58
|
throw new Error(
|
|
60
|
-
|
|
59
|
+
result.stderr ||
|
|
60
|
+
result.stdout ||
|
|
61
|
+
`Sandbox command failed with exit code ${result.exitCode}`,
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
|
-
return
|
|
64
|
+
return result.stdout || result.stderr;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
async function listRemoteWorkspaceEntries(
|
|
67
68
|
sandbox: Experimental_SandboxSession,
|
|
68
69
|
sandboxWorkDir: string,
|
|
69
|
-
): Promise<{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
): Promise<{
|
|
71
|
+
directories: string[];
|
|
72
|
+
files: Array<{ relativePath: string; sandboxPath: string }>;
|
|
73
|
+
}> {
|
|
74
74
|
// Enumerate only the `.pi`/`.agents` config subtrees plus the root-level
|
|
75
|
-
// context files — never the rest of the workspace.
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
// context files — never the rest of the workspace. Use shell glob traversal
|
|
76
|
+
// instead of `find -L`, which is not supported by all sandbox shells.
|
|
77
|
+
// Resolve each scoped root path component explicitly with `readlink`,
|
|
78
|
+
// retaining both the scoped mirror path and the resolved sandbox path. Once
|
|
79
|
+
// a root is resolved, queued descendants use physical parent paths and only
|
|
80
|
+
// need resolution when the descendant itself is a symlink. This avoids
|
|
81
|
+
// relying on shells that can inspect a symlinked directory but cannot
|
|
82
|
+
// traverse paths below it, including when the symlink is an ancestor of the
|
|
83
|
+
// sandbox work directory, without repeatedly resolving every component of
|
|
84
|
+
// deep ordinary paths.
|
|
85
|
+
const scopedPaths = [
|
|
86
|
+
...PI_CONFIG_DIRS.map(dir => `./${dir}`),
|
|
87
|
+
...PI_CONTEXT_FILENAMES.map(name => `./${name}`),
|
|
88
|
+
];
|
|
84
89
|
const listCommand = [
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
`
|
|
90
|
+
`pi_config_sources=(${scopedPaths
|
|
91
|
+
.map(scopedPath =>
|
|
92
|
+
shellQuote(path.posix.join(sandboxWorkDir, scopedPath.slice(2))),
|
|
93
|
+
)
|
|
94
|
+
.join(' ')})`,
|
|
95
|
+
`pi_config_relatives=(${scopedPaths
|
|
96
|
+
.map(scopedPath => shellQuote(scopedPath.slice(2)))
|
|
97
|
+
.join(' ')})`,
|
|
98
|
+
`pi_config_ancestors=(${scopedPaths.map(() => "''").join(' ')})`,
|
|
99
|
+
`pi_config_resolve_ancestors=(${scopedPaths.map(() => '1').join(' ')})`,
|
|
100
|
+
'pi_config_index=0',
|
|
101
|
+
'while [ "$pi_config_index" -lt "${#pi_config_sources[@]}" ]; do',
|
|
102
|
+
' source=${pi_config_sources[$pi_config_index]}',
|
|
103
|
+
' relative=${pi_config_relatives[$pi_config_index]}',
|
|
104
|
+
' ancestors=${pi_config_ancestors[$pi_config_index]}',
|
|
105
|
+
' resolve_ancestors=${pi_config_resolve_ancestors[$pi_config_index]}',
|
|
106
|
+
' pi_config_index=$((pi_config_index + 1))',
|
|
107
|
+
' if [ "$resolve_ancestors" = 1 ] || [ -L "$source" ]; then',
|
|
108
|
+
' pending=$source',
|
|
109
|
+
' resolved=',
|
|
110
|
+
' seen_links=',
|
|
111
|
+
' case "$pending" in',
|
|
112
|
+
' /*) ;;',
|
|
113
|
+
' *) pending=$PWD/$pending ;;',
|
|
114
|
+
' esac',
|
|
115
|
+
' while [ -n "$pending" ]; do',
|
|
116
|
+
' pending=${pending#/}',
|
|
117
|
+
' [ -n "$pending" ] || break',
|
|
118
|
+
' component=${pending%%/*}',
|
|
119
|
+
' if [ "$pending" = "$component" ]; then',
|
|
120
|
+
' pending=',
|
|
121
|
+
' else',
|
|
122
|
+
' pending=${pending#*/}',
|
|
123
|
+
' fi',
|
|
124
|
+
' case "$component" in',
|
|
125
|
+
' ""|.) continue ;;',
|
|
126
|
+
' ..)',
|
|
127
|
+
' resolved=${resolved%/*}',
|
|
128
|
+
' continue',
|
|
129
|
+
' ;;',
|
|
130
|
+
' esac',
|
|
131
|
+
' candidate=$resolved/$component',
|
|
132
|
+
' if [ -L "$candidate" ]; then',
|
|
133
|
+
' case "$seen_links" in',
|
|
134
|
+
' *"',
|
|
135
|
+
'"$candidate"',
|
|
136
|
+
'"*)',
|
|
137
|
+
` printf 'Pi config traversal encountered a symlink cycle at %s\\n' "$candidate" >&2`,
|
|
138
|
+
' exit 1',
|
|
139
|
+
' ;;',
|
|
140
|
+
' esac',
|
|
141
|
+
' seen_links=$seen_links"',
|
|
142
|
+
'"$candidate"',
|
|
143
|
+
'"',
|
|
144
|
+
' target=$(readlink "$candidate") || exit $?',
|
|
145
|
+
' case "$target" in',
|
|
146
|
+
' /*) pending=$target${pending:+/$pending} ;;',
|
|
147
|
+
' *) pending=${candidate%/*}/$target${pending:+/$pending} ;;',
|
|
148
|
+
' esac',
|
|
149
|
+
' resolved=',
|
|
150
|
+
' else',
|
|
151
|
+
' resolved=$candidate',
|
|
152
|
+
' fi',
|
|
153
|
+
' done',
|
|
154
|
+
' if [ -z "$resolved" ]; then',
|
|
155
|
+
` printf 'Pi config traversal resolved %s to the filesystem root; skipping\\n' "$relative" >&2`,
|
|
156
|
+
' continue',
|
|
157
|
+
' fi',
|
|
158
|
+
' source=$resolved',
|
|
159
|
+
' fi',
|
|
160
|
+
' if [ -d "$source" ]; then',
|
|
161
|
+
' case "$ancestors" in',
|
|
162
|
+
' *"',
|
|
163
|
+
'"$source"',
|
|
164
|
+
'"*)',
|
|
165
|
+
` printf 'Pi config traversal encountered a symlink cycle at %s\\n' "$relative" >&2`,
|
|
166
|
+
' exit 1',
|
|
167
|
+
' ;;',
|
|
168
|
+
' esac',
|
|
169
|
+
' ancestors=$ancestors"',
|
|
170
|
+
'"$source"',
|
|
171
|
+
'"',
|
|
172
|
+
` printf 'd\\t%s\\n' "$relative"`,
|
|
173
|
+
' for child in "$source"/* "$source"/.[!.]* "$source"/..?*; do',
|
|
174
|
+
' if [ -L "$child" ] || [ -d "$child" ] || [ -f "$child" ]; then',
|
|
175
|
+
' child_name=${child##*/}',
|
|
176
|
+
' pi_config_sources+=("$child")',
|
|
177
|
+
' pi_config_relatives+=("$relative/$child_name")',
|
|
178
|
+
' pi_config_ancestors+=("$ancestors")',
|
|
179
|
+
' pi_config_resolve_ancestors+=(0)',
|
|
180
|
+
' fi',
|
|
181
|
+
' done',
|
|
182
|
+
' elif [ -f "$source" ]; then',
|
|
183
|
+
` printf 'f\\t%s\\t%s\\n' "$relative" "$source"`,
|
|
95
184
|
' fi',
|
|
96
|
-
'
|
|
185
|
+
' true',
|
|
186
|
+
'done',
|
|
187
|
+
'true',
|
|
97
188
|
].join('\n');
|
|
98
189
|
|
|
99
190
|
const output = await readCommandOutput(
|
|
@@ -102,15 +193,17 @@ async function listRemoteWorkspaceEntries(
|
|
|
102
193
|
);
|
|
103
194
|
|
|
104
195
|
const directories: string[] = [];
|
|
105
|
-
const files: string
|
|
196
|
+
const files: Array<{ relativePath: string; sandboxPath: string }> = [];
|
|
106
197
|
|
|
107
198
|
for (const line of output.split('\n').filter(Boolean)) {
|
|
108
|
-
const [kind, rawPath] = line.split('\t',
|
|
199
|
+
const [kind, rawPath, sandboxPath] = line.split('\t', 3);
|
|
109
200
|
if (!rawPath) continue;
|
|
110
201
|
|
|
111
202
|
const relativePath = normalizeRelativePath(rawPath);
|
|
112
203
|
if (kind === 'd') directories.push(relativePath);
|
|
113
|
-
else if (kind === 'f')
|
|
204
|
+
else if (kind === 'f' && sandboxPath) {
|
|
205
|
+
files.push({ relativePath, sandboxPath });
|
|
206
|
+
}
|
|
114
207
|
}
|
|
115
208
|
|
|
116
209
|
return { directories, files };
|
|
@@ -180,14 +273,14 @@ async function collectHostScopedEntries(
|
|
|
180
273
|
|
|
181
274
|
function buildRequiredDirectories(
|
|
182
275
|
remoteDirectories: string[],
|
|
183
|
-
remoteFiles: string
|
|
276
|
+
remoteFiles: Array<{ relativePath: string }>,
|
|
184
277
|
): Set<string> {
|
|
185
278
|
const directories = new Set<string>();
|
|
186
279
|
for (const directory of remoteDirectories) {
|
|
187
280
|
directories.add(normalizeRelativePath(directory));
|
|
188
281
|
}
|
|
189
282
|
for (const file of remoteFiles) {
|
|
190
|
-
let current = path.dirname(normalizeRelativePath(file));
|
|
283
|
+
let current = path.dirname(normalizeRelativePath(file.relativePath));
|
|
191
284
|
while (current !== '.' && current !== path.sep && current.length > 0) {
|
|
192
285
|
directories.add(current);
|
|
193
286
|
current = path.dirname(current);
|
|
@@ -207,7 +300,9 @@ export async function syncHostWorkspaceFromSandbox(args: {
|
|
|
207
300
|
sandboxWorkDir,
|
|
208
301
|
);
|
|
209
302
|
const hostEntries = await collectHostScopedEntries(hostWorkDir);
|
|
210
|
-
const remoteFiles = new Set(
|
|
303
|
+
const remoteFiles = new Set(
|
|
304
|
+
remoteEntries.files.map(file => file.relativePath),
|
|
305
|
+
);
|
|
211
306
|
const requiredDirectories = buildRequiredDirectories(
|
|
212
307
|
remoteEntries.directories,
|
|
213
308
|
remoteEntries.files,
|
|
@@ -235,15 +330,11 @@ export async function syncHostWorkspaceFromSandbox(args: {
|
|
|
235
330
|
await mkdir(path.join(hostWorkDir, relativePath), { recursive: true });
|
|
236
331
|
}
|
|
237
332
|
|
|
238
|
-
for (const relativePath of remoteEntries.files) {
|
|
239
|
-
const
|
|
240
|
-
sandboxWorkDir,
|
|
241
|
-
relativePath.split(path.sep).join('/'),
|
|
242
|
-
);
|
|
243
|
-
const bytes = await sandbox.readBinaryFile({ path: remotePath });
|
|
333
|
+
for (const { relativePath, sandboxPath } of remoteEntries.files) {
|
|
334
|
+
const bytes = await sandbox.readBinaryFile({ path: sandboxPath });
|
|
244
335
|
if (!bytes) {
|
|
245
336
|
throw new Error(
|
|
246
|
-
`Sandbox workspace file disappeared during mirror sync: ${
|
|
337
|
+
`Sandbox workspace file disappeared during mirror sync: ${sandboxPath}`,
|
|
247
338
|
);
|
|
248
339
|
}
|
|
249
340
|
const content = Buffer.from(bytes);
|