@namzu/sdk 39.0.0 → 40.0.0
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 +151 -0
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +20 -6
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +8 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +12 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +11 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +14 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +2 -3
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/registry/tool/portable.d.ts +65 -0
- package/dist/registry/tool/portable.d.ts.map +1 -0
- package/dist/registry/tool/portable.js +244 -0
- package/dist/registry/tool/portable.js.map +1 -0
- package/dist/registry/tool/schema.d.ts +32 -5
- package/dist/registry/tool/schema.d.ts.map +1 -1
- package/dist/registry/tool/schema.js +35 -9
- package/dist/registry/tool/schema.js.map +1 -1
- package/dist/registry/toolset/catalog.js +8 -8
- package/dist/registry/toolset/catalog.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +215 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -0
- package/dist/runtime/jobs/awaited-jobs.js +259 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +33 -2
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +37 -0
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +28 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +39 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
- package/dist/runtime/query/file-evidence-context.js +159 -43
- package/dist/runtime/query/file-evidence-context.js.map +1 -1
- package/dist/runtime/query/file-evidence-replay.d.ts +260 -0
- package/dist/runtime/query/file-evidence-replay.d.ts.map +1 -0
- package/dist/runtime/query/file-evidence-replay.js +647 -0
- package/dist/runtime/query/file-evidence-replay.js.map +1 -0
- package/dist/runtime/query/file-evidence-seed.d.ts +50 -0
- package/dist/runtime/query/file-evidence-seed.d.ts.map +1 -0
- package/dist/runtime/query/file-evidence-seed.js +100 -0
- package/dist/runtime/query/file-evidence-seed.js.map +1 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +94 -2
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +87 -9
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +193 -28
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +5 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.d.ts +14 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +18 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/repeat-call.d.ts +17 -4
- package/dist/runtime/query/repeat-call.d.ts.map +1 -1
- package/dist/runtime/query/repeat-call.js +26 -19
- package/dist/runtime/query/repeat-call.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +11 -1
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +12 -1
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts +48 -2
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +102 -10
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +4 -10
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit-apply.d.ts +126 -0
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -0
- package/dist/tools/builtins/edit-apply.js +360 -0
- package/dist/tools/builtins/edit-apply.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts +143 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +37 -219
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/index.d.ts +1 -0
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +9 -3
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/job.js +1 -1
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +2 -2
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +50 -65
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +56 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -0
- package/dist/tools/builtins/read-render.js +73 -0
- package/dist/tools/builtins/read-render.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +67 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.js +108 -0
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -0
- package/dist/tools/builtins/wait-for-job.d.ts +6 -0
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -0
- package/dist/tools/builtins/wait-for-job.js +162 -0
- package/dist/tools/builtins/wait-for-job.js.map +1 -0
- package/dist/tools/builtins/write-file.js +5 -0
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +1 -7
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +88 -10
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/run/entity.d.ts +13 -0
- package/dist/types/run/entity.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +15 -14
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +109 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/utils/env.d.ts +19 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +25 -0
- package/dist/utils/env.js.map +1 -0
- package/package.json +1 -1
- package/src/connector/mcp/adapter.ts +20 -6
- package/src/manager/run/persistence.ts +12 -0
- package/src/public-runtime.ts +9 -1
- package/src/public-tools.ts +18 -0
- package/src/registry/tool/execute.ts +2 -4
- package/src/registry/tool/portable.ts +264 -0
- package/src/registry/tool/schema.ts +38 -8
- package/src/registry/toolset/catalog.ts +8 -9
- package/src/runtime/jobs/awaited-jobs.ts +271 -0
- package/src/runtime/jobs/registry.ts +50 -0
- package/src/runtime/query/executor.ts +49 -1
- package/src/runtime/query/file-evidence-context.ts +190 -46
- package/src/runtime/query/file-evidence-replay.ts +776 -0
- package/src/runtime/query/file-evidence-seed.ts +126 -0
- package/src/runtime/query/index.ts +104 -2
- package/src/runtime/query/iteration/index.ts +202 -28
- package/src/runtime/query/iteration/phases/context.ts +10 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +4 -0
- package/src/runtime/query/plugin-hooks.ts +20 -0
- package/src/runtime/query/repeat-call.ts +28 -18
- package/src/runtime/query/steering.ts +11 -0
- package/src/runtime/query/tooling.ts +3 -0
- package/src/scheduler/completion-inbox.ts +105 -9
- package/src/tools/builtins/bash.ts +4 -10
- package/src/tools/builtins/edit-apply.ts +456 -0
- package/src/tools/builtins/edit.ts +39 -270
- package/src/tools/builtins/index.ts +9 -3
- package/src/tools/builtins/job.ts +1 -1
- package/src/tools/builtins/read-file.ts +56 -77
- package/src/tools/builtins/read-render.ts +104 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +179 -0
- package/src/tools/builtins/wait-for-job.ts +184 -0
- package/src/tools/builtins/write-file.ts +5 -0
- package/src/tools/coordinator/index.ts +1 -7
- package/src/tools/file-read-tracker.ts +85 -7
- package/src/types/message/index.ts +2 -0
- package/src/types/run/entity.ts +14 -0
- package/src/types/sandbox/index.ts +15 -14
- package/src/types/tool/index.ts +104 -0
- package/src/utils/env.ts +23 -0
|
@@ -3,6 +3,7 @@ import { spawn } from 'node:child_process'
|
|
|
3
3
|
import { SANDBOX_KILL_GRACE_MS } from '../../constants/sandbox/index.js'
|
|
4
4
|
import { killTree } from '../../process/kill-tree.js'
|
|
5
5
|
import { scrubInheritedEnv } from '../../tools/env-scrub.js'
|
|
6
|
+
import { awaitWithAbort } from '../../utils/await-with-abort.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Work that outlives a tool call, owned by whoever started it.
|
|
@@ -310,6 +311,32 @@ export class BackgroundJobRegistry {
|
|
|
310
311
|
return entry.record
|
|
311
312
|
}
|
|
312
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Await one job's exit — the public counterpart to `onExit`, for a
|
|
316
|
+
* caller that wants a single result rather than a standing
|
|
317
|
+
* subscription. Resolves at once for a job that has already stopped, so
|
|
318
|
+
* a caller that lost the race against a fast-finishing job never blocks
|
|
319
|
+
* on a promise that would otherwise never settle.
|
|
320
|
+
*
|
|
321
|
+
* `signal` is honoured: an aborted wait rejects and detaches rather than
|
|
322
|
+
* holding the internal exit promise's continuation open for a job that
|
|
323
|
+
* may run for another hour. That only ends the WAIT — the job itself is
|
|
324
|
+
* untouched either way, exactly as a timed-out `kill`-less wait leaves
|
|
325
|
+
* it. See `wait-for-job-bounds.ts`, the first caller.
|
|
326
|
+
*/
|
|
327
|
+
waitForExit(id: string, opts: { signal?: AbortSignal } = {}): Promise<BackgroundJob> {
|
|
328
|
+
const entry = this.jobs.get(id)
|
|
329
|
+
if (!entry) throw new UnknownBackgroundJobError({ id })
|
|
330
|
+
if (entry.record.status !== 'running') return Promise.resolve(entry.record)
|
|
331
|
+
// `entry` is the same object the `finalize` closure in `start()`
|
|
332
|
+
// mutates in place, so reading `entry.record` after `exit` settles
|
|
333
|
+
// sees the final status — the same trick `kill()` already relies on.
|
|
334
|
+
return awaitWithAbort(
|
|
335
|
+
entry.exit.then(() => entry.record),
|
|
336
|
+
opts.signal,
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
313
340
|
/**
|
|
314
341
|
* Output since `fromOffset`, with what the cap dropped stated.
|
|
315
342
|
*
|
|
@@ -410,6 +437,17 @@ export function bindOwner(
|
|
|
410
437
|
readonly workingDirectory: string
|
|
411
438
|
readonly env?: Record<string, string>
|
|
412
439
|
}) => JobProcess
|
|
440
|
+
/**
|
|
441
|
+
* Be told that the model said it is waiting on a job's exit.
|
|
442
|
+
*
|
|
443
|
+
* The registry does not keep this: wait-intent belongs to the RUN that
|
|
444
|
+
* expressed it, not to a registry a host may share across runs and
|
|
445
|
+
* sessions. The run passes its own recorder here — `AwaitedJobs`, which
|
|
446
|
+
* is what the iteration loop holds open for. Absent means nobody is
|
|
447
|
+
* listening, and `markAwaited` is then absent from the bound ref rather
|
|
448
|
+
* than present and silently doing nothing.
|
|
449
|
+
*/
|
|
450
|
+
readonly onAwaited?: (id: string) => void
|
|
413
451
|
} = {},
|
|
414
452
|
) {
|
|
415
453
|
const mine = (id: string): BackgroundJob => {
|
|
@@ -443,6 +481,18 @@ export function bindOwner(
|
|
|
443
481
|
mine(id)
|
|
444
482
|
return registry.read(id, opts ?? {})
|
|
445
483
|
},
|
|
484
|
+
waitForExit: (id: string, opts?: { signal?: AbortSignal }) => {
|
|
485
|
+
mine(id)
|
|
486
|
+
return registry.waitForExit(id, opts ?? {})
|
|
487
|
+
},
|
|
488
|
+
...(defaults.onAwaited
|
|
489
|
+
? {
|
|
490
|
+
markAwaited: (id: string) => {
|
|
491
|
+
mine(id)
|
|
492
|
+
defaults.onAwaited?.(id)
|
|
493
|
+
},
|
|
494
|
+
}
|
|
495
|
+
: {}),
|
|
446
496
|
kill: async (id: string) => {
|
|
447
497
|
mine(id)
|
|
448
498
|
return await registry.kill(id)
|
|
@@ -51,6 +51,8 @@ import type { Logger } from '../../utils/logger.js'
|
|
|
51
51
|
import { compressShellOutput } from '../../utils/shell-compress.js'
|
|
52
52
|
import { type BackgroundJobRegistry, type JobProcess, bindOwner } from '../jobs/registry.js'
|
|
53
53
|
import { describeVisibleFileEvidence } from './file-evidence-context.js'
|
|
54
|
+
import { seedObservationLedger } from './file-evidence-seed.js'
|
|
55
|
+
import { skippedToolResultText } from './plugin-hooks.js'
|
|
54
56
|
import type { ToolResultObservation } from './project-instructions.js'
|
|
55
57
|
import { ToolCallBudget, assertMaxToolCalls } from './tool-call-budget.js'
|
|
56
58
|
import {
|
|
@@ -334,6 +336,16 @@ export interface ToolExecutorConfig {
|
|
|
334
336
|
* session and stops them itself when the session ends.
|
|
335
337
|
*/
|
|
336
338
|
backgroundJobOwner?: string
|
|
339
|
+
/**
|
|
340
|
+
* Where `wait_for_job` records that the model is waiting on a job.
|
|
341
|
+
*
|
|
342
|
+
* A callback rather than the recorder itself: the executor's part is to
|
|
343
|
+
* hand the tools a bound ref, and what the run does with the intent —
|
|
344
|
+
* hold itself open for the job — is the iteration loop's business. Absent
|
|
345
|
+
* means the bound ref has no `markAwaited` at all, so a host that wires no
|
|
346
|
+
* recorder gets no hold rather than a marking call that goes nowhere.
|
|
347
|
+
*/
|
|
348
|
+
onJobAwaited?: (id: string) => void
|
|
337
349
|
|
|
338
350
|
/**
|
|
339
351
|
* Where the `skill` tool reads from.
|
|
@@ -514,6 +526,8 @@ export class ToolExecutor {
|
|
|
514
526
|
/** Set per turn by the orchestrator; see {@link setStepAllowedTools}. */
|
|
515
527
|
private stepAllowedTools?: readonly string[]
|
|
516
528
|
private readonly fileReadTracker: FileReadTracker
|
|
529
|
+
/** A ledger is rebuilt from history at most once; a second pass would re-append its chains. */
|
|
530
|
+
private fileObservationsSeeded = false
|
|
517
531
|
|
|
518
532
|
constructor(
|
|
519
533
|
config: ToolExecutorConfig,
|
|
@@ -547,6 +561,39 @@ export class ToolExecutor {
|
|
|
547
561
|
this.config = { ...this.config, sandbox }
|
|
548
562
|
}
|
|
549
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Rebuild this run's observation ledger from history a resume restored.
|
|
566
|
+
*
|
|
567
|
+
* Once, and only from a history that has already been repaired — the ledger
|
|
568
|
+
* has to describe what the model is about to be shown, not what was
|
|
569
|
+
* checkpointed before the repair removed an abandoned call. `sandboxed` is
|
|
570
|
+
* passed rather than read off this executor's config because a resumed run
|
|
571
|
+
* restores its history before it acquires a sandbox, so the config does not
|
|
572
|
+
* know yet what the run's tool paths will be keyed on.
|
|
573
|
+
*
|
|
574
|
+
* Awaited, and the only filesystem work anywhere in this feature: the seed
|
|
575
|
+
* has to write its entries under the keys the mutation tools will look them
|
|
576
|
+
* up under, which on a host means resolving each path through its symlinks
|
|
577
|
+
* the way `write` and `edit` do. No file's content is read.
|
|
578
|
+
*/
|
|
579
|
+
async seedFileObservations(messages: readonly Message[], sandboxed: boolean): Promise<void> {
|
|
580
|
+
if (this.fileObservationsSeeded) return
|
|
581
|
+
this.fileObservationsSeeded = true
|
|
582
|
+
const report = await seedObservationLedger(messages, this.fileReadTracker, {
|
|
583
|
+
workingDirectory: this.config.workingDirectory,
|
|
584
|
+
...(this.config.additionalDirectories
|
|
585
|
+
? { additionalDirectories: this.config.additionalDirectories }
|
|
586
|
+
: {}),
|
|
587
|
+
sandboxed,
|
|
588
|
+
})
|
|
589
|
+
this.log.info('Rebuilt the file observation ledger from restored history', {
|
|
590
|
+
[NAMZU.RUN_ID]: this.config.runId,
|
|
591
|
+
'namzu.files.witnessed': report.pathsWitnessed,
|
|
592
|
+
'namzu.files.seen': report.pathsSeen,
|
|
593
|
+
'namzu.files.replayed_units': report.unitsReplayed,
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
|
|
550
597
|
/** Request-only evidence from the same ledger used by mutation admission. No filesystem I/O. */
|
|
551
598
|
describeFileEvidence(messages: readonly Message[]): string | undefined {
|
|
552
599
|
return describeVisibleFileEvidence(
|
|
@@ -1342,6 +1389,7 @@ export class ToolExecutor {
|
|
|
1342
1389
|
) as JobProcess,
|
|
1343
1390
|
}
|
|
1344
1391
|
: {}),
|
|
1392
|
+
...(this.config.onJobAwaited ? { onAwaited: this.config.onJobAwaited } : {}),
|
|
1345
1393
|
},
|
|
1346
1394
|
),
|
|
1347
1395
|
}
|
|
@@ -2265,7 +2313,7 @@ export class ToolExecutor {
|
|
|
2265
2313
|
return {
|
|
2266
2314
|
kind: 'skip',
|
|
2267
2315
|
input: currentInput,
|
|
2268
|
-
output:
|
|
2316
|
+
output: skippedToolResultText(toolName, result.reason),
|
|
2269
2317
|
}
|
|
2270
2318
|
case 'error':
|
|
2271
2319
|
return {
|
|
@@ -1,9 +1,42 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import { isClearedToolResult } from '../../compaction/tool-result-editing.js'
|
|
3
1
|
import { fingerprintContent } from '../../tools/builtins/content-fingerprint.js'
|
|
4
|
-
import {
|
|
5
|
-
import type { Message
|
|
2
|
+
import { ReadFileTool } from '../../tools/builtins/read-file.js'
|
|
3
|
+
import type { Message } from '../../types/message/index.js'
|
|
6
4
|
import type { FileReadTracker } from '../../types/tool/index.js'
|
|
5
|
+
import {
|
|
6
|
+
MAX_EDIT_CALLS,
|
|
7
|
+
MAX_PATH_UNITS,
|
|
8
|
+
MAX_WITNESSED_PATHS,
|
|
9
|
+
type ReplayBudget,
|
|
10
|
+
type VisibleHistory,
|
|
11
|
+
createReplayBudget,
|
|
12
|
+
indexVisibleHistory,
|
|
13
|
+
knownStale,
|
|
14
|
+
lexicalFileKeys,
|
|
15
|
+
replayHop,
|
|
16
|
+
spend,
|
|
17
|
+
visibleCall,
|
|
18
|
+
visibleEdit,
|
|
19
|
+
visibleWrite,
|
|
20
|
+
} from './file-evidence-replay.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Receipt text a read entry may point at.
|
|
24
|
+
*
|
|
25
|
+
* The same class of bound as the one on a write call's arguments, and set to
|
|
26
|
+
* the same number for the same reason: past it the text is not read at all. A
|
|
27
|
+
* bigger file is simply not admitted this way — the model re-reads it, which is
|
|
28
|
+
* what it does today.
|
|
29
|
+
*/
|
|
30
|
+
const MAX_RECEIPT_UNITS = 32_000
|
|
31
|
+
|
|
32
|
+
interface FileEvidence {
|
|
33
|
+
readonly path: string
|
|
34
|
+
/** Absent on the write-rooted entries; `'read'` on a body that is a receipt. */
|
|
35
|
+
readonly kind?: 'read'
|
|
36
|
+
readonly bodyInCall: string
|
|
37
|
+
readonly editsInCalls?: readonly string[]
|
|
38
|
+
readonly observedFingerprint: string
|
|
39
|
+
}
|
|
7
40
|
|
|
8
41
|
/** Join visible call bodies, successful receipts and this executor's observation ledger. */
|
|
9
42
|
export function describeVisibleFileEvidence(
|
|
@@ -14,52 +47,163 @@ export function describeVisibleFileEvidence(
|
|
|
14
47
|
): string | undefined {
|
|
15
48
|
// Execution-owned witnesses survive transparent wrappers; a name alone proves nothing.
|
|
16
49
|
if (!tracker.writeCallId || !tracker.fingerprint) return
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
const files = new Map<string, { path: string; bodyInCall: string; observedFingerprint: string }>()
|
|
29
|
-
for (const [id, call] of calls) {
|
|
30
|
-
const receipt = results.get(id)
|
|
31
|
-
if (
|
|
32
|
-
!call ||
|
|
33
|
-
call.function.name !== 'write' ||
|
|
34
|
-
call.metadata?.inputTruncated ||
|
|
35
|
-
call.function.arguments.length > 32_000 ||
|
|
36
|
-
id.length > 256 ||
|
|
37
|
-
!receipt ||
|
|
38
|
-
receipt.role !== 'tool' ||
|
|
39
|
-
receipt.isError ||
|
|
40
|
-
typeof receipt.content !== 'string' ||
|
|
41
|
-
isClearedToolResult(receipt.content)
|
|
42
|
-
)
|
|
43
|
-
continue
|
|
50
|
+
// Lexical, as this projection has always looked entries up: a spelling that
|
|
51
|
+
// does not resolve to the tool's canonical key finds nothing and the path is
|
|
52
|
+
// simply not admitted. Writing the ledger is the side that has to key the
|
|
53
|
+
// way the tools do — see `file-evidence-seed.ts`.
|
|
54
|
+
const history = indexVisibleHistory(messages, lexicalFileKeys(workingDirectory, sandboxed))
|
|
55
|
+
const budget = createReplayBudget()
|
|
56
|
+
const files = new Map<string, FileEvidence>()
|
|
57
|
+
for (const [id, call] of history.calls) {
|
|
58
|
+
const name = call?.function.name
|
|
59
|
+
if (name !== 'write' && name !== 'edit' && name !== 'read') continue
|
|
44
60
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const admitted =
|
|
62
|
+
name === 'write'
|
|
63
|
+
? writeEvidence(id, tracker, history)
|
|
64
|
+
: name === 'edit'
|
|
65
|
+
? chainEvidence(id, tracker, history, budget)
|
|
66
|
+
: readEvidence(id, tracker, history)
|
|
67
|
+
if (!admitted) continue
|
|
68
|
+
// A write-rooted entry outranks a read of the same file: it names a
|
|
69
|
+
// body the model composed itself, and edits can be replayed onto it.
|
|
70
|
+
// The ledger already withholds a read witness where one survives; this
|
|
71
|
+
// keeps a read that ran BEFORE the write from taking the path back.
|
|
72
|
+
const held = files.get(admitted.key)
|
|
73
|
+
if (admitted.evidence.kind === 'read' && held && held.kind !== 'read') continue
|
|
74
|
+
files.delete(admitted.key)
|
|
75
|
+
files.set(admitted.key, admitted.evidence)
|
|
76
|
+
if (files.size > MAX_WITNESSED_PATHS) files.delete(files.keys().next().value as string)
|
|
59
77
|
} catch {
|
|
60
78
|
// Malformed retained inputs or a custom tracker cannot establish evidence.
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
81
|
if (files.size === 0) return
|
|
64
|
-
return `Visible file evidence (this request only): each complete body
|
|
82
|
+
return `Visible file evidence (this request only): each entry's current body is the complete body in the named successful write call, with the edit calls in editsInCalls — when present — applied in that order. An entry marked kind:"read" instead names a successful read call whose own receipt shows the file WHOLE, rendered with every line behind its own N<tab> prefix; that receipt is the body, and such an entry never carries editsInCalls. This runtime performed that reconstruction and checked it against its own file observation; it is not a derivation left to you. Reuse the body for a targeted edit; a read solely to recall it is unnecessary. This is NOT a fresh disk check, and after a resume the observation behind an entry may itself have been rebuilt from this conversation's own earlier calls rather than made while this process ran. Built-in edit/write still compare the disk body at mutation admission and refuse observed drift; on refusal inspect the current file and replan. Missing entries establish nothing.\n${JSON.stringify([...files.values()])}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A full body that arrived whole in one call and is still the file's body. */
|
|
86
|
+
function writeEvidence(
|
|
87
|
+
id: string,
|
|
88
|
+
tracker: FileReadTracker,
|
|
89
|
+
history: VisibleHistory,
|
|
90
|
+
): { key: string; evidence: FileEvidence } | undefined {
|
|
91
|
+
const written = visibleWrite(history, id)
|
|
92
|
+
if (!written || knownStale(tracker, written.key)) return
|
|
93
|
+
const fingerprint = fingerprintContent(written.body)
|
|
94
|
+
if (
|
|
95
|
+
tracker.writeCallId?.(written.key) !== id ||
|
|
96
|
+
tracker.fingerprint?.(written.key) !== fingerprint
|
|
97
|
+
)
|
|
98
|
+
return
|
|
99
|
+
return {
|
|
100
|
+
key: written.key,
|
|
101
|
+
evidence: {
|
|
102
|
+
path: written.path,
|
|
103
|
+
bodyInCall: id,
|
|
104
|
+
observedFingerprint: fingerprint,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A full body plus the visible edits since, replayed and then checked.
|
|
111
|
+
*
|
|
112
|
+
* Only the chain's TIP establishes the path: a middle hop is validated as part
|
|
113
|
+
* of the walk below, and admitting one on its own would claim the file stops
|
|
114
|
+
* at an edit later calls have already moved past. The replay runs the same
|
|
115
|
+
* apply core the tool ran at mutation time, over call arguments that are
|
|
116
|
+
* already in the conversation — no filesystem read, and the result is compared
|
|
117
|
+
* to the ledger's disk-derived fingerprint, never written into it.
|
|
118
|
+
*/
|
|
119
|
+
function chainEvidence(
|
|
120
|
+
id: string,
|
|
121
|
+
tracker: FileReadTracker,
|
|
122
|
+
history: VisibleHistory,
|
|
123
|
+
budget: ReplayBudget,
|
|
124
|
+
): { key: string; evidence: FileEvidence } | undefined {
|
|
125
|
+
const tip = visibleEdit(history, id)
|
|
126
|
+
if (!tip || knownStale(tracker, tip.key)) return
|
|
127
|
+
const key = tip.key
|
|
128
|
+
const chain = tracker.editChain?.(key)
|
|
129
|
+
const observed = tracker.fingerprint?.(key)
|
|
130
|
+
if (!chain || observed === undefined || chain.editCallIds.at(-1) !== id) return
|
|
131
|
+
// The call bound before the walk, the unit bound before each operation it
|
|
132
|
+
// pays for. Neither is measured by doing the work it exists to refuse.
|
|
133
|
+
if (chain.editCallIds.length > MAX_EDIT_CALLS) return
|
|
134
|
+
const rootId = chain.rootWriteCallId
|
|
135
|
+
const root = visibleWrite(history, rootId)
|
|
136
|
+
// A root naming a different file is an ambiguity, not a base: the ledger
|
|
137
|
+
// and the visible call disagree about what was written where.
|
|
138
|
+
if (!root || root.key !== key) return
|
|
139
|
+
let content = root.body
|
|
140
|
+
if (!spend(budget, content.length)) return
|
|
141
|
+
for (const editId of chain.editCallIds) {
|
|
142
|
+
const hop = editId === id ? tip : visibleEdit(history, editId)
|
|
143
|
+
if (!hop || hop.key !== key) return
|
|
144
|
+
// Each of this hop's operations is measured against the body it is about
|
|
145
|
+
// to be applied to, charged, and only then applied — so a hop over the
|
|
146
|
+
// ceiling is refused before it is built rather than after, and one that
|
|
147
|
+
// fits is never turned away for a ceiling that guessed high. A hop the
|
|
148
|
+
// budget refuses, or one whose anchors no longer match, reconstructs
|
|
149
|
+
// nothing and withholds the whole path.
|
|
150
|
+
const replayed = replayHop(content, hop.input, budget)
|
|
151
|
+
if (replayed === undefined) return
|
|
152
|
+
content = replayed
|
|
153
|
+
}
|
|
154
|
+
if (fingerprintContent(content) !== observed) return
|
|
155
|
+
return {
|
|
156
|
+
key,
|
|
157
|
+
evidence: {
|
|
158
|
+
path: root.path,
|
|
159
|
+
bodyInCall: rootId,
|
|
160
|
+
editsInCalls: [...chain.editCallIds],
|
|
161
|
+
observedFingerprint: observed,
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A body the model can still see because a read of the whole file put it there.
|
|
168
|
+
*
|
|
169
|
+
* Structurally unlike the two above, and deliberately so. They name a body the
|
|
170
|
+
* model itself composed, in arguments this projection can parse and replay; this
|
|
171
|
+
* names a RESULT, and the only thing anyone is allowed to do with it is compare
|
|
172
|
+
* it. The tool fingerprinted the exact string it emitted, and the entry is
|
|
173
|
+
* admitted only while the receipt still fingerprints to that — so a receipt the
|
|
174
|
+
* output budget elided or spilled, one compaction cleared, or one changed in any
|
|
175
|
+
* other way withholds the path rather than pointing the model at a body that is
|
|
176
|
+
* no longer in front of it. Nothing here undoes the line numbering to recover
|
|
177
|
+
* text: the numbering is what makes the comparison conclusive, not an encoding
|
|
178
|
+
* to be reversed.
|
|
179
|
+
*
|
|
180
|
+
* No chain, ever. A read roots nothing, so an entry here is the file as that one
|
|
181
|
+
* call showed it, and the ledger drops the witness the moment an edit lands.
|
|
182
|
+
*/
|
|
183
|
+
function readEvidence(
|
|
184
|
+
id: string,
|
|
185
|
+
tracker: FileReadTracker,
|
|
186
|
+
history: VisibleHistory,
|
|
187
|
+
): { key: string; evidence: FileEvidence } | undefined {
|
|
188
|
+
if (!tracker.readWitness) return
|
|
189
|
+
const call = visibleCall(history, id, 'read')
|
|
190
|
+
const shown = history.results.get(id)?.content
|
|
191
|
+
if (!call || typeof shown !== 'string' || shown.length > MAX_RECEIPT_UNITS) return
|
|
192
|
+
const input = ReadFileTool.inputSchema.safeParse(JSON.parse(call.function.arguments))
|
|
193
|
+
if (!input.success || input.data.path.length > MAX_PATH_UNITS) return
|
|
194
|
+
const key = history.keyOf(input.data.path)
|
|
195
|
+
if (key === undefined || knownStale(tracker, key)) return
|
|
196
|
+
const witness = tracker.readWitness(key)
|
|
197
|
+
const observed = tracker.fingerprint?.(key)
|
|
198
|
+
if (!witness || witness.callId !== id || observed === undefined) return
|
|
199
|
+
if (fingerprintContent(shown) !== witness.renderedFingerprint) return
|
|
200
|
+
return {
|
|
201
|
+
key,
|
|
202
|
+
evidence: {
|
|
203
|
+
path: input.data.path,
|
|
204
|
+
kind: 'read',
|
|
205
|
+
bodyInCall: id,
|
|
206
|
+
observedFingerprint: observed,
|
|
207
|
+
},
|
|
208
|
+
}
|
|
65
209
|
}
|