@flowdular/sandbox 0.3.2 → 0.4.1
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/internal/coding-agent/src/drivers/byok.ts +16 -1
- package/internal/coding-agent/src/index.ts +1 -0
- package/internal/coding-agent/src/roles/defaults.ts +9 -0
- package/internal/coding-agent/src/roles/skills.ts +1 -0
- package/internal/coding-agent/src/types.ts +11 -0
- package/package.json +2 -2
- package/src/App.tsrx +77 -56
- package/src/client/DiffPane.tsrx +3 -7
- package/src/client/EjectModal.tsrx +7 -9
- package/src/client/GitHubSettingsModal.tsrx +8 -8
- package/src/client/ModelSettingsModal.tsrx +5 -5
- package/src/client/NewSessionForm.tsrx +1 -1
- package/src/client/PendingQuestionsCard.tsrx +1 -1
- package/src/client/SessionPicker.tsrx +49 -36
- package/src/client/SpecEditor.tsrx +1 -1
- package/src/client/locales/en.json +30 -16
- package/src/client/locales/pl.json +78 -28
- package/src/server/delivery/configuration.ts +16 -0
- package/src/server/delivery/git-pr.ts +51 -22
- package/src/server/delivery/types.ts +2 -0
- package/src/server/gates.ts +34 -8
- package/src/server/preview-modules.ts +43 -0
- package/src/server/preview-revision.ts +1 -0
- package/src/server/preview-runtime.ts +41 -4
- package/src/server/preview-seed.ts +115 -0
- package/src/server/recorded-adapters.ts +132 -0
- package/src/server/sample-data.ts +403 -0
- package/src/server/turns.ts +16 -1
- package/dist/client/assets/App-ClMOrpyg.js +0 -11
- package/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
- package/dist/client/assets/auto-buGEq6de.js +0 -3
- package/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- package/dist/client/assets/index-CvwhlQ87.js +0 -4
- package/dist/client/assets/styles-BPWaGf55.js +0 -3
- package/dist/client/assets/styles-Dx1_KOag.css +0 -1
- package/dist/client/favicon.svg +0 -1
- package/dist/server/assets/auto-CVGNYyb3.js +0 -12785
- package/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
- package/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
- package/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
- package/dist/server/assets/token-Bvaq7Anz.js +0 -55
- package/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
- package/dist/server/assets/token-util-CktFFT4z.js +0 -494
- package/dist/server/entry.js +0 -91671
- package/dist/server/favicon.svg +0 -1
- package/dist/server/index.html +0 -161
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { lstat, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { CodingAgentTool } from '#coding-agent';
|
|
4
|
+
import { MAX_ATTACHMENT_BYTES } from './attachments.ts';
|
|
5
|
+
import type { SessionAttachment } from './sessions.ts';
|
|
6
|
+
import { SandboxSetupError } from './workspace-root.ts';
|
|
7
|
+
|
|
8
|
+
export const SAMPLE_DATA_TOOL = 'sample-data';
|
|
9
|
+
export const SAMPLE_DATA_ROWS = 20;
|
|
10
|
+
export const SAMPLE_DATA_REFERENCE = 'reference/sample-data.json';
|
|
11
|
+
const MAX_COLUMNS = 40;
|
|
12
|
+
const MAX_CELL = 200;
|
|
13
|
+
const MAX_JSON_DEPTH = 4;
|
|
14
|
+
export const MAX_SAMPLE_PREVIEW_BYTES = 32 * 1024;
|
|
15
|
+
export const MAX_SAMPLE_LISTING_BYTES = 64 * 1024;
|
|
16
|
+
|
|
17
|
+
export type SampleDataFormat = 'csv' | 'json' | 'text';
|
|
18
|
+
|
|
19
|
+
const FORMATS: Readonly<Record<string, SampleDataFormat>> = {
|
|
20
|
+
csv: 'csv',
|
|
21
|
+
json: 'json',
|
|
22
|
+
txt: 'text',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface SampleDataPreview {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly format: SampleDataFormat;
|
|
28
|
+
readonly size: number;
|
|
29
|
+
/* Data rows without the CSV header, items of the JSON collection, or lines. */
|
|
30
|
+
readonly rowCount: number;
|
|
31
|
+
readonly columns: readonly string[];
|
|
32
|
+
readonly rows: readonly unknown[];
|
|
33
|
+
/* True when a row, column or value was left out of the preview. */
|
|
34
|
+
readonly truncated: boolean;
|
|
35
|
+
readonly delimiter?: string;
|
|
36
|
+
/* Where the JSON rows came from: `$` for a top-level array, `$.key` otherwise. */
|
|
37
|
+
readonly path?: string;
|
|
38
|
+
readonly note?: string;
|
|
39
|
+
readonly error?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SampleDataListing {
|
|
43
|
+
readonly sampleData: readonly SampleDataPreview[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sampleDataFormat(name: string): SampleDataFormat | null {
|
|
47
|
+
const dot = name.lastIndexOf('.');
|
|
48
|
+
return dot > 0 ? (FORMATS[name.slice(dot + 1).toLowerCase()] ?? null) : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sampleDataAttachments(
|
|
52
|
+
attachments: readonly SessionAttachment[],
|
|
53
|
+
): readonly SessionAttachment[] {
|
|
54
|
+
return attachments.filter(
|
|
55
|
+
(item) => item.kind === 'file' && sampleDataFormat(item.name) !== null,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cell(value: string): { value: string; cut: boolean } {
|
|
60
|
+
return value.length > MAX_CELL
|
|
61
|
+
? { value: `${value.slice(0, MAX_CELL)}...`, cut: true }
|
|
62
|
+
: { value, cut: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function delimiterOf(text: string): string {
|
|
66
|
+
const counts = new Map([
|
|
67
|
+
[',', 0],
|
|
68
|
+
[';', 0],
|
|
69
|
+
['\t', 0],
|
|
70
|
+
]);
|
|
71
|
+
let quoted = false;
|
|
72
|
+
for (const character of text) {
|
|
73
|
+
if (character === '"') quoted = !quoted;
|
|
74
|
+
else if (!quoted && (character === '\n' || character === '\r')) break;
|
|
75
|
+
else if (!quoted && counts.has(character))
|
|
76
|
+
counts.set(character, counts.get(character)! + 1);
|
|
77
|
+
}
|
|
78
|
+
let best = ',';
|
|
79
|
+
for (const [candidate, count] of counts)
|
|
80
|
+
if (count > counts.get(best)!) best = candidate;
|
|
81
|
+
return best;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* RFC 4180 with a detected delimiter: quoted fields may hold delimiters, line
|
|
85
|
+
breaks and doubled quotes. Every record is counted; only the header and the
|
|
86
|
+
first rows are kept. */
|
|
87
|
+
function previewCsv(
|
|
88
|
+
text: string,
|
|
89
|
+
): Pick<
|
|
90
|
+
SampleDataPreview,
|
|
91
|
+
'rowCount' | 'columns' | 'rows' | 'truncated' | 'delimiter'
|
|
92
|
+
> {
|
|
93
|
+
const delimiter = delimiterOf(text);
|
|
94
|
+
const kept: string[][] = [];
|
|
95
|
+
let records = 0;
|
|
96
|
+
let record: string[] = [];
|
|
97
|
+
let field = '';
|
|
98
|
+
let quoted = false;
|
|
99
|
+
let truncated = false;
|
|
100
|
+
const endField = () => {
|
|
101
|
+
record.push(field);
|
|
102
|
+
field = '';
|
|
103
|
+
};
|
|
104
|
+
const endRecord = () => {
|
|
105
|
+
endField();
|
|
106
|
+
const blank = record.length === 1 && record[0] === '';
|
|
107
|
+
if (!blank) {
|
|
108
|
+
if (kept.length <= SAMPLE_DATA_ROWS) kept.push(record);
|
|
109
|
+
records += 1;
|
|
110
|
+
}
|
|
111
|
+
record = [];
|
|
112
|
+
};
|
|
113
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
114
|
+
const character = text[index]!;
|
|
115
|
+
if (quoted) {
|
|
116
|
+
if (character !== '"') field += character;
|
|
117
|
+
else if (text[index + 1] === '"') {
|
|
118
|
+
field += '"';
|
|
119
|
+
index += 1;
|
|
120
|
+
} else quoted = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (character === '"' && field === '') quoted = true;
|
|
124
|
+
else if (character === delimiter) endField();
|
|
125
|
+
else if (character === '\n') endRecord();
|
|
126
|
+
else if (character !== '\r') field += character;
|
|
127
|
+
}
|
|
128
|
+
if (field !== '' || record.length > 0) endRecord();
|
|
129
|
+
const bound = (values: readonly string[]) => {
|
|
130
|
+
if (values.length > MAX_COLUMNS) truncated = true;
|
|
131
|
+
return values.slice(0, MAX_COLUMNS).map((value) => {
|
|
132
|
+
const bounded = cell(value);
|
|
133
|
+
if (bounded.cut) truncated = true;
|
|
134
|
+
return bounded.value;
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
const [header = [], ...rows] = kept;
|
|
138
|
+
const rowCount = Math.max(0, records - 1);
|
|
139
|
+
if (rowCount > rows.length) truncated = true;
|
|
140
|
+
return {
|
|
141
|
+
delimiter,
|
|
142
|
+
columns: bound(header),
|
|
143
|
+
rows: rows.map(bound),
|
|
144
|
+
rowCount,
|
|
145
|
+
truncated,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function boundJson(
|
|
150
|
+
value: unknown,
|
|
151
|
+
depth: number,
|
|
152
|
+
flag: { truncated: boolean },
|
|
153
|
+
): unknown {
|
|
154
|
+
if (typeof value === 'string') {
|
|
155
|
+
const bounded = cell(value);
|
|
156
|
+
if (bounded.cut) flag.truncated = true;
|
|
157
|
+
return bounded.value;
|
|
158
|
+
}
|
|
159
|
+
if (value === null || typeof value !== 'object') return value;
|
|
160
|
+
if (depth >= MAX_JSON_DEPTH) {
|
|
161
|
+
flag.truncated = true;
|
|
162
|
+
return Array.isArray(value) ? '[array]' : '[object]';
|
|
163
|
+
}
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
if (value.length > SAMPLE_DATA_ROWS) flag.truncated = true;
|
|
166
|
+
return value
|
|
167
|
+
.slice(0, SAMPLE_DATA_ROWS)
|
|
168
|
+
.map((item) => boundJson(item, depth + 1, flag));
|
|
169
|
+
}
|
|
170
|
+
const entries = Object.entries(value);
|
|
171
|
+
if (entries.length > MAX_COLUMNS) flag.truncated = true;
|
|
172
|
+
return Object.fromEntries(
|
|
173
|
+
entries
|
|
174
|
+
.slice(0, MAX_COLUMNS)
|
|
175
|
+
.map(([key, item]) => [key, boundJson(item, depth + 1, flag)]),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function previewJson(
|
|
180
|
+
text: string,
|
|
181
|
+
): Pick<
|
|
182
|
+
SampleDataPreview,
|
|
183
|
+
'rowCount' | 'columns' | 'rows' | 'truncated' | 'path' | 'error'
|
|
184
|
+
> {
|
|
185
|
+
let parsed: unknown;
|
|
186
|
+
try {
|
|
187
|
+
parsed = JSON.parse(text);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return {
|
|
190
|
+
rowCount: 0,
|
|
191
|
+
columns: [],
|
|
192
|
+
rows: [],
|
|
193
|
+
truncated: false,
|
|
194
|
+
error: `The file is not valid JSON: ${(error as Error).message.slice(0, 200)}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
let path = '$';
|
|
198
|
+
let collection: readonly unknown[] = [parsed];
|
|
199
|
+
if (Array.isArray(parsed)) collection = parsed;
|
|
200
|
+
else if (parsed && typeof parsed === 'object') {
|
|
201
|
+
const found = Object.entries(parsed).find(([, value]) =>
|
|
202
|
+
Array.isArray(value),
|
|
203
|
+
);
|
|
204
|
+
if (found) {
|
|
205
|
+
path = `$.${found[0]}`;
|
|
206
|
+
collection = found[1] as unknown[];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const flag = { truncated: collection.length > SAMPLE_DATA_ROWS };
|
|
210
|
+
const items = collection.slice(0, SAMPLE_DATA_ROWS);
|
|
211
|
+
const columns = new Set<string>();
|
|
212
|
+
for (const item of items) {
|
|
213
|
+
if (item && typeof item === 'object' && !Array.isArray(item))
|
|
214
|
+
for (const key of Object.keys(item)) columns.add(key);
|
|
215
|
+
}
|
|
216
|
+
if (columns.size > MAX_COLUMNS) flag.truncated = true;
|
|
217
|
+
return {
|
|
218
|
+
path,
|
|
219
|
+
rowCount: collection.length,
|
|
220
|
+
columns: [...columns]
|
|
221
|
+
.slice(0, MAX_COLUMNS)
|
|
222
|
+
.map((column) => cell(column).value),
|
|
223
|
+
rows: items.map((item) => boundJson(item, 1, flag)),
|
|
224
|
+
truncated: flag.truncated,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function previewText(
|
|
229
|
+
text: string,
|
|
230
|
+
): Pick<SampleDataPreview, 'rowCount' | 'columns' | 'rows' | 'truncated'> {
|
|
231
|
+
const lines = text.split(/\r?\n/);
|
|
232
|
+
if (lines.at(-1) === '') lines.pop();
|
|
233
|
+
let truncated = lines.length > SAMPLE_DATA_ROWS;
|
|
234
|
+
return {
|
|
235
|
+
rowCount: lines.length,
|
|
236
|
+
columns: [],
|
|
237
|
+
rows: lines.slice(0, SAMPLE_DATA_ROWS).map((line) => {
|
|
238
|
+
const bounded = cell(line);
|
|
239
|
+
if (bounded.cut) truncated = true;
|
|
240
|
+
return bounded.value;
|
|
241
|
+
}),
|
|
242
|
+
truncated,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function byteLength(value: unknown): number {
|
|
247
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* A preview stays under its byte budget by dropping trailing rows, so a wide
|
|
251
|
+
file still answers with its columns and as many rows as fit. */
|
|
252
|
+
export function previewSampleData(
|
|
253
|
+
name: string,
|
|
254
|
+
text: string,
|
|
255
|
+
): SampleDataPreview {
|
|
256
|
+
const format = sampleDataFormat(name);
|
|
257
|
+
if (!format) {
|
|
258
|
+
throw new SandboxSetupError(
|
|
259
|
+
'SAMPLE_DATA_NOT_FOUND',
|
|
260
|
+
`${name} is not a CSV, JSON or text attachment.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const source = text.startsWith('\uFEFF') ? text.slice(1) : text;
|
|
264
|
+
const parsed =
|
|
265
|
+
format === 'csv'
|
|
266
|
+
? previewCsv(source)
|
|
267
|
+
: format === 'json'
|
|
268
|
+
? previewJson(source)
|
|
269
|
+
: previewText(source);
|
|
270
|
+
const preview: SampleDataPreview = {
|
|
271
|
+
name,
|
|
272
|
+
format,
|
|
273
|
+
size: Buffer.byteLength(text, 'utf8'),
|
|
274
|
+
...parsed,
|
|
275
|
+
};
|
|
276
|
+
const rows = [...preview.rows];
|
|
277
|
+
let bounded = preview;
|
|
278
|
+
while (rows.length > 0 && byteLength(bounded) > MAX_SAMPLE_PREVIEW_BYTES) {
|
|
279
|
+
rows.pop();
|
|
280
|
+
bounded = { ...preview, rows: [...rows], truncated: true };
|
|
281
|
+
}
|
|
282
|
+
return bounded;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Reads the turn snapshot under reference/attachments, never the private store,
|
|
286
|
+
so the tool answers what the agent can already open and nothing newer. */
|
|
287
|
+
async function readSnapshot(
|
|
288
|
+
workspace: string,
|
|
289
|
+
attachment: SessionAttachment,
|
|
290
|
+
): Promise<string> {
|
|
291
|
+
const path = join(workspace, 'reference', 'attachments', attachment.name);
|
|
292
|
+
const info = await lstat(path).catch(() => null);
|
|
293
|
+
if (!info?.isFile() || info.size > MAX_ATTACHMENT_BYTES) {
|
|
294
|
+
throw new SandboxSetupError(
|
|
295
|
+
'SAMPLE_DATA_NOT_FOUND',
|
|
296
|
+
`${attachment.name} is not available in this turn.`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return readFile(path, 'utf8');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function readSampleData(
|
|
303
|
+
workspace: string,
|
|
304
|
+
attachments: readonly SessionAttachment[],
|
|
305
|
+
name?: string,
|
|
306
|
+
): Promise<SampleDataListing> {
|
|
307
|
+
const available = sampleDataAttachments(attachments);
|
|
308
|
+
if (name !== undefined) {
|
|
309
|
+
const attachment = available.find((item) => item.name === name);
|
|
310
|
+
if (!attachment) {
|
|
311
|
+
throw new SandboxSetupError(
|
|
312
|
+
'SAMPLE_DATA_NOT_FOUND',
|
|
313
|
+
`No sample data named ${name.slice(0, 128)}. Available: ${available.map((item) => item.name).join(', ') || 'none'}.`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
sampleData: [
|
|
318
|
+
previewSampleData(
|
|
319
|
+
attachment.name,
|
|
320
|
+
await readSnapshot(workspace, attachment),
|
|
321
|
+
),
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const sampleData: SampleDataPreview[] = [];
|
|
326
|
+
let used = 0;
|
|
327
|
+
for (const attachment of available) {
|
|
328
|
+
const preview = previewSampleData(
|
|
329
|
+
attachment.name,
|
|
330
|
+
await readSnapshot(workspace, attachment),
|
|
331
|
+
);
|
|
332
|
+
const size = byteLength(preview);
|
|
333
|
+
if (used + size <= MAX_SAMPLE_LISTING_BYTES) {
|
|
334
|
+
sampleData.push(preview);
|
|
335
|
+
used += size;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const summary: SampleDataPreview = {
|
|
339
|
+
...preview,
|
|
340
|
+
rows: [],
|
|
341
|
+
truncated: true,
|
|
342
|
+
note: `Rows left out to keep the listing small. Call ${SAMPLE_DATA_TOOL} with this name to read them.`,
|
|
343
|
+
};
|
|
344
|
+
sampleData.push(summary);
|
|
345
|
+
used += byteLength(summary);
|
|
346
|
+
}
|
|
347
|
+
return { sampleData };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function sampleDataTool(
|
|
351
|
+
workspace: string,
|
|
352
|
+
attachments: readonly SessionAttachment[],
|
|
353
|
+
): CodingAgentTool {
|
|
354
|
+
return {
|
|
355
|
+
name: SAMPLE_DATA_TOOL,
|
|
356
|
+
description: `Read-only. Lists the CSV, JSON and text files the operator attached as sample data, each with its columns and a parsed preview of the first ${SAMPLE_DATA_ROWS} rows. Pass name to preview one file.`,
|
|
357
|
+
inputSchema: {
|
|
358
|
+
type: 'object',
|
|
359
|
+
properties: {
|
|
360
|
+
name: {
|
|
361
|
+
type: 'string',
|
|
362
|
+
maxLength: 128,
|
|
363
|
+
description: 'One sample data file name.',
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
additionalProperties: false,
|
|
367
|
+
},
|
|
368
|
+
execute: async (input) =>
|
|
369
|
+
JSON.stringify(
|
|
370
|
+
await readSampleData(
|
|
371
|
+
workspace,
|
|
372
|
+
attachments,
|
|
373
|
+
typeof input.name === 'string' ? input.name : undefined,
|
|
374
|
+
),
|
|
375
|
+
),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* Local CLI drivers have no channel for a lent tool, so the same listing is left
|
|
380
|
+
in the read-only reference tree of the turn, and removed when nothing is
|
|
381
|
+
attached. It stays inside the session workspace. */
|
|
382
|
+
export async function materializeSampleData(
|
|
383
|
+
workspace: string,
|
|
384
|
+
attachments: readonly SessionAttachment[],
|
|
385
|
+
): Promise<boolean> {
|
|
386
|
+
const path = join(workspace, SAMPLE_DATA_REFERENCE);
|
|
387
|
+
await rm(path, { force: true });
|
|
388
|
+
if (sampleDataAttachments(attachments).length === 0) return false;
|
|
389
|
+
await writeFile(
|
|
390
|
+
path,
|
|
391
|
+
`${JSON.stringify(await readSampleData(workspace, attachments), null, 2)}\n`,
|
|
392
|
+
{ flag: 'wx' },
|
|
393
|
+
);
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function sampleDataInstruction(
|
|
398
|
+
attachments: readonly SessionAttachment[],
|
|
399
|
+
): string | null {
|
|
400
|
+
const names = sampleDataAttachments(attachments).map((item) => item.name);
|
|
401
|
+
if (names.length === 0) return null;
|
|
402
|
+
return `Sample data: ${names.join(', ')}. Call the ${SAMPLE_DATA_TOOL} tool, or read ${SAMPLE_DATA_REFERENCE}, for the columns and the first ${SAMPLE_DATA_ROWS} rows. Build the entities, tests/fixtures/*.json and preview/seed.json from its shape; replace real names, contacts and identifiers with invented values, because fixtures ship with the module and the sample itself never leaves this session.`;
|
|
403
|
+
}
|
package/src/server/turns.ts
CHANGED
|
@@ -55,6 +55,11 @@ import {
|
|
|
55
55
|
import type { PlatformClient } from './platform-client.ts';
|
|
56
56
|
import { readQuestions } from './questions.ts';
|
|
57
57
|
import { listSkills, writeAgentPointer } from './reference.ts';
|
|
58
|
+
import {
|
|
59
|
+
materializeSampleData,
|
|
60
|
+
sampleDataInstruction,
|
|
61
|
+
sampleDataTool,
|
|
62
|
+
} from './sample-data.ts';
|
|
58
63
|
import {
|
|
59
64
|
appendChatEntry,
|
|
60
65
|
basePathOf,
|
|
@@ -594,6 +599,11 @@ export async function* runTurn(
|
|
|
594
599
|
paths.workspace,
|
|
595
600
|
);
|
|
596
601
|
const attachmentNote = attachmentInstruction(session.attachments);
|
|
602
|
+
const hasSampleData = await materializeSampleData(
|
|
603
|
+
paths.workspace,
|
|
604
|
+
session.attachments,
|
|
605
|
+
);
|
|
606
|
+
const sampleDataNote = sampleDataInstruction(session.attachments);
|
|
597
607
|
|
|
598
608
|
yield await appendChatEntry(context.workspaceRoot, session, {
|
|
599
609
|
kind: 'user',
|
|
@@ -767,10 +777,15 @@ export async function* runTurn(
|
|
|
767
777
|
allowedPaths: turnAllowedPaths,
|
|
768
778
|
role: roleId,
|
|
769
779
|
systemInstruction: instruction,
|
|
770
|
-
prompt: attachmentNote
|
|
780
|
+
prompt: [attachmentNote, sampleDataNote, message]
|
|
781
|
+
.filter(Boolean)
|
|
782
|
+
.join('\n\n'),
|
|
771
783
|
resumeId,
|
|
772
784
|
history: [{ role: 'user', text: session.brief }, ...history.slice(0, -1)],
|
|
773
785
|
model: session.model,
|
|
786
|
+
...(hasSampleData
|
|
787
|
+
? { tools: [sampleDataTool(paths.workspace, session.attachments)] }
|
|
788
|
+
: {}),
|
|
774
789
|
signal: input.signal,
|
|
775
790
|
})) {
|
|
776
791
|
const recorded = await appendChatEntry(context.workspaceRoot, session, {
|