@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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect an invalid tool-call input at the transport boundary (nax#2047).
|
|
3
|
+
*
|
|
4
|
+
* The model returned RunCommand calls with `values:""` 69 times in a row —
|
|
5
|
+
* the empty string produced no rejected keys, the loop never named "values"
|
|
6
|
+
* as the wrong type, so the spin breaker never saw the repetition as
|
|
7
|
+
* repetition. This module is the gate the loop was missing: validate the
|
|
8
|
+
* call against the tool's JSON Schema before any handler runs. On violation,
|
|
9
|
+
* name the property and offer a schema-conforming exemplar so the next round
|
|
10
|
+
* trip sees what the tool would have accepted.
|
|
11
|
+
*
|
|
12
|
+
* Where the exemplar goes (nax#2200): #2047 wrote it over the model's call in
|
|
13
|
+
* the assistant message, so the malformed value would not persist as a
|
|
14
|
+
* few-shot example. That kept the purpose but attributed a call the model never
|
|
15
|
+
* made — `refs: ["<FILL IN: refs>"]` — to the assistant. The recorded call is
|
|
16
|
+
* now the model's own input with ONLY the rejected property removed: the
|
|
17
|
+
* malformed value still never persists, nothing fabricated is attributed to
|
|
18
|
+
* the model, and the exemplar lives in the tool-result's error text, where it
|
|
19
|
+
* is plainly guidance from the harness.
|
|
20
|
+
*
|
|
21
|
+
* Since nax#2151 this decision is produced by a built-in `before_tool`
|
|
22
|
+
* handler (`loop-handlers.ts`) instead of an inline branch of the loop: the
|
|
23
|
+
* repair is a tool-call decision — answer without invoking — which is exactly
|
|
24
|
+
* what the seam's `block` outcome is for. What stays here is the decision and
|
|
25
|
+
* its per-turn budget; the transcript rewrite is the loop's, because that is
|
|
26
|
+
* where the message array lives.
|
|
27
|
+
*
|
|
28
|
+
* Returns `undefined` when the call is valid, or when the tool is not in the
|
|
29
|
+
* local catalogue (no schema → no validation, success path). The success path
|
|
30
|
+
* costs one function call and no allocation.
|
|
31
|
+
*
|
|
32
|
+
* The per-turn budget (Task 4) lives in `createInvalidCallBudget()` — three
|
|
33
|
+
* identical invalid calls (same tool, same `stableStringify`'d input) end the
|
|
34
|
+
* turn with `{kind: "stopped"}` and NO tool-result, matching the spin
|
|
35
|
+
* breaker's stop branch ("a result nobody reads only grows the transcript").
|
|
36
|
+
* The counter is per-key, cumulative across interleaved valid calls, and lives
|
|
37
|
+
* in the factory's closure.
|
|
38
|
+
*/
|
|
39
|
+
import type { ToolCall, ToolDefinition } from "@nathapp/nax-ai";
|
|
40
|
+
import type { InvalidToolCallDetail } from "#src/session/session-types";
|
|
41
|
+
import type { TranscriptMessage as NativeTranscriptMessage } from "./compaction.ts";
|
|
42
|
+
/** Three identical invalid calls in a turn ends it (Task 4). */
|
|
43
|
+
export declare const INVALID_CALL_BUDGET_THRESHOLD = 3;
|
|
44
|
+
export type InvalidCallOutcome =
|
|
45
|
+
/**
|
|
46
|
+
* Answer the call with `content` instead of running it. `input` is what the
|
|
47
|
+
* transcript records for the call: the model's own input minus the rejected
|
|
48
|
+
* property (nax#2200), never a fabricated exemplar.
|
|
49
|
+
*/
|
|
50
|
+
{
|
|
51
|
+
readonly kind: "repair";
|
|
52
|
+
readonly input: Record<string, unknown>;
|
|
53
|
+
readonly content: string;
|
|
54
|
+
}
|
|
55
|
+
/** The per-key budget is spent: end the turn with no answer at all. */
|
|
56
|
+
| {
|
|
57
|
+
readonly kind: "stopped";
|
|
58
|
+
readonly detail: InvalidToolCallDetail;
|
|
59
|
+
};
|
|
60
|
+
export declare function decideInvalidToolCall(call: ToolCall, tools: readonly ToolDefinition[], counters: Map<string, number>): InvalidCallOutcome | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* The call's input with its `null`-valued optional properties removed, or
|
|
63
|
+
* `undefined` when the tool is not in the catalogue or nothing was removed
|
|
64
|
+
* (nax#2200). The validator already reads such a `null` as absent; this is
|
|
65
|
+
* what makes the tool's handler see the same thing.
|
|
66
|
+
*/
|
|
67
|
+
export declare function normalizeNullOptionals(call: ToolCall, tools: readonly ToolDefinition[]): Record<string, unknown> | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Replace one `toolCalls` entry's input on the LAST assistant message.
|
|
70
|
+
*
|
|
71
|
+
* Composes onto the last assistant message rather than the last array entry:
|
|
72
|
+
* after iteration N of a multi-call assistant message, iteration N+1 sees a
|
|
73
|
+
* tool-result for iteration N at the tail, and a naive implementation would
|
|
74
|
+
* rewrite only the last of two invalid calls.
|
|
75
|
+
*
|
|
76
|
+
* Never mutates: the returned array and the rewritten message are fresh values,
|
|
77
|
+
* and thinking blocks survive byte-for-byte (ADR-028 §8 — Anthropic needs the
|
|
78
|
+
* exact thinking block back to continue a thinking conversation).
|
|
79
|
+
*/
|
|
80
|
+
export declare function rewriteToolCallInput(messages: readonly NativeTranscriptMessage[], callId: string, input: Record<string, unknown>): NativeTranscriptMessage[];
|
|
81
|
+
/**
|
|
82
|
+
* Per-turn budget for repeated invalid tool calls (nax#2047, Task 4).
|
|
83
|
+
*
|
|
84
|
+
* The spin breaker's 50-call budget is for REPEATED *valid-shape* calls; a
|
|
85
|
+
* malformed shape is a stronger signal — the model will keep sending the
|
|
86
|
+
* same wrong shape for as long as it ignores the error result. This budget
|
|
87
|
+
* hard-stops a turn after three identical invalid calls (same tool, same
|
|
88
|
+
* `stableStringify`'d input) so the transcript never grows past the second
|
|
89
|
+
* error result.
|
|
90
|
+
*
|
|
91
|
+
* Per-key, cumulative across interleaved valid calls: a valid call does
|
|
92
|
+
* not reset the counter, and the same key returning later in the same turn
|
|
93
|
+
* keeps accumulating. Different keys do not stop each other — three distinct
|
|
94
|
+
* invalid calls (the model is exploring) are not a budget violation.
|
|
95
|
+
*
|
|
96
|
+
* On exceed, `observe()` returns `{kind: "stopped"}` and `exceeded` flips to
|
|
97
|
+
* `true`. The caller checks `exceeded` after the dispatch to break the batch
|
|
98
|
+
* with no tool-result and to set `TurnResult.invalidCallBudgetExceeded`,
|
|
99
|
+
* mirroring `spinStopped`'s reporting pattern.
|
|
100
|
+
*/
|
|
101
|
+
export interface InvalidCallBudget {
|
|
102
|
+
/** Whether the budget was tripped on any call so far. Read after the dispatch. */
|
|
103
|
+
readonly exceeded: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The call that tripped the budget — set together with `exceeded`, so the
|
|
106
|
+
* halt can be logged and classified by what was rejected (nax#2200).
|
|
107
|
+
*/
|
|
108
|
+
readonly halt: InvalidToolCallDetail | undefined;
|
|
109
|
+
observe(call: ToolCall, tools: readonly ToolDefinition[]): InvalidCallOutcome | undefined;
|
|
110
|
+
}
|
|
111
|
+
export declare function createInvalidCallBudget(): InvalidCallBudget;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect an invalid tool-call input at the transport boundary (nax#2047).
|
|
3
|
+
*
|
|
4
|
+
* The model returned RunCommand calls with `values:""` 69 times in a row —
|
|
5
|
+
* the empty string produced no rejected keys, the loop never named "values"
|
|
6
|
+
* as the wrong type, so the spin breaker never saw the repetition as
|
|
7
|
+
* repetition. This module is the gate the loop was missing: validate the
|
|
8
|
+
* call against the tool's JSON Schema before any handler runs. On violation,
|
|
9
|
+
* name the property and offer a schema-conforming exemplar so the next round
|
|
10
|
+
* trip sees what the tool would have accepted.
|
|
11
|
+
*
|
|
12
|
+
* Where the exemplar goes (nax#2200): #2047 wrote it over the model's call in
|
|
13
|
+
* the assistant message, so the malformed value would not persist as a
|
|
14
|
+
* few-shot example. That kept the purpose but attributed a call the model never
|
|
15
|
+
* made — `refs: ["<FILL IN: refs>"]` — to the assistant. The recorded call is
|
|
16
|
+
* now the model's own input with ONLY the rejected property removed: the
|
|
17
|
+
* malformed value still never persists, nothing fabricated is attributed to
|
|
18
|
+
* the model, and the exemplar lives in the tool-result's error text, where it
|
|
19
|
+
* is plainly guidance from the harness.
|
|
20
|
+
*
|
|
21
|
+
* Since nax#2151 this decision is produced by a built-in `before_tool`
|
|
22
|
+
* handler (`loop-handlers.ts`) instead of an inline branch of the loop: the
|
|
23
|
+
* repair is a tool-call decision — answer without invoking — which is exactly
|
|
24
|
+
* what the seam's `block` outcome is for. What stays here is the decision and
|
|
25
|
+
* its per-turn budget; the transcript rewrite is the loop's, because that is
|
|
26
|
+
* where the message array lives.
|
|
27
|
+
*
|
|
28
|
+
* Returns `undefined` when the call is valid, or when the tool is not in the
|
|
29
|
+
* local catalogue (no schema → no validation, success path). The success path
|
|
30
|
+
* costs one function call and no allocation.
|
|
31
|
+
*
|
|
32
|
+
* The per-turn budget (Task 4) lives in `createInvalidCallBudget()` — three
|
|
33
|
+
* identical invalid calls (same tool, same `stableStringify`'d input) end the
|
|
34
|
+
* turn with `{kind: "stopped"}` and NO tool-result, matching the spin
|
|
35
|
+
* breaker's stop branch ("a result nobody reads only grows the transcript").
|
|
36
|
+
* The counter is per-key, cumulative across interleaved valid calls, and lives
|
|
37
|
+
* in the factory's closure.
|
|
38
|
+
*/
|
|
39
|
+
import { byCodePoint } from "#src/internal/sort";
|
|
40
|
+
import { exemplarFor } from "./tool-input-exemplar.js";
|
|
41
|
+
import { stripNullOptionals, validateToolInput } from "./tool-input-validation.js";
|
|
42
|
+
/** Three identical invalid calls in a turn ends it (Task 4). */
|
|
43
|
+
export const INVALID_CALL_BUDGET_THRESHOLD = 3;
|
|
44
|
+
export function decideInvalidToolCall(call, tools, counters) {
|
|
45
|
+
const tool = tools.find((t) => t.name === call.name);
|
|
46
|
+
if (tool === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const violation = validateToolInput(tool.inputSchema, call.input);
|
|
49
|
+
if (violation === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
// Per-key counter: tool name + stableStringify(input). Mutated in place
|
|
52
|
+
// so the next iteration sees the updated count without us having to
|
|
53
|
+
// thread the new map back through.
|
|
54
|
+
const counterKey = `${call.name}\u0000${stableStringify(call.input)}`;
|
|
55
|
+
const nextCount = (counters.get(counterKey) ?? 0) + 1;
|
|
56
|
+
counters.set(counterKey, nextCount);
|
|
57
|
+
if (nextCount >= INVALID_CALL_BUDGET_THRESHOLD) {
|
|
58
|
+
const { property, expected, actual } = violation;
|
|
59
|
+
return { kind: "stopped", detail: { tool: call.name, property, expected, actual } };
|
|
60
|
+
}
|
|
61
|
+
const original = call.input;
|
|
62
|
+
const exemplar = exemplarFor(tool.inputSchema, original, violation);
|
|
63
|
+
const recorded = withoutProperty(original, violation.property);
|
|
64
|
+
return { kind: "repair", input: recorded, content: invalidCallMessage(violation, exemplar, recorded !== original) };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The call's input with its `null`-valued optional properties removed, or
|
|
68
|
+
* `undefined` when the tool is not in the catalogue or nothing was removed
|
|
69
|
+
* (nax#2200). The validator already reads such a `null` as absent; this is
|
|
70
|
+
* what makes the tool's handler see the same thing.
|
|
71
|
+
*/
|
|
72
|
+
export function normalizeNullOptionals(call, tools) {
|
|
73
|
+
const tool = tools.find((t) => t.name === call.name);
|
|
74
|
+
if (tool === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
return stripNullOptionals(tool.inputSchema, call.input);
|
|
77
|
+
}
|
|
78
|
+
/** `input` without `property`; the same object when the property is not there. */
|
|
79
|
+
function withoutProperty(input, property) {
|
|
80
|
+
if (property === "" || !(property in input))
|
|
81
|
+
return input;
|
|
82
|
+
const { [property]: _rejected, ...rest } = input;
|
|
83
|
+
return rest;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Replace one `toolCalls` entry's input on the LAST assistant message.
|
|
87
|
+
*
|
|
88
|
+
* Composes onto the last assistant message rather than the last array entry:
|
|
89
|
+
* after iteration N of a multi-call assistant message, iteration N+1 sees a
|
|
90
|
+
* tool-result for iteration N at the tail, and a naive implementation would
|
|
91
|
+
* rewrite only the last of two invalid calls.
|
|
92
|
+
*
|
|
93
|
+
* Never mutates: the returned array and the rewritten message are fresh values,
|
|
94
|
+
* and thinking blocks survive byte-for-byte (ADR-028 §8 — Anthropic needs the
|
|
95
|
+
* exact thinking block back to continue a thinking conversation).
|
|
96
|
+
*/
|
|
97
|
+
export function rewriteToolCallInput(messages, callId, input) {
|
|
98
|
+
let lastAssistantIndex = -1;
|
|
99
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
100
|
+
if (messages[i]?.role === "assistant") {
|
|
101
|
+
lastAssistantIndex = i;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const lastAssistant = messages[lastAssistantIndex];
|
|
106
|
+
if (lastAssistant === undefined || lastAssistant.role !== "assistant")
|
|
107
|
+
return [...messages];
|
|
108
|
+
const toolCalls = (lastAssistant.toolCalls ?? []).map((tc) => (tc.id === callId ? { ...tc, input } : tc));
|
|
109
|
+
return [
|
|
110
|
+
...messages.slice(0, lastAssistantIndex),
|
|
111
|
+
{
|
|
112
|
+
role: "assistant",
|
|
113
|
+
content: lastAssistant.content,
|
|
114
|
+
toolCalls,
|
|
115
|
+
...(lastAssistant.thinking !== undefined ? { thinking: lastAssistant.thinking } : {}),
|
|
116
|
+
},
|
|
117
|
+
...messages.slice(lastAssistantIndex + 1),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
function invalidCallMessage(violation, exemplar, removedFromRecord) {
|
|
121
|
+
return (`invalid tool call: property "${violation.property}" expected ${violation.expected}, ` +
|
|
122
|
+
`got ${violation.actual}. The call was not executed. ` +
|
|
123
|
+
(removedFromRecord ? `The rejected "${violation.property}" value was removed from the recorded call. ` : "") +
|
|
124
|
+
`An input of the expected shape looks like ${JSON.stringify(exemplar)} — ` +
|
|
125
|
+
"replace any <FILL IN> placeholder with a real value, and omit an optional property you have no value for.");
|
|
126
|
+
}
|
|
127
|
+
export function createInvalidCallBudget() {
|
|
128
|
+
const counters = new Map();
|
|
129
|
+
let halt;
|
|
130
|
+
return {
|
|
131
|
+
get exceeded() {
|
|
132
|
+
return halt !== undefined;
|
|
133
|
+
},
|
|
134
|
+
get halt() {
|
|
135
|
+
return halt;
|
|
136
|
+
},
|
|
137
|
+
observe(call, tools) {
|
|
138
|
+
const outcome = decideInvalidToolCall(call, tools, counters);
|
|
139
|
+
if (outcome?.kind === "stopped" && halt === undefined)
|
|
140
|
+
halt = outcome.detail;
|
|
141
|
+
return outcome;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Deterministic serializer independent of property insertion order, so
|
|
147
|
+
* `{a:1,b:2}` and `{b:2,a:1}` collide on the same counter key. Locally
|
|
148
|
+
* scoped on purpose — `src/runtime/spin-breaker` has its own private copy
|
|
149
|
+
* of the same algorithm. The two implementations are deliberately small and
|
|
150
|
+
* identical so a future extraction is a one-line move.
|
|
151
|
+
*/
|
|
152
|
+
function stableStringify(value) {
|
|
153
|
+
if (value === null || typeof value !== "object")
|
|
154
|
+
return JSON.stringify(value) ?? "null";
|
|
155
|
+
if (Array.isArray(value))
|
|
156
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
157
|
+
const entries = Object.entries(value).sort((a, b) => byCodePoint(a[0], b[0]));
|
|
158
|
+
return `{${entries.map(([key, val]) => `${JSON.stringify(key)}:${stableStringify(val)}`).join(",")}}`;
|
|
159
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function checkPrefixStable(before: readonly unknown[], after: readonly unknown[], anchorIndex: number | undefined): boolean;
|
|
2
|
+
export declare function applyHistoryPatch<T>(args: {
|
|
3
|
+
readonly before: readonly T[];
|
|
4
|
+
readonly patched: readonly T[] | undefined;
|
|
5
|
+
readonly anchorIndex: number | undefined;
|
|
6
|
+
readonly boundary: boolean;
|
|
7
|
+
readonly event: string;
|
|
8
|
+
}): {
|
|
9
|
+
readonly messages: readonly T[];
|
|
10
|
+
readonly honoured: boolean;
|
|
11
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
2
|
+
export function checkPrefixStable(before, after, anchorIndex) {
|
|
3
|
+
// No anchor means no cached prefix to protect (spec 3.5).
|
|
4
|
+
if (anchorIndex === undefined)
|
|
5
|
+
return true;
|
|
6
|
+
const end = Math.min(anchorIndex, before.length - 1);
|
|
7
|
+
if (after.length <= end)
|
|
8
|
+
return false;
|
|
9
|
+
for (let i = 0; i <= end; i += 1) {
|
|
10
|
+
// Reference identity: an equal-valued rebuild still breaks the wire cache.
|
|
11
|
+
if (before[i] !== after[i])
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
export function applyHistoryPatch(args) {
|
|
17
|
+
const { before, patched, anchorIndex, boundary, event } = args;
|
|
18
|
+
if (patched === undefined)
|
|
19
|
+
return { messages: before, honoured: false };
|
|
20
|
+
if (boundary || checkPrefixStable(before, patched, anchorIndex)) {
|
|
21
|
+
return { messages: patched, honoured: true };
|
|
22
|
+
}
|
|
23
|
+
getSafeLogger()?.warn("native-loop-events", "history patch rejected: prefix rewritten off-boundary", {
|
|
24
|
+
event,
|
|
25
|
+
...(anchorIndex !== undefined ? { anchorIndex } : {}),
|
|
26
|
+
});
|
|
27
|
+
// What stops is the PATCH, never the turn (spec 3.7).
|
|
28
|
+
return { messages: before, honoured: false };
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-002 — `wrapExternalHandler()`: bounded lifetime, context and attributed
|
|
3
|
+
* failure for plugin-contributed loop handlers.
|
|
4
|
+
*
|
|
5
|
+
* A `src/hooks/` handler is a child process with a 5s timeout; a plugin loop
|
|
6
|
+
* handler is an IN-PROCESS function called from inside `runNativeTurn`, so
|
|
7
|
+
* nothing bounds it and nothing catches it. A handler that never settles would
|
|
8
|
+
* hang the turn, and a handler that throws must not be able to silently pass a
|
|
9
|
+
* tool guard. This module answers both, for one staged entry at a time, and it
|
|
10
|
+
* never rethrows: a plugin's defect must not fail the turn.
|
|
11
|
+
*
|
|
12
|
+
* The payload is handed over untouched — including for `before_tool`, whose
|
|
13
|
+
* answer is a DECISION (`allow`/`nudge`/`block`/`terminate`) rather than a
|
|
14
|
+
* field patch. That answer is therefore validated rather than field-picked: a
|
|
15
|
+
* plugin module is plain JavaScript, is loaded from disk, and can return
|
|
16
|
+
* anything at all.
|
|
17
|
+
*/
|
|
18
|
+
import type { HandlerOf, LoopEvent, LoopHandlerContext, LoopHandlerEntry } from "./types.ts";
|
|
19
|
+
/**
|
|
20
|
+
* How long a plugin handler may run before the wrapper answers for it. A
|
|
21
|
+
* constant by ruling, not a `NaxConfig` key: it bounds the turn's liveness, it
|
|
22
|
+
* is not a policy a project tunes per package.
|
|
23
|
+
*/
|
|
24
|
+
export declare const LOOP_HANDLER_TIMEOUT_MS = 10000;
|
|
25
|
+
/**
|
|
26
|
+
* Test seam for the deadline only. Read at DISPATCH time, never captured when
|
|
27
|
+
* the handler is wrapped — a wrapped handler is installed once and dispatched
|
|
28
|
+
* many times.
|
|
29
|
+
*/
|
|
30
|
+
export declare const _externalHandlerDeps: {
|
|
31
|
+
timeoutMs: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Wrap one staged plugin registration into a built-in-shaped `HandlerOf<E>`:
|
|
35
|
+
* same payload, same patch, but with a bounded lifetime, the turn's context,
|
|
36
|
+
* and a failure that is attributed and never rethrown.
|
|
37
|
+
*/
|
|
38
|
+
export declare function wrapExternalHandler<E extends LoopEvent>(entry: LoopHandlerEntry, getCtx: () => LoopHandlerContext, signal: AbortSignal): HandlerOf<E>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-002 — `wrapExternalHandler()`: bounded lifetime, context and attributed
|
|
3
|
+
* failure for plugin-contributed loop handlers.
|
|
4
|
+
*
|
|
5
|
+
* A `src/hooks/` handler is a child process with a 5s timeout; a plugin loop
|
|
6
|
+
* handler is an IN-PROCESS function called from inside `runNativeTurn`, so
|
|
7
|
+
* nothing bounds it and nothing catches it. A handler that never settles would
|
|
8
|
+
* hang the turn, and a handler that throws must not be able to silently pass a
|
|
9
|
+
* tool guard. This module answers both, for one staged entry at a time, and it
|
|
10
|
+
* never rethrows: a plugin's defect must not fail the turn.
|
|
11
|
+
*
|
|
12
|
+
* The payload is handed over untouched — including for `before_tool`, whose
|
|
13
|
+
* answer is a DECISION (`allow`/`nudge`/`block`/`terminate`) rather than a
|
|
14
|
+
* field patch. That answer is therefore validated rather than field-picked: a
|
|
15
|
+
* plugin module is plain JavaScript, is loaded from disk, and can return
|
|
16
|
+
* anything at all.
|
|
17
|
+
*/
|
|
18
|
+
import { errorMessage } from "#src/infra/errors";
|
|
19
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
20
|
+
import { isCompleteBeforeToolOutcome } from "./registry.js";
|
|
21
|
+
/**
|
|
22
|
+
* How long a plugin handler may run before the wrapper answers for it. A
|
|
23
|
+
* constant by ruling, not a `NaxConfig` key: it bounds the turn's liveness, it
|
|
24
|
+
* is not a policy a project tunes per package.
|
|
25
|
+
*/
|
|
26
|
+
export const LOOP_HANDLER_TIMEOUT_MS = 10_000;
|
|
27
|
+
/**
|
|
28
|
+
* Test seam for the deadline only. Read at DISPATCH time, never captured when
|
|
29
|
+
* the handler is wrapped — a wrapped handler is installed once and dispatched
|
|
30
|
+
* many times.
|
|
31
|
+
*/
|
|
32
|
+
export const _externalHandlerDeps = {
|
|
33
|
+
timeoutMs: LOOP_HANDLER_TIMEOUT_MS,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The patch fields whose presence is worth tracing: the ones a later
|
|
37
|
+
* `applyHistoryPatch` can reject, so a rejection is attributable to a plugin.
|
|
38
|
+
*/
|
|
39
|
+
const TRACED_PATCH_FIELDS = ["seed", "history", "messages", "summary"];
|
|
40
|
+
/**
|
|
41
|
+
* Race the handler's settlement against the deadline and the turn's abort. The
|
|
42
|
+
* first to settle wins, every later settlement is discarded, and the timer is
|
|
43
|
+
* cleared the moment the race settles so an instant handler never holds the
|
|
44
|
+
* event loop open for the full deadline.
|
|
45
|
+
*
|
|
46
|
+
* The abort is honoured for the lifetime of THIS dispatch: the listener is
|
|
47
|
+
* wired synchronously, before the handler is called. A signal that was already
|
|
48
|
+
* aborted when the dispatch began is deliberately NOT honoured — one registry
|
|
49
|
+
* is installed once and repointed per turn (`./loop-handlers`), so a signal
|
|
50
|
+
* carried over from an earlier turn would otherwise fail-close every later
|
|
51
|
+
* `before_tool` dispatch to `block` without ever calling the handler. The
|
|
52
|
+
* deadline still bounds such a dispatch, so nothing hangs.
|
|
53
|
+
*
|
|
54
|
+
* `setTimeout` and not `Bun.sleep()`: the handle has to be cancelled mid-flight
|
|
55
|
+
* (`clearTimeout`), which is the sanctioned exception to the Bun-native delay
|
|
56
|
+
* rule.
|
|
57
|
+
*/
|
|
58
|
+
function raceSettlement(entry, payload, deps) {
|
|
59
|
+
const timeoutMs = _externalHandlerDeps.timeoutMs;
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
let settled = false;
|
|
62
|
+
let timer;
|
|
63
|
+
const finish = (result) => {
|
|
64
|
+
if (settled)
|
|
65
|
+
return;
|
|
66
|
+
settled = true;
|
|
67
|
+
if (timer !== undefined)
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
deps.signal.removeEventListener("abort", onAbort);
|
|
70
|
+
resolve(result);
|
|
71
|
+
};
|
|
72
|
+
const onAbort = () => finish({ ok: false, reason: "aborted before it settled" });
|
|
73
|
+
timer = setTimeout(() => finish({ ok: false, reason: `did not settle within ${timeoutMs}ms` }), timeoutMs);
|
|
74
|
+
deps.signal.addEventListener("abort", onAbort, { once: true });
|
|
75
|
+
// The context is read here, at dispatch, so a handler always sees the facts
|
|
76
|
+
// of the turn it is running in. Read in its OWN try: a `getCtx` throw is
|
|
77
|
+
// nax's own defect, and it must not be reported as the plugin's handler
|
|
78
|
+
// failing (US-002 review).
|
|
79
|
+
let ctx;
|
|
80
|
+
try {
|
|
81
|
+
ctx = deps.getCtx();
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
finish({ ok: false, reason: `reading the handler context failed: ${errorMessage(err)}` });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let returned;
|
|
88
|
+
try {
|
|
89
|
+
returned = entry.handler(payload, ctx);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
finish({ ok: false, reason: errorMessage(err) });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
Promise.resolve(returned).then((value) => finish({ ok: true, value }), (err) => finish({ ok: false, reason: errorMessage(err) }));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The attributed failure. On `before_tool` the call is answered with an error
|
|
100
|
+
* block — a failed guard must never read as a silent allow — and every other
|
|
101
|
+
* event answers `{}` (no patch). One warn per failure either way, naming the
|
|
102
|
+
* plugin the failure belongs to.
|
|
103
|
+
*
|
|
104
|
+
* The tool name is read defensively: this runs on the failure path, outside any
|
|
105
|
+
* try, and a wrapper whose contract is that it never rethrows must not throw
|
|
106
|
+
* from its own diagnosis of a malformed payload.
|
|
107
|
+
*/
|
|
108
|
+
function failure(entry, payload, reason) {
|
|
109
|
+
const logger = getSafeLogger();
|
|
110
|
+
if (entry.event === "before_tool") {
|
|
111
|
+
const tool = payload?.call?.name;
|
|
112
|
+
logger?.warn("native-loop-events", `plugin '${entry.plugin}' failed on before_tool; blocking the call`, {
|
|
113
|
+
plugin: entry.plugin,
|
|
114
|
+
event: entry.event,
|
|
115
|
+
...(tool !== undefined ? { tool } : {}),
|
|
116
|
+
error: reason,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
kind: "block",
|
|
120
|
+
isError: true,
|
|
121
|
+
content: `Blocked: loop handler from plugin '${entry.plugin}' failed (${reason}).`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
logger?.warn("native-loop-events", `plugin '${entry.plugin}' failed on ${entry.event}`, {
|
|
125
|
+
plugin: entry.plugin,
|
|
126
|
+
event: entry.event,
|
|
127
|
+
error: reason,
|
|
128
|
+
});
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Log which patch fields a handler carried, so a later `applyHistoryPatch`
|
|
133
|
+
* rejection can be traced back to the plugin that caused it. An empty patch
|
|
134
|
+
* traces nothing — there is no field to attribute.
|
|
135
|
+
*/
|
|
136
|
+
function tracePatch(entry, value) {
|
|
137
|
+
// A plain-JavaScript plugin can answer a non-object on any event; there is
|
|
138
|
+
// then no field to attribute, and reading one must not throw.
|
|
139
|
+
if (typeof value !== "object" || value === null)
|
|
140
|
+
return;
|
|
141
|
+
const patch = value;
|
|
142
|
+
const fields = TRACED_PATCH_FIELDS.filter((field) => patch[field] !== undefined);
|
|
143
|
+
if (fields.length === 0)
|
|
144
|
+
return;
|
|
145
|
+
getSafeLogger()?.debug("native-loop-events", `plugin '${entry.plugin}' patched ${entry.event}`, {
|
|
146
|
+
plugin: entry.plugin,
|
|
147
|
+
event: entry.event,
|
|
148
|
+
fields,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function describeValue(value) {
|
|
152
|
+
if (value === null)
|
|
153
|
+
return "null";
|
|
154
|
+
if (Array.isArray(value))
|
|
155
|
+
return "an array";
|
|
156
|
+
if (typeof value === "object")
|
|
157
|
+
return "an object whose kind is not a before_tool outcome";
|
|
158
|
+
return `a ${typeof value} value`;
|
|
159
|
+
}
|
|
160
|
+
/** What a settled (non-failure) handler answer becomes. */
|
|
161
|
+
function settled(entry, payload, value) {
|
|
162
|
+
if (entry.event === "before_tool") {
|
|
163
|
+
// `undefined` is the handler declining to decide, which is an allow.
|
|
164
|
+
if (value === undefined)
|
|
165
|
+
return { kind: "allow" };
|
|
166
|
+
// The plugin boundary, so the answer must be COMPLETE: a `kind` without
|
|
167
|
+
// the payload it promises (`block`/`terminate` content, `nudge` text) is
|
|
168
|
+
// malformed, not a decision.
|
|
169
|
+
if (!isCompleteBeforeToolOutcome(value))
|
|
170
|
+
return failure(entry, payload, `returned ${describeValue(value)}`);
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
if (value === undefined)
|
|
174
|
+
return {};
|
|
175
|
+
tracePatch(entry, value);
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Wrap one staged plugin registration into a built-in-shaped `HandlerOf<E>`:
|
|
180
|
+
* same payload, same patch, but with a bounded lifetime, the turn's context,
|
|
181
|
+
* and a failure that is attributed and never rethrown.
|
|
182
|
+
*/
|
|
183
|
+
export function wrapExternalHandler(entry, getCtx, signal) {
|
|
184
|
+
return async (payload) => {
|
|
185
|
+
const settlement = await raceSettlement(entry, payload, { getCtx, signal });
|
|
186
|
+
// `entry.event` carries the event at runtime; the erased patch is the union
|
|
187
|
+
// the dispatcher re-narrows, because it registered this handler per event.
|
|
188
|
+
const answer = settlement.ok
|
|
189
|
+
? settled(entry, payload, settlement.value)
|
|
190
|
+
: failure(entry, payload, settlement.reason);
|
|
191
|
+
return answer;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel for the loop-event seam (spec 4.5): the single-file `loop-events.ts`
|
|
3
|
+
* module became a directory, and this re-exports every name it exported, so
|
|
4
|
+
* no importer churns.
|
|
5
|
+
*/
|
|
6
|
+
export { type BuildToolResultArgs, buildToolResult, type DenialInfo, type ToolResultMessage } from "../tool-result.ts";
|
|
7
|
+
/**
|
|
8
|
+
* The external-handler surface (US-002). It is re-exported rather than reached
|
|
9
|
+
* through `./external-handler` because `check:alias-internals` rejects a VALUE
|
|
10
|
+
* import of `@/<dir>/<internal>` from `src/`, so the barrel is the only route
|
|
11
|
+
* US-003's installer and US-004's delivery code can take. Identity matters:
|
|
12
|
+
* `_externalHandlerDeps` is the object the wrapper reads at dispatch time.
|
|
13
|
+
*/
|
|
14
|
+
export { _externalHandlerDeps, LOOP_HANDLER_TIMEOUT_MS, wrapExternalHandler } from "./external-handler.ts";
|
|
15
|
+
export { createLoopEventRegistry, LOOP_EVENTS, type LoopEventRegistry } from "./registry.ts";
|
|
16
|
+
export type { AfterToolPatch, AfterToolPayload, BeforeToolOutcome, CompleteCallOptions, ExternalHandlerOf, LoopHandlerContext, LoopHandlerEntry, LoopHandlerSet, } from "./types.ts";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel for the loop-event seam (spec 4.5): the single-file `loop-events.ts`
|
|
3
|
+
* module became a directory, and this re-exports every name it exported, so
|
|
4
|
+
* no importer churns.
|
|
5
|
+
*/
|
|
6
|
+
export { buildToolResult } from "../tool-result.js";
|
|
7
|
+
/**
|
|
8
|
+
* The external-handler surface (US-002). It is re-exported rather than reached
|
|
9
|
+
* through `./external-handler` because `check:alias-internals` rejects a VALUE
|
|
10
|
+
* import of `@/<dir>/<internal>` from `src/`, so the barrel is the only route
|
|
11
|
+
* US-003's installer and US-004's delivery code can take. Identity matters:
|
|
12
|
+
* `_externalHandlerDeps` is the object the wrapper reads at dispatch time.
|
|
13
|
+
*/
|
|
14
|
+
export { _externalHandlerDeps, LOOP_HANDLER_TIMEOUT_MS, wrapExternalHandler } from "./external-handler.js";
|
|
15
|
+
export { createLoopEventRegistry, LOOP_EVENTS } from "./registry.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface ArrayRecord {
|
|
2
|
+
readonly field: string;
|
|
3
|
+
readonly array: unknown[];
|
|
4
|
+
readonly saved: readonly unknown[];
|
|
5
|
+
}
|
|
6
|
+
export type ArraySnapshot = readonly ArrayRecord[];
|
|
7
|
+
export declare function snapshotArrays(payload: object): ArraySnapshot;
|
|
8
|
+
export declare function restoreMutated(snapshot: ArraySnapshot, event: string, handlerIndex: number): void;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review #19: the cache boundary judges only RETURNED patches, but handlers
|
|
3
|
+
* receive the loop's live arrays (history, messages, tools). A handler that
|
|
4
|
+
* edits one in place would rewrite the transcript and the prompt-cache
|
|
5
|
+
* prefix unseen. Dispatch snapshots every array field before each handler and
|
|
6
|
+
* undoes an in-place change afterwards, keeping the SAME array object with
|
|
7
|
+
* its original elements.
|
|
8
|
+
*
|
|
9
|
+
* Not detected: edits INSIDE an element (a message's fields). Deep-freezing
|
|
10
|
+
* is not an option: the payload is live, and the loop itself pushes onto
|
|
11
|
+
* `messages` after a follow-up; freezing a clone would break the reference
|
|
12
|
+
* identity checkPrefixStable relies on.
|
|
13
|
+
*/
|
|
14
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
15
|
+
export function snapshotArrays(payload) {
|
|
16
|
+
return Object.entries(payload).flatMap(([field, value]) => Array.isArray(value) ? [{ field, array: value, saved: [...value] }] : []);
|
|
17
|
+
}
|
|
18
|
+
export function restoreMutated(snapshot, event, handlerIndex) {
|
|
19
|
+
for (const { field, array, saved } of snapshot) {
|
|
20
|
+
const changed = array.length !== saved.length || saved.some((item, i) => array[i] !== item);
|
|
21
|
+
if (!changed)
|
|
22
|
+
continue;
|
|
23
|
+
getSafeLogger()?.warn("native-loop-events", "handler mutated payload in place; restored", {
|
|
24
|
+
event,
|
|
25
|
+
handler: handlerIndex,
|
|
26
|
+
field,
|
|
27
|
+
});
|
|
28
|
+
// Deliberate in-place restore: the caller holds this exact array object.
|
|
29
|
+
array.splice(0, array.length, ...saved);
|
|
30
|
+
}
|
|
31
|
+
}
|