@nathapp/nax-agent 0.1.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 +24 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/coding-tools/coding-tool-bash.d.ts +46 -0
- package/dist/coding-tools/coding-tool-bash.js +44 -0
- package/dist/coding-tools/coding-tool-extras.d.ts +35 -0
- package/dist/coding-tools/coding-tool-extras.js +56 -0
- package/dist/coding-tools/coding-tool-sandbox.d.ts +74 -0
- package/dist/coding-tools/coding-tool-sandbox.js +215 -0
- package/dist/coding-tools/coding-tool-support.d.ts +132 -0
- package/dist/coding-tools/coding-tool-support.js +159 -0
- package/dist/coding-tools/universal-coding-tools.d.ts +11 -0
- package/dist/coding-tools/universal-coding-tools.js +5 -0
- package/dist/command-interceptor/index.d.ts +55 -0
- package/dist/command-interceptor/index.js +60 -0
- package/dist/command-interceptor/shell.d.ts +36 -0
- package/dist/command-interceptor/shell.js +137 -0
- package/dist/command-safety/build.d.ts +19 -0
- package/dist/command-safety/build.js +47 -0
- package/dist/command-safety/guard.d.ts +32 -0
- package/dist/command-safety/guard.js +179 -0
- package/dist/command-safety/identifiers.d.ts +24 -0
- package/dist/command-safety/identifiers.js +20 -0
- package/dist/command-safety/index.d.ts +17 -0
- package/dist/command-safety/index.js +11 -0
- package/dist/command-safety/questions.d.ts +19 -0
- package/dist/command-safety/questions.js +60 -0
- package/dist/command-safety/row.d.ts +2 -0
- package/dist/command-safety/row.js +14 -0
- package/dist/command-safety/rule-scorer.d.ts +9 -0
- package/dist/command-safety/rule-scorer.js +391 -0
- package/dist/command-safety/shadow.d.ts +33 -0
- package/dist/command-safety/shadow.js +188 -0
- package/dist/command-safety/systemone-client.d.ts +16 -0
- package/dist/command-safety/systemone-client.js +92 -0
- package/dist/command-safety/tap.d.ts +37 -0
- package/dist/command-safety/tap.js +77 -0
- package/dist/command-safety/temp-only.d.ts +6 -0
- package/dist/command-safety/temp-only.js +215 -0
- package/dist/command-safety/tmp-write.d.ts +2 -0
- package/dist/command-safety/tmp-write.js +112 -0
- package/dist/command-safety/types.d.ts +185 -0
- package/dist/command-safety/types.js +21 -0
- package/dist/config/bash-approval.d.ts +33 -0
- package/dist/config/bash-approval.js +30 -0
- package/dist/config/catalog-overrides.d.ts +94 -0
- package/dist/config/catalog-overrides.js +7 -0
- package/dist/config/native-agent/index.d.ts +8 -0
- package/dist/config/native-agent/index.js +8 -0
- package/dist/config/schemas-sandbox.d.ts +31 -0
- package/dist/config/schemas-sandbox.js +45 -0
- package/dist/cost/core/index.d.ts +8 -0
- package/dist/cost/core/index.js +7 -0
- package/dist/cost/estimate.d.ts +34 -0
- package/dist/cost/estimate.js +65 -0
- package/dist/cost/model-spec.d.ts +28 -0
- package/dist/cost/model-spec.js +29 -0
- package/dist/cost/standard-types.d.ts +9 -0
- package/dist/cost/standard-types.js +1 -0
- package/dist/cost/usage-math.d.ts +18 -0
- package/dist/cost/usage-math.js +46 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +21 -0
- package/dist/infra/agent-logger.d.ts +22 -0
- package/dist/infra/agent-logger.js +19 -0
- package/dist/infra/credentials-config.d.ts +20 -0
- package/dist/infra/credentials-config.js +15 -0
- package/dist/infra/errors.d.ts +9 -0
- package/dist/infra/errors.js +11 -0
- package/dist/infra/index.d.ts +7 -0
- package/dist/infra/index.js +7 -0
- package/dist/infra/nax-error.d.ts +10 -0
- package/dist/infra/nax-error.js +16 -0
- package/dist/infra/spin-breaker/hash.d.ts +2 -0
- package/dist/infra/spin-breaker/hash.js +5 -0
- package/dist/infra/spin-breaker/index.d.ts +120 -0
- package/dist/infra/spin-breaker/index.js +340 -0
- package/dist/internal/agent-output-env.d.ts +44 -0
- package/dist/internal/agent-output-env.js +57 -0
- package/dist/internal/argv-exec.d.ts +58 -0
- package/dist/internal/argv-exec.js +224 -0
- package/dist/internal/bounded-io.d.ts +24 -0
- package/dist/internal/bounded-io.js +51 -0
- package/dist/internal/command-spec/index.d.ts +29 -0
- package/dist/internal/command-spec/index.js +36 -0
- package/dist/internal/describe-value-type.d.ts +8 -0
- package/dist/internal/describe-value-type.js +14 -0
- package/dist/internal/exec-framing.d.ts +18 -0
- package/dist/internal/exec-framing.js +22 -0
- package/dist/internal/file-lock.d.ts +91 -0
- package/dist/internal/file-lock.js +212 -0
- package/dist/internal/file-size.d.ts +2 -0
- package/dist/internal/file-size.js +12 -0
- package/dist/internal/git-add.d.ts +43 -0
- package/dist/internal/git-add.js +59 -0
- package/dist/internal/git-env.d.ts +59 -0
- package/dist/internal/git-env.js +93 -0
- package/dist/internal/git-exec.d.ts +58 -0
- package/dist/internal/git-exec.js +115 -0
- package/dist/internal/path-file-lock.d.ts +23 -0
- package/dist/internal/path-file-lock.js +25 -0
- package/dist/internal/process-alive.d.ts +42 -0
- package/dist/internal/process-alive.js +56 -0
- package/dist/internal/process-kill.d.ts +27 -0
- package/dist/internal/process-kill.js +54 -0
- package/dist/internal/realpath.d.ts +29 -0
- package/dist/internal/realpath.js +52 -0
- package/dist/internal/redact.d.ts +40 -0
- package/dist/internal/redact.js +161 -0
- package/dist/internal/shell-quote.d.ts +2 -0
- package/dist/internal/shell-quote.js +4 -0
- package/dist/internal/sort.d.ts +20 -0
- package/dist/internal/sort.js +20 -0
- package/dist/internal/strip-control-chars.d.ts +19 -0
- package/dist/internal/strip-control-chars.js +38 -0
- package/dist/internal/thenable.d.ts +10 -0
- package/dist/internal/thenable.js +15 -0
- package/dist/internal.d.ts +91 -0
- package/dist/internal.js +91 -0
- package/dist/native/adapter-deps.d.ts +46 -0
- package/dist/native/adapter-deps.js +43 -0
- package/dist/native/auth-types.d.ts +70 -0
- package/dist/native/auth-types.js +13 -0
- package/dist/native/auth.d.ts +133 -0
- package/dist/native/auth.js +362 -0
- package/dist/native/client.d.ts +50 -0
- package/dist/native/client.js +174 -0
- package/dist/native/complete.d.ts +33 -0
- package/dist/native/complete.js +88 -0
- package/dist/native/credentials/chained-store.d.ts +30 -0
- package/dist/native/credentials/chained-store.js +68 -0
- package/dist/native/credentials/change-guard.d.ts +51 -0
- package/dist/native/credentials/change-guard.js +136 -0
- package/dist/native/credentials/exec-source.d.ts +50 -0
- package/dist/native/credentials/exec-source.js +222 -0
- package/dist/native/credentials/fingerprint.d.ts +26 -0
- package/dist/native/credentials/fingerprint.js +174 -0
- package/dist/native/credentials/helper-process.d.ts +79 -0
- package/dist/native/credentials/helper-process.js +259 -0
- package/dist/native/credentials/helper-protocol.d.ts +36 -0
- package/dist/native/credentials/helper-protocol.js +79 -0
- package/dist/native/credentials/index.d.ts +60 -0
- package/dist/native/credentials/index.js +184 -0
- package/dist/native/errors.d.ts +34 -0
- package/dist/native/errors.js +109 -0
- package/dist/native/index.d.ts +25 -0
- package/dist/native/index.js +23 -0
- package/dist/native/model-resolver.d.ts +57 -0
- package/dist/native/model-resolver.js +73 -0
- package/dist/native/models.d.ts +116 -0
- package/dist/native/models.js +183 -0
- package/dist/native/session/ask-human.d.ts +15 -0
- package/dist/native/session/ask-human.js +22 -0
- package/dist/native/session/compaction.d.ts +94 -0
- package/dist/native/session/compaction.js +170 -0
- package/dist/native/session/handle-invalid-tool-call.d.ts +111 -0
- package/dist/native/session/handle-invalid-tool-call.js +159 -0
- package/dist/native/session/loop-events/cache-boundary.d.ts +11 -0
- package/dist/native/session/loop-events/cache-boundary.js +29 -0
- package/dist/native/session/loop-events/external-handler.d.ts +38 -0
- package/dist/native/session/loop-events/external-handler.js +193 -0
- package/dist/native/session/loop-events/index.d.ts +16 -0
- package/dist/native/session/loop-events/index.js +15 -0
- package/dist/native/session/loop-events/payload-guard.d.ts +9 -0
- package/dist/native/session/loop-events/payload-guard.js +31 -0
- package/dist/native/session/loop-events/registry.d.ts +84 -0
- package/dist/native/session/loop-events/registry.js +305 -0
- package/dist/native/session/loop-events/types.d.ts +251 -0
- package/dist/native/session/loop-events/types.js +14 -0
- package/dist/native/session/loop-handlers.d.ts +73 -0
- package/dist/native/session/loop-handlers.js +137 -0
- package/dist/native/session/nudge.d.ts +29 -0
- package/dist/native/session/nudge.js +35 -0
- package/dist/native/session/rate-provenance.d.ts +17 -0
- package/dist/native/session/rate-provenance.js +39 -0
- package/dist/native/session/session.d.ts +139 -0
- package/dist/native/session/session.js +208 -0
- package/dist/native/session/tool-input-exemplar.d.ts +2 -0
- package/dist/native/session/tool-input-exemplar.js +66 -0
- package/dist/native/session/tool-input-validation.d.ts +15 -0
- package/dist/native/session/tool-input-validation.js +146 -0
- package/dist/native/session/tool-mapping.d.ts +18 -0
- package/dist/native/session/tool-mapping.js +27 -0
- package/dist/native/session/tool-result.d.ts +37 -0
- package/dist/native/session/tool-result.js +25 -0
- package/dist/native/session/transcript-store.d.ts +77 -0
- package/dist/native/session/transcript-store.js +200 -0
- package/dist/native/session/truncation-handler.d.ts +47 -0
- package/dist/native/session/truncation-handler.js +83 -0
- package/dist/native/session/turn-accumulator.d.ts +45 -0
- package/dist/native/session/turn-accumulator.js +67 -0
- package/dist/native/session/turn-ask-human.d.ts +23 -0
- package/dist/native/session/turn-ask-human.js +41 -0
- package/dist/native/session/turn-compaction-step.d.ts +74 -0
- package/dist/native/session/turn-compaction-step.js +128 -0
- package/dist/native/session/turn-complete-step.d.ts +77 -0
- package/dist/native/session/turn-complete-step.js +159 -0
- package/dist/native/session/turn-end-event.d.ts +3 -0
- package/dist/native/session/turn-end-event.js +17 -0
- package/dist/native/session/turn-events.d.ts +63 -0
- package/dist/native/session/turn-events.js +48 -0
- package/dist/native/session/turn-loop-round-trip.d.ts +116 -0
- package/dist/native/session/turn-loop-round-trip.js +318 -0
- package/dist/native/session/turn-loop.d.ts +23 -0
- package/dist/native/session/turn-loop.js +250 -0
- package/dist/native/session/turn-result.d.ts +39 -0
- package/dist/native/session/turn-result.js +64 -0
- package/dist/native/session/turn-retry.d.ts +120 -0
- package/dist/native/session/turn-retry.js +182 -0
- package/dist/native/session/turn-tool-batch.d.ts +61 -0
- package/dist/native/session/turn-tool-batch.js +259 -0
- package/dist/native/session/turn-types.d.ts +164 -0
- package/dist/native/session/turn-types.js +36 -0
- package/dist/native/session-adapter.d.ts +56 -0
- package/dist/native/session-adapter.js +369 -0
- package/dist/native/session-affinity.d.ts +36 -0
- package/dist/native/session-affinity.js +43 -0
- package/dist/native/tier-providers.d.ts +18 -0
- package/dist/native/tier-providers.js +20 -0
- package/dist/permissions/approval-audit.d.ts +10 -0
- package/dist/permissions/approval-audit.js +22 -0
- package/dist/permissions/approvals-link.d.ts +20 -0
- package/dist/permissions/approvals-link.js +62 -0
- package/dist/permissions/approvals-store.d.ts +168 -0
- package/dist/permissions/approvals-store.js +259 -0
- package/dist/permissions/approvals-taint.d.ts +33 -0
- package/dist/permissions/approvals-taint.js +138 -0
- package/dist/permissions/ask-chain.d.ts +69 -0
- package/dist/permissions/ask-chain.js +32 -0
- package/dist/permissions/ask.d.ts +23 -0
- package/dist/permissions/ask.js +25 -0
- package/dist/permissions/bash-lex.d.ts +59 -0
- package/dist/permissions/bash-lex.js +279 -0
- package/dist/permissions/grammar.d.ts +12 -0
- package/dist/permissions/grammar.js +24 -0
- package/dist/permissions/index.d.ts +11 -0
- package/dist/permissions/index.js +9 -0
- package/dist/permissions/secret-spans.d.ts +19 -0
- package/dist/permissions/secret-spans.js +88 -0
- package/dist/permissions/types.d.ts +85 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/node-glob.d.ts +3 -0
- package/dist/runtime/node-glob.js +83 -0
- package/dist/runtime/node-runtime.d.ts +3 -0
- package/dist/runtime/node-runtime.js +126 -0
- package/dist/runtime/slot.d.ts +11 -0
- package/dist/runtime/slot.js +17 -0
- package/dist/runtime/types.d.ts +51 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/which.d.ts +8 -0
- package/dist/runtime/which.js +32 -0
- package/dist/sandbox/argv-quote.d.ts +11 -0
- package/dist/sandbox/argv-quote.js +13 -0
- package/dist/sandbox/defaults.d.ts +27 -0
- package/dist/sandbox/defaults.js +47 -0
- package/dist/sandbox/git-guards.d.ts +64 -0
- package/dist/sandbox/git-guards.js +161 -0
- package/dist/sandbox/index.d.ts +11 -0
- package/dist/sandbox/index.js +10 -0
- package/dist/sandbox/launcher.d.ts +37 -0
- package/dist/sandbox/launcher.js +160 -0
- package/dist/sandbox/messages.d.ts +17 -0
- package/dist/sandbox/messages.js +46 -0
- package/dist/sandbox/policy-builder.d.ts +44 -0
- package/dist/sandbox/policy-builder.js +149 -0
- package/dist/sandbox/policy-inputs.d.ts +53 -0
- package/dist/sandbox/policy-inputs.js +76 -0
- package/dist/sandbox/probe.d.ts +8 -0
- package/dist/sandbox/probe.js +66 -0
- package/dist/sandbox/registry.d.ts +17 -0
- package/dist/sandbox/registry.js +57 -0
- package/dist/sandbox/session-tmp.d.ts +20 -0
- package/dist/sandbox/session-tmp.js +95 -0
- package/dist/sandbox/srt-backend.d.ts +21 -0
- package/dist/sandbox/srt-backend.js +99 -0
- package/dist/sandbox/types.d.ts +102 -0
- package/dist/sandbox/types.js +1 -0
- package/dist/session/adapter-failure.d.ts +60 -0
- package/dist/session/adapter-failure.js +11 -0
- package/dist/session/agent-stream-events.d.ts +103 -0
- package/dist/session/agent-stream-events.js +6 -0
- package/dist/session/interaction-handler.d.ts +53 -0
- package/dist/session/interaction-handler.js +2 -0
- package/dist/session/no-op-interaction-handler.d.ts +9 -0
- package/dist/session/no-op-interaction-handler.js +11 -0
- package/dist/session/protocol-types.d.ts +12 -0
- package/dist/session/protocol-types.js +1 -0
- package/dist/session/session-types.d.ts +456 -0
- package/dist/session/session-types.js +64 -0
- package/dist/session/tool-descriptor.d.ts +23 -0
- package/dist/session/tool-descriptor.js +5 -0
- package/dist/session/turn-deadline.d.ts +17 -0
- package/dist/session/turn-deadline.js +23 -0
- package/dist/tools/ask-request.d.ts +35 -0
- package/dist/tools/ask-request.js +57 -0
- package/dist/tools/bash-cwd.d.ts +82 -0
- package/dist/tools/bash-cwd.js +55 -0
- package/dist/tools/bash.d.ts +69 -0
- package/dist/tools/bash.js +283 -0
- package/dist/tools/delete.d.ts +57 -0
- package/dist/tools/delete.js +154 -0
- package/dist/tools/denial-redirect.d.ts +42 -0
- package/dist/tools/denial-redirect.js +266 -0
- package/dist/tools/deny-paths.d.ts +35 -0
- package/dist/tools/deny-paths.js +76 -0
- package/dist/tools/edit-region.d.ts +50 -0
- package/dist/tools/edit-region.js +90 -0
- package/dist/tools/edit.d.ts +25 -0
- package/dist/tools/edit.js +114 -0
- package/dist/tools/exec-allowlist-text.d.ts +18 -0
- package/dist/tools/exec-allowlist-text.js +24 -0
- package/dist/tools/exec-guard.d.ts +69 -0
- package/dist/tools/exec-guard.js +151 -0
- package/dist/tools/git-commit.d.ts +59 -0
- package/dist/tools/git-commit.js +220 -0
- package/dist/tools/git-flags/index.d.ts +10 -0
- package/dist/tools/git-flags/index.js +10 -0
- package/dist/tools/git.d.ts +88 -0
- package/dist/tools/git.js +397 -0
- package/dist/tools/glob.d.ts +29 -0
- package/dist/tools/glob.js +153 -0
- package/dist/tools/grep.d.ts +21 -0
- package/dist/tools/grep.js +162 -0
- package/dist/tools/index.d.ts +36 -0
- package/dist/tools/index.js +28 -0
- package/dist/tools/narrow-grants.d.ts +19 -0
- package/dist/tools/narrow-grants.js +54 -0
- package/dist/tools/nax-owned-writes.d.ts +135 -0
- package/dist/tools/nax-owned-writes.js +259 -0
- package/dist/tools/package-managers-table.d.ts +146 -0
- package/dist/tools/package-managers-table.js +325 -0
- package/dist/tools/package-managers-types.d.ts +36 -0
- package/dist/tools/package-managers-types.js +6 -0
- package/dist/tools/package-managers.d.ts +85 -0
- package/dist/tools/package-managers.js +391 -0
- package/dist/tools/path-list.d.ts +38 -0
- package/dist/tools/path-list.js +62 -0
- package/dist/tools/policy-bash-raw.d.ts +19 -0
- package/dist/tools/policy-bash-raw.js +210 -0
- package/dist/tools/policy-bash.d.ts +64 -0
- package/dist/tools/policy-bash.js +232 -0
- package/dist/tools/policy-command-branch.d.ts +32 -0
- package/dist/tools/policy-command-branch.js +63 -0
- package/dist/tools/policy-input.d.ts +2 -0
- package/dist/tools/policy-input.js +10 -0
- package/dist/tools/policy-match.d.ts +75 -0
- package/dist/tools/policy-match.js +117 -0
- package/dist/tools/policy-paths-branch.d.ts +97 -0
- package/dist/tools/policy-paths-branch.js +314 -0
- package/dist/tools/policy.d.ts +65 -0
- package/dist/tools/policy.js +232 -0
- package/dist/tools/protected-paths.d.ts +26 -0
- package/dist/tools/protected-paths.js +8 -0
- package/dist/tools/provider-adapt.d.ts +4 -0
- package/dist/tools/provider-adapt.js +32 -0
- package/dist/tools/provider-advertise.d.ts +39 -0
- package/dist/tools/provider-advertise.js +50 -0
- package/dist/tools/provider-grants.d.ts +36 -0
- package/dist/tools/provider-grants.js +72 -0
- package/dist/tools/provider-sanitize.d.ts +19 -0
- package/dist/tools/provider-sanitize.js +53 -0
- package/dist/tools/provider-types.d.ts +41 -0
- package/dist/tools/provider-types.js +26 -0
- package/dist/tools/read-continuation.d.ts +147 -0
- package/dist/tools/read-continuation.js +176 -0
- package/dist/tools/read-file.d.ts +32 -0
- package/dist/tools/read-file.js +85 -0
- package/dist/tools/read.d.ts +25 -0
- package/dist/tools/read.js +182 -0
- package/dist/tools/registry.d.ts +125 -0
- package/dist/tools/registry.js +67 -0
- package/dist/tools/request-capability.d.ts +13 -0
- package/dist/tools/request-capability.js +23 -0
- package/dist/tools/run-command-exec.d.ts +14 -0
- package/dist/tools/run-command-exec.js +161 -0
- package/dist/tools/run-command.d.ts +107 -0
- package/dist/tools/run-command.js +398 -0
- package/dist/tools/runtime-calltool.d.ts +146 -0
- package/dist/tools/runtime-calltool.js +223 -0
- package/dist/tools/runtime.d.ts +149 -0
- package/dist/tools/runtime.js +345 -0
- package/dist/tools/scratchpad.d.ts +20 -0
- package/dist/tools/scratchpad.js +197 -0
- package/dist/tools/spill.d.ts +81 -0
- package/dist/tools/spill.js +251 -0
- package/dist/tools/tool-audit.d.ts +171 -0
- package/dist/tools/tool-audit.js +183 -0
- package/dist/tools/truncate.d.ts +98 -0
- package/dist/tools/truncate.js +297 -0
- package/dist/tools/types.d.ts +145 -0
- package/dist/tools/types.js +24 -0
- package/dist/tools/verb-denial-argv-hint.d.ts +35 -0
- package/dist/tools/verb-denial-argv-hint.js +40 -0
- package/dist/tools/write.d.ts +9 -0
- package/dist/tools/write.js +45 -0
- package/package.json +54 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ranged-read limit-stop footer (US-001) and cap footer / cap cut (US-002).
|
|
3
|
+
*
|
|
4
|
+
* A ranged `Read` whose `limit` ended before the file's known line count
|
|
5
|
+
* appends a single trailer line that names both the count of unseen lines
|
|
6
|
+
* and the offset to call back with. Without it, a model that asked for the
|
|
7
|
+
* first five lines of a fifty-line file would have no in-band signal that
|
|
8
|
+
* the file continues or how to ask for the next slice — every continuation
|
|
9
|
+
* had to be inferred by trial and error.
|
|
10
|
+
*
|
|
11
|
+
* The trailer is composed deterministically by `limitStopFooter` from three
|
|
12
|
+
* inputs the tool already has on hand: the slice's last line number, the
|
|
13
|
+
* floor or true total from the ranged read, and whether that total is a
|
|
14
|
+
* floor (the read stopped at its I/O bound — `maxFileBytes` on this path).
|
|
15
|
+
*
|
|
16
|
+
* The exact predicate that gates the footer lives here as well: a limit
|
|
17
|
+
* was supplied AND the slice ends before the last line the tool can see.
|
|
18
|
+
* That second clause is what suppresses the footer in the two ACs where
|
|
19
|
+
* it would be wrong — `endLine == totalLines`, and the offset-past-last-read
|
|
20
|
+
* case where the floor header already proves nothing more is reachable
|
|
21
|
+
* within the byte budget.
|
|
22
|
+
*
|
|
23
|
+
* US-002 — the cap footer replaces the limit-stop footer when a Read
|
|
24
|
+
* result overflows either model-facing budget. `applyCapCut` is the
|
|
25
|
+
* fit-check → overlong-line → cap-cut → no-k-fits state machine; the cut
|
|
26
|
+
* keeps the largest `k` such that the header, the first `k` body lines,
|
|
27
|
+
* and the cap footer fit both budgets.
|
|
28
|
+
*
|
|
29
|
+
* The overlong-line step (US-001 of the spill-marker story) is what keeps a
|
|
30
|
+
* single enormous line from hiding the file's tail: a whole-line cut can
|
|
31
|
+
* neither shorten such a line nor skip it, so stopping just before it drops
|
|
32
|
+
* every line after it — including the last output line of a spilled command,
|
|
33
|
+
* which is often the only signal in the body. See its comment in
|
|
34
|
+
* `applyCapCut`.
|
|
35
|
+
*
|
|
36
|
+
* File-local on purpose. These helpers are properties of the `Read`
|
|
37
|
+
* composition, nothing else composes them, and `src/tools` does not
|
|
38
|
+
* export them from its barrel — `read.ts` imports them by relative path.
|
|
39
|
+
*/
|
|
40
|
+
/** Inputs the ranged branch already has on hand. */
|
|
41
|
+
export interface LimitStopFooterInput {
|
|
42
|
+
/** The offset to continue from: `endLine + 1`. */
|
|
43
|
+
readonly nextOffset: number;
|
|
44
|
+
/** True iff the read stopped at the I/O bound (`maxFileBytes` here). */
|
|
45
|
+
readonly totalIsFloor: boolean;
|
|
46
|
+
/** The slice's last line number — call it `b` in the spec. */
|
|
47
|
+
readonly endLine: number;
|
|
48
|
+
/** The total the ranged branch reported (the `T` of `R = T - b`). */
|
|
49
|
+
readonly totalLines: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compose the single trailer line that names the continuation offset.
|
|
53
|
+
*
|
|
54
|
+
* `R = totalLines - endLine`, with a trailing `+` exactly when the total
|
|
55
|
+
* is a floor — the read stopped at its I/O bound and the model cannot
|
|
56
|
+
* know how many more lines the file really has. The footer is the result's
|
|
57
|
+
* LAST line: the ranged `Read` joins the body and the footer with `\n`
|
|
58
|
+
* and writes nothing after the footer.
|
|
59
|
+
*
|
|
60
|
+
* The "should the footer be appended at all" check is
|
|
61
|
+
* `shouldAppendLimitStopFooter` — callers decide first, then ask this helper
|
|
62
|
+
* to compose the trailer. Splitting the gate from the composition keeps the
|
|
63
|
+
* no-footer case a pure body with no trailing delimiter to manage.
|
|
64
|
+
*/
|
|
65
|
+
export declare function limitStopFooter(input: LimitStopFooterInput): string;
|
|
66
|
+
/**
|
|
67
|
+
* True iff the ranged branch should append the limit-stop footer. Two
|
|
68
|
+
* conditions, both required:
|
|
69
|
+
*
|
|
70
|
+
* 1. `limit` was supplied. Without a limit the slice already runs to
|
|
71
|
+
* the end of what the tool can read, so there is nothing to name.
|
|
72
|
+
* 2. `endLine < totalLines`. When the slice's last line IS the last line
|
|
73
|
+
* the tool can see, the model cannot ask for anything more — the
|
|
74
|
+
* floor header already names that ceiling. Appending a footer that
|
|
75
|
+
* points back inside the slice would mislead the model into reading
|
|
76
|
+
* the same lines twice.
|
|
77
|
+
*
|
|
78
|
+
* Hides the predicate behind a function so the `read.ts` call site reads
|
|
79
|
+
* as the spec, not as the arithmetic.
|
|
80
|
+
*/
|
|
81
|
+
export declare function shouldAppendLimitStopFooter(hasLimit: boolean, endLine: number, totalLines: number): boolean;
|
|
82
|
+
/** Inputs to compose a single cap footer line. */
|
|
83
|
+
export interface CapFooterInput {
|
|
84
|
+
/** First selected line number: 1 on the whole-file path, `offset` on the ranged path. */
|
|
85
|
+
readonly firstLine: number;
|
|
86
|
+
/** Last selected line number in the cut. The cap footer names `firstLine..lastLine`. */
|
|
87
|
+
readonly lastLine: number;
|
|
88
|
+
/**
|
|
89
|
+
* Pre-formatted total label: `1500` when the read saw the whole file,
|
|
90
|
+
* `6+` when the read stopped at its I/O bound (the `readCeiling` floor on
|
|
91
|
+
* the whole-file path, `maxFileBytes` floor on the ranged path). The
|
|
92
|
+
* trailing `+` is the caller's responsibility; the helper does not
|
|
93
|
+
* re-derive the floor because it does not know which I/O bound was used.
|
|
94
|
+
*/
|
|
95
|
+
readonly totalLabel: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The cap footer: `[Showing lines a-b of T. Use offset=X to continue.]`,
|
|
99
|
+
* where `X = b + 1`. Same text the limit-stop footer uses for `X`; the
|
|
100
|
+
* difference is the rest of the message — the cap footer names the
|
|
101
|
+
* delivered range, the limit-stop footer names the unseen count.
|
|
102
|
+
*
|
|
103
|
+
* The footer is the result's last line. `read.ts` joins it to the body
|
|
104
|
+
* with `\n` and writes nothing after it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function capFooter(input: CapFooterInput): string;
|
|
107
|
+
/** Inputs to `applyCapCut` — the tool's two paths pass them in their own shape. */
|
|
108
|
+
export interface ApplyCapCutInput {
|
|
109
|
+
/** Header line, already composed: `[1500 lines]`, `[6+ lines]`, `[lines 100-1299 of 1500]`. */
|
|
110
|
+
readonly header: string;
|
|
111
|
+
/** Selected lines, already split by `\n`. Whole-file: every line of the prefix; ranged: `offset..endLine`. */
|
|
112
|
+
readonly lines: readonly string[];
|
|
113
|
+
/** First selected line number (1 on whole-file, `offset` on ranged). */
|
|
114
|
+
readonly firstLine: number;
|
|
115
|
+
/** Pre-formatted total label with the floor `+` if applicable. */
|
|
116
|
+
readonly totalLabel: string;
|
|
117
|
+
/** Limit-stop footer text if rule 1 applied, otherwise `""`. Replaced by the cap footer on a cut. */
|
|
118
|
+
readonly limitStopFooter: string;
|
|
119
|
+
/** Today's unshaped body — returned verbatim when no `k >= 1` fits. */
|
|
120
|
+
readonly unshapedBody: string;
|
|
121
|
+
/** Model-facing byte ceiling (`ctx.maxBytes`). */
|
|
122
|
+
readonly maxBytes: number;
|
|
123
|
+
/** Model-facing line ceiling (`MODEL_MAX_LINES`). */
|
|
124
|
+
readonly maxLines: number;
|
|
125
|
+
}
|
|
126
|
+
/** Output of `applyCapCut`. */
|
|
127
|
+
export interface ApplyCapCutResult {
|
|
128
|
+
/** The composed result content. */
|
|
129
|
+
readonly content: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Compose the final result: fit the candidate if it fits, else cut at the
|
|
133
|
+
* largest `k >= 1` such that `header + first k lines + cap footer` fits
|
|
134
|
+
* both budgets, else return the unshaped body.
|
|
135
|
+
*
|
|
136
|
+
* The byte budget is measured over the WHOLE result (header and footer
|
|
137
|
+
* included); the line budget counts the whole result's lines (the same
|
|
138
|
+
* `MODEL_MAX_LINES` the after_tool policy uses). The cap footer is the
|
|
139
|
+
* last line and replaces the limit-stop footer when it fires.
|
|
140
|
+
*
|
|
141
|
+
* On no-`k`-fits, the limit-stop footer (if rule 1 produced one) is
|
|
142
|
+
* appended to today's unshaped body — the spec's "today's result, plus
|
|
143
|
+
* the limit-stop footer from rule 1 if it applied" — so an over-the-cap
|
|
144
|
+
* read that the tool cannot even trim by one line still tells the model
|
|
145
|
+
* how to continue. The after_tool policy then shapes the bytes.
|
|
146
|
+
*/
|
|
147
|
+
export declare function applyCapCut(input: ApplyCapCutInput): ApplyCapCutResult;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ranged-read limit-stop footer (US-001) and cap footer / cap cut (US-002).
|
|
3
|
+
*
|
|
4
|
+
* A ranged `Read` whose `limit` ended before the file's known line count
|
|
5
|
+
* appends a single trailer line that names both the count of unseen lines
|
|
6
|
+
* and the offset to call back with. Without it, a model that asked for the
|
|
7
|
+
* first five lines of a fifty-line file would have no in-band signal that
|
|
8
|
+
* the file continues or how to ask for the next slice — every continuation
|
|
9
|
+
* had to be inferred by trial and error.
|
|
10
|
+
*
|
|
11
|
+
* The trailer is composed deterministically by `limitStopFooter` from three
|
|
12
|
+
* inputs the tool already has on hand: the slice's last line number, the
|
|
13
|
+
* floor or true total from the ranged read, and whether that total is a
|
|
14
|
+
* floor (the read stopped at its I/O bound — `maxFileBytes` on this path).
|
|
15
|
+
*
|
|
16
|
+
* The exact predicate that gates the footer lives here as well: a limit
|
|
17
|
+
* was supplied AND the slice ends before the last line the tool can see.
|
|
18
|
+
* That second clause is what suppresses the footer in the two ACs where
|
|
19
|
+
* it would be wrong — `endLine == totalLines`, and the offset-past-last-read
|
|
20
|
+
* case where the floor header already proves nothing more is reachable
|
|
21
|
+
* within the byte budget.
|
|
22
|
+
*
|
|
23
|
+
* US-002 — the cap footer replaces the limit-stop footer when a Read
|
|
24
|
+
* result overflows either model-facing budget. `applyCapCut` is the
|
|
25
|
+
* fit-check → overlong-line → cap-cut → no-k-fits state machine; the cut
|
|
26
|
+
* keeps the largest `k` such that the header, the first `k` body lines,
|
|
27
|
+
* and the cap footer fit both budgets.
|
|
28
|
+
*
|
|
29
|
+
* The overlong-line step (US-001 of the spill-marker story) is what keeps a
|
|
30
|
+
* single enormous line from hiding the file's tail: a whole-line cut can
|
|
31
|
+
* neither shorten such a line nor skip it, so stopping just before it drops
|
|
32
|
+
* every line after it — including the last output line of a spilled command,
|
|
33
|
+
* which is often the only signal in the body. See its comment in
|
|
34
|
+
* `applyCapCut`.
|
|
35
|
+
*
|
|
36
|
+
* File-local on purpose. These helpers are properties of the `Read`
|
|
37
|
+
* composition, nothing else composes them, and `src/tools` does not
|
|
38
|
+
* export them from its barrel — `read.ts` imports them by relative path.
|
|
39
|
+
*/
|
|
40
|
+
import { splitModelLines } from "./truncate.js";
|
|
41
|
+
/**
|
|
42
|
+
* Compose the single trailer line that names the continuation offset.
|
|
43
|
+
*
|
|
44
|
+
* `R = totalLines - endLine`, with a trailing `+` exactly when the total
|
|
45
|
+
* is a floor — the read stopped at its I/O bound and the model cannot
|
|
46
|
+
* know how many more lines the file really has. The footer is the result's
|
|
47
|
+
* LAST line: the ranged `Read` joins the body and the footer with `\n`
|
|
48
|
+
* and writes nothing after the footer.
|
|
49
|
+
*
|
|
50
|
+
* The "should the footer be appended at all" check is
|
|
51
|
+
* `shouldAppendLimitStopFooter` — callers decide first, then ask this helper
|
|
52
|
+
* to compose the trailer. Splitting the gate from the composition keeps the
|
|
53
|
+
* no-footer case a pure body with no trailing delimiter to manage.
|
|
54
|
+
*/
|
|
55
|
+
export function limitStopFooter(input) {
|
|
56
|
+
const { nextOffset, totalIsFloor, endLine, totalLines } = input;
|
|
57
|
+
const remaining = Math.max(0, totalLines - endLine);
|
|
58
|
+
const countLabel = totalIsFloor ? `${remaining}+` : `${remaining}`;
|
|
59
|
+
return `[${countLabel} more lines in file. Use offset=${nextOffset} to continue.]`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* True iff the ranged branch should append the limit-stop footer. Two
|
|
63
|
+
* conditions, both required:
|
|
64
|
+
*
|
|
65
|
+
* 1. `limit` was supplied. Without a limit the slice already runs to
|
|
66
|
+
* the end of what the tool can read, so there is nothing to name.
|
|
67
|
+
* 2. `endLine < totalLines`. When the slice's last line IS the last line
|
|
68
|
+
* the tool can see, the model cannot ask for anything more — the
|
|
69
|
+
* floor header already names that ceiling. Appending a footer that
|
|
70
|
+
* points back inside the slice would mislead the model into reading
|
|
71
|
+
* the same lines twice.
|
|
72
|
+
*
|
|
73
|
+
* Hides the predicate behind a function so the `read.ts` call site reads
|
|
74
|
+
* as the spec, not as the arithmetic.
|
|
75
|
+
*/
|
|
76
|
+
export function shouldAppendLimitStopFooter(hasLimit, endLine, totalLines) {
|
|
77
|
+
return hasLimit && endLine < totalLines;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The cap footer: `[Showing lines a-b of T. Use offset=X to continue.]`,
|
|
81
|
+
* where `X = b + 1`. Same text the limit-stop footer uses for `X`; the
|
|
82
|
+
* difference is the rest of the message — the cap footer names the
|
|
83
|
+
* delivered range, the limit-stop footer names the unseen count.
|
|
84
|
+
*
|
|
85
|
+
* The footer is the result's last line. `read.ts` joins it to the body
|
|
86
|
+
* with `\n` and writes nothing after it.
|
|
87
|
+
*/
|
|
88
|
+
export function capFooter(input) {
|
|
89
|
+
const { firstLine, lastLine, totalLabel } = input;
|
|
90
|
+
return `[Showing lines ${firstLine}-${lastLine} of ${totalLabel}. Use offset=${lastLine + 1} to continue.]`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Compose the final result: fit the candidate if it fits, else cut at the
|
|
94
|
+
* largest `k >= 1` such that `header + first k lines + cap footer` fits
|
|
95
|
+
* both budgets, else return the unshaped body.
|
|
96
|
+
*
|
|
97
|
+
* The byte budget is measured over the WHOLE result (header and footer
|
|
98
|
+
* included); the line budget counts the whole result's lines (the same
|
|
99
|
+
* `MODEL_MAX_LINES` the after_tool policy uses). The cap footer is the
|
|
100
|
+
* last line and replaces the limit-stop footer when it fires.
|
|
101
|
+
*
|
|
102
|
+
* On no-`k`-fits, the limit-stop footer (if rule 1 produced one) is
|
|
103
|
+
* appended to today's unshaped body — the spec's "today's result, plus
|
|
104
|
+
* the limit-stop footer from rule 1 if it applied" — so an over-the-cap
|
|
105
|
+
* read that the tool cannot even trim by one line still tells the model
|
|
106
|
+
* how to continue. The after_tool policy then shapes the bytes.
|
|
107
|
+
*/
|
|
108
|
+
export function applyCapCut(input) {
|
|
109
|
+
const { header, lines, firstLine, totalLabel, limitStopFooter, unshapedBody, maxBytes, maxLines } = input;
|
|
110
|
+
// Rule 1's candidate: today's body, plus the limit-stop footer if it
|
|
111
|
+
// applied. The fit check decides whether this form is what the model
|
|
112
|
+
// sees (returning it unchanged when it fits) or whether the cap cut
|
|
113
|
+
// has to fire.
|
|
114
|
+
const candidate = limitStopFooter === "" ? unshapedBody : `${unshapedBody}\n${limitStopFooter}`;
|
|
115
|
+
// Step 1 — fit check. If today's candidate fits both the line cap and
|
|
116
|
+
// the byte cap, return it unchanged. The after_tool policy then sees a
|
|
117
|
+
// within-cap result and is a no-op on it.
|
|
118
|
+
//
|
|
119
|
+
// The line count uses `splitModelLines`, the same helper the cut path
|
|
120
|
+
// and the after_tool policy use, so the cap matches across all three
|
|
121
|
+
// seams. A raw `split("\n")` would count the trailing newline as a
|
|
122
|
+
// phantom empty line and reject a candidate that is exactly at the
|
|
123
|
+
// cap (e.g. header + 999 body lines + "\n" reports as 1001 lines via
|
|
124
|
+
// raw split but 1000 via `splitModelLines`).
|
|
125
|
+
if (splitModelLines(candidate).length <= maxLines && Buffer.byteLength(candidate, "utf8") <= maxBytes) {
|
|
126
|
+
return { content: candidate };
|
|
127
|
+
}
|
|
128
|
+
// Step 2 — overlong line: hand the body to the after_tool policy.
|
|
129
|
+
//
|
|
130
|
+
// A line whose own bytes exceed the WHOLE model-facing byte budget can
|
|
131
|
+
// never be delivered by this cut, whatever precedes it: the cut is
|
|
132
|
+
// whole-line, so it can neither shorten such a line nor skip it. Cutting
|
|
133
|
+
// just before it would therefore spend the result on the short lines in
|
|
134
|
+
// front of it and drop every line behind it — including the LAST line of a
|
|
135
|
+
// spilled command's output, which is exactly the line a caller reaches for.
|
|
136
|
+
// (`[3 lines] / exit 0 / <160 KB blob> / LAST LINE` cut to the 68 bytes
|
|
137
|
+
// before the blob is the shape this rules out.)
|
|
138
|
+
//
|
|
139
|
+
// So the candidate passes through unshaped, exactly as the no-k-fits case
|
|
140
|
+
// below does, and the after_tool policy shapes it: that policy caps an
|
|
141
|
+
// overlong line at the per-line ceiling instead of evicting the lines
|
|
142
|
+
// around it, and spills what it cuts, so the tail stays reachable.
|
|
143
|
+
if (lines.some((line) => Buffer.byteLength(line, "utf8") > maxBytes)) {
|
|
144
|
+
return { content: candidate };
|
|
145
|
+
}
|
|
146
|
+
// Step 3 — cap cut. Try k from largest to smallest; the first one that
|
|
147
|
+
// fits both budgets is the cut. Walking largest-first is what guarantees
|
|
148
|
+
// the result holds the most whole lines that can fit — picking a smaller
|
|
149
|
+
// k would leave whole lines on the floor that could have been delivered.
|
|
150
|
+
//
|
|
151
|
+
// The upper bound is min(lines.length, maxLines - 2): the line cap
|
|
152
|
+
// already bars any `k` greater than `maxLines - 2`, so iterating higher
|
|
153
|
+
// would only hit the `continue` guard. For a 2 MB ceiling of 1-byte lines
|
|
154
|
+
// (`lines.length` ≈ 1 000 000) the unconstrained start wastes ~998 000
|
|
155
|
+
// pointless iterations before reaching the meaningful range.
|
|
156
|
+
for (let k = Math.min(lines.length, maxLines - 2); k >= 1; k -= 1) {
|
|
157
|
+
// Line budget: header + k body lines + cap footer line.
|
|
158
|
+
if (1 + k + 1 > maxLines)
|
|
159
|
+
continue;
|
|
160
|
+
const lastLine = firstLine + k - 1;
|
|
161
|
+
const footer = capFooter({ firstLine, lastLine, totalLabel });
|
|
162
|
+
// Compose: header\n + (k lines joined by \n) + \n + footer.
|
|
163
|
+
// No trailing newline — the cap footer is the last line, with
|
|
164
|
+
// nothing after it. A cut result never ends with a newline.
|
|
165
|
+
const bodyJoined = lines.slice(0, k).join("\n");
|
|
166
|
+
const cut = `${header}\n${bodyJoined}\n${footer}`;
|
|
167
|
+
if (Buffer.byteLength(cut, "utf8") <= maxBytes) {
|
|
168
|
+
return { content: cut };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Step 4 — no k fits: today's candidate passes through unchanged. The
|
|
172
|
+
// cap footer is absent here — the rule is "no line fits with the header
|
|
173
|
+
// and cap footer", so neither can be appended. The after_tool policy
|
|
174
|
+
// is the backstop for these cases.
|
|
175
|
+
return { content: candidate };
|
|
176
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ranged file-read core — US-001.
|
|
3
|
+
*
|
|
4
|
+
* A single implementation that backs both `readTool` and (under US-003)
|
|
5
|
+
* `ScratchpadRead`. The `readCeiling` parameter is the tool-layer I/O bound,
|
|
6
|
+
* distinct from `maxFileBytes` which remains the whole-file Edit/Write cap.
|
|
7
|
+
*/
|
|
8
|
+
export interface ReadFileSliceOptions {
|
|
9
|
+
/** Tool-layer I/O bound. Omit to default to `READ_CEILING`. */
|
|
10
|
+
readonly readCeiling?: number;
|
|
11
|
+
/** 1-based line number to start reading from. Omit to start at the first line. */
|
|
12
|
+
readonly offset?: number;
|
|
13
|
+
/** Maximum number of lines to return. Omit to read until the end of the file. */
|
|
14
|
+
readonly limit?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ReadFileSliceResult {
|
|
17
|
+
/** The slice body (empty string when the offset is past the end). */
|
|
18
|
+
readonly content: string;
|
|
19
|
+
/** True when the file is larger than the supplied `readCeiling`. */
|
|
20
|
+
readonly bounded: boolean;
|
|
21
|
+
/** Total number of lines the file holds (a floor when `bounded` is true). */
|
|
22
|
+
readonly totalLines: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read a ranged slice of a UTF-8 file.
|
|
26
|
+
*
|
|
27
|
+
* Honours the trailing-newline convention: a body ending in "\n" has that
|
|
28
|
+
* newline TERMINATE the last line rather than open an empty one, so the
|
|
29
|
+
* returned `totalLines` agrees with `readTool`'s `[N lines]` header and the
|
|
30
|
+
* model's mental model of a file. An empty body has zero lines.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readFileSlice(target: string, opts?: ReadFileSliceOptions): Promise<ReadFileSliceResult>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ranged file-read core — US-001.
|
|
3
|
+
*
|
|
4
|
+
* A single implementation that backs both `readTool` and (under US-003)
|
|
5
|
+
* `ScratchpadRead`. The `readCeiling` parameter is the tool-layer I/O bound,
|
|
6
|
+
* distinct from `maxFileBytes` which remains the whole-file Edit/Write cap.
|
|
7
|
+
*/
|
|
8
|
+
import { open } from "node:fs/promises";
|
|
9
|
+
import { NaxError } from "#src/infra/index";
|
|
10
|
+
import { fileSizeOrZero } from "#src/internal/file-size";
|
|
11
|
+
import { cutBufferToByteCap, READ_CEILING } from "./truncate.js";
|
|
12
|
+
/** Reject offset/limit values that would silently drift to the file's last line. */
|
|
13
|
+
function validateRange(opts) {
|
|
14
|
+
if (opts.offset !== undefined && (!Number.isInteger(opts.offset) || opts.offset <= 0)) {
|
|
15
|
+
throw new NaxError(`offset must be a positive integer (1-based); got ${JSON.stringify(opts.offset)}`, "READ_RANGE_INVALID", { stage: "tools", offset: opts.offset });
|
|
16
|
+
}
|
|
17
|
+
if (opts.limit !== undefined && (!Number.isInteger(opts.limit) || opts.limit <= 0)) {
|
|
18
|
+
throw new NaxError(`limit must be a positive integer; got ${JSON.stringify(opts.limit)}`, "READ_RANGE_INVALID", {
|
|
19
|
+
stage: "tools",
|
|
20
|
+
limit: opts.limit,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read a ranged slice of a UTF-8 file.
|
|
26
|
+
*
|
|
27
|
+
* Honours the trailing-newline convention: a body ending in "\n" has that
|
|
28
|
+
* newline TERMINATE the last line rather than open an empty one, so the
|
|
29
|
+
* returned `totalLines` agrees with `readTool`'s `[N lines]` header and the
|
|
30
|
+
* model's mental model of a file. An empty body has zero lines.
|
|
31
|
+
*/
|
|
32
|
+
export async function readFileSlice(target, opts = {}) {
|
|
33
|
+
validateRange(opts);
|
|
34
|
+
const ceiling = opts.readCeiling ?? READ_CEILING;
|
|
35
|
+
// Probe the file size with `fileSizeOrZero(target)` so we know whether the
|
|
36
|
+
// file exceeds the ceiling without reading past it. That stat is also why
|
|
37
|
+
// this reads exactly `ceiling` bytes rather than `readPrefix`'s
|
|
38
|
+
// ceiling-plus-one: the overshoot exists to infer "there was more" from the
|
|
39
|
+
// read itself, and here the stat already answers that — so the extra byte
|
|
40
|
+
// would only carry the read past the bound it is meant to enforce.
|
|
41
|
+
const fileSize = fileSizeOrZero(target);
|
|
42
|
+
const readBudget = Math.min(ceiling, fileSize);
|
|
43
|
+
const handle = await open(target, "r");
|
|
44
|
+
let body = "";
|
|
45
|
+
try {
|
|
46
|
+
const buffer = Buffer.alloc(readBudget);
|
|
47
|
+
const { bytesRead } = await handle.read(buffer, 0, readBudget, 0);
|
|
48
|
+
// The bound can land inside a codepoint, and a partial tail decodes to
|
|
49
|
+
// U+FFFD (3 bytes) — which would put the returned body back over the
|
|
50
|
+
// ceiling it was just capped at. Trim to a clean boundary so the byte
|
|
51
|
+
// budget holds for the caller.
|
|
52
|
+
body = cutBufferToByteCap(buffer.subarray(0, bytesRead), ceiling).toString("utf8");
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await handle.close();
|
|
56
|
+
}
|
|
57
|
+
const bounded = fileSize > ceiling;
|
|
58
|
+
// Trailing-newline convention: strip the trailing "\n" before splitting,
|
|
59
|
+
// so "a\nb\n" yields two lines, not three.
|
|
60
|
+
const trailingNewline = body.endsWith("\n");
|
|
61
|
+
const lines = trailingNewline ? body.slice(0, -1).split("\n") : body === "" ? [] : body.split("\n");
|
|
62
|
+
// When bounded, we read exactly `ceiling` bytes — there are more lines past
|
|
63
|
+
// what we observed. The "lines.length + 1" floor captures that: a bounded
|
|
64
|
+
// file's last visible line either is truncated (a partial line continuing
|
|
65
|
+
// past the ceiling) or ended with the trailing "\n" we already stripped, so
|
|
66
|
+
// at least one more line lives beyond what we read.
|
|
67
|
+
const totalLines = bounded ? lines.length + 1 : lines.length;
|
|
68
|
+
// Offset/limit slicing.
|
|
69
|
+
if (opts.offset !== undefined && opts.offset > totalLines) {
|
|
70
|
+
return { content: "", bounded, totalLines };
|
|
71
|
+
}
|
|
72
|
+
if (opts.offset === undefined && opts.limit === undefined) {
|
|
73
|
+
// Whole-file path: return the read body verbatim so callers see the
|
|
74
|
+
// same trailing newline the file holds.
|
|
75
|
+
return { content: body, bounded, totalLines };
|
|
76
|
+
}
|
|
77
|
+
const startIndex = (opts.offset ?? 1) - 1;
|
|
78
|
+
const endLine = opts.limit === undefined ? lines.length : Math.min(startIndex + opts.limit, lines.length);
|
|
79
|
+
// Joined with no synthesised terminator, mirroring `readTool`'s offset/limit
|
|
80
|
+
// path (src/tools/read.ts): a caller asking for lines 3-4 gets lines 3 and 4,
|
|
81
|
+
// not a newline it did not ask for. The whole-file path above stays verbatim,
|
|
82
|
+
// trailing newline included, because there the caller asked for the file.
|
|
83
|
+
const content = lines.slice(startIndex, endLine).join("\n");
|
|
84
|
+
return { content, bounded, totalLines };
|
|
85
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read one file, already resolved and approved by the policy.
|
|
3
|
+
*
|
|
4
|
+
* The tool never resolves a path itself: it uses ctx.resolvedPaths, which the
|
|
5
|
+
* policy produced. That is what keeps containment in one seam.
|
|
6
|
+
*
|
|
7
|
+
* offset/limit (#1923): models constantly ask for a line range under six
|
|
8
|
+
* different invented spellings, and every one of them was silently discarded
|
|
9
|
+
* because `run`'s input parameter was unused. offset/limit is the spelling
|
|
10
|
+
* they reach for most (144 of 187 observed range arguments), so that is the
|
|
11
|
+
* only shape honoured -- every other spelling is rejected by name rather than
|
|
12
|
+
* silently dropped, the same principle policy.ts uses for a denied argv: name
|
|
13
|
+
* the supported forms instead of a bare refusal.
|
|
14
|
+
*
|
|
15
|
+
* Self-cap at whole-line model caps (US-002): when today's body overflows the
|
|
16
|
+
* line or byte ceiling, the tool cuts at the largest whole-line boundary that
|
|
17
|
+
* still fits and appends a single trailer naming the delivered range. The
|
|
18
|
+
* after_tool policy (`applyModelTruncationPolicy`) is the unconditional
|
|
19
|
+
* backstop — a result that already fits every cap passes through it untouched
|
|
20
|
+
* (its within-cap contract), so no spill file is written for it. The cap
|
|
21
|
+
* footer replaces the limit-stop footer when it fires; a result never carries
|
|
22
|
+
* both.
|
|
23
|
+
*/
|
|
24
|
+
import type { CodingTool } from "./registry.ts";
|
|
25
|
+
export declare const readTool: CodingTool;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read one file, already resolved and approved by the policy.
|
|
3
|
+
*
|
|
4
|
+
* The tool never resolves a path itself: it uses ctx.resolvedPaths, which the
|
|
5
|
+
* policy produced. That is what keeps containment in one seam.
|
|
6
|
+
*
|
|
7
|
+
* offset/limit (#1923): models constantly ask for a line range under six
|
|
8
|
+
* different invented spellings, and every one of them was silently discarded
|
|
9
|
+
* because `run`'s input parameter was unused. offset/limit is the spelling
|
|
10
|
+
* they reach for most (144 of 187 observed range arguments), so that is the
|
|
11
|
+
* only shape honoured -- every other spelling is rejected by name rather than
|
|
12
|
+
* silently dropped, the same principle policy.ts uses for a denied argv: name
|
|
13
|
+
* the supported forms instead of a bare refusal.
|
|
14
|
+
*
|
|
15
|
+
* Self-cap at whole-line model caps (US-002): when today's body overflows the
|
|
16
|
+
* line or byte ceiling, the tool cuts at the largest whole-line boundary that
|
|
17
|
+
* still fits and appends a single trailer naming the delivered range. The
|
|
18
|
+
* after_tool policy (`applyModelTruncationPolicy`) is the unconditional
|
|
19
|
+
* backstop — a result that already fits every cap passes through it untouched
|
|
20
|
+
* (its within-cap contract), so no spill file is written for it. The cap
|
|
21
|
+
* footer replaces the limit-stop footer when it fires; a result never carries
|
|
22
|
+
* both.
|
|
23
|
+
*/
|
|
24
|
+
import { readPrefix } from "#src/internal/bounded-io";
|
|
25
|
+
import { applyCapCut, limitStopFooter, shouldAppendLimitStopFooter } from "./read-continuation.js";
|
|
26
|
+
import { MODEL_MAX_BYTES, MODEL_MAX_LINES, READ_CEILING, splitModelLines } from "./truncate.js";
|
|
27
|
+
/** Range arguments models invent instead of offset/limit -- rejected by name, never silently dropped. */
|
|
28
|
+
const UNSUPPORTED_RANGE_ALIASES = ["start_line", "end_line", "start", "end", "line", "lineEnd", "size"];
|
|
29
|
+
/** A positive integer, or an error string naming which constraint failed. */
|
|
30
|
+
function parsePositiveInt(value, field) {
|
|
31
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
32
|
+
return `${field} must be an integer`;
|
|
33
|
+
if (value < 1)
|
|
34
|
+
return `${field} must be >= 1`;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
/** Count lines in a UTF-8 string. Empty string returns 0. Trailing newline is not a line. */
|
|
38
|
+
function countLines(prefix) {
|
|
39
|
+
if (prefix === "")
|
|
40
|
+
return 0;
|
|
41
|
+
const trimmed = prefix.endsWith("\n") ? prefix.slice(0, -1) : prefix;
|
|
42
|
+
return trimmed.split("\n").length;
|
|
43
|
+
}
|
|
44
|
+
export const readTool = {
|
|
45
|
+
name: "Read",
|
|
46
|
+
description: "Read a UTF-8 text file from the repository. Paths are relative to the repository root. " +
|
|
47
|
+
"Optionally pass offset (1-based line number to start from) and/or limit (maximum number of " +
|
|
48
|
+
"lines to return) to read a slice instead of the whole file. " +
|
|
49
|
+
"Use Read to examine files instead of cat, sed, head, tail or awk in Bash. " +
|
|
50
|
+
"A read that stops before the end of the file ends with a line naming the offset to continue from. " +
|
|
51
|
+
"For a large file, read the part you need with offset/limit; when you need the whole file, continue with offset until complete. " +
|
|
52
|
+
`Output is capped at ${MODEL_MAX_LINES} lines or ${MODEL_MAX_BYTES} bytes.`,
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
path: { type: "string", description: "Path relative to the repository root" },
|
|
57
|
+
offset: { type: "integer", minimum: 1, description: "1-based line number to start reading from" },
|
|
58
|
+
limit: { type: "integer", minimum: 1, description: "Maximum number of lines to return" },
|
|
59
|
+
},
|
|
60
|
+
required: ["path"],
|
|
61
|
+
},
|
|
62
|
+
scope: { pathFields: ["path"] },
|
|
63
|
+
async run(input, ctx) {
|
|
64
|
+
const [target] = ctx.resolvedPaths;
|
|
65
|
+
if (target === undefined)
|
|
66
|
+
return { content: "no path supplied", isError: true };
|
|
67
|
+
const usedAlias = UNSUPPORTED_RANGE_ALIASES.find((alias) => input[alias] !== undefined);
|
|
68
|
+
if (usedAlias !== undefined) {
|
|
69
|
+
return {
|
|
70
|
+
content: `"${usedAlias}" is not a supported Read argument -- use offset (1-based line number) and/or limit (line count) instead`,
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const hasOffset = input.offset !== undefined;
|
|
75
|
+
const hasLimit = input.limit !== undefined;
|
|
76
|
+
try {
|
|
77
|
+
if (!hasOffset && !hasLimit) {
|
|
78
|
+
// Leading [N lines] header so an agent has a way to count the lines it
|
|
79
|
+
// is reading. The count comes from the maxBytes prefix, not a second,
|
|
80
|
+
// larger read: readPrefix asks for maxBytes + 1 and the overshoot byte
|
|
81
|
+
// is what tells us the prefix hit the ceiling -- in that case the count
|
|
82
|
+
// is a floor and we mark it with '+'. The ranged branch reads with
|
|
83
|
+
// maxFileBytes and compares against maxFileBytes for the same reason.
|
|
84
|
+
const readCeiling = ctx.readCeiling ?? READ_CEILING;
|
|
85
|
+
const prefix = await readPrefix(target, readCeiling);
|
|
86
|
+
const bounded = Buffer.byteLength(prefix, "utf8") > readCeiling;
|
|
87
|
+
const lineCount = countLines(prefix);
|
|
88
|
+
const totalLabel = bounded ? `${lineCount}+` : `${lineCount}`;
|
|
89
|
+
const header = `[${totalLabel} lines]`;
|
|
90
|
+
// US-002: the whole-file prefix is shaped at the model-facing caps
|
|
91
|
+
// before being handed to the runtime. Today's unshaped body is
|
|
92
|
+
// `header + (prefix when non-empty)`. The cap cut tries the largest
|
|
93
|
+
// k such that `header + first k lines + cap footer` fits both budgets;
|
|
94
|
+
// when no k fits, today's body passes through unchanged and the
|
|
95
|
+
// after_tool policy shapes it.
|
|
96
|
+
const unshaped = prefix === "" ? header : `${header}\n${prefix}`;
|
|
97
|
+
const result = applyCapCut({
|
|
98
|
+
header,
|
|
99
|
+
lines: splitModelLines(prefix),
|
|
100
|
+
firstLine: 1,
|
|
101
|
+
totalLabel,
|
|
102
|
+
limitStopFooter: "",
|
|
103
|
+
unshapedBody: unshaped,
|
|
104
|
+
maxBytes: ctx.maxBytes,
|
|
105
|
+
maxLines: MODEL_MAX_LINES,
|
|
106
|
+
});
|
|
107
|
+
return { content: result.content };
|
|
108
|
+
}
|
|
109
|
+
let offset = 1;
|
|
110
|
+
if (hasOffset) {
|
|
111
|
+
const parsed = parsePositiveInt(input.offset, "offset");
|
|
112
|
+
if (typeof parsed === "string")
|
|
113
|
+
return { content: parsed, isError: true };
|
|
114
|
+
offset = parsed;
|
|
115
|
+
}
|
|
116
|
+
let limit;
|
|
117
|
+
if (hasLimit) {
|
|
118
|
+
const parsed = parsePositiveInt(input.limit, "limit");
|
|
119
|
+
if (typeof parsed === "string")
|
|
120
|
+
return { content: parsed, isError: true };
|
|
121
|
+
limit = parsed;
|
|
122
|
+
}
|
|
123
|
+
// Read up to maxFileBytes, not maxBytes: the requested range may start
|
|
124
|
+
// past the display ceiling, and we still need to reach it to slice.
|
|
125
|
+
const body = await readPrefix(target, ctx.maxFileBytes);
|
|
126
|
+
// readPrefix asks for maxFileBytes + 1, so overshooting the ceiling is how
|
|
127
|
+
// we know the file continues past what we read. The line count is then a
|
|
128
|
+
// floor, not a total, and must not be reported as one -- claiming a false
|
|
129
|
+
// total to the model is the same class of defect #1923 is fixing.
|
|
130
|
+
const bounded = Buffer.byteLength(body, "utf8") > ctx.maxFileBytes;
|
|
131
|
+
const totalLines = countLines(body);
|
|
132
|
+
const totalLabel = bounded ? `${totalLines}+` : `${totalLines}`;
|
|
133
|
+
if (offset > totalLines) {
|
|
134
|
+
return {
|
|
135
|
+
content: bounded
|
|
136
|
+
? `offset ${offset} is past the first ${totalLines} lines, which is all that could be read within the ${ctx.maxFileBytes}-byte ceiling`
|
|
137
|
+
: `offset ${offset} is past the end of the file -- it has ${totalLines} lines`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const lines = splitModelLines(body);
|
|
141
|
+
const startIndex = offset - 1;
|
|
142
|
+
const endLine = limit === undefined ? totalLines : Math.min(startIndex + limit, totalLines);
|
|
143
|
+
const selected = lines.slice(startIndex, endLine).join("\n");
|
|
144
|
+
const headerLine = `[lines ${offset}-${endLine} of ${totalLabel}]`;
|
|
145
|
+
// US-001: when a `limit` cut the slice short of the file's known line
|
|
146
|
+
// count, append a single trailer that names the continuation offset.
|
|
147
|
+
// The helper hides the predicate (limit given AND endLine < totalLines)
|
|
148
|
+
// and the `+`-on-floor rule, both of which are easy to drift apart from
|
|
149
|
+
// the header if inlined.
|
|
150
|
+
const limitStop = shouldAppendLimitStopFooter(limit !== undefined, endLine, totalLines)
|
|
151
|
+
? limitStopFooter({
|
|
152
|
+
nextOffset: endLine + 1,
|
|
153
|
+
totalIsFloor: bounded,
|
|
154
|
+
endLine,
|
|
155
|
+
totalLines,
|
|
156
|
+
})
|
|
157
|
+
: "";
|
|
158
|
+
// US-002: shape today's body at the model-facing caps. The cap cut
|
|
159
|
+
// replaces the limit-stop footer when it fires; when no whole line
|
|
160
|
+
// fits with the header and cap footer, today's body is returned
|
|
161
|
+
// unchanged (plus the limit-stop footer from rule 1 if it applied),
|
|
162
|
+
// and the after_tool policy then shapes it.
|
|
163
|
+
const unshapedBody = `${headerLine}\n${selected}`;
|
|
164
|
+
const result = applyCapCut({
|
|
165
|
+
header: headerLine,
|
|
166
|
+
lines: lines.slice(startIndex, endLine),
|
|
167
|
+
firstLine: offset,
|
|
168
|
+
totalLabel,
|
|
169
|
+
limitStopFooter: limitStop,
|
|
170
|
+
unshapedBody,
|
|
171
|
+
maxBytes: ctx.maxBytes,
|
|
172
|
+
maxLines: MODEL_MAX_LINES,
|
|
173
|
+
});
|
|
174
|
+
return { content: result.content };
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
// An unreadable file is a tool ERROR the model can react to, never a
|
|
178
|
+
// denial: the policy already said yes.
|
|
179
|
+
return { content: err instanceof Error ? err.message : String(err), isError: true };
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|