dsh-reference-anything 0.3.1 → 0.3.3
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/LICENSE +22 -22
- package/NOTICE.md +52 -25
- package/README.md +342 -284
- package/cordis.patch.yml +89 -18
- package/lib/client.js +13534 -521
- package/lib/contract.d.ts +155 -3
- package/lib/contract.js +68 -4
- package/lib/download-directory.d.ts +18 -0
- package/lib/download-directory.js +55 -0
- package/lib/errors.d.ts +4 -0
- package/lib/host.d.ts +96 -3
- package/lib/host.js +88 -5
- package/lib/index.d.ts +22 -2
- package/lib/index.js +57 -7
- package/lib/opencli.d.ts +5 -3
- package/lib/opencli.js +13 -6
- package/lib/openlist/host.d.ts +157 -0
- package/lib/openlist/host.js +873 -0
- package/lib/openlist/index.d.ts +84 -0
- package/lib/openlist/index.js +507 -0
- package/lib/render.d.ts +12 -7
- package/lib/render.js +27 -21
- package/lib/sources/cloud-drive/cache.d.ts +60 -0
- package/lib/sources/cloud-drive/cache.js +107 -0
- package/lib/sources/cloud-drive/index.d.ts +190 -0
- package/lib/sources/cloud-drive/index.js +526 -0
- package/lib/sources/cloud-drive/providers/http.d.ts +27 -0
- package/lib/sources/cloud-drive/providers/http.js +73 -0
- package/lib/sources/cloud-drive/providers/openlist.d.ts +59 -0
- package/lib/sources/cloud-drive/providers/openlist.js +535 -0
- package/lib/sources/cloud-drive/registry.d.ts +53 -0
- package/lib/sources/cloud-drive/registry.js +101 -0
- package/lib/sources/cloud-drive/types.d.ts +169 -0
- package/lib/sources/cloud-drive/types.js +17 -0
- package/lib/sources/file.d.ts +2 -0
- package/lib/sources/file.js +2 -0
- package/lib/sources/local-agent/adapters/claude-code.d.ts +16 -0
- package/lib/sources/local-agent/adapters/claude-code.js +171 -0
- package/lib/sources/local-agent/adapters/codex.d.ts +20 -0
- package/lib/sources/local-agent/adapters/codex.js +204 -0
- package/lib/sources/local-agent/adapters/cursor.d.ts +23 -0
- package/lib/sources/local-agent/adapters/cursor.js +121 -0
- package/lib/sources/local-agent/adapters/gemini-cli.d.ts +20 -0
- package/lib/sources/local-agent/adapters/gemini-cli.js +141 -0
- package/lib/sources/local-agent/adapters/grokbuild.d.ts +22 -0
- package/lib/sources/local-agent/adapters/grokbuild.js +76 -0
- package/lib/sources/local-agent/adapters/hermes.d.ts +19 -0
- package/lib/sources/local-agent/adapters/hermes.js +85 -0
- package/lib/sources/local-agent/adapters/kimi.d.ts +28 -0
- package/lib/sources/local-agent/adapters/kimi.js +261 -0
- package/lib/sources/local-agent/adapters/openclaw.d.ts +21 -0
- package/lib/sources/local-agent/adapters/openclaw.js +120 -0
- package/lib/sources/local-agent/adapters/opencode.d.ts +44 -0
- package/lib/sources/local-agent/adapters/opencode.js +236 -0
- package/lib/sources/local-agent/adapters/pi.d.ts +26 -0
- package/lib/sources/local-agent/adapters/pi.js +242 -0
- package/lib/sources/local-agent/adapters/qoder.d.ts +19 -0
- package/lib/sources/local-agent/adapters/qoder.js +93 -0
- package/lib/sources/local-agent/adapters/reasonix.d.ts +23 -0
- package/lib/sources/local-agent/adapters/reasonix.js +90 -0
- package/lib/sources/local-agent/adapters/shared.d.ts +163 -0
- package/lib/sources/local-agent/adapters/shared.js +415 -0
- package/lib/sources/local-agent/adapters/sqlite-shared.d.ts +92 -0
- package/lib/sources/local-agent/adapters/sqlite-shared.js +215 -0
- package/lib/sources/local-agent/adapters/zcode.d.ts +41 -0
- package/lib/sources/local-agent/adapters/zcode.js +159 -0
- package/lib/sources/local-agent/index.d.ts +222 -0
- package/lib/sources/local-agent/index.js +656 -0
- package/lib/sources/local-agent/page.d.ts +55 -0
- package/lib/sources/local-agent/page.js +83 -0
- package/lib/sources/local-agent/path.d.ts +8 -0
- package/lib/sources/local-agent/path.js +43 -0
- package/lib/sources/local-agent/registry.d.ts +68 -0
- package/lib/sources/local-agent/registry.js +116 -0
- package/lib/sources/local-agent/scan.d.ts +175 -0
- package/lib/sources/local-agent/scan.js +472 -0
- package/lib/sources/local-agent/sqlite.d.ts +45 -0
- package/lib/sources/local-agent/sqlite.js +113 -0
- package/lib/sources/local-agent/store.d.ts +93 -0
- package/lib/sources/local-agent/store.js +113 -0
- package/lib/sources/local-agent/types.d.ts +338 -0
- package/lib/sources/local-agent/types.js +20 -0
- package/lib/sources/web-chat/index.d.ts +5 -1
- package/lib/sources/web-chat/index.js +47 -8
- package/lib/store/spec.d.ts +50 -0
- package/lib/store/spec.js +2 -2
- package/lib/sync/index.d.ts +1 -0
- package/lib/sync/index.js +28 -8
- package/lib/tool.d.ts +5 -0
- package/lib/tool.js +418 -45
- package/lib/typert.js +13 -1
- package/lib/types.d.ts +28 -2
- package/lib/update.d.ts +6 -0
- package/lib/update.js +29 -1
- package/lib/web.js +5 -1
- package/lib/wire.d.ts +80 -0
- package/lib/wire.js +30 -1
- package/opencli-plugin/chatgpt.js +30 -27
- package/opencli-plugin/claude.js +12 -8
- package/opencli-plugin/common.js +266 -187
- package/opencli-plugin/deepseek.js +8 -5
- package/opencli-plugin/gemini.js +42 -38
- package/opencli-plugin/grok.js +22 -21
- package/opencli-plugin/kimi.js +34 -31
- package/opencli-plugin/opencli-plugin.json +6 -6
- package/opencli-plugin/package.json +8 -8
- package/package.json +200 -185
package/lib/tool.js
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module dsh-reference-anything/tool
|
|
10
10
|
*/
|
|
11
|
-
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
11
|
+
import { lstat, mkdtemp, open, readFile, readdir, rm, rmdir, unlink } from 'node:fs/promises';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
|
-
import { basename
|
|
13
|
+
import { basename } from 'node:path';
|
|
14
14
|
import z from '@deepseek-ai/schemastery';
|
|
15
15
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
16
16
|
import { AttachmentId } from '@deepseek-ai/dsh-attachment';
|
|
17
|
+
import { appendDownloadDirectoryPrefix, validateDownloadDirectory } from "./download-directory.js";
|
|
18
|
+
import { ReferenceAnythingError } from "./errors.js";
|
|
19
|
+
import { DEFAULT_OPENCLI_MAX_STDOUT_BYTES, OpenCliError, OpenCliRunner } from "./opencli.js";
|
|
17
20
|
import { addUnavailableAttachmentNotices, continuationFooter, frameReferenceBlock } from "./render.js";
|
|
18
21
|
import { retainConversation } from "./retain.js";
|
|
19
22
|
import { decodeReferenceUri, encodeReferenceUri } from "./uri.js";
|
|
@@ -30,6 +33,30 @@ export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
30
33
|
export const DEFAULT_READ_TURNS = 10;
|
|
31
34
|
/** Ceiling on the model-supplied turn limit when none is configured. */
|
|
32
35
|
export const DEFAULT_MAX_READ_TURNS = 100;
|
|
36
|
+
/** Hard attachment cap shared by drive and Web materialization. */
|
|
37
|
+
export const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
38
|
+
/** Successful materializations remain usable for one hour. */
|
|
39
|
+
export const ATTACHMENT_LIFETIME_MS = 60 * 60 * 1000;
|
|
40
|
+
function fileIdentity(stats) {
|
|
41
|
+
return { dev: stats.dev, ino: stats.ino };
|
|
42
|
+
}
|
|
43
|
+
function sameFileIdentity(left, right) {
|
|
44
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
45
|
+
}
|
|
46
|
+
async function selectedBaseIdentity(path) {
|
|
47
|
+
try {
|
|
48
|
+
const stats = await lstat(path);
|
|
49
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
50
|
+
throw new ReferenceAnythingError('selected cloud-drive download directory must be a real directory, not a symbolic link', 'REFERENCE_INVALID_CONFIG');
|
|
51
|
+
}
|
|
52
|
+
return fileIdentity(stats);
|
|
53
|
+
}
|
|
54
|
+
catch (cause) {
|
|
55
|
+
if (cause instanceof ReferenceAnythingError)
|
|
56
|
+
throw cause;
|
|
57
|
+
throw new ReferenceAnythingError('selected cloud-drive download directory is unavailable', 'REFERENCE_INVALID_CONFIG', { cause });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
33
60
|
/** Runtime schema for {@link Config}. */
|
|
34
61
|
export const Config = z.object({
|
|
35
62
|
listLimit: z.number().step(1).min(1).default(20),
|
|
@@ -139,6 +166,11 @@ export function apply(ctx, config = {}) {
|
|
|
139
166
|
type: 'string',
|
|
140
167
|
description: 'Opaque nextCursor returned by the previous page. Do not combine with before.',
|
|
141
168
|
},
|
|
169
|
+
detail: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
enum: ['summary', 'full'],
|
|
172
|
+
description: 'For external Agent sessions: summary (default) keeps only tool names; full includes complete arguments and outputs.',
|
|
173
|
+
},
|
|
142
174
|
},
|
|
143
175
|
output: {
|
|
144
176
|
schema: {
|
|
@@ -161,11 +193,18 @@ export function apply(ctx, config = {}) {
|
|
|
161
193
|
type: 'object',
|
|
162
194
|
additionalProperties: false,
|
|
163
195
|
properties: {
|
|
164
|
-
role: {
|
|
196
|
+
role: {
|
|
197
|
+
type: 'string',
|
|
198
|
+
required: true,
|
|
199
|
+
enum: ['user', 'assistant', 'document'],
|
|
200
|
+
description: 'Who produced the text. `document` means it is file content rather than '
|
|
201
|
+
+ 'anything anyone said, so it carries no speaker\'s authority at all.',
|
|
202
|
+
},
|
|
165
203
|
text: { type: 'string', required: true },
|
|
166
204
|
},
|
|
167
205
|
},
|
|
168
206
|
},
|
|
207
|
+
toolDetail: { type: 'string', enum: ['summary', 'full'] },
|
|
169
208
|
},
|
|
170
209
|
},
|
|
171
210
|
render: (_args, value) => [{
|
|
@@ -181,7 +220,7 @@ export function apply(ctx, config = {}) {
|
|
|
181
220
|
...value.totalTurns === undefined ? {} : { totalTurns: value.totalTurns },
|
|
182
221
|
hasOlder: value.hasOlder,
|
|
183
222
|
...value.nextCursor === undefined ? {} : { nextCursor: value.nextCursor },
|
|
184
|
-
})}`,
|
|
223
|
+
})}${value.toolDetail === 'summary' ? '\n\n(Tool arguments and outputs are omitted. Rerun this window with detail: "full" when needed.)' : ''}`,
|
|
185
224
|
}],
|
|
186
225
|
presentationMeta: (_args, value) => ({
|
|
187
226
|
label: value.label,
|
|
@@ -194,12 +233,14 @@ export function apply(ctx, config = {}) {
|
|
|
194
233
|
isConcurrencySafe: () => true,
|
|
195
234
|
async execute(args, exec) {
|
|
196
235
|
const ref = decodeReferenceUri(args.uri);
|
|
197
|
-
ctx.references.
|
|
236
|
+
ctx.references.assertSessionGranted(exec.agent?.session, ref);
|
|
198
237
|
const chatHistory = ctx.get('referenceChatHistory');
|
|
199
238
|
const userMaximum = ref.source === 'web-chat' && chatHistory
|
|
200
239
|
? Math.min(maxReadTurns, chatHistory.store.settings.maxReadTurns)
|
|
201
240
|
: maxReadTurns;
|
|
202
|
-
const
|
|
241
|
+
const toolDetail = ref.source === 'local-agent' ? args.detail ?? 'summary' : undefined;
|
|
242
|
+
const window = { ...parseWindow(args, Math.min(readTurns, userMaximum), userMaximum),
|
|
243
|
+
...toolDetail === undefined ? {} : { detail: toolDetail } };
|
|
203
244
|
const snapshot = await ctx.references.read(ref, window, exec.signal);
|
|
204
245
|
const slice = snapshot.body;
|
|
205
246
|
// The turn window is the model's bound; this byte budget is only a
|
|
@@ -220,6 +261,7 @@ export function apply(ctx, config = {}) {
|
|
|
220
261
|
omittedBytes: outcome.omittedBytes,
|
|
221
262
|
...(snapshot.revision ? { revision: snapshot.revision } : {}),
|
|
222
263
|
...(slice.nextCursor && dropped === 0 ? { nextCursor: slice.nextCursor } : {}),
|
|
264
|
+
...toolDetail === undefined ? {} : { toolDetail },
|
|
223
265
|
messages: outcome.items.map(item => ({ role: item.role, text: item.text })),
|
|
224
266
|
};
|
|
225
267
|
},
|
|
@@ -230,13 +272,235 @@ export function apply(ctx, config = {}) {
|
|
|
230
272
|
rawInput: args.uri,
|
|
231
273
|
}),
|
|
232
274
|
})), 'reference-tool.reference_read()');
|
|
233
|
-
const tempRoots = new
|
|
275
|
+
const tempRoots = new Map();
|
|
276
|
+
let disposed = false;
|
|
277
|
+
const scheduleTempCleanup = (root, delay) => {
|
|
278
|
+
const state = tempRoots.get(root);
|
|
279
|
+
if (!state || disposed)
|
|
280
|
+
return;
|
|
281
|
+
if (state.timer !== undefined)
|
|
282
|
+
clearTimeout(state.timer);
|
|
283
|
+
const timer = setTimeout(() => {
|
|
284
|
+
void removeTempRoot(root).catch(() => {
|
|
285
|
+
ctx.logger.warn('reference attachment temporary cleanup failed; the tracked path will be retried');
|
|
286
|
+
});
|
|
287
|
+
}, delay);
|
|
288
|
+
timer.unref?.();
|
|
289
|
+
state.timer = timer;
|
|
290
|
+
};
|
|
291
|
+
const removeTempRoot = async (root) => {
|
|
292
|
+
const state = tempRoots.get(root);
|
|
293
|
+
if (!state) {
|
|
294
|
+
ctx.logger.warn('ignored cleanup request for an untracked attachment temporary directory');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (state.removal)
|
|
298
|
+
return state.removal;
|
|
299
|
+
if (state.timer !== undefined)
|
|
300
|
+
clearTimeout(state.timer);
|
|
301
|
+
state.timer = undefined;
|
|
302
|
+
state.removed = false;
|
|
303
|
+
const removal = (async () => {
|
|
304
|
+
if (state.selectedRootIdentity) {
|
|
305
|
+
if (!state.selectedBasePath || !state.selectedBaseIdentity) {
|
|
306
|
+
ctx.logger.warn('selected attachment temporary directory lost its base identity; cleanup was abandoned');
|
|
307
|
+
return 'abandoned';
|
|
308
|
+
}
|
|
309
|
+
let currentBase;
|
|
310
|
+
try {
|
|
311
|
+
const baseStats = await lstat(state.selectedBasePath);
|
|
312
|
+
if (!baseStats.isDirectory() || baseStats.isSymbolicLink()) {
|
|
313
|
+
ctx.logger.warn('selected attachment base directory changed type; cleanup was abandoned');
|
|
314
|
+
return 'abandoned';
|
|
315
|
+
}
|
|
316
|
+
currentBase = fileIdentity(baseStats);
|
|
317
|
+
}
|
|
318
|
+
catch (cause) {
|
|
319
|
+
if (cause.code === 'ENOENT') {
|
|
320
|
+
ctx.logger.warn('selected attachment base directory disappeared; cleanup was abandoned');
|
|
321
|
+
return 'abandoned';
|
|
322
|
+
}
|
|
323
|
+
throw cause;
|
|
324
|
+
}
|
|
325
|
+
if (!sameFileIdentity(currentBase, state.selectedBaseIdentity)) {
|
|
326
|
+
ctx.logger.warn('selected attachment base directory identity changed; cleanup was abandoned');
|
|
327
|
+
return 'abandoned';
|
|
328
|
+
}
|
|
329
|
+
let current;
|
|
330
|
+
try {
|
|
331
|
+
current = fileIdentity(await lstat(root));
|
|
332
|
+
}
|
|
333
|
+
catch (cause) {
|
|
334
|
+
if (cause.code === 'ENOENT')
|
|
335
|
+
return 'missing';
|
|
336
|
+
throw cause;
|
|
337
|
+
}
|
|
338
|
+
if (!sameFileIdentity(current, state.selectedRootIdentity)) {
|
|
339
|
+
ctx.logger.warn('selected attachment temporary directory identity changed; cleanup was abandoned');
|
|
340
|
+
return 'abandoned';
|
|
341
|
+
}
|
|
342
|
+
const selectedFiles = state.selectedFiles ?? new Map();
|
|
343
|
+
for (const [name, expected] of selectedFiles) {
|
|
344
|
+
let actual;
|
|
345
|
+
try {
|
|
346
|
+
actual = fileIdentity(await lstat(appendDownloadDirectoryPrefix(root, name)));
|
|
347
|
+
}
|
|
348
|
+
catch (cause) {
|
|
349
|
+
if (cause.code === 'ENOENT') {
|
|
350
|
+
selectedFiles.delete(name);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
ctx.logger.warn('selected attachment temporary file identity could not be verified; cleanup was abandoned');
|
|
354
|
+
return 'abandoned';
|
|
355
|
+
}
|
|
356
|
+
if (!sameFileIdentity(actual, expected)) {
|
|
357
|
+
ctx.logger.warn('selected attachment temporary file identity changed; cleanup was abandoned');
|
|
358
|
+
return 'abandoned';
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const rootBeforeUnlink = fileIdentity(await lstat(root));
|
|
362
|
+
if (!sameFileIdentity(rootBeforeUnlink, state.selectedRootIdentity)) {
|
|
363
|
+
ctx.logger.warn('selected attachment temporary directory identity changed; cleanup was abandoned');
|
|
364
|
+
return 'abandoned';
|
|
365
|
+
}
|
|
366
|
+
for (const name of [...selectedFiles.keys()]) {
|
|
367
|
+
try {
|
|
368
|
+
await unlink(appendDownloadDirectoryPrefix(root, name));
|
|
369
|
+
}
|
|
370
|
+
catch (cause) {
|
|
371
|
+
if (cause.code !== 'ENOENT')
|
|
372
|
+
throw cause;
|
|
373
|
+
}
|
|
374
|
+
selectedFiles.delete(name);
|
|
375
|
+
}
|
|
376
|
+
if ((await readdir(root)).length !== 0) {
|
|
377
|
+
ctx.logger.warn('selected attachment temporary directory contains foreign entries; directory removal was abandoned');
|
|
378
|
+
return 'abandoned';
|
|
379
|
+
}
|
|
380
|
+
const rootBeforeRemoval = fileIdentity(await lstat(root));
|
|
381
|
+
if (!sameFileIdentity(rootBeforeRemoval, state.selectedRootIdentity)) {
|
|
382
|
+
ctx.logger.warn('selected attachment temporary directory identity changed; cleanup was abandoned');
|
|
383
|
+
return 'abandoned';
|
|
384
|
+
}
|
|
385
|
+
await rmdir(root);
|
|
386
|
+
return 'removed';
|
|
387
|
+
}
|
|
388
|
+
await rm(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
389
|
+
return 'removed';
|
|
390
|
+
})()
|
|
391
|
+
.then((outcome) => {
|
|
392
|
+
state.removal = undefined;
|
|
393
|
+
if (outcome === 'removed' || outcome === 'missing') {
|
|
394
|
+
state.removed = true;
|
|
395
|
+
if (state.activeOperations === 0)
|
|
396
|
+
tempRoots.delete(root);
|
|
397
|
+
}
|
|
398
|
+
else if (!disposed && state.retries < 3) {
|
|
399
|
+
state.retries += 1;
|
|
400
|
+
scheduleTempCleanup(root, 1_000);
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
.catch((cause) => {
|
|
404
|
+
state.removal = undefined;
|
|
405
|
+
if (!disposed && state.retries < 3) {
|
|
406
|
+
state.retries += 1;
|
|
407
|
+
scheduleTempCleanup(root, 1_000);
|
|
408
|
+
}
|
|
409
|
+
throw new ReferenceAnythingError('could not remove attachment temporary files', 'REFERENCE_READ_FAILED', { cause });
|
|
410
|
+
});
|
|
411
|
+
state.removal = removal;
|
|
412
|
+
return removal;
|
|
413
|
+
};
|
|
414
|
+
const createTempRoot = async (base, prefix, expectedBaseIdentity) => {
|
|
415
|
+
if (disposed)
|
|
416
|
+
throw new ReferenceAnythingError('attachment materializer has been disposed', 'REFERENCE_CANCELLED');
|
|
417
|
+
const selectedBase = expectedBaseIdentity !== undefined;
|
|
418
|
+
if (expectedBaseIdentity) {
|
|
419
|
+
const currentBaseIdentity = await selectedBaseIdentity(base);
|
|
420
|
+
if (!sameFileIdentity(currentBaseIdentity, expectedBaseIdentity)) {
|
|
421
|
+
throw new ReferenceAnythingError('selected cloud-drive download directory changed before temporary storage was created', 'REFERENCE_INVALID_CONFIG');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let root;
|
|
425
|
+
try {
|
|
426
|
+
root = await mkdtemp(appendDownloadDirectoryPrefix(base, prefix));
|
|
427
|
+
}
|
|
428
|
+
catch (cause) {
|
|
429
|
+
throw new ReferenceAnythingError(selectedBase
|
|
430
|
+
? 'selected cloud-drive download directory is unavailable or not writable'
|
|
431
|
+
: 'could not create attachment temporary storage', selectedBase ? 'REFERENCE_INVALID_CONFIG' : 'REFERENCE_READ_FAILED', { cause });
|
|
432
|
+
}
|
|
433
|
+
let selectedRootIdentity;
|
|
434
|
+
if (selectedBase) {
|
|
435
|
+
try {
|
|
436
|
+
selectedRootIdentity = fileIdentity(await lstat(root));
|
|
437
|
+
}
|
|
438
|
+
catch (cause) {
|
|
439
|
+
ctx.logger.warn('could not record selected attachment temporary directory identity; cleanup was abandoned');
|
|
440
|
+
throw new ReferenceAnythingError('could not secure selected cloud-drive temporary storage', 'REFERENCE_INVALID_CONFIG', { cause });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
tempRoots.set(root, {
|
|
444
|
+
retries: 0,
|
|
445
|
+
activeOperations: 0,
|
|
446
|
+
removed: false,
|
|
447
|
+
...(selectedRootIdentity && expectedBaseIdentity ? {
|
|
448
|
+
selectedBasePath: base,
|
|
449
|
+
selectedBaseIdentity: expectedBaseIdentity,
|
|
450
|
+
selectedRootIdentity,
|
|
451
|
+
selectedFiles: new Map(),
|
|
452
|
+
} : {}),
|
|
453
|
+
});
|
|
454
|
+
if (expectedBaseIdentity) {
|
|
455
|
+
const baseAfterCreation = await selectedBaseIdentity(base);
|
|
456
|
+
if (!sameFileIdentity(baseAfterCreation, expectedBaseIdentity)) {
|
|
457
|
+
ctx.logger.warn('selected attachment base changed while temporary storage was being created; cleanup was abandoned');
|
|
458
|
+
throw new ReferenceAnythingError('selected cloud-drive download directory changed while temporary storage was created', 'REFERENCE_INVALID_CONFIG');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (disposed) {
|
|
462
|
+
await removeTempRoot(root);
|
|
463
|
+
throw new ReferenceAnythingError('attachment materializer has been disposed', 'REFERENCE_CANCELLED');
|
|
464
|
+
}
|
|
465
|
+
return root;
|
|
466
|
+
};
|
|
467
|
+
const retainTempRoot = async (root) => {
|
|
468
|
+
if (disposed) {
|
|
469
|
+
await removeTempRoot(root);
|
|
470
|
+
throw new ReferenceAnythingError('attachment materializer has been disposed', 'REFERENCE_CANCELLED');
|
|
471
|
+
}
|
|
472
|
+
scheduleTempCleanup(root, ATTACHMENT_LIFETIME_MS);
|
|
473
|
+
};
|
|
474
|
+
const beginTempRootOperation = (root) => {
|
|
475
|
+
const state = tempRoots.get(root);
|
|
476
|
+
if (!state)
|
|
477
|
+
throw new ReferenceAnythingError('attachment temporary storage is no longer tracked', 'REFERENCE_CANCELLED');
|
|
478
|
+
state.activeOperations += 1;
|
|
479
|
+
};
|
|
480
|
+
const endTempRootOperation = (root) => {
|
|
481
|
+
const state = tempRoots.get(root);
|
|
482
|
+
if (!state)
|
|
483
|
+
return;
|
|
484
|
+
state.activeOperations = Math.max(0, state.activeOperations - 1);
|
|
485
|
+
if (state.activeOperations === 0 && state.removed && !state.removal)
|
|
486
|
+
tempRoots.delete(root);
|
|
487
|
+
};
|
|
488
|
+
const registerSelectedTempFile = (root, localPath, identity) => {
|
|
489
|
+
const selectedFiles = tempRoots.get(root)?.selectedFiles;
|
|
490
|
+
if (!selectedFiles)
|
|
491
|
+
return;
|
|
492
|
+
selectedFiles.set(basename(localPath), identity);
|
|
493
|
+
};
|
|
234
494
|
ctx.effect(() => async () => {
|
|
235
|
-
|
|
495
|
+
disposed = true;
|
|
496
|
+
const settled = await Promise.allSettled([...tempRoots.keys()].map(removeTempRoot));
|
|
497
|
+
const failure = settled.find(result => result.status === 'rejected');
|
|
498
|
+
if (failure?.status === 'rejected')
|
|
499
|
+
throw failure.reason;
|
|
236
500
|
}, 'reference-tool.attachmentTempCleanup()');
|
|
237
501
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
238
502
|
name: 'reference_attachment_read',
|
|
239
|
-
description: '
|
|
503
|
+
description: 'Materialize one authorized Web-conversation attachment or cloud-drive document on demand.',
|
|
240
504
|
parameters: {
|
|
241
505
|
uri: { type: 'string', required: true, description: 'The dsh-ref URI that exposed the attachment.' },
|
|
242
506
|
attachmentId: { type: 'string', required: true, description: 'Attachment id exactly as shown in the conversation page.' },
|
|
@@ -270,45 +534,107 @@ export function apply(ctx, config = {}) {
|
|
|
270
534
|
isConcurrencySafe: () => true,
|
|
271
535
|
async execute(args, exec) {
|
|
272
536
|
const ref = decodeReferenceUri(args.uri);
|
|
273
|
-
ctx.references.
|
|
274
|
-
if (ref.source
|
|
275
|
-
|
|
537
|
+
ctx.references.assertSessionGranted(exec.agent?.session, ref);
|
|
538
|
+
if (ref.source === 'cloud-drive') {
|
|
539
|
+
if (args.attachmentId !== 'file') {
|
|
540
|
+
throw new ReferenceAnythingError('cloud-drive documents use attachmentId "file" exactly as shown by reference_read', 'ATTACHMENT_UNAVAILABLE');
|
|
541
|
+
}
|
|
542
|
+
const service = ctx.get('referenceCloudDrive');
|
|
543
|
+
if (!service)
|
|
544
|
+
throw new ReferenceAnythingError('cloud-drive service is not mounted', 'SOURCE_UNAVAILABLE');
|
|
545
|
+
const history = ctx.get('referenceChatHistory');
|
|
546
|
+
const configuredValue = history?.store.settings
|
|
547
|
+
?.cloudDriveDownloadDirectory;
|
|
548
|
+
if (configuredValue !== undefined && typeof configuredValue !== 'string') {
|
|
549
|
+
throw new ReferenceAnythingError('selected cloud-drive download directory must be a string path', 'REFERENCE_INVALID_CONFIG');
|
|
550
|
+
}
|
|
551
|
+
const configuredBase = await validateDownloadDirectory(configuredValue ?? '');
|
|
552
|
+
const usesConfiguredBase = configuredBase !== '';
|
|
553
|
+
const configuredBaseIdentity = usesConfiguredBase ? await selectedBaseIdentity(configuredBase) : undefined;
|
|
554
|
+
let attachment;
|
|
555
|
+
try {
|
|
556
|
+
attachment = await service.attachment(ref, MAX_ATTACHMENT_BYTES, exec.signal);
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
throw attachmentFailure(error, exec.signal, 'cloud-drive');
|
|
560
|
+
}
|
|
561
|
+
if (attachment.bytes.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
562
|
+
throw new ReferenceAnythingError('cloud-drive attachment exceeds 25 MiB', 'ATTACHMENT_TOO_LARGE');
|
|
563
|
+
}
|
|
564
|
+
const root = await createTempRoot(usesConfiguredBase ? configuredBase : tmpdir(), 'dsh-reference-drive-', configuredBaseIdentity);
|
|
565
|
+
beginTempRootOperation(root);
|
|
566
|
+
try {
|
|
567
|
+
const localPath = attachmentLocalPath(root, attachment.name);
|
|
568
|
+
const handle = await open(localPath, 'wx', 0o600);
|
|
569
|
+
try {
|
|
570
|
+
registerSelectedTempFile(root, localPath, fileIdentity(await handle.stat()));
|
|
571
|
+
await handle.writeFile(attachment.bytes);
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
await handle.close();
|
|
575
|
+
}
|
|
576
|
+
const bytes = Buffer.from(attachment.bytes);
|
|
577
|
+
const mimeType = sniffMime(bytes, attachment.mimeType);
|
|
578
|
+
const image = isRenderableImage(mimeType) ? await saveImage(ctx, bytes, mimeType, attachment.name) : undefined;
|
|
579
|
+
await retainTempRoot(root);
|
|
580
|
+
return { name: attachment.name, mimeType, size: bytes.byteLength, localPath, ...(image ? { image } : {}) };
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
await removeTempRoot(root);
|
|
584
|
+
throw attachmentFailure(error, exec.signal, 'cloud-drive');
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
endTempRootOperation(root);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (ref.source !== 'web-chat') {
|
|
591
|
+
throw new ReferenceAnythingError('attachments are available only for synchronized Web conversations and cloud-drive documents', 'ATTACHMENT_UNAVAILABLE');
|
|
592
|
+
}
|
|
276
593
|
const service = ctx.get('referenceChatHistory');
|
|
277
594
|
if (!service)
|
|
278
|
-
throw new
|
|
595
|
+
throw new ReferenceAnythingError('Web conversation history service is not mounted', 'SOURCE_UNAVAILABLE');
|
|
279
596
|
const conversation = service.store.conversations.get(ref.id);
|
|
280
597
|
const revision = conversation?.currentRevision;
|
|
281
598
|
if (!conversation)
|
|
282
|
-
throw new
|
|
599
|
+
throw new ReferenceAnythingError('conversation is not in the local title index', 'REFERENCE_NOT_FOUND');
|
|
600
|
+
if (!/^[a-f0-9]{64}$/.test(conversation.accountScope)) {
|
|
601
|
+
throw new ReferenceAnythingError(`conversation has no verified ${conversation.provider} account scope; ask the user to sync ${conversation.provider} and reselect the conversation from the refreshed @ list, then retry`, 'REFERENCE_ACCOUNT_MISMATCH');
|
|
602
|
+
}
|
|
283
603
|
const attachment = service.store.settings.historyMode === 'metadata-only'
|
|
284
604
|
? service.liveAttachment(ref.id, args.attachmentId)
|
|
285
605
|
: revision ? service.store.attachment(ref.id, revision, args.attachmentId) : undefined;
|
|
286
606
|
if (!attachment?.locator || attachment.status !== 'available') {
|
|
287
|
-
throw new
|
|
607
|
+
throw new ReferenceAnythingError('provider supplied no stable attachment locator', 'ATTACHMENT_UNAVAILABLE');
|
|
608
|
+
}
|
|
609
|
+
const root = await createTempRoot(tmpdir(), 'dsh-reference-attachment-');
|
|
610
|
+
beginTempRootOperation(root);
|
|
611
|
+
try {
|
|
612
|
+
const localPath = attachmentLocalPath(root, attachment.name);
|
|
613
|
+
const settings = service.store.settings;
|
|
614
|
+
const runner = new OpenCliRunner({
|
|
615
|
+
executable: settings.opencliPath, profile: settings.profile, timeoutMs,
|
|
616
|
+
maxStdoutBytes: DEFAULT_OPENCLI_MAX_STDOUT_BYTES,
|
|
617
|
+
});
|
|
618
|
+
await runner.attachment(conversation.provider, attachment.locator, localPath, MAX_ATTACHMENT_BYTES, exec.signal, conversation.accountScope);
|
|
619
|
+
const bytes = await readFile(localPath);
|
|
620
|
+
if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
621
|
+
throw new ReferenceAnythingError('attachment exceeds 25 MiB', 'ATTACHMENT_TOO_LARGE');
|
|
622
|
+
}
|
|
623
|
+
const mimeType = sniffMime(bytes, attachment.mimeType);
|
|
624
|
+
const image = isRenderableImage(mimeType) ? await saveImage(ctx, bytes, mimeType, attachment.name) : undefined;
|
|
625
|
+
await retainTempRoot(root);
|
|
626
|
+
return {
|
|
627
|
+
name: attachment.name, mimeType, size: bytes.byteLength, localPath,
|
|
628
|
+
...(image ? { image } : {}),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
await removeTempRoot(root);
|
|
633
|
+
throw attachmentFailure(error, exec.signal, conversation.provider);
|
|
634
|
+
}
|
|
635
|
+
finally {
|
|
636
|
+
endTempRootOperation(root);
|
|
288
637
|
}
|
|
289
|
-
const root = await mkdtemp(join(tmpdir(), 'dsh-reference-attachment-'));
|
|
290
|
-
tempRoots.add(root);
|
|
291
|
-
const safeName = basename(attachment.name).replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') || 'attachment';
|
|
292
|
-
const localPath = join(root, safeName);
|
|
293
|
-
const settings = service.store.settings;
|
|
294
|
-
const runner = new (await import("./opencli.js")).OpenCliRunner({
|
|
295
|
-
executable: settings.opencliPath, profile: settings.profile, timeoutMs, maxStdoutBytes: 32 * 1024 * 1024,
|
|
296
|
-
});
|
|
297
|
-
await runner.attachment(conversation.provider, attachment.locator, localPath, 25 * 1024 * 1024, exec.signal);
|
|
298
|
-
const bytes = await readFile(localPath);
|
|
299
|
-
if (bytes.byteLength > 25 * 1024 * 1024)
|
|
300
|
-
throw new Error('ATTACHMENT_TOO_LARGE: attachment exceeds 25 MiB');
|
|
301
|
-
const mimeType = sniffMime(bytes, attachment.mimeType);
|
|
302
|
-
const timer = setTimeout(() => {
|
|
303
|
-
tempRoots.delete(root);
|
|
304
|
-
void rm(root, { recursive: true, force: true });
|
|
305
|
-
}, 60 * 60 * 1000);
|
|
306
|
-
timer.unref?.();
|
|
307
|
-
const image = mimeType.startsWith('image/') ? await saveImage(ctx, bytes, mimeType, attachment.name) : undefined;
|
|
308
|
-
return {
|
|
309
|
-
name: attachment.name, mimeType, size: bytes.byteLength, localPath,
|
|
310
|
-
...(image ? { image } : {}),
|
|
311
|
-
};
|
|
312
638
|
},
|
|
313
639
|
presentCall: args => ({ card: 'generic', title: 'Read conversation attachment', kind: 'read', rawInput: args.attachmentId }),
|
|
314
640
|
})), 'reference-tool.reference_attachment_read()');
|
|
@@ -349,16 +675,63 @@ function sniffMime(bytes, declared) {
|
|
|
349
675
|
: detected.subarray(0, 4).toString('ascii') === 'RIFF' && detected.subarray(8, 12).toString('ascii') === 'WEBP' ? 'image/webp'
|
|
350
676
|
: detected.subarray(0, 5).toString('ascii') === '%PDF-' ? 'application/pdf'
|
|
351
677
|
: 'application/octet-stream';
|
|
352
|
-
if (declared
|
|
353
|
-
throw new
|
|
678
|
+
if (isRenderableImage(declared) && mime !== declared) {
|
|
679
|
+
throw new ReferenceAnythingError('attachment MIME did not match its bytes', 'ATTACHMENT_UNAVAILABLE');
|
|
680
|
+
}
|
|
354
681
|
return mime === 'application/octet-stream' && declared ? declared : mime;
|
|
355
682
|
}
|
|
683
|
+
function isRenderableImage(mimeType) {
|
|
684
|
+
return mimeType === 'image/png' || mimeType === 'image/jpeg' || mimeType === 'image/webp' || mimeType === 'image/gif';
|
|
685
|
+
}
|
|
686
|
+
function safeAttachmentName(name) {
|
|
687
|
+
const leaf = basename(name).replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').replace(/[. ]+$/g, '_') || 'file';
|
|
688
|
+
return `attachment-${leaf}`;
|
|
689
|
+
}
|
|
690
|
+
function attachmentLocalPath(root, name) {
|
|
691
|
+
// `safeAttachmentName` guarantees one leaf segment. Preserve the exact root
|
|
692
|
+
// string so a validated symlink/`..` path keeps the same OS resolution
|
|
693
|
+
// semantics for creation, use, and cleanup.
|
|
694
|
+
return appendDownloadDirectoryPrefix(root, safeAttachmentName(name));
|
|
695
|
+
}
|
|
356
696
|
async function saveImage(ctx, bytes, mimeType, name) {
|
|
357
697
|
const attachments = ctx.get('attachments');
|
|
358
|
-
if (!attachments)
|
|
359
|
-
throw new
|
|
360
|
-
|
|
361
|
-
|
|
698
|
+
if (!attachments) {
|
|
699
|
+
throw new ReferenceAnythingError('no DSH attachment store is mounted for image output', 'ATTACHMENT_UNAVAILABLE');
|
|
700
|
+
}
|
|
701
|
+
let saved;
|
|
702
|
+
try {
|
|
703
|
+
saved = await attachments.saveImage({ data: bytes, mediaType: mimeType, name });
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
throw new ReferenceAnythingError('DSH image storage refused the attachment', 'ATTACHMENT_UNAVAILABLE', { cause: error });
|
|
707
|
+
}
|
|
708
|
+
if (!saved || typeof saved.attachmentId !== 'string' || !isRenderableImage(saved.mediaType)
|
|
709
|
+
|| !Number.isInteger(saved.bytes) || !Number.isInteger(saved.width) || !Number.isInteger(saved.height)) {
|
|
710
|
+
throw new ReferenceAnythingError('DSH image storage returned invalid metadata', 'ATTACHMENT_UNAVAILABLE');
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
attachmentId: saved.attachmentId,
|
|
714
|
+
mediaType: saved.mediaType,
|
|
715
|
+
bytes: saved.bytes,
|
|
716
|
+
width: saved.width,
|
|
717
|
+
height: saved.height,
|
|
718
|
+
...(typeof saved.name === 'string' ? { name: saved.name } : {}),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function attachmentFailure(error, signal, provider) {
|
|
722
|
+
if (error instanceof ReferenceAnythingError)
|
|
723
|
+
return error;
|
|
724
|
+
if (signal?.aborted) {
|
|
725
|
+
return new ReferenceAnythingError('attachment materialization was cancelled', 'REFERENCE_CANCELLED', { cause: signal.reason });
|
|
726
|
+
}
|
|
727
|
+
if (error instanceof OpenCliError && error.code === 'PROVIDER_ACCOUNT_MISMATCH') {
|
|
728
|
+
return new ReferenceAnythingError(`attachment belongs to a different logged-in ${provider} account; ask the user to sync ${provider} and reselect the conversation from the refreshed @ list, then retry`, 'REFERENCE_ACCOUNT_MISMATCH', { cause: error });
|
|
729
|
+
}
|
|
730
|
+
if (error instanceof OpenCliError && error.code === 'ATTACHMENT_TOO_LARGE') {
|
|
731
|
+
return new ReferenceAnythingError('attachment exceeds 25 MiB', 'ATTACHMENT_TOO_LARGE', { cause: error });
|
|
732
|
+
}
|
|
733
|
+
if (error instanceof OpenCliError && error.code === 'PROVIDER_RATE_LIMIT') {
|
|
734
|
+
return new ReferenceAnythingError(`${provider} rate-limited account verification; wait before retrying the attachment`, 'PROVIDER_RATE_LIMIT', { cause: error });
|
|
362
735
|
}
|
|
363
|
-
return
|
|
736
|
+
return new ReferenceAnythingError(`could not materialize the attachment from ${provider}`, 'REFERENCE_READ_FAILED', { cause: error });
|
|
364
737
|
}
|
package/lib/typert.js
CHANGED
|
@@ -5,6 +5,8 @@ export const TYPERT_MANIFEST = {
|
|
|
5
5
|
key: 'referenceAnything', exportName: 'ReferenceAnythingRemote',
|
|
6
6
|
description: 'Local conversation mirror, synchronization, and settings for @Conversation.', tags: [],
|
|
7
7
|
members: [
|
|
8
|
+
{ kind: 'method', name: 'agentSearch', signature: 'agentSearch(agent: Agent, input: AgentSearchInput, signal: AbortSignal): Promise<AgentCandidate[]>' },
|
|
9
|
+
{ kind: 'method', name: 'driveSearch', signature: 'driveSearch(input: DriveSearchInput, signal: AbortSignal): Promise<DriveCandidate[]>' },
|
|
8
10
|
{ kind: 'method', name: 'search', signature: 'search(input: SearchInput): Promise<SearchResult[]>' },
|
|
9
11
|
{ kind: 'method', name: 'health', signature: 'health(signal: AbortSignal): Promise<Health>' },
|
|
10
12
|
{ kind: 'method', name: 'quickHealth', signature: 'quickHealth(signal: AbortSignal): Promise<Health>' },
|
|
@@ -26,10 +28,20 @@ export const TYPERT_MANIFEST = {
|
|
|
26
28
|
{ kind: 'method', name: 'deleteConversation', signature: 'deleteConversation(input: DeleteInput): Promise<boolean>' },
|
|
27
29
|
{ kind: 'method', name: 'storageStats', signature: 'storageStats(): StorageStats' },
|
|
28
30
|
{ kind: 'method', name: 'clearProvider', signature: 'clearProvider(input: ClearProviderInput): Promise<number>' },
|
|
29
|
-
{ kind: 'method', name: 'clearOlder', signature: 'clearOlder(input: ClearOlderInput): Promise<number>' },
|
|
30
31
|
{ kind: 'method', name: 'clearRemoteMissing', signature: 'clearRemoteMissing(signal: AbortSignal): Promise<number>' },
|
|
31
32
|
{ kind: 'method', name: 'clearOldAccounts', signature: 'clearOldAccounts(signal: AbortSignal): Promise<number>' },
|
|
32
33
|
{ kind: 'method', name: 'syncStates', signature: 'syncStates(): ProviderSyncState[]' },
|
|
34
|
+
{ kind: 'method', name: 'openListStatus', signature: 'openListStatus(signal: AbortSignal): Promise<OpenListStatus>' },
|
|
35
|
+
{ kind: 'method', name: 'openListInstall', signature: 'openListInstall(signal: AbortSignal): Promise<OpenListStatus>' },
|
|
36
|
+
{ kind: 'method', name: 'openListUpgrade', signature: 'openListUpgrade(input: OpenListUpgradeInput, signal: AbortSignal): Promise<OpenListStatus>' },
|
|
37
|
+
{ kind: 'method', name: 'openListConnectExternal', signature: 'openListConnectExternal(input: OpenListExternalConnect, signal: AbortSignal): Promise<OpenListStatus>' },
|
|
38
|
+
{ kind: 'method', name: 'openListDisconnect', signature: 'openListDisconnect(signal: AbortSignal): Promise<OpenListStatus>' },
|
|
39
|
+
{ kind: 'method', name: 'openListDrivers', signature: 'openListDrivers(signal: AbortSignal): Promise<OpenListDriver[]>' },
|
|
40
|
+
{ kind: 'method', name: 'openListMounts', signature: 'openListMounts(signal: AbortSignal): Promise<OpenListMount[]>' },
|
|
41
|
+
{ kind: 'method', name: 'openListCreateMount', signature: 'openListCreateMount(input: OpenListMountCreate, signal: AbortSignal): Promise<OpenListMount>' },
|
|
42
|
+
{ kind: 'method', name: 'openListDisableMount', signature: 'openListDisableMount(input: OpenListDisableMountInput, signal: AbortSignal): Promise<boolean>' },
|
|
43
|
+
{ kind: 'method', name: 'openListRemoveMount', signature: 'openListRemoveMount(input: OpenListMountInput, signal: AbortSignal): Promise<boolean>' },
|
|
44
|
+
{ kind: 'method', name: 'openListReindex', signature: 'openListReindex(input: OpenListMountInput, signal: AbortSignal): Promise<OpenListReindex>' },
|
|
33
45
|
], types: [],
|
|
34
46
|
}], events: [], objects: [] },
|
|
35
47
|
invocations: REFERENCE_ANYTHING_INVOCATIONS,
|
package/lib/types.d.ts
CHANGED
|
@@ -44,10 +44,25 @@ export interface ReferenceSummary {
|
|
|
44
44
|
readonly messageCount?: number;
|
|
45
45
|
readonly partial?: boolean;
|
|
46
46
|
readonly syncedAt?: number;
|
|
47
|
+
/** Discovery used a bounded fallback and may not cover the whole source. */
|
|
48
|
+
readonly searchIncomplete?: boolean;
|
|
49
|
+
/** Picker navigation row; directories are never inserted as references. */
|
|
50
|
+
readonly isDirectory?: boolean;
|
|
47
51
|
}
|
|
48
|
-
/** One projected turn of a referenced conversation. */
|
|
52
|
+
/** One projected turn of a referenced conversation, or one block of a referenced file. */
|
|
49
53
|
export interface ConversationItem {
|
|
50
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Who produced this text.
|
|
56
|
+
*
|
|
57
|
+
* `document` exists because not every referent is a conversation. A file
|
|
58
|
+
* pulled from a cloud drive has an author, but not one of these two, and
|
|
59
|
+
* labelling its contents `assistant` would assert that some model said them
|
|
60
|
+
* while labelling it `user` would assert the current user did — the second
|
|
61
|
+
* being the more dangerous lie, since text that appears to come from the
|
|
62
|
+
* user reads as instruction rather than as data. A third value states the
|
|
63
|
+
* provenance instead of picking the least-wrong falsehood.
|
|
64
|
+
*/
|
|
65
|
+
readonly role: 'user' | 'assistant' | 'document';
|
|
51
66
|
/** Visible text of that turn. Tool calls, reasoning, and injected context are not projected. */
|
|
52
67
|
readonly text: string;
|
|
53
68
|
/** Attachment metadata only; bytes stay behind an explicit authorized read. */
|
|
@@ -78,6 +93,8 @@ export interface ReferenceWindow {
|
|
|
78
93
|
readonly before?: number;
|
|
79
94
|
/** Opaque revision-pinned continuation returned by a previous read. */
|
|
80
95
|
readonly cursor?: string;
|
|
96
|
+
/** Local-agent tool fidelity for this read; other sources ignore it. */
|
|
97
|
+
readonly detail?: 'summary' | 'full';
|
|
81
98
|
}
|
|
82
99
|
/**
|
|
83
100
|
* The turns a source returned, and where they sit in the whole conversation.
|
|
@@ -131,6 +148,15 @@ export interface ReferenceSnapshot extends ReferenceSummary {
|
|
|
131
148
|
export interface ReferenceSource {
|
|
132
149
|
/** Stable registry key, also the `source` half of every {@link ReferenceRef} it owns. */
|
|
133
150
|
readonly id: string;
|
|
151
|
+
/**
|
|
152
|
+
* Whether reading this source's items needs a per-task grant.
|
|
153
|
+
*
|
|
154
|
+
* Sources whose items live outside the task's own workspace — another
|
|
155
|
+
* assistant's transcript, a remote drive — opt in, so the model can only
|
|
156
|
+
* read what its user actually named. Omitting it means ungated, which is
|
|
157
|
+
* right only when the material is already inside the task's scope.
|
|
158
|
+
*/
|
|
159
|
+
readonly requiresGrant?: boolean;
|
|
134
160
|
/**
|
|
135
161
|
* Whether this source can currently be used. Must stay cheap and local —
|
|
136
162
|
* no network calls, no subprocess launches — because discovery calls it on
|