@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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spill writer and the model-facing composition of the truncation policy
|
|
3
|
+
* (US-003).
|
|
4
|
+
*
|
|
5
|
+
* The policy itself (`truncateForModel`) decides *what* the model may see; this
|
|
6
|
+
* module decides what happens to the rest. On truncation only, the untruncated
|
|
7
|
+
* body — up to `READ_CEILING` — is written under the scratchpad at
|
|
8
|
+
* `spill/<toolName>-<callId>.txt`, and the marker naming that path and both byte
|
|
9
|
+
* counts is appended to the content the model receives. `writeSpill` keeps
|
|
10
|
+
* returning the scratchpad-relative spelling; `applyModelTruncationPolicy`
|
|
11
|
+
* turns it into the path the marker names — root-relative
|
|
12
|
+
* (`.nax/scratchpad/spill/...`) by default, so the session's own tools and
|
|
13
|
+
* shell can open it verbatim, or absolute when the root is not the session's
|
|
14
|
+
* shell root. The spilled body stays recoverable with `offset`/`limit`.
|
|
15
|
+
*
|
|
16
|
+
* The composition lives here, beside the writer, because the marker and the
|
|
17
|
+
* spill are one decision: the path may only appear in the marker when the write
|
|
18
|
+
* actually succeeded. Both transports reach it — the coding-tool runtime applies
|
|
19
|
+
* it to every genuine tool result, and the native session applies it at the
|
|
20
|
+
* `after_tool` chokepoint — so there is exactly one implementation of the
|
|
21
|
+
* model-facing cap rather than one per tool.
|
|
22
|
+
*
|
|
23
|
+
* Filesystem calls are injectable (`_spillDeps`), following `_scratchpadWipeDeps`
|
|
24
|
+
* in src/execution/lifecycle/scratchpad-wipe.ts, and writes go through
|
|
25
|
+
* asynchronous Node filesystem APIs.
|
|
26
|
+
*/
|
|
27
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
import { errorMessage } from "#src/infra/errors";
|
|
30
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
31
|
+
import { SCRATCHPAD_DIR } from "./scratchpad.js";
|
|
32
|
+
import { capModelLine, cutToByteCap, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, READ_CEILING, splitModelLines, truncateForModel, truncationDirectionFor, } from "./truncate.js";
|
|
33
|
+
/** Directory under the scratchpad that holds spilled tool output. */
|
|
34
|
+
export const SPILL_DIR = "spill";
|
|
35
|
+
/** Injectable filesystem seam (see docs/architecture/conventions.md §2). */
|
|
36
|
+
export const _spillDeps = {
|
|
37
|
+
mkdir: (path) => mkdir(path, { recursive: true }),
|
|
38
|
+
writeFile: async (path, data) => {
|
|
39
|
+
await writeFile(path, data, "utf8");
|
|
40
|
+
return Buffer.byteLength(data);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/** A tool name or call id can only contribute path-safe characters. */
|
|
44
|
+
function fileSafe(value) {
|
|
45
|
+
return value.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
46
|
+
}
|
|
47
|
+
/** `spill/<toolName>-<callId>.txt`, relative to the scratchpad directory. */
|
|
48
|
+
export function spillRelativePath(toolName, callId) {
|
|
49
|
+
return `${SPILL_DIR}/${fileSafe(toolName)}-${fileSafe(callId)}.txt`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The spill file's body: the untruncated output, bounded by `READ_CEILING`.
|
|
53
|
+
*
|
|
54
|
+
* A body past the ceiling is itself cut, and the cut is recorded on the file's
|
|
55
|
+
* own last line: a spill that silently ends mid-output is indistinguishable
|
|
56
|
+
* from one that ends where the tool did, and the model is being told it holds
|
|
57
|
+
* the whole thing. The note is paid for out of the ceiling, never added on top
|
|
58
|
+
* of it — the file is the tool-layer bound, so it must not exceed it either.
|
|
59
|
+
*/
|
|
60
|
+
function spillBody(body) {
|
|
61
|
+
const total = Buffer.byteLength(body, "utf8");
|
|
62
|
+
if (total <= READ_CEILING)
|
|
63
|
+
return body;
|
|
64
|
+
// Two passes because the note quotes how much was kept, which depends on the
|
|
65
|
+
// note's own length. The first pass sizes the note with the largest number it
|
|
66
|
+
// could report (the ceiling), so the second can only make it shorter.
|
|
67
|
+
const noteOf = (kept) => `\n... [spill incomplete: showing ${kept} of ${total} bytes]`;
|
|
68
|
+
const firstNote = noteOf(READ_CEILING);
|
|
69
|
+
const kept = cutToByteCap(body, READ_CEILING - Buffer.byteLength(firstNote, "utf8"));
|
|
70
|
+
return `${kept}${noteOf(Buffer.byteLength(kept, "utf8"))}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Write the untruncated body under the scratchpad and return its path relative
|
|
74
|
+
* to the scratchpad, or `undefined` when the write failed.
|
|
75
|
+
*
|
|
76
|
+
* Failure is fail-open by contract: the caller still delivers the truncated
|
|
77
|
+
* result. What it must not do is name a file that is not there, which is why
|
|
78
|
+
* this returns the path rather than a boolean.
|
|
79
|
+
*/
|
|
80
|
+
export async function writeSpill(req) {
|
|
81
|
+
const relative = spillRelativePath(req.toolName, req.callId);
|
|
82
|
+
const target = join(req.root, SCRATCHPAD_DIR, relative);
|
|
83
|
+
try {
|
|
84
|
+
await _spillDeps.mkdir(dirname(target));
|
|
85
|
+
await _spillDeps.writeFile(target, spillBody(req.body));
|
|
86
|
+
return relative;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
getSafeLogger()?.warn("tools", "[spill] could not write the spilled tool result", {
|
|
90
|
+
tool: req.toolName,
|
|
91
|
+
path: target,
|
|
92
|
+
error: errorMessage(err),
|
|
93
|
+
});
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The marker shapes, widest first. A narrower shape is used when the wider one
|
|
99
|
+
* cannot fit the byte budget — a 40 000-byte ceiling fits a marker naming the
|
|
100
|
+
* spill path, a 32-byte one fits barely more than the word "truncated". The
|
|
101
|
+
* spill path is simply absent from a shape that has no room for it, which is
|
|
102
|
+
* also the shape failure takes: a marker never names a file that is not there.
|
|
103
|
+
*/
|
|
104
|
+
function markerShapes(originalBytes, spillPath) {
|
|
105
|
+
const shapes = [];
|
|
106
|
+
if (spillPath !== undefined) {
|
|
107
|
+
shapes.push((d) => `... [truncated: full output at ${spillPath} (open with Read or ScratchpadRead); showing ${d} of ${originalBytes} bytes]`);
|
|
108
|
+
}
|
|
109
|
+
shapes.push((d) => `... [truncated: showing ${d} of ${originalBytes} bytes]`);
|
|
110
|
+
shapes.push(() => "... [truncated]");
|
|
111
|
+
return shapes;
|
|
112
|
+
}
|
|
113
|
+
/** Lines `head` may keep once the marker occupies one of them. */
|
|
114
|
+
function trimToLineBudget(head, maxLines) {
|
|
115
|
+
const lines = splitModelLines(head);
|
|
116
|
+
if (lines.length <= maxLines)
|
|
117
|
+
return head;
|
|
118
|
+
return lines.slice(0, maxLines).join("\n");
|
|
119
|
+
}
|
|
120
|
+
/** A marker line, plus the newline that puts it on one. */
|
|
121
|
+
function renderedMarkerBytes(render, deliveredBytes) {
|
|
122
|
+
return Buffer.byteLength(render(deliveredBytes), "utf8") + 1;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Compose the head-direction result: the policy's content, then the marker on
|
|
126
|
+
* its own last line. The marker is inside the byte and line budgets, never
|
|
127
|
+
* appended past them.
|
|
128
|
+
*/
|
|
129
|
+
function composeHead(content, originalBytes, byteCap, spillPath) {
|
|
130
|
+
for (const render of markerShapes(originalBytes, spillPath)) {
|
|
131
|
+
const bodyBudget = byteCap - renderedMarkerBytes(render, originalBytes);
|
|
132
|
+
if (bodyBudget < 0)
|
|
133
|
+
continue;
|
|
134
|
+
const head = trimToLineBudget(cutToByteCap(content, bodyBudget), MODEL_MAX_LINES - 1);
|
|
135
|
+
return `${head}\n${render(Buffer.byteLength(head, "utf8"))}`;
|
|
136
|
+
}
|
|
137
|
+
return cutToByteCap(content, byteCap);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Compose the tail-with-first-line result: the body's first line, the marker,
|
|
141
|
+
* then the tail.
|
|
142
|
+
*
|
|
143
|
+
* The marker sits between them rather than after the tail because the tail is
|
|
144
|
+
* the whole point of the direction — a marker appended last would push the
|
|
145
|
+
* final stderr lines out of view, which is the failure the direction exists to
|
|
146
|
+
* prevent. Command output is kept line-wise, and a line too large for the
|
|
147
|
+
* remaining budget is skipped rather than cut: one 160 KB stdout blob must not
|
|
148
|
+
* spend the entire budget on itself and evict the short lines that follow it.
|
|
149
|
+
*/
|
|
150
|
+
function composeTail(body, content, originalBytes, byteCap, spillPath) {
|
|
151
|
+
const lines = splitModelLines(body);
|
|
152
|
+
const fullFirstLine = content.split("\n")[0] ?? "";
|
|
153
|
+
for (const render of markerShapes(originalBytes, spillPath)) {
|
|
154
|
+
// One byte for the newline after the first line, then the marker.
|
|
155
|
+
const forFirstLine = byteCap - Buffer.byteLength(render(originalBytes), "utf8") - 1;
|
|
156
|
+
if (forFirstLine < 0)
|
|
157
|
+
continue;
|
|
158
|
+
const firstLine = Buffer.byteLength(fullFirstLine, "utf8") > forFirstLine
|
|
159
|
+
? cutToByteCap(fullFirstLine, forFirstLine)
|
|
160
|
+
: fullFirstLine;
|
|
161
|
+
// One more byte for the newline that opens the tail.
|
|
162
|
+
const tailBudget = forFirstLine - Buffer.byteLength(firstLine, "utf8") - 1;
|
|
163
|
+
const tail = tailBudget < 0 ? "" : selectTail(lines, tailBudget);
|
|
164
|
+
const firstLineBytes = Buffer.byteLength(firstLine, "utf8");
|
|
165
|
+
const tailBytes = Buffer.byteLength(tail, "utf8");
|
|
166
|
+
// The delivered count is the body text the model can read back: the retained
|
|
167
|
+
// first line, the body's own newline that still separates it from the tail,
|
|
168
|
+
// and the tail. The newline that puts the marker on a line of its own is not
|
|
169
|
+
// a body byte — here it is the one that opens the tail, and with no tail
|
|
170
|
+
// retained there is no body byte after the first line at all. `composeHead`
|
|
171
|
+
// reports its head with no marker-line byte either; counting one only here
|
|
172
|
+
// made the same marker mean a different width per tool, and reported one
|
|
173
|
+
// byte more than the model can actually read.
|
|
174
|
+
const delivered = tail === "" ? firstLineBytes : firstLineBytes + 1 + tailBytes;
|
|
175
|
+
const marker = render(delivered);
|
|
176
|
+
return tail === "" ? `${firstLine}\n${marker}` : `${firstLine}\n${marker}\n${tail}`;
|
|
177
|
+
}
|
|
178
|
+
return cutToByteCap(content, byteCap);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The trailing lines that fit `budget` bytes, taken from the end of the body.
|
|
182
|
+
*
|
|
183
|
+
* A line whose own length exceeds what is left is skipped, not truncated: the
|
|
184
|
+
* alternative spends the whole budget inside that one line and returns a slice
|
|
185
|
+
* of it in place of the lines that carry the signal (an echoed command, the
|
|
186
|
+
* error text). Each line kept is still shaped by the per-line cap.
|
|
187
|
+
*/
|
|
188
|
+
function selectTail(lines, budget) {
|
|
189
|
+
const kept = [];
|
|
190
|
+
let remaining = budget;
|
|
191
|
+
// The first line is retained separately, so the walk starts below it, and the
|
|
192
|
+
// first line plus the marker occupy one line each of the line budget.
|
|
193
|
+
for (let i = lines.length - 1; i >= 1 && kept.length < MODEL_MAX_LINES - 2; i -= 1) {
|
|
194
|
+
const line = lines[i] ?? "";
|
|
195
|
+
const cost = Buffer.byteLength(line, "utf8") + (kept.length === 0 ? 0 : 1);
|
|
196
|
+
if (cost > remaining)
|
|
197
|
+
continue;
|
|
198
|
+
kept.unshift(capModelLine(line, MODEL_MAX_LINE_CHARS));
|
|
199
|
+
remaining -= cost;
|
|
200
|
+
}
|
|
201
|
+
return kept.join("\n");
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Apply the model-facing truncation policy to one tool result.
|
|
205
|
+
*
|
|
206
|
+
* Returns the body unchanged when it is within every cap — no spill, no marker.
|
|
207
|
+
* Otherwise the shared policy shapes the content, the untruncated body is
|
|
208
|
+
* spilled when a root is known, and the marker naming both byte counts and the
|
|
209
|
+
* spill path is composed inside the same ceilings the body obeys.
|
|
210
|
+
*
|
|
211
|
+
* The byte ceiling is the caller's `maxBytes`, defaulting to `MODEL_MAX_BYTES`;
|
|
212
|
+
* the line ceilings always come from the shared policy's constants.
|
|
213
|
+
*/
|
|
214
|
+
export async function applyModelTruncationPolicy(body, opts) {
|
|
215
|
+
const direction = truncationDirectionFor(opts.toolName);
|
|
216
|
+
const originalBytes = Buffer.byteLength(body, "utf8");
|
|
217
|
+
const byteCap = Math.max(0, opts.maxBytes ?? MODEL_MAX_BYTES);
|
|
218
|
+
// The one call to the shared policy: it applies the per-line cap, the
|
|
219
|
+
// line-count cap and its own byte cap, and tells us whether anything was cut.
|
|
220
|
+
const shaped = truncateForModel(body, { direction });
|
|
221
|
+
if (!shaped.truncated && originalBytes <= byteCap)
|
|
222
|
+
return body;
|
|
223
|
+
const spillPath = await spillMarkerPath(opts, body);
|
|
224
|
+
return direction === "head"
|
|
225
|
+
? composeHead(shaped.content, originalBytes, byteCap, spillPath)
|
|
226
|
+
: composeTail(body, shaped.content, originalBytes, byteCap, spillPath);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Write the body and return the path as the marker should spell it, or
|
|
230
|
+
* `undefined` when there is no root or the write failed. The spill writer
|
|
231
|
+
* keeps returning the scratchpad-relative `spill/<Tool>-<callId>.txt`; this is
|
|
232
|
+
* the one place that turns it into the model-facing spelling, so the two
|
|
233
|
+
* composers receive a finished path and stay unaware of the style.
|
|
234
|
+
*
|
|
235
|
+
* The path appears whenever the wide marker shape fits the byte budget, for a
|
|
236
|
+
* body of any size: AC1/AC2/AC7/AC8 name the spill path for every truncated
|
|
237
|
+
* result, and the marker's job is to point at the file nax wrote for this call.
|
|
238
|
+
* A body past `READ_CEILING` is written capped (see `spillBody`) — that is the
|
|
239
|
+
* spill writer's own long-standing bound, pinned by `spill-recovery.test.ts`
|
|
240
|
+
* AC9 — and the cap is recorded on the file's last line rather than hidden.
|
|
241
|
+
*/
|
|
242
|
+
async function spillMarkerPath(opts, body) {
|
|
243
|
+
if (opts.root === undefined)
|
|
244
|
+
return undefined;
|
|
245
|
+
const relative = await writeSpill({ root: opts.root, toolName: opts.toolName, callId: opts.callId, body });
|
|
246
|
+
if (relative === undefined)
|
|
247
|
+
return undefined;
|
|
248
|
+
return opts.spillPathStyle === "absolute"
|
|
249
|
+
? join(opts.root, SCRATCHPAD_DIR, relative)
|
|
250
|
+
: `${SCRATCHPAD_DIR}/${relative}`;
|
|
251
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { SandboxRecord } from "../sandbox/index.ts";
|
|
2
|
+
export interface ToolCallRecord {
|
|
3
|
+
readonly tool: string;
|
|
4
|
+
readonly outcome: "ok" | "error" | "denied" | "denied:ask";
|
|
5
|
+
readonly breach?: boolean;
|
|
6
|
+
readonly input: Record<string, unknown>;
|
|
7
|
+
readonly resultBytes: number;
|
|
8
|
+
readonly storyId?: string;
|
|
9
|
+
readonly at: string;
|
|
10
|
+
/**
|
|
11
|
+
* Why a "denied" outcome was refused. runtime.ts's log() computes this for
|
|
12
|
+
* the console logger (under the `error` key) but used to drop it before it
|
|
13
|
+
* reached this sink, so a denied row's `error` field read null and the
|
|
14
|
+
* refusal reason was unrecoverable -- the exact gap this field closes.
|
|
15
|
+
*/
|
|
16
|
+
readonly reason?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The argv that actually ran, after normalization (workspace scoping, an
|
|
19
|
+
* appended no-scripts flag). Set only for the argv branch (`Exec`).
|
|
20
|
+
* Deliberately alongside `input.argv` (the model's requested argv) rather
|
|
21
|
+
* than instead of it: either half alone is uninformative -- `input` cannot
|
|
22
|
+
* show what actually ran, and `executed` alone cannot show whether the
|
|
23
|
+
* normalization was faithful to what was requested.
|
|
24
|
+
*/
|
|
25
|
+
readonly executed?: readonly string[];
|
|
26
|
+
/** Which workspace root the argv branch executed against. */
|
|
27
|
+
readonly target?: "package" | "repoRoot";
|
|
28
|
+
/**
|
|
29
|
+
* Who approved an ask-matched call and how long the decision took. Recorded
|
|
30
|
+
* on BOTH the allow and deny paths so a human-approved execution is
|
|
31
|
+
* distinguishable in the ledger from a mechanically-allowed one.
|
|
32
|
+
*
|
|
33
|
+
* `remembered` is always false today: `AskVerdict` carries no "remembered"
|
|
34
|
+
* signal, so this is a placeholder for when the cache reports a hit.
|
|
35
|
+
*/
|
|
36
|
+
readonly approval?: {
|
|
37
|
+
readonly decidedBy: string;
|
|
38
|
+
readonly remembered: boolean;
|
|
39
|
+
readonly latencyMs: number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* P4: `{ backend, wrapped, reason?, denialHint?, argv? }` on every Bash /
|
|
43
|
+
* Exec row; the exit runs gate on `wrapped`, and `argv` (wrapped calls only)
|
|
44
|
+
* is the argv the sandbox actually executed, beside the logical `executed`.
|
|
45
|
+
* Additive and optional, so `TOOL_AUDIT_SCHEMA_VERSION` stays 1 (as
|
|
46
|
+
* `approval` did).
|
|
47
|
+
*/
|
|
48
|
+
readonly sandbox?: SandboxRecord;
|
|
49
|
+
/**
|
|
50
|
+
* Bash rows only: the shell's exit code, present when nax did not kill the
|
|
51
|
+
* process group (absent on timeout and turn abort; see `ToolResult.audit`).
|
|
52
|
+
* `outcome` is unchanged -- every non-zero exit is still `"error"` -- so a
|
|
53
|
+
* reader separates a negative answer (a no-match grep, exit 1) from a real
|
|
54
|
+
* failure by reading `exitCode` together with `executed`. N >= 128 may be a
|
|
55
|
+
* signal nax did not send. Additive and optional, so
|
|
56
|
+
* `TOOL_AUDIT_SCHEMA_VERSION` stays 1 (as `sandbox` did).
|
|
57
|
+
*/
|
|
58
|
+
readonly exitCode?: number;
|
|
59
|
+
/** Provider id for a provider-supplied tool; absent for built-ins. */
|
|
60
|
+
readonly provider?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Result size BEFORE the maxBytes slice. `resultBytes` is measured after,
|
|
63
|
+
* so elision is otherwise invisible — a 2 MB result and a 40 KB one both
|
|
64
|
+
* ledger as 40000.
|
|
65
|
+
*/
|
|
66
|
+
readonly resultBytesPreTruncation?: number;
|
|
67
|
+
/**
|
|
68
|
+
* The `callOp` invocation this tool call happened under.
|
|
69
|
+
*
|
|
70
|
+
* NOT unique: one callId spans every retry, agent-swap hop and turn of the
|
|
71
|
+
* invocation, so it is 1:N over cost rows (10.0% of groups hold more than
|
|
72
|
+
* one). It is a foreign key. Use `turnId` to select a single cost row.
|
|
73
|
+
*
|
|
74
|
+
* This is the OPERATION-layer callId (`DispatchEventBase.callId`), not the
|
|
75
|
+
* stream-layer field of the same name in `agent-stream-events.ts` — joining
|
|
76
|
+
* on that one produced nax#2045's 0-of-1,940 match rate.
|
|
77
|
+
*/
|
|
78
|
+
readonly callId?: string;
|
|
79
|
+
/** Caller-defined region spanning many callOp invocations. */
|
|
80
|
+
readonly scopeId?: string;
|
|
81
|
+
/**
|
|
82
|
+
* The turn this call happened in. One turn is one cost row, so this is the
|
|
83
|
+
* field that prices a tool call. Native sessions only — ACP does not route
|
|
84
|
+
* coding tools through the turn loop.
|
|
85
|
+
*/
|
|
86
|
+
readonly turnId?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Model round-trip index WITHIN the turn, 1-based.
|
|
89
|
+
*
|
|
90
|
+
* Not a turn index. `turn-loop.ts` pushes an interaction field named
|
|
91
|
+
* `turnIndex` whose value is this same round-trip counter; the two names
|
|
92
|
+
* have been conflated before. Native sessions only.
|
|
93
|
+
*/
|
|
94
|
+
readonly roundTrips?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Provider-assigned `tool_use` id, passed through verbatim — nax never mints
|
|
97
|
+
* or namespaces it. Unique within a session in practice, with no global
|
|
98
|
+
* guarantee, and NOT stable across a retry: a retried turn produces fresh
|
|
99
|
+
* ids. The unique tuple is (runId, sessionName, toolCallId).
|
|
100
|
+
*/
|
|
101
|
+
readonly toolCallId?: string;
|
|
102
|
+
}
|
|
103
|
+
export interface ToolAuditSink {
|
|
104
|
+
record(entry: ToolCallRecord): void;
|
|
105
|
+
flush(): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The sink `createToolAuditSink` returns, plus the shutdown-time flush.
|
|
109
|
+
*
|
|
110
|
+
* `flushPartial()` writes whatever is still buffered with `partial: true` so a
|
|
111
|
+
* sink whose hop `finally` never runs before `process.exit` still leaves its
|
|
112
|
+
* audit file behind. After it runs, the sink's own `flush()` is a no-op — the
|
|
113
|
+
* hop `finally` that runs later must not write a second file.
|
|
114
|
+
*/
|
|
115
|
+
export interface RegisteredSink extends ToolAuditSink {
|
|
116
|
+
flushPartial(): Promise<void>;
|
|
117
|
+
}
|
|
118
|
+
/** Register a sink so the run's shutdown can flush whatever it still holds. */
|
|
119
|
+
export declare function registerToolAuditSink(runId: string, sink: RegisteredSink): void;
|
|
120
|
+
/** Drop a sink from the run's registry (see registerToolAuditSink). */
|
|
121
|
+
export declare function unregisterToolAuditSink(runId: string, sink: RegisteredSink): void;
|
|
122
|
+
/**
|
|
123
|
+
* Flush every still-registered sink for `runId` with `partial: true`, then
|
|
124
|
+
* clear them. Never rejects: one sink's failure must not stop the drain.
|
|
125
|
+
*/
|
|
126
|
+
export declare function flushOpenToolAuditSinks(runId: string): Promise<void>;
|
|
127
|
+
export declare function createNoOpToolAuditSink(): ToolAuditSink;
|
|
128
|
+
/**
|
|
129
|
+
* tool-audit file schema version.
|
|
130
|
+
*
|
|
131
|
+
* 1 — first versioned generation. Adds a file header (`runId`, `featureName`,
|
|
132
|
+
* `storyId`, `sessionRole`) and per-call correlation ids (`callId`,
|
|
133
|
+
* `scopeId`, `turnId`, `roundTrips`, `toolCallId`).
|
|
134
|
+
*
|
|
135
|
+
* Files written before this field existed carry none of the above and
|
|
136
|
+
* cannot be backfilled: `runId` in particular was never in scope at the
|
|
137
|
+
* sink's construction path, so an unversioned file's only identity is its
|
|
138
|
+
* filename.
|
|
139
|
+
*
|
|
140
|
+
* `sessionName` is a HUMAN LABEL from this version on, never a join key.
|
|
141
|
+
* It is constant-prefixed and stable across re-runs by construction, so it
|
|
142
|
+
* collides: 7.5% of review-audit sessionNames span more than one runId.
|
|
143
|
+
* Join on `runId` plus `callId`/`turnId` instead.
|
|
144
|
+
*
|
|
145
|
+
* NOTE ON `resultBytes`: it is measured AFTER the shared model-truncation
|
|
146
|
+
* policy (applyModelTruncationPolicy, src/tools/spill.ts) and so counts what
|
|
147
|
+
* the model actually received — the spill marker is composed inside the
|
|
148
|
+
* measured content, so its bytes count. That boundary moved at the
|
|
149
|
+
* `native-loop-events` merge (f4b3bbc7a): files written before it measured
|
|
150
|
+
* each tool's own post-truncation `result.content.length` at `ctx.maxBytes`
|
|
151
|
+
* instead. The policy additionally applies `MODEL_MAX_LINES` (1_000),
|
|
152
|
+
* `MODEL_MAX_LINE_CHARS` (2_000) and a `MODEL_MAX_BYTES` (40_000) ceiling,
|
|
153
|
+
* so the two generations are not comparable call for call. The unit was
|
|
154
|
+
* never bytes: it is `String#length`, i.e. UTF-16 code units, so a
|
|
155
|
+
* multi-byte result under-reports against the field name.
|
|
156
|
+
* `resultBytesPreTruncation` is the pre-policy size and, where a tool sets
|
|
157
|
+
* it, is UTF-8 bytes (`Buffer.byteLength` / `Bun.file().size`).
|
|
158
|
+
*/
|
|
159
|
+
export declare const TOOL_AUDIT_SCHEMA_VERSION = 1;
|
|
160
|
+
/** Run-scoped identity stamped once per tool-audit file. */
|
|
161
|
+
export interface ToolAuditHeader {
|
|
162
|
+
readonly runId?: string;
|
|
163
|
+
readonly featureName?: string;
|
|
164
|
+
readonly storyId?: string;
|
|
165
|
+
readonly sessionRole?: string;
|
|
166
|
+
}
|
|
167
|
+
export declare function createToolAuditSink(opts: {
|
|
168
|
+
dir: string;
|
|
169
|
+
sessionName: string;
|
|
170
|
+
header?: ToolAuditHeader;
|
|
171
|
+
}): RegisteredSink;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable record of every coding-tool call.
|
|
3
|
+
*
|
|
4
|
+
* runtime.ts already logs each outcome, and says why: a refused call that
|
|
5
|
+
* leaves no trace is indistinguishable from a call never made. But it logs
|
|
6
|
+
* through getSafeLogger(), and issue #1359 closed on a measured zero taken off
|
|
7
|
+
* exactly such a counter while the persisted records still held ten in-window
|
|
8
|
+
* findings. The zero meant "no data retained" and was read as "did not happen".
|
|
9
|
+
*
|
|
10
|
+
* So a signal a later decision depends on is written here, not there. The
|
|
11
|
+
* logger keeps its calls for operator visibility; neither replaces the other.
|
|
12
|
+
*
|
|
13
|
+
* File shape mirrors src/review/review-audit.ts: one JSON file per session,
|
|
14
|
+
* named <runId>-<epochMs>-<sessionName>.json when the runId is known, and
|
|
15
|
+
* <epochMs>-<sessionName>.json otherwise.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { errorMessage } from "#src/infra/errors";
|
|
20
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
21
|
+
import { redactRowStrings } from "#src/permissions/index";
|
|
22
|
+
/**
|
|
23
|
+
* Still-open sinks, keyed by the `runId` they were created under.
|
|
24
|
+
*
|
|
25
|
+
* A sink is addressable here precisely because its hop `finally` has not run
|
|
26
|
+
* yet: `createToolAuditSink` registers it on creation and unregisters it on its
|
|
27
|
+
* first write, so the registry only ever holds buffers that would otherwise be
|
|
28
|
+
* lost to `process.exit`.
|
|
29
|
+
*/
|
|
30
|
+
const openToolAuditSinks = new Map();
|
|
31
|
+
/** Register a sink so the run's shutdown can flush whatever it still holds. */
|
|
32
|
+
export function registerToolAuditSink(runId, sink) {
|
|
33
|
+
const sinks = openToolAuditSinks.get(runId);
|
|
34
|
+
if (sinks === undefined) {
|
|
35
|
+
openToolAuditSinks.set(runId, new Set([sink]));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
sinks.add(sink);
|
|
39
|
+
}
|
|
40
|
+
/** Drop a sink from the run's registry (see registerToolAuditSink). */
|
|
41
|
+
export function unregisterToolAuditSink(runId, sink) {
|
|
42
|
+
const sinks = openToolAuditSinks.get(runId);
|
|
43
|
+
if (sinks === undefined)
|
|
44
|
+
return;
|
|
45
|
+
sinks.delete(sink);
|
|
46
|
+
if (sinks.size === 0)
|
|
47
|
+
openToolAuditSinks.delete(runId);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Flush every still-registered sink for `runId` with `partial: true`, then
|
|
51
|
+
* clear them. Never rejects: one sink's failure must not stop the drain.
|
|
52
|
+
*/
|
|
53
|
+
export async function flushOpenToolAuditSinks(runId) {
|
|
54
|
+
const sinks = openToolAuditSinks.get(runId);
|
|
55
|
+
if (sinks === undefined)
|
|
56
|
+
return;
|
|
57
|
+
// Clear before flushing: the sink's own write path calls
|
|
58
|
+
// unregisterToolAuditSink, and re-entering the registry mid-iteration would
|
|
59
|
+
// mutate the map we are reading.
|
|
60
|
+
openToolAuditSinks.delete(runId);
|
|
61
|
+
// Started concurrently, so a slow or never-settling sink cannot starve the
|
|
62
|
+
// writes of the sinks after it. Each sink owns a distinct file, so the
|
|
63
|
+
// completion order is irrelevant. Every task swallows its own rejection --
|
|
64
|
+
// one sink's failure is logged and the rest still flush.
|
|
65
|
+
await Promise.all([...sinks].map(async (sink) => {
|
|
66
|
+
try {
|
|
67
|
+
await sink.flushPartial();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
getSafeLogger()?.warn("tools", "tool-audit partial flush failed", { runId, error: errorMessage(error) });
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
export function createNoOpToolAuditSink() {
|
|
75
|
+
return { record() { }, async flush() { } };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* tool-audit file schema version.
|
|
79
|
+
*
|
|
80
|
+
* 1 — first versioned generation. Adds a file header (`runId`, `featureName`,
|
|
81
|
+
* `storyId`, `sessionRole`) and per-call correlation ids (`callId`,
|
|
82
|
+
* `scopeId`, `turnId`, `roundTrips`, `toolCallId`).
|
|
83
|
+
*
|
|
84
|
+
* Files written before this field existed carry none of the above and
|
|
85
|
+
* cannot be backfilled: `runId` in particular was never in scope at the
|
|
86
|
+
* sink's construction path, so an unversioned file's only identity is its
|
|
87
|
+
* filename.
|
|
88
|
+
*
|
|
89
|
+
* `sessionName` is a HUMAN LABEL from this version on, never a join key.
|
|
90
|
+
* It is constant-prefixed and stable across re-runs by construction, so it
|
|
91
|
+
* collides: 7.5% of review-audit sessionNames span more than one runId.
|
|
92
|
+
* Join on `runId` plus `callId`/`turnId` instead.
|
|
93
|
+
*
|
|
94
|
+
* NOTE ON `resultBytes`: it is measured AFTER the shared model-truncation
|
|
95
|
+
* policy (applyModelTruncationPolicy, src/tools/spill.ts) and so counts what
|
|
96
|
+
* the model actually received — the spill marker is composed inside the
|
|
97
|
+
* measured content, so its bytes count. That boundary moved at the
|
|
98
|
+
* `native-loop-events` merge (f4b3bbc7a): files written before it measured
|
|
99
|
+
* each tool's own post-truncation `result.content.length` at `ctx.maxBytes`
|
|
100
|
+
* instead. The policy additionally applies `MODEL_MAX_LINES` (1_000),
|
|
101
|
+
* `MODEL_MAX_LINE_CHARS` (2_000) and a `MODEL_MAX_BYTES` (40_000) ceiling,
|
|
102
|
+
* so the two generations are not comparable call for call. The unit was
|
|
103
|
+
* never bytes: it is `String#length`, i.e. UTF-16 code units, so a
|
|
104
|
+
* multi-byte result under-reports against the field name.
|
|
105
|
+
* `resultBytesPreTruncation` is the pre-policy size and, where a tool sets
|
|
106
|
+
* it, is UTF-8 bytes (`Buffer.byteLength` / `Bun.file().size`).
|
|
107
|
+
*/
|
|
108
|
+
export const TOOL_AUDIT_SCHEMA_VERSION = 1;
|
|
109
|
+
/**
|
|
110
|
+
* Monotonic millisecond stamp for audit filenames.
|
|
111
|
+
*
|
|
112
|
+
* `Date.now()` alone is not enough: `flushOpenToolAuditSinks` writes every
|
|
113
|
+
* still-open sink for a run in one tight loop, so two sinks sharing a
|
|
114
|
+
* `sessionName` routinely land on the same millisecond -- the second
|
|
115
|
+
* `writeFile` then silently clobbers the first and one hop's calls are gone.
|
|
116
|
+
* Nudging the stamp forward on a repeat keeps the filename shape
|
|
117
|
+
* (`<runId>-<stamp>-<sessionName>.json`) and guarantees one file per sink.
|
|
118
|
+
*/
|
|
119
|
+
let lastFileStamp = 0;
|
|
120
|
+
function nextFileStamp() {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
lastFileStamp = now > lastFileStamp ? now : lastFileStamp + 1;
|
|
123
|
+
return lastFileStamp;
|
|
124
|
+
}
|
|
125
|
+
export function createToolAuditSink(opts) {
|
|
126
|
+
const calls = [];
|
|
127
|
+
const runId = opts.header?.runId;
|
|
128
|
+
// Set once the shutdown path has written this sink. The hop `finally` that
|
|
129
|
+
// may still run afterwards must not write a second, non-partial file.
|
|
130
|
+
let partialFlushed = false;
|
|
131
|
+
const write = async (partial) => {
|
|
132
|
+
if (calls.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
await mkdir(opts.dir, { recursive: true });
|
|
135
|
+
const body = JSON.stringify({
|
|
136
|
+
schemaVersion: TOOL_AUDIT_SCHEMA_VERSION,
|
|
137
|
+
...(opts.header ?? {}),
|
|
138
|
+
sessionName: opts.sessionName,
|
|
139
|
+
// Additive and optional: absent on a normal flush, `true` only when the
|
|
140
|
+
// shutdown path persisted a buffer its hop never closed.
|
|
141
|
+
...(partial ? { partial: true } : {}),
|
|
142
|
+
calls: redactRowStrings(calls),
|
|
143
|
+
}, null, 2);
|
|
144
|
+
const prefix = runId !== undefined ? `${runId}-` : "";
|
|
145
|
+
await writeFile(join(opts.dir, `${prefix}${nextFileStamp()}-${opts.sessionName}.json`), body);
|
|
146
|
+
};
|
|
147
|
+
const sink = {
|
|
148
|
+
record(entry) {
|
|
149
|
+
// The partial file is this sink's last write (a second would duplicate
|
|
150
|
+
// it), so a row arriving after a SUCCESSFUL partial flush cannot be
|
|
151
|
+
// persisted. Drop it loudly rather than losing it without a trace.
|
|
152
|
+
if (partialFlushed) {
|
|
153
|
+
getSafeLogger()?.warn("tools", "tool-audit row recorded after the partial flush was dropped", {
|
|
154
|
+
runId,
|
|
155
|
+
tool: entry.tool,
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
calls.push(entry);
|
|
160
|
+
},
|
|
161
|
+
async flushPartial() {
|
|
162
|
+
if (partialFlushed)
|
|
163
|
+
return;
|
|
164
|
+
await write(true);
|
|
165
|
+
// Only now is the buffer spent. A rejected write leaves the sink open so
|
|
166
|
+
// the hop `finally` can still write the full file instead of nothing.
|
|
167
|
+
partialFlushed = true;
|
|
168
|
+
if (runId !== undefined)
|
|
169
|
+
unregisterToolAuditSink(runId, sink);
|
|
170
|
+
},
|
|
171
|
+
async flush() {
|
|
172
|
+
// A later hop `finally` after the partial flush: nothing left to write.
|
|
173
|
+
if (partialFlushed)
|
|
174
|
+
return;
|
|
175
|
+
if (runId !== undefined)
|
|
176
|
+
unregisterToolAuditSink(runId, sink);
|
|
177
|
+
await write(false);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
if (runId !== undefined)
|
|
181
|
+
registerToolAuditSink(runId, sink);
|
|
182
|
+
return sink;
|
|
183
|
+
}
|