@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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The round-trip loop and turn-end phase of the native turn loop.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `./turn-loop.ts` during the complexity drain
|
|
5
|
+
* (docs/plans/STATUS-complexity-drain.md A2): `runNativeTurn` scored 97 as one
|
|
6
|
+
* function covering setup, the round-trip loop (deliberately unbounded —
|
|
7
|
+
* bounded by the deadline, the idle watchdog, and the spin breaker, not a
|
|
8
|
+
* call count), and the `before_turn_end` continuation decision. Both loop
|
|
9
|
+
* bodies move here unchanged; only the plumbing that threads state across
|
|
10
|
+
* calls changed shape. A pure extraction: no loop rule changed.
|
|
11
|
+
*
|
|
12
|
+
* `runRoundTripLoop` mutates its `state` argument in place rather than
|
|
13
|
+
* returning a new one, and does so on every field the moment each value is
|
|
14
|
+
* known — not only at a clean exit. The original was one function where
|
|
15
|
+
* `messages` (and friends) were plain `let`s: a mid-loop throw (the
|
|
16
|
+
* `batch.cancelled` path below) still left the catch block reading the
|
|
17
|
+
* latest values, because it was the same variable. Returning a fresh object
|
|
18
|
+
* only at the end of the function would lose every update since the last
|
|
19
|
+
* return the moment a throw skips it — exactly the synthetic tool-result
|
|
20
|
+
* messages `runToolBatch` just appended before signalling cancellation. The
|
|
21
|
+
* caller passes one `TurnLoopState` and keeps reading through that same
|
|
22
|
+
* reference after an `await` that might throw.
|
|
23
|
+
*
|
|
24
|
+
* `spinStopped` / `spinWarned` were two `let`s in `runNativeTurn`, one of them
|
|
25
|
+
* (`spinWarned`) mutated by an `onSpinStop` closure registered ONCE in setup,
|
|
26
|
+
* before either extracted function is ever called, and read/written across
|
|
27
|
+
* every outer-loop iteration after that. Splitting the loop body out cannot
|
|
28
|
+
* split that closure's target too, so both flags now live in one mutable
|
|
29
|
+
* `SpinFlags` object created in setup and threaded by reference into every
|
|
30
|
+
* call here — the same shared-mutable-state semantics as the original two
|
|
31
|
+
* `let`s, just addressed through an object instead of two closed-over
|
|
32
|
+
* bindings.
|
|
33
|
+
*/
|
|
34
|
+
import { inputClassTokens } from "#src/cost/core/index";
|
|
35
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
36
|
+
import { estimateContextTokens, shouldCompact, } from "./compaction.js";
|
|
37
|
+
import { nativeSessionLastUsage } from "./session.js";
|
|
38
|
+
import { usageBeat } from "./turn-accumulator.js";
|
|
39
|
+
import { runProactiveCompaction } from "./turn-compaction-step.js";
|
|
40
|
+
import { completeWithRecovery } from "./turn-complete-step.js";
|
|
41
|
+
import { runToolBatch } from "./turn-tool-batch.js";
|
|
42
|
+
/**
|
|
43
|
+
* The per-turn bound on `before_turn_end`'s followUp channel (spec 6.4): the
|
|
44
|
+
* only event that can spend money on its own, so injections are capped per
|
|
45
|
+
* turn regardless of what handlers return. At the cap the channel stops and
|
|
46
|
+
* the turn ends.
|
|
47
|
+
*/
|
|
48
|
+
export const MAX_FOLLOW_UPS_PER_TURN = 3;
|
|
49
|
+
/**
|
|
50
|
+
* Compaction runs at most once per round trip. That bound is what stops a
|
|
51
|
+
* compact-still-over-compact loop when the pinned prompt alone is too large.
|
|
52
|
+
* The returned `summarizeFailed` is read by the overflow-retry backstop
|
|
53
|
+
* (`canRetry = ... && !summarizeFailed && ...`) inside `completeWithRecovery`,
|
|
54
|
+
* which suppresses a doomed retry after the summarizer has already failed
|
|
55
|
+
* this round trip.
|
|
56
|
+
*/
|
|
57
|
+
async function maybeCompact(state, params) {
|
|
58
|
+
const { handle, opts, deps, loopEvents } = params;
|
|
59
|
+
// Guard clauses, not one big `&&`: each keeps TS's narrowing of deps.summarize
|
|
60
|
+
// / contextWindow / compaction live for the `deps: {...}` literal below,
|
|
61
|
+
// which a hoisted boolean would have thrown away.
|
|
62
|
+
if (deps.summarize === undefined || deps.contextWindow === undefined || deps.compaction === undefined) {
|
|
63
|
+
return { summarizeFailed: false };
|
|
64
|
+
}
|
|
65
|
+
if (!shouldCompact(estimateContextTokens(state.messages, state.lastUsage, state.anchorIndex), deps.contextWindow, deps.compaction)) {
|
|
66
|
+
return { summarizeFailed: false };
|
|
67
|
+
}
|
|
68
|
+
const step = await runProactiveCompaction({
|
|
69
|
+
messages: state.messages,
|
|
70
|
+
usage: params.usage,
|
|
71
|
+
sessionName: handle.id,
|
|
72
|
+
lastUsage: state.lastUsage,
|
|
73
|
+
anchorIndex: state.anchorIndex,
|
|
74
|
+
// The loop's local, not deps.loopEvents — the same registry
|
|
75
|
+
// completeWithRecovery and runToolBatch receive below, so every
|
|
76
|
+
// dispatch seam observes the same handlers.
|
|
77
|
+
loopEvents,
|
|
78
|
+
// Copied, not the `deps` object itself: the guards above narrow the
|
|
79
|
+
// three properties to defined, and a fresh object is what carries that
|
|
80
|
+
// narrowing into the step's `CompactionStepDeps` parameter.
|
|
81
|
+
deps: {
|
|
82
|
+
summarize: deps.summarize,
|
|
83
|
+
contextWindow: deps.contextWindow,
|
|
84
|
+
compaction: deps.compaction,
|
|
85
|
+
onActivity: deps.onActivity,
|
|
86
|
+
deadline: deps.deadline,
|
|
87
|
+
},
|
|
88
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
89
|
+
});
|
|
90
|
+
state.messages = [...step.messages];
|
|
91
|
+
if (step.compacted) {
|
|
92
|
+
// The anchor described the pre-compaction array; it is meaningless now.
|
|
93
|
+
state.lastUsage = undefined;
|
|
94
|
+
state.anchorIndex = undefined;
|
|
95
|
+
}
|
|
96
|
+
return { summarizeFailed: step.summarizeFailed };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* One model call and its bookkeeping: `completeWithRecovery`, the usage/anchor
|
|
100
|
+
* updates, the activity beats, `after_response`, and pushing the settled
|
|
101
|
+
* assistant message. Returns the tool calls the model asked for, if any —
|
|
102
|
+
* `undefined` means the turn completed normally (mutates
|
|
103
|
+
* `state.completedNormally`).
|
|
104
|
+
*/
|
|
105
|
+
async function runModelRoundTrip(state, params, summarizeFailed) {
|
|
106
|
+
const { handle, opts, deps, tools, loopEvents } = params;
|
|
107
|
+
const step = await completeWithRecovery({
|
|
108
|
+
messages: state.messages,
|
|
109
|
+
tools,
|
|
110
|
+
usage: params.usage,
|
|
111
|
+
summarizeFailed,
|
|
112
|
+
sessionName: handle.id,
|
|
113
|
+
lastUsage: state.lastUsage,
|
|
114
|
+
anchorIndex: state.anchorIndex,
|
|
115
|
+
// The loop's local, not deps.loopEvents — the same registry runToolBatch
|
|
116
|
+
// below receives, so both dispatch seams observe the same handlers.
|
|
117
|
+
loopEvents,
|
|
118
|
+
// 1-based, matching the usage beat below: the request being issued is
|
|
119
|
+
// round trip roundTrips + 1 — before_request(0-based N) and a later
|
|
120
|
+
// after_response(1-based N) would otherwise disagree on the same trip.
|
|
121
|
+
roundTrip: state.roundTrips + 1,
|
|
122
|
+
...(handle.modelDef?.model !== undefined ? { model: handle.modelDef.model } : {}),
|
|
123
|
+
deps,
|
|
124
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
125
|
+
});
|
|
126
|
+
const res = step.res;
|
|
127
|
+
state.messages = [...step.messages];
|
|
128
|
+
// The anchor described the pre-compaction array (compacted), or a
|
|
129
|
+
// prefix the provider was never sent (a boundary-exempt transform_context
|
|
130
|
+
// rewrite — spec 3.6: the wire changed even though the saved array did
|
|
131
|
+
// not); it is meaningless now. Compaction invalidates it unconditionally;
|
|
132
|
+
// a transform_context honour clears only when it rode the boundary
|
|
133
|
+
// exemption — a prefix-stable honour leaves the anchor valid (spec 6.6:
|
|
134
|
+
// wire and persisted prefix are reference-identical, nothing to invalidate).
|
|
135
|
+
if (step.compacted || (step.honoured && step.boundary)) {
|
|
136
|
+
state.lastUsage = undefined;
|
|
137
|
+
state.anchorIndex = undefined;
|
|
138
|
+
}
|
|
139
|
+
state.roundTrips += 1;
|
|
140
|
+
params.usage.add(res.usage, res.costUsd, res.rates);
|
|
141
|
+
state.output = res.text;
|
|
142
|
+
// nax#1852: the anchor is the whole prompt the provider charged for, not
|
|
143
|
+
// just its uncached portion. Under prompt caching (which the round trip
|
|
144
|
+
// above always requests) the cached prefix arrives in the cache fields,
|
|
145
|
+
// and counting inputTokens alone reads a 71k-token context as ~16.
|
|
146
|
+
const promptTokens = inputClassTokens(res.usage);
|
|
147
|
+
state.lastUsage = { promptTokens };
|
|
148
|
+
state.anchorIndex = state.messages.length - 1;
|
|
149
|
+
nativeSessionLastUsage.set(handle.id, {
|
|
150
|
+
promptTokens,
|
|
151
|
+
anchorIndex: state.anchorIndex,
|
|
152
|
+
...(params.transcriptModel !== undefined ? { model: params.transcriptModel } : {}),
|
|
153
|
+
});
|
|
154
|
+
// 1-based; `roundTrips` is incremented above, before this beat fires.
|
|
155
|
+
deps.onActivity?.(usageBeat(res.usage, res.costUsd, state.roundTrips));
|
|
156
|
+
if (res.text.length > 0)
|
|
157
|
+
deps.onActivity?.({ kind: "message", bytes: res.text.length });
|
|
158
|
+
if (res.thinking !== undefined && res.thinking.length > 0) {
|
|
159
|
+
deps.onActivity?.({
|
|
160
|
+
kind: "thinking",
|
|
161
|
+
bytes: res.thinking.reduce((n, t) => n + t.text.length, 0),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// P3 `after_response` (spec 6.1): fires once per round trip on the
|
|
165
|
+
// settled assistant message, BEFORE it enters the array — a patch is
|
|
166
|
+
// safe by construction because it shapes the message, never the array,
|
|
167
|
+
// so the anchorIndex recorded above (`messages.length - 1`, which runs
|
|
168
|
+
// before this push) keeps describing the prefix the provider charged
|
|
169
|
+
// for. `usage`/`costUsd` are surfaced readonly: the registry reads only
|
|
170
|
+
// the patchable fields off a return, so billing truth cannot surface
|
|
171
|
+
// even from a handler that bypasses the type.
|
|
172
|
+
const afterResponse = await loopEvents.dispatch("after_response", {
|
|
173
|
+
text: res.text,
|
|
174
|
+
...(res.toolCalls !== undefined ? { toolCalls: res.toolCalls } : {}),
|
|
175
|
+
...(res.thinking !== undefined ? { thinking: res.thinking } : {}),
|
|
176
|
+
usage: res.usage,
|
|
177
|
+
costUsd: res.costUsd,
|
|
178
|
+
roundTrip: state.roundTrips,
|
|
179
|
+
});
|
|
180
|
+
// The pushed message carries the patched shape, and the loop acts on
|
|
181
|
+
// what it records: answering the ORIGINAL calls while recording patched
|
|
182
|
+
// ones would desync the transcript from what actually executed.
|
|
183
|
+
const assistantText = afterResponse.text ?? res.text;
|
|
184
|
+
const assistantToolCalls = afterResponse.toolCalls ?? res.toolCalls;
|
|
185
|
+
const assistantThinking = afterResponse.thinking ?? res.thinking;
|
|
186
|
+
// Thinking blocks are appended, not merely representable: Anthropic needs
|
|
187
|
+
// the exact block back to continue a thinking conversation (ADR-028 s8).
|
|
188
|
+
state.messages.push({
|
|
189
|
+
role: "assistant",
|
|
190
|
+
content: assistantText,
|
|
191
|
+
...(assistantToolCalls !== undefined ? { toolCalls: assistantToolCalls } : {}),
|
|
192
|
+
...(assistantThinking !== undefined ? { thinking: assistantThinking } : {}),
|
|
193
|
+
});
|
|
194
|
+
if (assistantToolCalls === undefined || assistantToolCalls.length === 0) {
|
|
195
|
+
state.completedNormally = true;
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
return assistantToolCalls;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Answers the round trip's tool calls and decides whether the round-trip loop
|
|
202
|
+
* should keep going. `state` is already up to date (mutated above, not
|
|
203
|
+
* returned — see the file header) the moment `batch.cancelled` throws, so the
|
|
204
|
+
* caller's catch block still sees this batch's messages.
|
|
205
|
+
*/
|
|
206
|
+
async function dispatchToolBatch(state, params, toolCalls) {
|
|
207
|
+
const { opts, deps, tools, codingToolNames, loopEvents, invalidCallBudget, spinBreaker, spinFlags, maxInteractions } = params;
|
|
208
|
+
const batch = await runToolBatch({
|
|
209
|
+
messages: state.messages,
|
|
210
|
+
toolCalls,
|
|
211
|
+
tools,
|
|
212
|
+
codingToolNames,
|
|
213
|
+
roundTrips: state.roundTrips,
|
|
214
|
+
opts,
|
|
215
|
+
deps,
|
|
216
|
+
loopEvents,
|
|
217
|
+
invalidCallBudget,
|
|
218
|
+
spinBreaker,
|
|
219
|
+
maxInteractions,
|
|
220
|
+
spinWarned: spinFlags.warned,
|
|
221
|
+
interactionsSoFar: params.interactions.length,
|
|
222
|
+
});
|
|
223
|
+
state.messages = [...batch.messages];
|
|
224
|
+
params.interactions.push(...batch.interactions);
|
|
225
|
+
params.codingToolsCalled.push(...batch.codingToolsCalled);
|
|
226
|
+
if (batch.spinStopped)
|
|
227
|
+
spinFlags.stopped = true;
|
|
228
|
+
if (batch.spinStopped || batch.budgetExceeded)
|
|
229
|
+
return { shouldBreak: true };
|
|
230
|
+
// US-002: a cancelled turn signal during the batch answers every
|
|
231
|
+
// outstanding call synthetically and stops the loop. The throw routes
|
|
232
|
+
// through the existing catch block — its best-effort saveTranscript
|
|
233
|
+
// persists the synthetic results before the throw propagates, keeping one
|
|
234
|
+
// result per assistant id in the saved transcript (AC2 / AC3). AC4 carries
|
|
235
|
+
// the abort reason verbatim; AC6 — a no-reason abort — produces a
|
|
236
|
+
// DOMException named AbortError, the contract `build-hop-callback` and the
|
|
237
|
+
// idle-watchdog key on.
|
|
238
|
+
if (batch.cancelled) {
|
|
239
|
+
throw deps.signal?.reason ?? new DOMException("signal is aborted without reason", "AbortError");
|
|
240
|
+
}
|
|
241
|
+
return { shouldBreak: false };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Deliberately unbounded by COUNT of varied calls. A coding agent working a
|
|
245
|
+
* story is bounded by wall clock (deps.deadline), by the idle watchdog, and
|
|
246
|
+
* — since nax#2013 — by the spin breaker, which ends a turn that keeps
|
|
247
|
+
* REPEATING a call it already made. `agent.maxInteractionTurns` is NOT this
|
|
248
|
+
* budget — it bounds human Q&A exchanges, which are counted separately.
|
|
249
|
+
*/
|
|
250
|
+
export async function runRoundTripLoop(state, params) {
|
|
251
|
+
while (true) {
|
|
252
|
+
// Checked before starting a round-trip rather than after finishing one:
|
|
253
|
+
// starting a call we know cannot finish inside the budget spends money for
|
|
254
|
+
// an answer we will discard.
|
|
255
|
+
if (params.deps.deadline?.expired() === true) {
|
|
256
|
+
state.timedOut = true;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
const { summarizeFailed } = await maybeCompact(state, params);
|
|
260
|
+
const toolCalls = await runModelRoundTrip(state, params, summarizeFailed);
|
|
261
|
+
if (toolCalls === undefined)
|
|
262
|
+
break; // completedNormally already set
|
|
263
|
+
const { shouldBreak } = await dispatchToolBatch(state, params, toolCalls);
|
|
264
|
+
if (shouldBreak)
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* P3 `before_turn_end` (spec 6.4): fires at every turn ENDING, before the
|
|
270
|
+
* final saveTranscript. `stopped` is dispatcher-computed: the three endings
|
|
271
|
+
* that are stops, not completions — the spin breaker (nax#2120), the
|
|
272
|
+
* invalid-call budget (nax#2047), and the deadline — close the followUp
|
|
273
|
+
* channel entirely, because resurrecting a turn a breaker just killed would
|
|
274
|
+
* re-open the loops those breakers exist to close. The payload's `stopped`
|
|
275
|
+
* flag is all a handler sees: WHICH breaker fired is not leaked, and any
|
|
276
|
+
* followUp returned against a stop is ignored (with a warn — a handler
|
|
277
|
+
* trying to resurrect is worth a trace). The channel is also capped per turn
|
|
278
|
+
* at MAX_FOLLOW_UPS_PER_TURN.
|
|
279
|
+
*/
|
|
280
|
+
export async function runTurnEndPhase(state, params) {
|
|
281
|
+
const stopped = params.spinFlags.stopped || params.invalidCallBudget.exceeded || state.timedOut;
|
|
282
|
+
const turnEnd = await params.loopEvents.dispatch("before_turn_end", {
|
|
283
|
+
messages: state.messages,
|
|
284
|
+
roundTrips: state.roundTrips,
|
|
285
|
+
ended: "completed",
|
|
286
|
+
stopped,
|
|
287
|
+
followUpsSoFar: state.followUpsSoFar,
|
|
288
|
+
});
|
|
289
|
+
if (stopped || state.followUpsSoFar >= MAX_FOLLOW_UPS_PER_TURN) {
|
|
290
|
+
if (turnEnd.followUp !== undefined) {
|
|
291
|
+
getSafeLogger()?.warn("native-loop-events", "before_turn_end followUp ignored", {
|
|
292
|
+
sessionName: params.handle.id,
|
|
293
|
+
...(stopped ? { reason: "stopped" } : { reason: "cap", cap: MAX_FOLLOW_UPS_PER_TURN }),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return { action: "break" };
|
|
297
|
+
}
|
|
298
|
+
if (turnEnd.followUp === undefined)
|
|
299
|
+
return { action: "break" };
|
|
300
|
+
// Honoured: the user message enters the array and the outer loop
|
|
301
|
+
// re-enters the round-trip loop — each injection counts as its own
|
|
302
|
+
// round trip through the normal loop, and the turn's result is NOT
|
|
303
|
+
// built here.
|
|
304
|
+
const messages = [...state.messages, { role: "user", content: turnEnd.followUp }];
|
|
305
|
+
// The continuation must earn its own clean exit, and a reprieved stop's
|
|
306
|
+
// terminal-round-trip snapshot does not carry into it — the breaker's
|
|
307
|
+
// own cumulative, session-lifetime counters (nax#2047) still enforce.
|
|
308
|
+
params.spinFlags.warned = false;
|
|
309
|
+
return {
|
|
310
|
+
action: "continue",
|
|
311
|
+
state: {
|
|
312
|
+
...state,
|
|
313
|
+
messages,
|
|
314
|
+
followUpsSoFar: state.followUpsSoFar + 1,
|
|
315
|
+
completedNormally: false,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native turn loop.
|
|
3
|
+
*
|
|
4
|
+
* nax owns the conversation, so a turn is: append the prompt, call the model,
|
|
5
|
+
* and while it asks for tools, execute them and call again. Tools are executed
|
|
6
|
+
* through the InteractionHandler the ACP adapter already uses — this file never
|
|
7
|
+
* touches the context engine.
|
|
8
|
+
*
|
|
9
|
+
* The public types this loop consumes (`TurnDeps`, `NativeTurnResponse`,
|
|
10
|
+
* `NativeSummaryResponse`) and the failure-usage ledger
|
|
11
|
+
* (`readNativeTurnFailureUsage` / `recordNativeTurnFailureUsage`) live in
|
|
12
|
+
* `./turn-types.ts`. Splitting them out keeps this file focused on the
|
|
13
|
+
* algorithm and below the 600-line hard limit (project-conventions.md).
|
|
14
|
+
*
|
|
15
|
+
* The round-trip loop and the `before_turn_end` continuation decision live in
|
|
16
|
+
* `./turn-loop-round-trip.ts` (complexity drain A2,
|
|
17
|
+
* docs/plans/STATUS-complexity-drain.md) — this file does setup (transcript
|
|
18
|
+
* load, event registry, `before_turn`), sequences the outer while(true) over
|
|
19
|
+
* those two phases, and handles the error/completion tail.
|
|
20
|
+
*/
|
|
21
|
+
import type { SendTurnOpts, SessionHandle, TurnResult } from "#src/session/session-types";
|
|
22
|
+
import { type TurnDeps } from "./turn-types.ts";
|
|
23
|
+
export declare function runNativeTurn(handle: SessionHandle, prompt: string, opts: SendTurnOpts, deps: TurnDeps): Promise<TurnResult>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native turn loop.
|
|
3
|
+
*
|
|
4
|
+
* nax owns the conversation, so a turn is: append the prompt, call the model,
|
|
5
|
+
* and while it asks for tools, execute them and call again. Tools are executed
|
|
6
|
+
* through the InteractionHandler the ACP adapter already uses — this file never
|
|
7
|
+
* touches the context engine.
|
|
8
|
+
*
|
|
9
|
+
* The public types this loop consumes (`TurnDeps`, `NativeTurnResponse`,
|
|
10
|
+
* `NativeSummaryResponse`) and the failure-usage ledger
|
|
11
|
+
* (`readNativeTurnFailureUsage` / `recordNativeTurnFailureUsage`) live in
|
|
12
|
+
* `./turn-types.ts`. Splitting them out keeps this file focused on the
|
|
13
|
+
* algorithm and below the 600-line hard limit (project-conventions.md).
|
|
14
|
+
*
|
|
15
|
+
* The round-trip loop and the `before_turn_end` continuation decision live in
|
|
16
|
+
* `./turn-loop-round-trip.ts` (complexity drain A2,
|
|
17
|
+
* docs/plans/STATUS-complexity-drain.md) — this file does setup (transcript
|
|
18
|
+
* load, event registry, `before_turn`), sequences the outer while(true) over
|
|
19
|
+
* those two phases, and handles the error/completion tail.
|
|
20
|
+
*/
|
|
21
|
+
import { getSafeLogger, NaxError } from "#src/infra/index";
|
|
22
|
+
import { askHumanToolDefinition } from "./ask-human.js";
|
|
23
|
+
import { createInvalidCallBudget } from "./handle-invalid-tool-call.js";
|
|
24
|
+
import { applyHistoryPatch } from "./loop-events/cache-boundary.js";
|
|
25
|
+
import { createLoopEventRegistry } from "./loop-events/index.js";
|
|
26
|
+
import { registerBuiltinLoopHandlers } from "./loop-handlers.js";
|
|
27
|
+
import { nativeSessionTranscriptOwners, nativeTranscriptDirs, sessionAnchorFor } from "./session.js";
|
|
28
|
+
import { codingToolsToDefinitions, toToolDefinitions } from "./tool-mapping.js";
|
|
29
|
+
import { loadTranscript, saveTranscript, transcriptModelIdentity, } from "./transcript-store.js";
|
|
30
|
+
import { createTurnAccumulator } from "./turn-accumulator.js";
|
|
31
|
+
import { dispatchTurnEndOnError } from "./turn-end-event.js";
|
|
32
|
+
import { runRoundTripLoop, runTurnEndPhase } from "./turn-loop-round-trip.js";
|
|
33
|
+
import { buildTurnResult, logTurnTailWarnings } from "./turn-result.js";
|
|
34
|
+
import { recordNativeTurnFailureUsage } from "./turn-types.js";
|
|
35
|
+
/**
|
|
36
|
+
* The messages `before_turn` appends as the turn's seed: the handler's patch
|
|
37
|
+
* when it is a non-empty user-role array (spec 6.1's stop rule), otherwise the
|
|
38
|
+
* prompt the caller handed the turn. A bad patch is rejected the way a bad
|
|
39
|
+
* history patch is — warn, original kept (spec 3.7) — because a seed that is
|
|
40
|
+
* empty or speaks with another role hands the model a conversation it cannot
|
|
41
|
+
* answer as itself.
|
|
42
|
+
*/
|
|
43
|
+
function beforeTurnSeed(patch, prompt) {
|
|
44
|
+
if (patch === undefined)
|
|
45
|
+
return [{ role: "user", content: prompt }];
|
|
46
|
+
if (patch.length > 0 && patch.every((m) => m.role === "user"))
|
|
47
|
+
return [...patch];
|
|
48
|
+
getSafeLogger()?.warn("native-loop-events", "before_turn seed patch rejected: seed must be a non-empty user-role array", { seedLength: patch.length });
|
|
49
|
+
return [{ role: "user", content: prompt }];
|
|
50
|
+
}
|
|
51
|
+
export async function runNativeTurn(handle, prompt, opts, deps) {
|
|
52
|
+
const dir = nativeTranscriptDirs.get(handle.id);
|
|
53
|
+
if (dir === undefined) {
|
|
54
|
+
throw new NaxError(`no transcript directory for session "${handle.id}"`, "NATIVE_TRANSCRIPT_DIR_MISSING", {
|
|
55
|
+
stage: "native-session",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// nax#1877: an owner mismatch reads as a new conversation, so an abandoned
|
|
59
|
+
// invocation's history cannot ride along on the first request of this one.
|
|
60
|
+
// nax#2150 (P3 spec 8.3): so does a recorded different model — the store
|
|
61
|
+
// owns that guarantee, whatever the session layer above decided.
|
|
62
|
+
const transcriptIdentity = {
|
|
63
|
+
owner: nativeSessionTranscriptOwners.get(handle.id),
|
|
64
|
+
model: transcriptModelIdentity(handle.modelDef?.model),
|
|
65
|
+
};
|
|
66
|
+
let messages = [...(await loadTranscript(dir, handle.id, transcriptIdentity))];
|
|
67
|
+
// nax#2151: the invalid-call repair and the spin breaker are `before_tool`
|
|
68
|
+
// registrations rather than inline branches. Absent a caller-supplied
|
|
69
|
+
// registry the loop owns one, so both built-ins run exactly as they did
|
|
70
|
+
// before the seam. The block sits above `before_turn` now: the dispatch
|
|
71
|
+
// needs the registry, and the built-ins must be installed before ANY event
|
|
72
|
+
// fires, exactly as they were installed before any round trip ran.
|
|
73
|
+
const spinFlags = { stopped: false, warned: false };
|
|
74
|
+
const invalidCallBudget = createInvalidCallBudget();
|
|
75
|
+
const loopEvents = deps.loopEvents ?? createLoopEventRegistry();
|
|
76
|
+
registerBuiltinLoopHandlers(loopEvents, {
|
|
77
|
+
sessionName: handle.id,
|
|
78
|
+
budget: invalidCallBudget,
|
|
79
|
+
...(deps.spinBreaker !== undefined ? { spinBreaker: deps.spinBreaker } : {}),
|
|
80
|
+
...(deps.loopHandlers !== undefined ? { loopHandlers: deps.loopHandlers } : {}),
|
|
81
|
+
...(deps.loopHandlerContext !== undefined ? { loopHandlerContext: deps.loopHandlerContext } : {}),
|
|
82
|
+
...(deps.signal !== undefined ? { signal: deps.signal } : {}),
|
|
83
|
+
onSpinStop: () => {
|
|
84
|
+
spinFlags.warned = true;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const anchor = sessionAnchorFor(handle.id, transcriptIdentity.model);
|
|
88
|
+
const lastUsage = anchor?.promptTokens !== undefined ? { promptTokens: anchor.promptTokens } : undefined;
|
|
89
|
+
const anchorIndex = anchor?.anchorIndex;
|
|
90
|
+
// P3 `before_turn` (spec 6.1): fires ONCE, after the transcript loads and
|
|
91
|
+
// before the seed push. `boundary` is dispatcher-computed (spec 3.4) and
|
|
92
|
+
// always false: the model-change boundary never arises, because the
|
|
93
|
+
// transcript store refuses another model's history (spec 8.3), so the
|
|
94
|
+
// history channel is honoured only at an undefined anchor (spec 3.5) — an
|
|
95
|
+
// off-boundary patch is rejected + warned by applyHistoryPatch and the turn
|
|
96
|
+
// proceeds on the loaded history.
|
|
97
|
+
const turnStart = await loopEvents.dispatch("before_turn", {
|
|
98
|
+
prompt,
|
|
99
|
+
history: messages,
|
|
100
|
+
sessionName: handle.id,
|
|
101
|
+
boundary: false,
|
|
102
|
+
});
|
|
103
|
+
// An honoured history patch (reachable only at an undefined anchor today)
|
|
104
|
+
// rewrites the IN-MEMORY array, and the saveTranscript at the turn's end
|
|
105
|
+
// persists it — before_turn is the conversation-rewriting event, unlike
|
|
106
|
+
// transform_context's wire-copy-only ruling (spec 6.6).
|
|
107
|
+
messages = [
|
|
108
|
+
...applyHistoryPatch({
|
|
109
|
+
before: messages,
|
|
110
|
+
patched: turnStart.history,
|
|
111
|
+
anchorIndex,
|
|
112
|
+
boundary: false,
|
|
113
|
+
event: "before_turn",
|
|
114
|
+
}).messages,
|
|
115
|
+
];
|
|
116
|
+
messages.push(...beforeTurnSeed(turnStart.seed, prompt));
|
|
117
|
+
const codingTools = opts.codingTools ?? [];
|
|
118
|
+
const codingToolNames = new Set(codingTools.map((t) => t.name));
|
|
119
|
+
// Native spends the budget ONLY on ask_human exchanges. Unlike ACP it is
|
|
120
|
+
// not this loop's bound — the loop is `while (true)`, bounded by the
|
|
121
|
+
// whole-turn deadline and the idle watchdog (issue #1820).
|
|
122
|
+
//
|
|
123
|
+
// Advertised only while the Q&A budget can still be spent; a tool the model
|
|
124
|
+
// cannot successfully call is worse than no tool.
|
|
125
|
+
const maxInteractions = opts.maxInteractions ?? 0;
|
|
126
|
+
const tools = [
|
|
127
|
+
...toToolDefinitions(opts.contextPullTools ?? []),
|
|
128
|
+
...codingToolsToDefinitions(codingTools),
|
|
129
|
+
...(maxInteractions > 0 ? [askHumanToolDefinition] : []),
|
|
130
|
+
];
|
|
131
|
+
const usage = createTurnAccumulator();
|
|
132
|
+
// Reported on the result so the review guards can corroborate a reviewer's
|
|
133
|
+
// self-declared inspection trail against calls it actually made.
|
|
134
|
+
const codingToolsCalled = [];
|
|
135
|
+
const interactions = [];
|
|
136
|
+
const params = {
|
|
137
|
+
handle,
|
|
138
|
+
opts,
|
|
139
|
+
deps,
|
|
140
|
+
transcriptModel: transcriptIdentity.model,
|
|
141
|
+
tools,
|
|
142
|
+
codingToolNames,
|
|
143
|
+
loopEvents,
|
|
144
|
+
invalidCallBudget,
|
|
145
|
+
spinBreaker: deps.spinBreaker,
|
|
146
|
+
spinFlags,
|
|
147
|
+
maxInteractions,
|
|
148
|
+
interactions,
|
|
149
|
+
codingToolsCalled,
|
|
150
|
+
usage,
|
|
151
|
+
};
|
|
152
|
+
let state = {
|
|
153
|
+
messages,
|
|
154
|
+
lastUsage,
|
|
155
|
+
anchorIndex,
|
|
156
|
+
roundTrips: 0,
|
|
157
|
+
output: "",
|
|
158
|
+
// Set ONLY on the clean exit — the model returned no further tool calls.
|
|
159
|
+
// Every other way out of the loop (the deadline, or an abort) leaves work
|
|
160
|
+
// the model asked for unexecuted.
|
|
161
|
+
completedNormally: false,
|
|
162
|
+
timedOut: false,
|
|
163
|
+
// `before_turn_end`'s followUp injections so far this turn (spec 6.4) —
|
|
164
|
+
// reset every turn, capped at MAX_FOLLOW_UPS_PER_TURN.
|
|
165
|
+
followUpsSoFar: 0,
|
|
166
|
+
};
|
|
167
|
+
// nax#1838: the save below the loop is the clean exit's alone. A turn that
|
|
168
|
+
// throws must persist too — the retry reopens the same deterministic session
|
|
169
|
+
// name, so an unsaved conversation is one the model silently resumes without.
|
|
170
|
+
try {
|
|
171
|
+
// The OUTER loop is `before_turn_end`'s followUp continuation (spec 6.4):
|
|
172
|
+
// an honoured followUp re-enters the round-trip loop via runRoundTripLoop
|
|
173
|
+
// instead of building the turn's result — the result is built when the
|
|
174
|
+
// turn ENDS.
|
|
175
|
+
while (true) {
|
|
176
|
+
// Mutates `state` in place, including up to the point of a throw — see
|
|
177
|
+
// runRoundTripLoop's own header for why this is not `state = await ...`.
|
|
178
|
+
await runRoundTripLoop(state, params);
|
|
179
|
+
const turnEnd = await runTurnEndPhase(state, params);
|
|
180
|
+
if (turnEnd.action === "break")
|
|
181
|
+
break;
|
|
182
|
+
state = turnEnd.state;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
// Review #20: the event fires on EVERY ending, so a throwing turn
|
|
187
|
+
// dispatches it before the transcript save — a handler sees the failure
|
|
188
|
+
// ending with `ended: "aborted"` (signal fired) or `"errored"`, and its
|
|
189
|
+
// result is ignored: there is no turn left to continue. A rejecting
|
|
190
|
+
// dispatch must not replace the original error or skip the save below —
|
|
191
|
+
// it is logged and skipped, the same skip-not-fail contract the registry
|
|
192
|
+
// applies to a single throwing handler.
|
|
193
|
+
await dispatchTurnEndOnError(loopEvents, {
|
|
194
|
+
messages: state.messages,
|
|
195
|
+
roundTrips: state.roundTrips,
|
|
196
|
+
stopped: spinFlags.stopped || invalidCallBudget.exceeded || state.timedOut,
|
|
197
|
+
followUpsSoFar: state.followUpsSoFar,
|
|
198
|
+
}, deps.signal).catch((dispatchErr) => {
|
|
199
|
+
getSafeLogger()?.warn("native-loop-events", "before_turn_end dispatch failed on the error path; skipping it", {
|
|
200
|
+
sessionName: handle.id,
|
|
201
|
+
error: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr),
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
// Best-effort, and deliberately unlike the clean-exit save: there a write
|
|
205
|
+
// failure fails the turn, because continuing on unstored history is silent
|
|
206
|
+
// degradation. Here a failure is already in flight, and masking it with a
|
|
207
|
+
// write error would lose the cause.
|
|
208
|
+
await saveTranscript(dir, handle.id, state.messages, transcriptIdentity).catch((saveErr) => {
|
|
209
|
+
getSafeLogger()?.warn("native-adapter", "could not persist the transcript of a failed turn", {
|
|
210
|
+
sessionName: handle.id,
|
|
211
|
+
error: saveErr instanceof Error ? saveErr.message : String(saveErr),
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
// nax#1840: attach what was already spent, keyed on the error's own
|
|
215
|
+
// identity so the error itself is rethrown byte-for-byte unmodified.
|
|
216
|
+
if (typeof err === "object" && err !== null) {
|
|
217
|
+
recordNativeTurnFailureUsage(err, {
|
|
218
|
+
tokenUsage: usage.tokens(),
|
|
219
|
+
costUsd: usage.costUsd(),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
logTurnTailWarnings({
|
|
225
|
+
sessionName: handle.id,
|
|
226
|
+
completedNormally: state.completedNormally,
|
|
227
|
+
spinStopped: spinFlags.stopped,
|
|
228
|
+
roundTrips: state.roundTrips,
|
|
229
|
+
timedOut: state.timedOut,
|
|
230
|
+
spinBreaker: deps.spinBreaker,
|
|
231
|
+
invalidCallHalt: invalidCallBudget.halt,
|
|
232
|
+
});
|
|
233
|
+
// Persisted before returning, and a write failure fails the turn: continuing
|
|
234
|
+
// on a history that could not be stored is the silent degradation #1794
|
|
235
|
+
// removed from the pipeline (ADR-028 s4).
|
|
236
|
+
await saveTranscript(dir, handle.id, state.messages, transcriptIdentity);
|
|
237
|
+
return buildTurnResult({
|
|
238
|
+
output: state.output,
|
|
239
|
+
usage,
|
|
240
|
+
roundTrips: state.roundTrips,
|
|
241
|
+
codingTools,
|
|
242
|
+
codingToolsCalled,
|
|
243
|
+
completedNormally: state.completedNormally,
|
|
244
|
+
timedOut: state.timedOut,
|
|
245
|
+
spinStopped: spinFlags.stopped,
|
|
246
|
+
invalidCallHalt: invalidCallBudget.halt,
|
|
247
|
+
interactions,
|
|
248
|
+
pricingSource: deps.pricingSource,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn's tail: the warnings emitted once the loop has exited, and the
|
|
3
|
+
* `TurnResult` assembled from what the loop accumulated.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `turn-loop.ts`, which keeps the `saveTranscript` call that
|
|
6
|
+
* sits between the two: a write failure there fails the turn, unlike the
|
|
7
|
+
* best-effort save in the catch, and that distinction belongs with the control
|
|
8
|
+
* flow rather than in a result builder.
|
|
9
|
+
*/
|
|
10
|
+
import type { InteractionExchange, InvalidToolCallDetail, TurnResult } from "#src/session/session-types";
|
|
11
|
+
import type { CodingTool } from "#src/tools/index";
|
|
12
|
+
import type { TurnAccumulator } from "./turn-accumulator.ts";
|
|
13
|
+
import type { TurnDeps } from "./turn-types.ts";
|
|
14
|
+
export interface LogTurnTailWarningsArgs {
|
|
15
|
+
readonly sessionName: string;
|
|
16
|
+
readonly completedNormally: boolean;
|
|
17
|
+
readonly spinStopped: boolean;
|
|
18
|
+
readonly roundTrips: number;
|
|
19
|
+
readonly timedOut: boolean;
|
|
20
|
+
readonly spinBreaker: TurnDeps["spinBreaker"];
|
|
21
|
+
/** The call that tripped the invalid-call budget; absent when it did not trip. */
|
|
22
|
+
readonly invalidCallHalt?: InvalidToolCallDetail;
|
|
23
|
+
}
|
|
24
|
+
export declare function logTurnTailWarnings(args: LogTurnTailWarningsArgs): void;
|
|
25
|
+
export interface BuildTurnResultArgs {
|
|
26
|
+
readonly output: string;
|
|
27
|
+
readonly usage: TurnAccumulator;
|
|
28
|
+
readonly roundTrips: number;
|
|
29
|
+
readonly codingTools: readonly CodingTool[];
|
|
30
|
+
readonly codingToolsCalled: readonly string[];
|
|
31
|
+
readonly completedNormally: boolean;
|
|
32
|
+
readonly timedOut: boolean;
|
|
33
|
+
readonly spinStopped: boolean;
|
|
34
|
+
/** The call that tripped the invalid-call budget; absent when it did not trip. */
|
|
35
|
+
readonly invalidCallHalt: InvalidToolCallDetail | undefined;
|
|
36
|
+
readonly interactions: readonly InteractionExchange[];
|
|
37
|
+
readonly pricingSource: TurnDeps["pricingSource"];
|
|
38
|
+
}
|
|
39
|
+
export declare function buildTurnResult(args: BuildTurnResultArgs): TurnResult;
|