chainlesschain 0.166.136 → 0.166.139
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/README.md +43 -1
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/agent.js +12 -0
- package/src/data/changelog.json +39 -0
- package/src/gateways/ws/ws-server.js +16 -0
- package/src/harness/prompt-compressor.js +57 -0
- package/src/harness/provider-backed-compaction.js +6 -0
- package/src/lib/coding-change-review-context.js +51 -0
- package/src/lib/coding-task-guidance.js +3 -1
- package/src/lib/context-memory-kernel/jsonl-session-context-port.js +19 -10
- package/src/lib/context-memory-kernel/live-compaction.js +114 -6
- package/src/lib/context-memory-kernel/message-adapter.js +17 -13
- package/src/lib/context-memory-kernel/task-binding.js +41 -0
- package/src/lib/context-memory-kernel/task-worklog-port.js +26 -3
- package/src/lib/evolution/evidence-json-text.js +324 -14
- package/src/lib/evolution/evolution-evidence-projector.js +67 -1
- package/src/lib/read-file-loop-guard.js +146 -13
- package/src/lib/read-file-page.js +4 -1
- package/src/lib/source-read-recovery.js +89 -0
- package/src/lib/tool-result-preview.js +18 -5
- package/src/repl/agent-repl.js +2 -0
- package/src/runtime/agent-core.js +103 -40
- package/src/runtime/headless-runner.js +1 -0
- package/src/runtime/headless-stream.js +82 -10
- package/src/runtime/message-roles.js +28 -0
- package/src/runtime/policies/agent-policy.js +1 -0
|
@@ -4,6 +4,7 @@ import { isToolRecoveryPaused } from "./tool-recovery-result.js";
|
|
|
4
4
|
import { hostToolOutputIdentity } from "./tool-result-preview.js";
|
|
5
5
|
import { serializeRetainedContext } from "./retained-context-preview.js";
|
|
6
6
|
const MAX_FILES = 12;
|
|
7
|
+
const MAX_UNDELIVERED_READS = 6;
|
|
7
8
|
|
|
8
9
|
function keyFor(filePath, args) {
|
|
9
10
|
return JSON.stringify([
|
|
@@ -94,6 +95,44 @@ export class ReadFileLoopGuard {
|
|
|
94
95
|
retainContext(context);
|
|
95
96
|
}
|
|
96
97
|
}
|
|
98
|
+
for (const entry of this.progress.values()) {
|
|
99
|
+
for (const pending of entry.pendingPages) {
|
|
100
|
+
if (this.isPageVisible(pending.page)) {
|
|
101
|
+
entry.confirmedSpans = mergeSpan(
|
|
102
|
+
entry.confirmedSpans,
|
|
103
|
+
pending.page.readSpan,
|
|
104
|
+
);
|
|
105
|
+
if (pending.page.readSpan.end >= entry.confirmedCursorEnd) {
|
|
106
|
+
entry.confirmedCursorEnd = pending.page.readSpan.end;
|
|
107
|
+
entry.confirmedCursor = {
|
|
108
|
+
lastReadRange: pending.page.range,
|
|
109
|
+
nextRead: pending.page.nextRead || null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
entry.undeliveredReads++;
|
|
114
|
+
if (pending.reread) entry.rereads.delete(pending.reread);
|
|
115
|
+
if (pending.restoring) entry.contextRestoreOffered = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
entry.pendingPages = [];
|
|
119
|
+
// Execution reserves ranges synchronously for parallel reads. Only
|
|
120
|
+
// source actually selected for the provider becomes historical coverage.
|
|
121
|
+
entry.spans = entry.confirmedSpans.map((span) => [...span]);
|
|
122
|
+
if (entry.summary) {
|
|
123
|
+
const through =
|
|
124
|
+
entry.confirmedSpans[0]?.[0] === 0 ? entry.confirmedSpans[0][1] : 0;
|
|
125
|
+
entry.summary.readThrough = through;
|
|
126
|
+
entry.summary.reachedEnd = through >= entry.total;
|
|
127
|
+
entry.summary.pendingDelivery = false;
|
|
128
|
+
if (entry.confirmedCursor)
|
|
129
|
+
Object.assign(entry.summary, entry.confirmedCursor);
|
|
130
|
+
else {
|
|
131
|
+
delete entry.summary.lastReadRange;
|
|
132
|
+
entry.summary.nextRead = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
97
136
|
}
|
|
98
137
|
|
|
99
138
|
isPageVisible(page) {
|
|
@@ -103,8 +142,29 @@ export class ReadFileLoopGuard {
|
|
|
103
142
|
keyFor(visible.path, {}) === keyFor(page.path, {}) &&
|
|
104
143
|
visible.fileVersion === page.fileVersion &&
|
|
105
144
|
typeof page.content === "string" &&
|
|
106
|
-
page.content.length > 0 &&
|
|
107
|
-
visible.
|
|
145
|
+
(page.content.length > 0 || page.readSpan?.total === 0) &&
|
|
146
|
+
((visible.readSpan &&
|
|
147
|
+
page.readSpan &&
|
|
148
|
+
visible.readSpan.total === page.readSpan.total &&
|
|
149
|
+
visible.readSpan.start <= page.readSpan.start &&
|
|
150
|
+
visible.readSpan.end >= page.readSpan.end &&
|
|
151
|
+
visible.readSpan.end - visible.readSpan.start >=
|
|
152
|
+
visible.content.length &&
|
|
153
|
+
visible.readSpan.end - visible.readSpan.start <=
|
|
154
|
+
visible.content.length + 1 &&
|
|
155
|
+
page.readSpan.end - page.readSpan.start >= page.content.length &&
|
|
156
|
+
page.readSpan.end - page.readSpan.start <=
|
|
157
|
+
page.content.length + 1 &&
|
|
158
|
+
visible.content.slice(
|
|
159
|
+
page.readSpan.start - visible.readSpan.start,
|
|
160
|
+
page.readSpan.start -
|
|
161
|
+
visible.readSpan.start +
|
|
162
|
+
page.content.length,
|
|
163
|
+
) === page.content) ||
|
|
164
|
+
(!visible.readSpan &&
|
|
165
|
+
!visible.contentTruncated &&
|
|
166
|
+
JSON.stringify(visible.range) === JSON.stringify(page.range) &&
|
|
167
|
+
visible.content === page.content)),
|
|
108
168
|
) === true
|
|
109
169
|
);
|
|
110
170
|
}
|
|
@@ -166,6 +226,11 @@ export class ReadFileLoopGuard {
|
|
|
166
226
|
entry = {
|
|
167
227
|
fileVersion,
|
|
168
228
|
spans: [],
|
|
229
|
+
confirmedSpans: [],
|
|
230
|
+
pendingPages: [],
|
|
231
|
+
undeliveredReads: 0,
|
|
232
|
+
confirmedCursor: null,
|
|
233
|
+
confirmedCursorEnd: 0,
|
|
169
234
|
rereads: new Set(),
|
|
170
235
|
summary: null,
|
|
171
236
|
excerpts: [],
|
|
@@ -184,15 +249,27 @@ export class ReadFileLoopGuard {
|
|
|
184
249
|
readProgress: { newContent: false },
|
|
185
250
|
};
|
|
186
251
|
}
|
|
252
|
+
if (entry.undeliveredReads >= MAX_UNDELIVERED_READS) {
|
|
253
|
+
return {
|
|
254
|
+
path: filePath,
|
|
255
|
+
fileVersion,
|
|
256
|
+
code: "CC_READ_SOURCE_NOT_DELIVERED",
|
|
257
|
+
error:
|
|
258
|
+
"Repeated file reads did not deliver source content to the model. Report this limitation or use a focused search; do not claim the file was read.",
|
|
259
|
+
contentOmitted: true,
|
|
260
|
+
reachedEnd: false,
|
|
261
|
+
readRecovery: { action: "source-unavailable" },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
187
264
|
let span = page.readSpan;
|
|
265
|
+
let reread;
|
|
266
|
+
let restoringPage = false;
|
|
188
267
|
const covered = entry.spans.some(
|
|
189
268
|
([start, end]) => start <= span.start && end >= span.end,
|
|
190
269
|
);
|
|
191
|
-
|
|
192
|
-
Number(args.offset) > 0 &&
|
|
193
|
-
|
|
194
|
-
Number(args.limit) <= 200 &&
|
|
195
|
-
!page.truncated;
|
|
270
|
+
const explicitRange =
|
|
271
|
+
Number(args.offset) > 0 && Number(args.limit) > 0 && !page.truncated;
|
|
272
|
+
let targeted = explicitRange;
|
|
196
273
|
const coveredPrefix = entry.spans.some(
|
|
197
274
|
([start, end]) => start <= span.start && end > span.start,
|
|
198
275
|
);
|
|
@@ -221,6 +298,7 @@ export class ReadFileLoopGuard {
|
|
|
221
298
|
span = page.readSpan;
|
|
222
299
|
targeted = true;
|
|
223
300
|
entry.contextRestoreOffered = true;
|
|
301
|
+
restoringPage = true;
|
|
224
302
|
}
|
|
225
303
|
}
|
|
226
304
|
if (covered || (coveredPrefix && !targeted)) {
|
|
@@ -240,13 +318,14 @@ export class ReadFileLoopGuard {
|
|
|
240
318
|
entry.rereads.size < 3
|
|
241
319
|
) {
|
|
242
320
|
entry.rereads.add(fingerprint);
|
|
321
|
+
reread = fingerprint;
|
|
243
322
|
page.readRecovery = {
|
|
244
323
|
action: restoring ? "context-restore" : "targeted-review",
|
|
245
324
|
};
|
|
246
325
|
if (restoring) page.readRecovery.fileVersion = fileVersion;
|
|
247
326
|
} else {
|
|
248
327
|
const unread = entry.spans[0]?.[0] === 0 ? entry.spans[0][1] : 0;
|
|
249
|
-
if (unread < span.total) {
|
|
328
|
+
if (unread < span.total && !explicitRange) {
|
|
250
329
|
const requestedRange = page.range;
|
|
251
330
|
page = continuePage(unread);
|
|
252
331
|
if (page.error || !page.readSpan) return page;
|
|
@@ -257,6 +336,24 @@ export class ReadFileLoopGuard {
|
|
|
257
336
|
};
|
|
258
337
|
newContent = true;
|
|
259
338
|
} else {
|
|
339
|
+
const confirmedEnd =
|
|
340
|
+
entry.confirmedSpans[0]?.[0] === 0 ? entry.confirmedSpans[0][1] : 0;
|
|
341
|
+
if (
|
|
342
|
+
this.visiblePages !== null &&
|
|
343
|
+
!entry.confirmedSpans.some(
|
|
344
|
+
([start, end]) => start <= span.start && end >= span.end,
|
|
345
|
+
)
|
|
346
|
+
) {
|
|
347
|
+
return {
|
|
348
|
+
path: filePath,
|
|
349
|
+
fileVersion,
|
|
350
|
+
contentOmitted: true,
|
|
351
|
+
reachedEnd: false,
|
|
352
|
+
readProgress: { newContent: false },
|
|
353
|
+
readRecovery: { action: "pending-delivery" },
|
|
354
|
+
hint: "This range has a read pending in the current tool batch. Historical source coverage is recorded only after content reaches the model.",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
260
357
|
return {
|
|
261
358
|
path: filePath,
|
|
262
359
|
fileVersion,
|
|
@@ -264,7 +361,7 @@ export class ReadFileLoopGuard {
|
|
|
264
361
|
hashed: args.hashed === true,
|
|
265
362
|
alreadyRead: true,
|
|
266
363
|
contentOmitted: true,
|
|
267
|
-
reachedEnd:
|
|
364
|
+
reachedEnd: confirmedEnd >= span.total,
|
|
268
365
|
readProgress: { newContent: false },
|
|
269
366
|
readRecovery: { action: "use-findings" },
|
|
270
367
|
...(entry.summary?.outline
|
|
@@ -272,17 +369,36 @@ export class ReadFileLoopGuard {
|
|
|
272
369
|
: {}),
|
|
273
370
|
recoveryRead: {
|
|
274
371
|
path: filePath,
|
|
275
|
-
offset: 1,
|
|
372
|
+
offset: explicitRange ? page.range.startLine : 1,
|
|
276
373
|
limit: 80,
|
|
277
374
|
...(args.hashed === true ? { hashed: true } : {}),
|
|
278
375
|
...(args.raw === true ? { raw: true } : {}),
|
|
279
376
|
},
|
|
280
|
-
hint: "
|
|
377
|
+
hint: "This requested range was read earlier, but historical coverage does not mean its source is visible now. The runtime has not substituted an unrelated file range. Use source actually present in this request; recover missing details with a focused search or the bounded recoveryRead. Reading the file is not task completion.",
|
|
281
378
|
};
|
|
282
379
|
}
|
|
283
380
|
}
|
|
284
381
|
}
|
|
285
382
|
entry.spans = mergeSpan(entry.spans, page.readSpan);
|
|
383
|
+
entry.total = page.readSpan.total;
|
|
384
|
+
if (this.visiblePages === null) {
|
|
385
|
+
// Direct tool consumers without provider selection keep synchronous
|
|
386
|
+
// semantics. Agent runs observe the initial request before any reads.
|
|
387
|
+
entry.confirmedSpans = entry.spans.map((span) => [...span]);
|
|
388
|
+
} else {
|
|
389
|
+
entry.pendingPages.push({
|
|
390
|
+
page: {
|
|
391
|
+
path: page.path,
|
|
392
|
+
fileVersion: page.fileVersion,
|
|
393
|
+
content: page.content,
|
|
394
|
+
readSpan: { ...page.readSpan },
|
|
395
|
+
...(page.range ? { range: { ...page.range } } : {}),
|
|
396
|
+
...(page.nextRead ? { nextRead: { ...page.nextRead } } : {}),
|
|
397
|
+
},
|
|
398
|
+
reread,
|
|
399
|
+
restoring: restoringPage,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
286
402
|
entry.lastReadRequest = this.visibleRequest;
|
|
287
403
|
// Coverage alone cannot restore source code after compaction. Keep bounded
|
|
288
404
|
// observed pages for every file type, not only large Markdown outlines.
|
|
@@ -302,10 +418,12 @@ export class ReadFileLoopGuard {
|
|
|
302
418
|
].slice(-2);
|
|
303
419
|
}
|
|
304
420
|
const allRead =
|
|
305
|
-
entry.
|
|
421
|
+
entry.confirmedSpans[0]?.[0] === 0 &&
|
|
422
|
+
entry.confirmedSpans[0][1] >= page.readSpan.total;
|
|
306
423
|
// A read of the tail alone must never imply coverage of earlier sections.
|
|
307
424
|
const previousEnd = entry.summary?.readThrough || 0;
|
|
308
|
-
const updateCursor =
|
|
425
|
+
const updateCursor =
|
|
426
|
+
this.visiblePages === null && page.readSpan.end >= previousEnd;
|
|
309
427
|
entry.summary = {
|
|
310
428
|
...entry.summary,
|
|
311
429
|
path: filePath,
|
|
@@ -317,8 +435,16 @@ export class ReadFileLoopGuard {
|
|
|
317
435
|
}
|
|
318
436
|
: {}),
|
|
319
437
|
reachedEnd: allRead,
|
|
438
|
+
...(this.visiblePages !== null ? { pendingDelivery: true } : {}),
|
|
320
439
|
...(page.outline ? { outline: page.outline } : {}),
|
|
321
440
|
};
|
|
441
|
+
if (updateCursor) {
|
|
442
|
+
entry.confirmedCursorEnd = page.readSpan.end;
|
|
443
|
+
entry.confirmedCursor = {
|
|
444
|
+
lastReadRange: page.range,
|
|
445
|
+
nextRead: page.nextRead || null,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
322
448
|
this.progress.delete(key);
|
|
323
449
|
this.progress.set(key, entry);
|
|
324
450
|
// Discard pathological fragmented coverage instead of inventing coverage.
|
|
@@ -336,6 +462,13 @@ export class ReadFileLoopGuard {
|
|
|
336
462
|
// as another duplicate would immediately schedule another "one-turn"
|
|
337
463
|
// pause and prevent read_file from ever becoming available again.
|
|
338
464
|
if (isToolRecoveryPaused(result)) return;
|
|
465
|
+
if (
|
|
466
|
+
tool === "read_file" &&
|
|
467
|
+
result?.code === "CC_READ_SOURCE_NOT_DELIVERED"
|
|
468
|
+
) {
|
|
469
|
+
this.repeatedBatches = Math.max(this.repeatedBatches, 6);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
339
472
|
if (
|
|
340
473
|
!this.batch ||
|
|
341
474
|
result?.error ||
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { recoverCompleteSourceRead } from "./source-read-recovery.js";
|
|
2
|
+
|
|
1
3
|
/** Build a bounded read result with a cursor describing what was actually read. */
|
|
2
4
|
export function buildReadFilePage(
|
|
3
5
|
rendered,
|
|
@@ -61,7 +63,7 @@ export function buildReadFilePage(
|
|
|
61
63
|
const charEnd = truncated
|
|
62
64
|
? charStart + count
|
|
63
65
|
: lines.slice(0, end).reduce((n, line) => n + line.length + 1, 0);
|
|
64
|
-
|
|
66
|
+
const page = {
|
|
65
67
|
path: filePath,
|
|
66
68
|
...(fileVersion ? { fileVersion } : {}),
|
|
67
69
|
...(offset || limit || column > 1 || truncated
|
|
@@ -96,6 +98,7 @@ export function buildReadFilePage(
|
|
|
96
98
|
hashed: args.hashed === true,
|
|
97
99
|
content,
|
|
98
100
|
};
|
|
101
|
+
return recoverCompleteSourceRead(page, rendered, args, maxChars);
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
/** A bounded navigation index, not a substitute for reading the cited lines. */
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { parse } from "acorn";
|
|
2
|
+
import { canProjectHostToolExcerpt } from "./tool-result-preview.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A cut source container is not safe evidence. Recover its complete top-level
|
|
6
|
+
* statements from the very same read snapshot, with explicit range metadata.
|
|
7
|
+
* This does not exempt source from either projection or provider sanitization.
|
|
8
|
+
*/
|
|
9
|
+
export function recoverCompleteSourceRead(page, source, args, maxChars) {
|
|
10
|
+
if (
|
|
11
|
+
typeof page?.content !== "string" ||
|
|
12
|
+
typeof page.fileVersion !== "string" ||
|
|
13
|
+
!/\.(?:[cm]?js)$/iu.test(page.path || "") ||
|
|
14
|
+
args.hashed === true ||
|
|
15
|
+
!page.range ||
|
|
16
|
+
!page.readSpan ||
|
|
17
|
+
source.length > 512 * 1024 ||
|
|
18
|
+
canProjectHostToolExcerpt(page.content, "read_file")
|
|
19
|
+
)
|
|
20
|
+
return page;
|
|
21
|
+
|
|
22
|
+
let program;
|
|
23
|
+
let tokens = 0;
|
|
24
|
+
try {
|
|
25
|
+
program = parse(source, {
|
|
26
|
+
ecmaVersion: "latest",
|
|
27
|
+
sourceType: "module",
|
|
28
|
+
allowReturnOutsideFunction: true,
|
|
29
|
+
onToken() {
|
|
30
|
+
if (++tokens > 32768)
|
|
31
|
+
throw new RangeError("Source recovery token bound");
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
} catch {
|
|
35
|
+
// Malformed or oversized source retains the ordinary omission boundary.
|
|
36
|
+
return page;
|
|
37
|
+
}
|
|
38
|
+
const statements = program.body.filter(
|
|
39
|
+
(node) => node.start < page.readSpan.end && node.end > page.readSpan.start,
|
|
40
|
+
);
|
|
41
|
+
if (!statements.length) return page;
|
|
42
|
+
const start = Math.min(
|
|
43
|
+
page.readSpan.start,
|
|
44
|
+
source.lastIndexOf("\n", statements[0].start - 1) + 1,
|
|
45
|
+
);
|
|
46
|
+
const followingNewline = source.indexOf("\n", statements.at(-1).end);
|
|
47
|
+
const end = Math.max(
|
|
48
|
+
page.readSpan.end,
|
|
49
|
+
followingNewline < 0 ? source.length : followingNewline + 1,
|
|
50
|
+
);
|
|
51
|
+
const content = source.slice(start, end);
|
|
52
|
+
if (
|
|
53
|
+
content === page.content ||
|
|
54
|
+
!canProjectHostToolExcerpt(content, "read_file")
|
|
55
|
+
)
|
|
56
|
+
return page;
|
|
57
|
+
const startLine = 1 + (source.slice(0, start).match(/\n/gu) || []).length;
|
|
58
|
+
const endLine =
|
|
59
|
+
startLine +
|
|
60
|
+
(content.match(/\n/gu) || []).length -
|
|
61
|
+
(content.endsWith("\n") ? 1 : 0);
|
|
62
|
+
const recovered = {
|
|
63
|
+
...page,
|
|
64
|
+
content,
|
|
65
|
+
range: { startLine, endLine, totalLines: page.range.totalLines },
|
|
66
|
+
readSpan: { start, end, total: source.length },
|
|
67
|
+
sourceRecovery: {
|
|
68
|
+
action: "complete-source-container",
|
|
69
|
+
fileVersion: page.fileVersion,
|
|
70
|
+
requestedRange: { ...page.range },
|
|
71
|
+
requestedSpan: { ...page.readSpan },
|
|
72
|
+
note: "The requested excerpt cut a source container. These complete surrounding statements come from the same file version; the range and readSpan describe the content actually returned.",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
delete recovered.truncated;
|
|
76
|
+
delete recovered.size;
|
|
77
|
+
delete recovered.nextRead;
|
|
78
|
+
delete recovered.hint;
|
|
79
|
+
if (end < source.length) {
|
|
80
|
+
recovered.nextRead = { ...args, offset: endLine + 1, column: 1 };
|
|
81
|
+
recovered.hint = "Continue with nextRead only if more source is needed.";
|
|
82
|
+
}
|
|
83
|
+
// The read guard, host telemetry and delivery envelope still add metadata.
|
|
84
|
+
// Reuse the ordinary page's headroom: an almost-full recovery would be
|
|
85
|
+
// projected back to an omission by the final cap before reaching a provider.
|
|
86
|
+
return JSON.stringify(recovered).length <= Math.max(0, maxChars - 2048)
|
|
87
|
+
? recovered
|
|
88
|
+
: page;
|
|
89
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createEvidenceJsonTextBoundary,
|
|
3
3
|
AGENT_CODE_TEXT_RESULT_TOOLS,
|
|
4
|
+
EVIDENCE_JSON_POLICY,
|
|
4
5
|
} from "./evolution/evidence-json-text.js";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
6
7
|
|
|
@@ -32,8 +33,12 @@ const PREVIEW_HINT =
|
|
|
32
33
|
"Tool output truncated. Narrow the request: filter/count locally, select fields, search_files, or read_file with offset/limit. Omitted text is not full-output evidence.";
|
|
33
34
|
|
|
34
35
|
/** Check only host-created excerpts; original uncut tool text still reaches ingress. */
|
|
35
|
-
function
|
|
36
|
-
const scan = createEvidenceJsonTextBoundary((message) => new Error(message)
|
|
36
|
+
export function canProjectHostToolExcerpt(text, tool) {
|
|
37
|
+
const scan = createEvidenceJsonTextBoundary((message) => new Error(message), {
|
|
38
|
+
...EVIDENCE_JSON_POLICY,
|
|
39
|
+
sourceLiteralPolicy: "explicit-source-containers-v1",
|
|
40
|
+
maxSourceTokens: 32768,
|
|
41
|
+
});
|
|
37
42
|
const visit = (value, layer) => {
|
|
38
43
|
if (typeof value === "string") return project(value, layer);
|
|
39
44
|
if (Array.isArray(value)) return value.map((child) => visit(child, layer));
|
|
@@ -48,6 +53,7 @@ function completeExcerpt(text, tool) {
|
|
|
48
53
|
scan(value, {
|
|
49
54
|
layer,
|
|
50
55
|
codeText: AGENT_CODE_TEXT_RESULT_TOOLS.includes(tool),
|
|
56
|
+
sourceText: layer === 0,
|
|
51
57
|
visit,
|
|
52
58
|
plain: (plain) => plain,
|
|
53
59
|
equal: (left, right) => JSON.stringify(left) === JSON.stringify(right),
|
|
@@ -233,9 +239,13 @@ export function safeHostToolResultForModel(tool, result) {
|
|
|
233
239
|
let safe = result;
|
|
234
240
|
for (const field of fields) {
|
|
235
241
|
const text = result[field];
|
|
236
|
-
if (typeof text !== "string" ||
|
|
242
|
+
if (typeof text !== "string" || canProjectHostToolExcerpt(text, tool))
|
|
243
|
+
continue;
|
|
237
244
|
if (safe === result) safe = { ...result };
|
|
238
|
-
safe[field] =
|
|
245
|
+
safe[field] =
|
|
246
|
+
tool === "read_file"
|
|
247
|
+
? `Text omitted (${text.length} chars); this excerpt could not be safely projected. Read a complete surrounding statement or container; expand the range or read the full file within existing size limits when necessary. Omitted content has not been inspected.`
|
|
248
|
+
: omitted(text.length);
|
|
239
249
|
safe[`${field}Omitted`] = true;
|
|
240
250
|
safe[`${field}OmittedChars`] = text.length;
|
|
241
251
|
}
|
|
@@ -259,7 +269,10 @@ export function boundedCommandOutput(output, max, tool = "run_code") {
|
|
|
259
269
|
output,
|
|
260
270
|
Math.max(1, Math.floor(max / 2)),
|
|
261
271
|
);
|
|
262
|
-
if (
|
|
272
|
+
if (
|
|
273
|
+
JSON.stringify(preview).length <= max &&
|
|
274
|
+
canProjectHostToolExcerpt(preview, tool)
|
|
275
|
+
)
|
|
263
276
|
return preview;
|
|
264
277
|
} else if (!/[{}\[\]"']/u.test(output)) {
|
|
265
278
|
let low = 0;
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -3544,6 +3544,7 @@ async function startAgentReplInWorkspaceOwned(
|
|
|
3544
3544
|
});
|
|
3545
3545
|
if (feature("PROMPT_COMPRESSOR") || contextMemoryCutover.canonical) {
|
|
3546
3546
|
_compressor = new PromptCompressor({
|
|
3547
|
+
contextMemoryModelWindowTokens: options.contextMemoryModelWindowTokens,
|
|
3547
3548
|
model,
|
|
3548
3549
|
provider,
|
|
3549
3550
|
llmQuery: async (prompt) => {
|
|
@@ -9852,6 +9853,7 @@ async function startAgentReplInWorkspaceOwned(
|
|
|
9852
9853
|
usageEvents,
|
|
9853
9854
|
thinking: reasoning,
|
|
9854
9855
|
} = await agentLoop(messages, {
|
|
9856
|
+
contextMemoryModelWindowTokens: options.contextMemoryModelWindowTokens,
|
|
9855
9857
|
...captureSkillRuntimeDependencies(options),
|
|
9856
9858
|
...liveOpts,
|
|
9857
9859
|
waitForOutput: _replOutputFlow.wait,
|