@sema-agent/core 7.10.0 → 7.11.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 +64 -0
- package/dist/agents/child-model-seat.d.ts +45 -18
- package/dist/agents/child-model-seat.js +12 -8
- package/dist/agents/subagent.js +6 -5
- package/dist/agents/teacher.js +2 -2
- package/dist/core/auto-mode-defaults.d.ts +15 -3
- package/dist/core/auto-mode-defaults.js +1 -0
- package/dist/core/auto-mode.d.ts +24 -20
- package/dist/core/auto-mode.js +12 -12
- package/dist/core/gate-fold.js +1 -0
- package/dist/core/gate-lanes.d.ts +6 -1
- package/dist/core/gate-lanes.js +45 -18
- package/dist/core/hooks.d.ts +13 -0
- package/dist/core/permission-rule-model.d.ts +5 -3
- package/dist/core/permission-rule-model.js +7 -3
- package/dist/core/persisted-rule-arms.js +4 -3
- package/dist/core/read-only-shell-table.d.ts +87 -0
- package/dist/core/read-only-shell-table.js +485 -0
- package/dist/core/read-only-shell.d.ts +42 -0
- package/dist/core/read-only-shell.js +316 -0
- package/dist/core/roles.d.ts +3 -2
- package/dist/core/runner/gate-exit.d.ts +5 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +22 -11
- package/dist/core/runner/prepare-gate-stations.js +9 -0
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/prepare-turn-wiring.js +1 -1
- package/dist/core/shell-lexer.d.ts +18 -0
- package/dist/core/shell-lexer.js +17 -10
- package/dist/core/shell-wrapper-table.js +8 -5
- package/dist/core/tool-policy.d.ts +4 -1
- package/dist/core/tool-policy.js +1 -1
- package/dist/core/tools.d.ts +28 -7
- package/dist/core/tools.js +44 -4
- package/dist/core/trace.d.ts +15 -0
- package/dist/engine/harness/agent-harness.d.ts +3 -1
- package/dist/engine/harness/agent-harness.js +1 -1
- package/dist/engine/harness/types.d.ts +4 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/orchestration/run-workflow-tool.d.ts +5 -2
- package/dist/orchestration/run-workflow-tool.js +2 -1
- package/dist/orchestration/workflow-governance.d.ts +3 -2
- package/dist/orchestration/workflow-primitives.d.ts +4 -1
- package/dist/orchestration/workflow-primitives.js +1 -6
- package/dist/orchestration/workflow.d.ts +12 -4
- package/dist/orchestration/workflow.js +24 -7
- package/dist/prompt-assembly/turn-snapshot.d.ts +4 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +35 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.11.0 — 2026-09-09
|
|
4
|
+
|
|
5
|
+
### Fixes — the auto-mode classifier reads its verdict the way CC 2.1.250 does, and re-asks once when a capped reply came back empty (#661 ① ⑤; B-056; @cli 1.0.106 @test @server)
|
|
6
|
+
- **The field failure.** A local gateway model under auto mode answered every ask with "好的。`<block>no</block>`\n(理由…)" — or, when its thinking could not be switched off, with nothing at all — and the engine logged `parse_error` three times, opened the session breaker, and sent every later ask to a person.
|
|
7
|
+
- **① The verdict reader is the CC form (`wCe` over `xCe`-stripped text).** `parseAutoModeResponse` collects the `<block>yes|no` hits (word-boundary form, closing tag optional, ANY position); zero hits ⇒ `parse_error`; two distinct answers in one reply ⇒ `parse_error` (the RAW text — thinking included — is read for the contradiction first, so a scratch-space "yes" against a visible "no" is refused, never allowed); otherwise `no` = allow, `yes` = block. The previous reader demanded `^<block>` and a bare `<block>no</block>` with nothing after it ("declared stricter than CC"); that declaration is gone. Category stays the free-text rule NAME the prompt asks for; reason is the first `<reason>…</reason>` pair on the stripped text. Behavior moves: `<block>no</block> extra prose` ⇒ allow (was parse_error); a preamble before the tag ⇒ the verdict it carries (was parse_error); `<block>yes</block><block>no</block>` ⇒ parse_error (was block); `<block> yes </block>` — whitespace inside the tag — ⇒ parse_error (was block).
|
|
8
|
+
- **⑤ The empty-at-cap re-ask (CC `TCe`/`cEt` + the `E4`/`BHe` allowance).** The verdict-sized cap (`AUTO_MODE_CLASSIFIER_MAX_TOKENS`, 256) applies where the wire can say off, and a catalog entry with no `reasoning`/`compat` reads as "off is expressible" — so a model that reasons regardless spent the cap on thinking and answered nothing. That reply (text `""` ∧ stopReason `length`, or `error` with `errorKind:"length_empty"`) is now re-asked ONCE at 256 + `AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS` (2048, CC's allowance for a model that rejects disabled thinking), thinking still off. A second empty reply, a non-empty reply with no verdict, an empty reply with a clean stop, and an uncapped seat are not re-asked. The re-ask is inside the classify leg: the decider still sees ONE decision per ask, and no breaker-cause word was added. CC re-asks such a reply up to `maxRetries` = 4 times at the same cap; one re-ask at the widened cap is the subset the engine spends. Known residual: a model that reasons IN its visible text and is cut at the cap (non-empty, no verdict) stays a `parse_error` — #621's compat-level declaration is the fix for that shape.
|
|
9
|
+
- `@cli` 1.0.106: pick up this version — an auto-mode session on a gateway model that prefixes or suffixes its verdict keeps classifying instead of falling back to the person after three asks; a model that cannot switch thinking off keeps classifying when its thinking FITS the widened cap (256 + 2048) — one that exhausts that too still fails the contract once per ask and trips the breaker after three (the ② breaker is out of this change's scope); the card's "why am I being asked" rendering is the shell's half. `@test`: a stub gateway answering three prose-wrapped verdicts (`好的。<block>no</block>\n(理由…)`) — pre-fix `parse_error` ×3 ⇒ `auto_mode_breaker_open`; post-fix allow ×3, no trip; a stub answering empty text with `finish_reason:"length"` then `<block>no</block>` — post-fix one allow, two requests, the second at `max_tokens` 2304 with thinking off; pure prose with no `<block>` stays `parse_error` (negative control). `@server`: zero surface (no wire key, no closed-set change; `AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS` is module-internal).
|
|
10
|
+
- Pins: `test/backlog661-auto-mode-parse-cc-form.test.ts` (positive controls, contradiction arms, the pure-prose negative control, the gateway-shaped reply through the real decider and the Runner's gate); `test/backlog661-classifier-cap-retry.test.ts` (both empty-at-cap forms ⇒ one re-ask at 256+2048 with reasoning off ⇒ allow; both empty ⇒ two requests, one parse_error; three negative controls). Re-pinned in `test/auto-mode.test.ts` and `test/backlog618-classifier-request-shape.test.ts` (the contract-failure stimulus is now a verdict-free reply).
|
|
11
|
+
|
|
12
|
+
### Fixes — an UNDECLARED thinking level no longer becomes an explicit off: the provider's default reasoning is back (#659; B-054; @cli 1.0.106 @server @test)
|
|
13
|
+
- **The defect.** The harness seat folded "nothing declared" into `"off"` (`thinking ?? "off"` in prepare-task, and the harness's own `?? "off"`). Harmless while off and unset were the same wire bytes; since 7.6.2 an explicit off is SPELLED on the wire (anthropic/deepseek `thinking:{type:"disabled"}`, qwen/zai `enable_thinking:false`, openrouter/together `reasoning:{enabled:false}`, the responses lane's declared off tier), so every main run without an effort setting told a default-on model to stop reasoning (field: deepseek-v4-flash on the Anthropic-compatible endpoint, `reasoning_delta` 12421 frames → 0).
|
|
14
|
+
- **The rule — two intents, two values, one seat.** `undefined` (spec/role/model declared nothing) rides UNCHANGED to the brain and writes NO thinking key — provider / model-tier default, the CC form when no effort is set; `"off"` is a caller's word and keeps the disable key (#618: the auto-mode classifier's explicit off, a definition's off, a caller's off are unchanged). The brains are untouched. `model.defaultThinking` still applies when the spec is silent (design/111 chain).
|
|
15
|
+
- **Readers of the harness level.** `AgentHarness.getThinkingLevel()`, the turn state, the systemPrompt callback's `thinkingLevel` and `thinking_level_select.previousLevel` now read `ThinkingLevel | undefined` (engine-internal faces; the public `ToolExecuteContext.thinkingLevel` was already optional). The child thinking seat's parent rung reads undefined ⇒ a child of an undeclared parent inherits UNSET (no key either; #651/#655 pins re-pinned — they had named the fold "the harness contract"). The compaction request already read the resolved level (unset there was never off). The turn snapshot's request-policy identity carries `null` for undeclared, so unset and off are different identities (they are different requests). Unchanged and out of scope: the persisted transcript's `SessionContext.thinkingLevel` still derives `"off"` when no `thinking_level_change` entry was ever written (a transcript face `@server` audit reads as a string; never fed back into a request).
|
|
16
|
+
- `@cli` 1.0.106: pick up this version and the no-`/effort` session reasons at the provider's default again — no shell-side default compensation. `@server`: zero surface. `@test` live criterion: deepseek-v4-flash via the Anthropic-compatible endpoint, a long-reasoning objective with no effort ⇒ `reasoning_delta` > 0; `thinking:"off"` ⇒ 0 (negative control) — `test/live-thinking-default.test.ts` is in the live gate's default surface.
|
|
17
|
+
- **Declared identity move (workflow resume journals; @server).** A workflow call's identity hashes the host's entry-time thinking snapshot when the authored spec left `thinking` unset; on a host that declared no level that snapshot was the fold's `"off"` and is now absent, so a journal written by an earlier release for such a host misses on its agent entries and the suffix RE-RUNS live instead of replaying — the same declared form as the design/164 spec-shape move (`workflowAgentCallKey`): the recorded results belong to child runs that were launched with reasoning explicitly disabled, a request this engine no longer produces for that spec, so a replay would be the stale-result bug the identity exists to prevent. A host with a declared level, or a script/definition that sets `thinking`, keeps its keys.
|
|
18
|
+
- Pins: `test/backlog659-thinking-unset-provider-default.test.ts` (four wire lanes × {unset, off, high} through the real Runner; the seat, the tool-context read, the harness contract, the child seat, the fork arm, the snapshot identity, the workflow call identity).
|
|
19
|
+
- **A fork runs as the caller on the thinking axis too (#660, found by the review of this fix).** The delegation station's fork arm used to hand the caller's level to the child seat's EXPLICIT slot; with an undeclared caller that slot is empty and the seat fell through to the deployment's stated `roles.subagent.thinking` — a stated `off` made the fork send the disable key while its caller reasoned at the provider default, a stated `high` changed the tier. Unreachable before this fix (the caller was always an explicit off). Now the fork copies the caller's level verbatim — undeclared included — exactly as its model chain is the caller's own (no role identity, no stated rung); the non-fork arm still walks the seat (definition > stated > caller). Pinned: undeclared caller × stated {off, high} × fork ⇒ the child request carries no reasoning; declared caller `low` × fork ⇒ `low`. Known residual (the #658 family, not changed here): a fork's spec carries the caller's explicit model with no role identity, so when the caller declared nothing the role chain's `roles.default.thinking` preset still applies to the fork (a caller running under `modelRole: "subagent"` with a `roles.default.thinking` preset set) — tracked with #658.
|
|
20
|
+
|
|
21
|
+
### Gates — `gate:closed-sets` mint obligations gain an exemption instrument BY IDENTITY (#629 ruled (A)); the G8 floors catch up with the census
|
|
22
|
+
- **The ruling.** The producer leg's discriminator stays "the code word decides a site, not the class" — full coverage of the mint face (subclasses, aliases, `ns[K]`, assertions) outranks a narrower discriminator's blind spots. Its one stated price — a construction that carries an obligated code as DATA (`new RegExp("checkpoint.unsupported_version")`) is judged as a mint — is answered the way this repo's other gates answer it: an exemption **by identity**, never a count baseline. New section `## Mint exemptions — by identity` in `docs/CLOSED-SETS.md`: `file` + `symbol` (the WHOLE lexical declaration chain enclosing the construction, dot-joined: `f`, `Class.method`, `f.value`, `OBJ.member`, `<module>` — so two functions' same-named locals are two identities; a review reproduced one row covering an unrelated mint under a nearest-name reading) + `code`; a row must match EXACTLY ONE construction — anonymous scopes (arrow arguments, IIFEs) add no chain segment, so a row matching two is refused as ambiguous instead of covering a construction its reason never argued for; hits are DISTINCT constructions (file + node start), so two obligations sharing one code word do not turn one exempted data-use into a false ambiguity — with a `reason` (≥ 20 characters), an `owner` and an `expires` date. An exempted construction is neither judged nor counted toward the population floor; a row that matches no construction reds (rot), an expired row reds, a malformed row is refused at load, and a document without the table is refused (the instrument stays declared even while empty — it IS empty on this tree).
|
|
23
|
+
- `scripts/verify-closed-sets.mjs`: `loadMintExemptions(root, today)`, `enclosingSymbolOf(node)`, every mint site now carries its `symbol`, `closedSetProblems(registry, root, obligations, exemptions)` (fourth parameter, default none), census `exemptions`. `KNOWN_ROWS` 10 → 18 and `KNOWN_SWITCHES` 3 → 5: re-measured — eight registered rows and two classified switches had joined the census without the floor following (the drift the #627 note already described once).
|
|
24
|
+
- Pins: `test/closed-sets-gate.test.ts` — a data-use reds nameless without a row and is clean (uncounted) with one; identity not file (the same code one symbol over still reds; a row naming a real mint exempts that mint by design and the floor then names the vanished site); rot and expiry red; the symbol forms; the real registry's empty table loads and a document without it is refused.
|
|
25
|
+
|
|
26
|
+
### Narrowing — a `defineTool()` PRODUCT on `TaskSpec.tools` runs with the run's trusted ctx, as a raw ToolSpec does (#656; @server manual `createRunWorkflowTool` mounts @test)
|
|
27
|
+
- **One mount law.** Every caller tool on `spec.tools` receives the run's ctx enrichment (`reportUsage`, `model`, `principal`, `onAsk`, `durableApprovalForChildren`, `inheritedGateForChildren`, `forwardEvent`, the tool-face / read-face / clamp seats, …). A raw ToolSpec always did; a product of the public `defineTool` factory was mounted AS-IS with the factory's minimal `{toolCallId, signal}` ctx because the AgentTool `execute(toolCallId, rawParams, signal, onUpdate)` contract has no ctx parameter to inject into. The product now carries a REBIND seat beside its brand (stamped together at the one stamp site, non-enumerable like the brand): the mount rebuilds the product's `execute` over its own spec with the run's ctx builder composed in (the run's seats first, a builder the product was built with refining them, the per-call identity re-stamped after both) — a fresh object per mount with the product's face, never a mutation of the product. Rules: 2 → 1 (the "product ⇒ as-is, unenriched" arm is gone; there is no enrich-by-identity table and no "cannot enrich" notice, because a branded object without the seat cannot be constructed).
|
|
28
|
+
- **The field shape (test [6744] G5):** a deployment mounting `createRunWorkflowTool(...)` by hand on `spec.tools` (instead of the `selfOrchestration` switch) had its Workflow tool read `ctx.onAsk ?? d.parentOnAsk` with BOTH absent — every workflow child's ask was denied fail-closed while the host's approver was never consulted, zero report. Now `ctx.onAsk`, `ctx.durableApprovalForChildren`, `ctx.inheritedGateForChildren`, `ctx.forwardEvent`, `ctx.autoModeReview`, the clamps and the rest reach the hand-mounted tool exactly as the switch's mount threads them through deps. `@server`: a deployment that mounts the Workflow (or any defineTool product) by hand and ALSO threads `parent*` deps sees the ctx seat win (the tool's documented `ctx.X ?? d.parentX` precedence); a deployment relying on the product being blind to the run (no known case) should author it as a raw ToolSpec that ignores its ctx. `@test`: hand-mount `createRunWorkflowTool` with an ask-gated child tool and a host `onAsk` returning `true` ⇒ the host approver is called and the child's tool runs (it used to be denied with "no reachable operator"); a defineTool product's `execute` sees `reportUsage` / `principal` / `onAsk` / `durableApprovalForChildren`.
|
|
29
|
+
- Narrowing half: a hand-mounted delegation product now inherits the host's tighten-only seats (`inheritedGateForChildren`, `handsReadOnly`, `interactiveTools:false`, read-face roots, read-deny additions) into its children — a subtree that used to escape the parent's gate through a hand mount no longer does.
|
|
30
|
+
- `teacher`'s logging wrapper (the one other legitimate brand stamper) forwards the seat: a mount rebinding the wrapper gets the same wrapper over the rebound product. Internal only — `rebindDefineToolCtx` / `stampDefineToolBrand` are not on the export face.
|
|
31
|
+
- Two review findings folded in: (a) the per-call identity is now written over the enriched ctx as own properties instead of a spread — the spread evaluated the Runner enricher's LIVE getters (the memory-capture floor seats) once at call start and handed every `enrichCtx` mount (the first-party SendMessage mount included) a frozen snapshot; a long-running Workflow call now sees a mid-call capture flip for the children it spawns later. (b) A product whose `execute` was replaced IN PLACE after the factory (a guard written over the property while the brand stayed) is refused at the mount with a message naming the supported wrapper form — rebuilding it from the spec would have run the original body and silently dropped the guard. Round 2 closed the class: the seal is applied by the stamp site itself (every stamper gets it), it compares the MOUNTED object to the stamped one, and the brand is an OWN-property read — a prototype child, a transparent Proxy or a descriptor copy carrying a borrowed brand is refused, never rebound to the original body; and the per-call ctx is a FRESH object carrying the enriched object's descriptors (getters kept), so an enricher handing back one shared or frozen object across calls never leaks call A's identity into call B. Round 3: the seal reads `execute` as an own DATA descriptor (an accessor answering the original "while allowed" is refused), the rebound copy is built from descriptors rather than a spread (no accessor runs during the rebind) and the seal is re-checked after the rebuild — the wrapper-bypass root ran three review rounds (in-place / borrowed brand / accessor + re-entrancy) and is closed at the descriptor level; a further variant would have to defeat `Object.getOwnPropertyDescriptor` itself.
|
|
32
|
+
- Pins: `test/backlog656-caller-mounted-product-ctx.test.ts` (generic product ctx; hand-mounted Workflow ⇒ host approver consulted, gated tool runs; the seat as a unit — composition order, identity re-stamp, re-rebind, spread loses it); RB-362 pins re-pinned (two symbols; the mount hands the wrapper a fresh rebound product).
|
|
33
|
+
|
|
34
|
+
### Fixes — an EXPLICIT-model child carries the subagent role identity; the fork is the caller continued (#658; @test @server)
|
|
35
|
+
- **One seat law, two arms.** `childModelSeat` now returns `modelRole: "subagent"` on EVERY engine-spawned seat — the explicit-model arm too (per-call / definition / tool-level `model`); the arms differ only in whether a model rides the seat (explicit or inherited) or none does (a stated `roles.subagent` tier resolves through the role chain). `resolveTaskModel`'s explicit branch keys the `systemPrompt` / `thinking` presets on `modelRole`, so a child spawned with an explicit model used to read `roles.default`'s presets — the deployment's MAIN persona and the default role's thinking — while its sibling with no explicit model read `roles.subagent`; the workflow station (which writes the stated presets onto the spec since #655) disagreed with the delegation station on the same input. The workflow launch site (`withWorkflowChildSeats`) writes the identity on an explicit-model spec as well (a trusted script's own `modelRole` is kept; the call identity is computed before the seats fold, so resume journals keep their keys).
|
|
36
|
+
- **The fork carries none** (`forkModelSeat`): a fork runs on the caller's model with no role identity — its system prompt is meant to be the parent's, so the presets it reads are the `default` role's, as the caller's own explicit-model run does; a deployment's stated subagent persona never reaches a fork. With no caller model readable (only outside a Runner-enriched ctx) the fork's seat is empty — the `default` role chain — where it used to fall to the subagent role.
|
|
37
|
+
- Observable: `@test` a delegation with `createSubagentTool({ model })` or an agent definition carrying `model` under `roles.subagent: { systemPrompt, thinking }` — the child's system prompt carries the stated preset and its thinking is the stated level (it used to be the default persona / the default role's level); a `subagent_type: "fork"` child under the same roles carries NEITHER. `@server`: no wire change; deployments that state a `roles.subagent.systemPrompt` see it applied to explicit-model children they used to see under the main persona.
|
|
38
|
+
- Pins: `test/backlog658-explicit-model-role-identity.test.ts` (delegation × {tool-level, definition} × {stated, unstated}, fork, workflow parity, the seat as data); seat-shape pins in `backlog641` / `backlog651` / `backlog655` re-pinned to the constant identity.
|
|
39
|
+
|
|
40
|
+
### Narrowing — a provably READ-ONLY shell command runs without a question: upstream's read-only tables, transcribed (#619; @cli @server @client-core @test)
|
|
41
|
+
- **The station** (`gate-lanes.ts`, contract `read_only_shell.every_segment`): the allow layer gains a SECOND member beside the persisted allow rule — for the shell tool (`Bash`), after every tightening lane (org, persisted deny/ask rows, policy deny/ask rules) and after the person's own allow rules, a surviving ask whose FINAL command `readOnlyShellVerdict` proves read-only becomes an allow (`decisionReason: "read_only"`, `DECISION_REASONS` +1) — no classifier round, no approver, no park. Same consumption predicate as a stored allow rule (`allowLayerMayClear`, one predicate for both members): never over a mandated ask (egress / the tool's own irreversibility / `shellGate:"always"`), a hook's ask, an explicit ask rule (`matchedAskRule`, either lane), governance (`requiresRealApproval`), the reserved question tool or a marked call. Anchored on CC 2.1.250's shell tool `isReadOnly` (`v2e`: parse, split into simple commands, EVERY one must be read-only) and its position in `F2e` (deny → ask → allow rules → read-only → "requires approval").
|
|
42
|
+
- **The reader** (`src/core/read-only-shell.ts`) runs over the tightening lexer (`readShellCommand`, which now also reports each segment's `redirections`, the line's `backgrounded` bit and its `strayRedirection` bit — a redirection on a piece that runs no program, `true; >x`, which the shell still opens; additive on `ShellSegment` / `ShellCommandShape`; a hand-built shape literal needs the three members): a line is read-only iff it reads whole (no unreadable segment, no group/subshell/control structure, no background `&`, no stray redirection), no word carries a `$…`/`` `…` ``/`$((…))`/process substitution (anywhere in the word), and every segment is: redirections only read or discard (`<` family, `/dev/null`, `N>&M`), leading assignments only from `READ_ONLY_ENV_NAMES` with literal values, and the program admitted by one of three readings in order — a GLOB segment only under `READ_ONLY_GLOB_PROGRAMS` (upstream `B3t`, 31); the structural forms (`READ_ONLY_BARE_PROGRAMS` = upstream `jpe` 48, the exact forms, `echo`/`ls`/`cd`/`find`/`printf`/`history`/`arch`/`ifconfig`); the flag-vetted `READ_ONLY_COMMAND_TABLE` (upstream `w3t` and its spreads: 24 git, 22 gh, 2 docker, rg/grep/egrep/fgrep, fd/fdfind, sort, file, tree, date, hostname, lsof, pgrep, tput, ss, ps, netstat, man, help, base64, xargs, pyright, test, 3 checksums — 74 rows; upstream carries 75 with `sed`) with each row's `additionalCommandIsDangerousCallback` transcribed. A `cd` beside a `git` in one line is never read-only (the `command`/`builtin`/`noglob` prefixes and leading assignments are peeled before the names are compared). Pipelines and `&&`/`;`/`||` lists of read-only segments ARE read-only (`git status | grep x`), as upstream. Deliberate narrowings vs upstream: a BRACE pattern (`-{delete,name}`) is read as a many-word glob (admitted only under the glob programs; upstream reads it as literal text); a pattern with a parenthesised part (an extended pattern, or under zsh a glob QUALIFIER `*(e:'cmd':)` that runs code) and the zsh-only `=cmd` / `~[…]` spellings are never read-only; `run_in_background: true` is the parameter spelling of `&` (the shell probe's ruled reading #125) — not read-only at the gate although the command text alone would be. Lexer fix beside it (tightening reader too): a test operand's EVERY array subscript is judged for a hidden run (`[[ 0 -eq 'a[0]+b[$(…)]' ]]` was read as literal — now `unreadable`).
|
|
43
|
+
- **An unanswered reversibility probe is a mandate.** The out-of-root read mandate is minted from the shell probe's answer alone, so a probe that times out or throws leaves the call's boundary UNKNOWN: the fold latches `GatePass.probeUnanswered`, `persistedRuleMandateOf` reads it as the new word `probe_unanswered`, and NEITHER allow-layer member (a stored allow rule, the read-only reader) clears the surviving ask — a person confirms it, and a matching rule is disclosed as shadowed with that sentence. (Before this version a hanging probe let a stored allow rule clear an out-of-root read.)
|
|
44
|
+
- **Stated gaps (fail-closed, never silent):** `sed` is NOT read-only here (upstream vets the sed SCRIPT with a dedicated analyzer this version does not carry — a follow-up); the Windows PowerShell table and every Windows-only arm are not transcribed; upstream's two filesystem probes beside the tables (bare-repository indicators, a `.git` redirection) have no seat here. The existing `bashReversibilityProbe` read-only classifier (`shellGate:"classify"`, `BASH_CLASSIFY_DEFAULT_ALLOW`) is untouched in this version — a sibling to unify onto this reader (follow-up ticket).
|
|
45
|
+
- **Wire (additive):** trace kind `permission.read_only_allowed` (`{ toolName, toolCallId, command }` — the allow-side attribution, twin of `permission.persisted_rule_allowed`); `ToolGateInput.onReadOnlyAllowed?` seat. `@cli`: default/auto mode — `ls -la`, `git status`, `cat f | head` run with zero cards and zero classifier rounds; render the trace as "allowed: read-only command"; `@server`: `decisionReason` closed set +1 word (`read_only`) on the allow face, trace whitelist +1 kind; `@client-core`: `DecisionReason` mirror +1; `@test` (G): `ls -la` / `git status` / `cat f` ⇒ zero cards, zero classifier rounds, one `permission.read_only_allowed` trace; `cat $F` / `find . -delete` / `ls > out.txt` / `rm -rf x` ⇒ the ordinary ask (classifier in auto, card in default); `deny:["Bash(cat secret.txt)"]` ⇒ still refused; `ask:["Bash(git status)"]` ⇒ still a card; `shellGate:"always"` ⇒ still a card; `git status | grep x` ⇒ zero cards.
|
|
46
|
+
- Exports +17 (`readOnlyShellVerdict`, the twelve table constants, `ReadOnlyShellVerdict`, `ReadOnlyFlagArity`, `ReadOnlyCommandRow`, `ShellRedirection`; export-surface snapshot regenerated, all additive). CLOSED-SETS +1 (`ReadOnlyFlagArity` with its `FLAG_VALUE_ACCEPTS` fence). Pins: `test/backlog619-read-only-shell.test.ts` (the reader), `test/backlog619-read-only-gate.test.ts` (the station).
|
|
47
|
+
|
|
48
|
+
### Narrowing — a `/**`-tailed bare path rule (`secrets/**`) reads at ANY depth for deny/ask, anchored for allow: upstream's peel kept verbatim (#657; @cli @server @test)
|
|
49
|
+
- **The rule** (`resolvePathPattern`, contract `path_rule.bare_name` extended): a trailing `/**` is peeled BEFORE the bare-name test — a cwd-relative deny/ask body that is a bare name plus the tail (`Edit(secrets/**)`, `Write(*.d/**)`, `./secrets/**`) names every such directory at any depth under the working directory (`<cwd>/**/secrets`), exactly as #654 already read the untailed bare name. A body that still carries a `/` after the peel (`a/b/**`, `dist/**/x`) stays anchored; the explicit `**/x/**` spelling, the home/root/absolute forms and `ruleBasesNeeded` are unchanged. Anchored on CC 2.1.250's `ki(pattern, isAllow)`: the tail comes off, and the bare remainder is fed to the gitignore engine as-is for deny/ask (any depth) and as `/name` for allow (anchored).
|
|
50
|
+
- **The ONE behavior asymmetry** — upstream's own shape, kept rather than levelled (ruled): for the peeled form ALONE an ALLOW rule is anchored at cwd. `pathRuleReachOf` now takes the rule's `behavior` off the rule it is handed (`Pick<PersistedRule, "match" | "command" | "behavior">` — every caller already passes a parsed rule; a hand-built rule object must now carry `behavior`). The allow arm has no rule form on any lane (parse `invalid.grammar` / `unsupported.tool`, compile `unsupported.file_glob`) — written for parity, pinned unreachable at both doors and pinned in shape at the function seam.
|
|
51
|
+
- **Narrowing half (deny/ask reach WIDENS = stricter):** a migrated `deny: ["Edit(secrets/**)"]` now also refuses `a/secrets/x`; a persisted `Read(secrets/**)` deny/ask row reaches every `secrets` directory under cwd. One reader serves the persisted lane, the content-rule lane and its compile step. `@cli`: the `/permissions` breadth wording for `name/**` deny/ask rules is "every `name` directory under the working directory"; `@server`: task-settings content rules ride the same lane, zero code change; `@test`: `deny:["Edit(secrets/**)"]` ⇒ `<cwd>/a/secrets/x` and `<cwd>/secrets/x` refused, `ask` same reach, `Edit(a/b/**)` still only `<cwd>/a/b`, `**/secrets/**` unchanged.
|
|
52
|
+
- **Widening half — EMPTY** (no allow gets wider: the allow arm cannot be constructed). Pins: `test/backlog657-tailed-bare-path-rule-depth.test.ts`; the #654 file's anchored example moved from `dist/**` to `dist/**/x`.
|
|
53
|
+
|
|
54
|
+
### Narrowing — a cwd-relative BARE path rule reads at ANY depth, the upstream file-rule (gitignore) anchoring (#654; @cli @server @test)
|
|
55
|
+
- **The rule** (`resolvePathPattern`, contract `path_rule.bare_name`): a cwd-relative pattern body with NO `/` — `Edit(.env)`, `Read(secret.txt)`, `Read(*.env)` — names that segment at any depth under the working directory (`<cwd>/**/<body>`); a body carrying a `/` (`dist/**`, `a/b`) stays anchored at cwd; `./x` is the same pattern as `x` (the `./` marker only spells the cwd word); the `~/x` / `/x` / `//x` forms are unchanged (anchored at home / root / absolute). Anchored on CC 2.1.250's matcher: the base-relative pattern is handed to a gitignore engine, so a slash-less pattern matches at every depth. `ruleBasesNeeded` is unchanged — a bare name still names `cwd` (the root stands in when cwd is absent).
|
|
56
|
+
- **Narrowing half (deny/ask get WIDER reach = stricter):** a migrated `deny: ["Edit(.env)"]` now refuses `nested/.env` too; a persisted `Read(secret.txt)` deny/ask row reaches every `secret.txt` under the working directory. ONE reader (`pathRuleReachOf`) serves the persisted store lane, the content-rule policy lane and its compile step, so all move together. `@cli`: the `/permissions` editor's breadth wording for slash-less path rules can drop the root-only caveat; `@server`: task-settings content rules ride the same lane, zero code change; `@test`: black-box `deny:["Edit(.env)"]` ⇒ `nested/.env` refused, `Read(*.env)` ask reaches `a/b/prod.env`, `Write(dist/**/x)` still only `<cwd>/dist/…/x` (the `dist/**` tail form is #657's), `./x` ≡ `x`.
|
|
57
|
+
- **Widening half — EMPTY:** an allow in the pattern form has no rule form on any lane (persisted parse `invalid.grammar` / `unsupported.tool`, policy compile `unsupported.file_glob`; only the `Read(//abs/**)` directory form is a standing approval), so no allow gets wider — pinned.
|
|
58
|
+
- The `/**`-tailed form (`secrets/**`) is the #657 entry below — the residual this entry once carried is closed there.
|
|
59
|
+
|
|
60
|
+
### Fixes — the child thinking seat's DEFINITION rung on the observer and workflow stations; the workflow child gets its role identity and the stated subagent persona (#655; B-053 residue; @test @server)
|
|
61
|
+
- **Observer sidecar:** `childThinkingSeat(observerDef.thinking, stated, parent)` — the watcher definition's own `thinking` is the explicit rung (it was hard-coded absent, so a definition's level never reached the observer).
|
|
62
|
+
- **Workflow lane — definition folds BEFORE inheritance fills.** The host level used to be written into the child spec inside `buildWorkflowPrimitives`, before `applyWorkflowAgentType` folded the agentType definition, so the fold's "when unset" guard was always false and a definition's `thinking` never reached the child. The fill is gone from the primitives; the host getter rides `RunWorkflowOptions.defaultThinking` (the `defaultModel` twin — snapshotted once at call entry whenever the authored spec left `thinking` unset and folded into the call identity exactly as the pre-fold primitives composed it, so resume journals written by earlier versions keep their keys; a definition's level applies at launch without entering the identity, as the model axis already does) and every child seat is applied at the launch site AFTER the fold by `withWorkflowChildSeats`. `buildWorkflowPrimitives` loses its `parentThinking` positional (internal helper, not on the export face). `RunWorkflowToolDeps.parentThinking` keeps its name and meaning.
|
|
63
|
+
- **Workflow child role identity:** the inherited-model arm now carries `modelRole: "subagent"` beside the host's Model (bare `{ modelRole: "subagent" }` when no host model is readable; an explicit model keeps the explicit shape; a trusted script's own `modelRole` is kept) — the same seat shape as the delegation and observer stations, so `resolveTaskModel` keys the SUBAGENT role's presets for a workflow child (a stated `roles.subagent.thinking` reaches it through the role chain as well as the written level).
|
|
64
|
+
- **Workflow child persona:** the deployment's STATED `roles.subagent.systemPrompt` (new reader `deploymentSubagentSystemPrompt`, the thinking reader's twin on the same seat) reaches a workflow child that named no persona — written before the persona frame, so the frame appends the return-contract NOTE to it as to any custom persona instead of replacing it with the default workflow-subagent persona. An agentType/script persona still outranks it; with nothing stated the default persona is byte-identical to before.
|
|
65
|
+
- Pins: `test/backlog655-child-thinking-seat-definition-layer.test.ts` — definition × {sync, background, observer, workflow} × {stated, unstated} × {parent none, parent low} = 32 cells through real Runners, plus the persona and seat-shape pins.
|
|
66
|
+
|
|
3
67
|
## 7.10.0 — 2026-09-09
|
|
4
68
|
|
|
5
69
|
### BREAKING — `Runner.swapModels` is retired into ONE hot-swap door, `Runner.swapDeps` (#628 + #616 seat door; @server S-174 @cli @client-core)
|
|
@@ -18,6 +18,17 @@ export declare function deploymentStatesSubagentModel(roles: ModelRoles | undefi
|
|
|
18
18
|
* already reflects when the parent ran on it).
|
|
19
19
|
*/
|
|
20
20
|
export declare function deploymentSubagentThinking(roles: ModelRoles | undefined): ThinkingLevel | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Did the deployment STATE a system prompt for its subagents — and which? The `systemPrompt` member of an
|
|
23
|
+
* object-form `roles.subagent` entry; a string ref, a `Model` object or an entry without the member states
|
|
24
|
+
* none. The third axis, read off the SAME seat as {@link deploymentSubagentThinking}. It has no parent rung:
|
|
25
|
+
* a child never inherits its PARENT's system prompt, only the deployment's statement for subagents. Which
|
|
26
|
+
* station needs to read it: one whose default persona is written onto the spec itself (the workflow lane
|
|
27
|
+
* frames an empty `systemPrompt` with the workflow-subagent persona, so the preset must be written first, as
|
|
28
|
+
* a custom persona to that frame); the delegation station's default rides the trusted internals channel
|
|
29
|
+
* below the role chain, so prepare reads the preset there without this helper.
|
|
30
|
+
*/
|
|
31
|
+
export declare function deploymentSubagentSystemPrompt(roles: ModelRoles | undefined): string | undefined;
|
|
21
32
|
/**
|
|
22
33
|
* The child run's THINKING seat — {@link childModelSeat}'s sister, the same rung order on the other axis
|
|
23
34
|
* (#651): an EXPLICIT level for this spawn wins (the agent definition's `thinking`; a fork's explicit is the
|
|
@@ -26,11 +37,12 @@ export declare function deploymentSubagentThinking(roles: ModelRoles | undefined
|
|
|
26
37
|
* a mid-run degrade/boost carries into new children); else nothing (the role chain decides, as before).
|
|
27
38
|
*
|
|
28
39
|
* The stated level is WRITTEN onto the spec rather than left to the role chain, unlike the model axis,
|
|
29
|
-
* because the two axes resolve differently inside `resolveTaskModel`:
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the
|
|
33
|
-
* every seat
|
|
40
|
+
* because the two axes resolve differently inside `resolveTaskModel`: the role chain reads `thinking` off
|
|
41
|
+
* the role that SUPPLIED THE MODEL (the `subagent` entry when it states one, else the fallback role that
|
|
42
|
+
* did), so a `roles.subagent` entry stating a level but no model would have its level skipped in favour of
|
|
43
|
+
* the fallback role's. Writing the literal is the same value the seat rule resolves, on every seat shape;
|
|
44
|
+
* the seat's role identity (`modelRole: "subagent"`, on every engine-spawned seat) still keys the
|
|
45
|
+
* `systemPrompt` preset, which has no such fallback drift.
|
|
34
46
|
*
|
|
35
47
|
* Why the parent's level used to be wrong here: the parent's RESOLVED run level (`"off"` when the parent
|
|
36
48
|
* declared nothing — the harness contract) was inherited as if it were an explicit declaration, and
|
|
@@ -53,16 +65,22 @@ export declare function deploymentSubagentTierModel(deps: {
|
|
|
53
65
|
} | undefined): Model | undefined;
|
|
54
66
|
/**
|
|
55
67
|
* The child run's model seat — ONE rule for every engine-spawned run (the delegation child at
|
|
56
|
-
* `buildChildSpec`, the observer sidecar): an EXPLICIT model for this spawn wins
|
|
57
|
-
* agent definition's `model`, the tool-level `model`); else a deployment that STATED
|
|
58
|
-
* it (
|
|
59
|
-
* else the child runs on the PARENT run's effective model (the caller's current `Model` object,
|
|
60
|
-
* per-model routing/credentials ride along); else — no parent model readable, which only happens outside
|
|
68
|
+
* `buildChildSpec`, the observer sidecar, the workflow launch site): an EXPLICIT model for this spawn wins
|
|
69
|
+
* (per-call `model`, the agent definition's `model`, the tool-level `model`); else a deployment that STATED
|
|
70
|
+
* a subagent tier gets it (no model on the seat — the role chain resolves the stated entry, `select` forms
|
|
71
|
+
* included); else the child runs on the PARENT run's effective model (the caller's current `Model` object,
|
|
72
|
+
* so per-model routing/credentials ride along); else — no parent model readable, which only happens outside
|
|
61
73
|
* a Runner-enriched ctx — the bare role, whose chain ends at the deployment default.
|
|
62
74
|
*
|
|
63
|
-
*
|
|
64
|
-
* `resolveTaskModel` keys the role's `systemPrompt` / `thinking` presets on
|
|
65
|
-
* `roles
|
|
75
|
+
* THE ROLE IDENTITY IS CONSTANT: every seat carries `modelRole: "subagent"`, the explicit-model seat too.
|
|
76
|
+
* `resolveTaskModel` keys the role's `systemPrompt` / `thinking` presets on `modelRole` in BOTH of its
|
|
77
|
+
* branches (an explicit model reads `roles[modelRole]`'s presets; a role-resolved model reads them along the
|
|
78
|
+
* chain), so a deployment's `roles.subagent` preset reaches a child whatever decided its model. An explicit
|
|
79
|
+
* seat without the identity read the `default` role's presets instead — a child spawned with a per-call or
|
|
80
|
+
* definition model silently ran under the deployment's MAIN persona while its sibling with no explicit model
|
|
81
|
+
* ran under the subagent one, and the workflow station (which writes the stated persona onto the spec)
|
|
82
|
+
* disagreed with the delegation station on the same input. The two arms below differ ONLY in whether a
|
|
83
|
+
* model rides the seat; the identity never varies. A FORK is not a seat of this rule — see {@link forkModelSeat}.
|
|
66
84
|
*
|
|
67
85
|
* Why "default" was the wrong last resort: `FALLBACK.subagent = ["default"]` resolves to the deployment's
|
|
68
86
|
* boot default, which is not the model the parent run was switched to — a run whose main turn was moved to
|
|
@@ -70,12 +88,21 @@ export declare function deploymentSubagentTierModel(deps: {
|
|
|
70
88
|
* from the roster spawned children that failed at prepare). The parent's effective model is the honest
|
|
71
89
|
* default; the deployment's stated tier keeps outranking it because that statement is operator intent.
|
|
72
90
|
*/
|
|
73
|
-
/** The seat:
|
|
91
|
+
/** The seat: the role identity, with the model that rides it (explicit or inherited) or none (the stated tier resolves through the role chain). */
|
|
74
92
|
export type ChildModelSeat = {
|
|
75
|
-
model: ModelRef;
|
|
76
|
-
modelRole?: undefined;
|
|
77
|
-
} | {
|
|
78
93
|
modelRole: "subagent";
|
|
79
|
-
model?:
|
|
94
|
+
model?: ModelRef;
|
|
80
95
|
};
|
|
81
96
|
export declare function childModelSeat(explicit: ModelRef | undefined, deploymentStated: boolean, parent: Model | undefined): ChildModelSeat;
|
|
97
|
+
/**
|
|
98
|
+
* The FORK's model seat — the caller CONTINUED, not a subagent to the role chain. A fork always runs on the
|
|
99
|
+
* caller's model (the caller's current `Model`, else the delegation tool's own `model`), and it carries NO
|
|
100
|
+
* role identity: its system prompt is meant to be the PARENT's (the delegation station leaves the fork's
|
|
101
|
+
* `systemPrompt` unset and threads no default persona, so prepare resolves the same role presets the
|
|
102
|
+
* caller's own explicit-model run reads — the `default` role's — never the subagent persona a deployment
|
|
103
|
+
* stated for its workers). No caller model readable (only outside a Runner-enriched ctx) ⇒ an empty seat: the
|
|
104
|
+
* role chain's `default` start, exactly what a caller run with no `modelRole` of its own resolves.
|
|
105
|
+
*/
|
|
106
|
+
export declare function forkModelSeat(callerModel: ModelRef | undefined): {
|
|
107
|
+
model?: ModelRef;
|
|
108
|
+
};
|
|
@@ -21,12 +21,18 @@ export function deploymentStatesSubagentModel(roles) {
|
|
|
21
21
|
return cfg.select !== undefined;
|
|
22
22
|
}
|
|
23
23
|
export function deploymentSubagentThinking(roles) {
|
|
24
|
+
return subagentRolePresets(roles)?.thinking;
|
|
25
|
+
}
|
|
26
|
+
export function deploymentSubagentSystemPrompt(roles) {
|
|
27
|
+
return subagentRolePresets(roles)?.systemPrompt;
|
|
28
|
+
}
|
|
29
|
+
function subagentRolePresets(roles) {
|
|
24
30
|
const s = roles?.subagent;
|
|
25
31
|
if (s == null || typeof s === "string")
|
|
26
32
|
return undefined;
|
|
27
33
|
if ("id" in s && "api" in s)
|
|
28
34
|
return undefined;
|
|
29
|
-
return s
|
|
35
|
+
return s;
|
|
30
36
|
}
|
|
31
37
|
export function childThinkingSeat(explicit, deploymentStated, parent) {
|
|
32
38
|
const level = explicit ?? deploymentStated ?? parent;
|
|
@@ -36,11 +42,9 @@ export function deploymentSubagentTierModel(deps) {
|
|
|
36
42
|
return deps !== undefined && deploymentStatesSubagentModel(deps.roles) ? resolveTaskModel({ modelRole: "subagent" }, deps).model : undefined;
|
|
37
43
|
}
|
|
38
44
|
export function childModelSeat(explicit, deploymentStated, parent) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return { modelRole: "subagent", model: parent };
|
|
45
|
-
return { modelRole: "subagent" };
|
|
45
|
+
const model = explicit ?? (deploymentStated ? undefined : parent);
|
|
46
|
+
return { modelRole: "subagent", ...(model !== undefined ? { model } : {}) };
|
|
47
|
+
}
|
|
48
|
+
export function forkModelSeat(callerModel) {
|
|
49
|
+
return callerModel !== undefined ? { model: callerModel } : {};
|
|
46
50
|
}
|
package/dist/agents/subagent.js
CHANGED
|
@@ -7,7 +7,7 @@ import { newDelegationProvenanceAggregate, reduceDelegationAttestation } from ".
|
|
|
7
7
|
import { registerDelegationLaunch, replayExternalSettlementEffects, settleDelegation } from "../core/memory-engine/delegation-settlement.js";
|
|
8
8
|
import { enqueueMemoryAnnouncement } from "../core/memory-engine/layout.js";
|
|
9
9
|
import { resolveModel, resolveModelDisplayLabel } from "../core/roles.js";
|
|
10
|
-
import { childModelSeat, childThinkingSeat, deploymentStatesSubagentModel, deploymentSubagentThinking, runnerRoleMap } from "./child-model-seat.js";
|
|
10
|
+
import { childModelSeat, childThinkingSeat, deploymentStatesSubagentModel, deploymentSubagentThinking, forkModelSeat, runnerRoleMap } from "./child-model-seat.js";
|
|
11
11
|
export { childThinkingSeat, deploymentSubagentThinking, deploymentSubagentTierModel } from "./child-model-seat.js";
|
|
12
12
|
import { OUTPUT_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME } from "../core/runner/synthetic-tools.js";
|
|
13
13
|
import { TOOL_SEARCH_NAME } from "../core/runner/tool-disclosure.js";
|
|
@@ -1808,7 +1808,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1808
1808
|
}
|
|
1809
1809
|
}
|
|
1810
1810
|
}
|
|
1811
|
-
const childSeat = childModelSeat(childModel, deploymentStatedSubagentModel, ctx.model);
|
|
1811
|
+
const childSeat = wantsFork ? forkModelSeat(childModel) : childModelSeat(childModel, deploymentStatedSubagentModel, ctx.model);
|
|
1812
1812
|
const seatModel = childSeat.model;
|
|
1813
1813
|
const spawnModel = typeof seatModel === "string"
|
|
1814
1814
|
? (() => {
|
|
@@ -1893,7 +1893,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1893
1893
|
...(resume ? { requireExistingSession: true } : {}),
|
|
1894
1894
|
objective: objectiveText,
|
|
1895
1895
|
...childModelSeat(undefined, deploymentStatesSubagentModel(runnerRoleMap(opts.runner)), ctx.model),
|
|
1896
|
-
...childThinkingSeat(
|
|
1896
|
+
...childThinkingSeat(observerDef.thinking, deploymentSubagentThinking(runnerRoleMap(opts.runner)), ctx.thinkingLevel),
|
|
1897
1897
|
...(observerDef.systemPrompt !== undefined && !resume ? { systemPrompt: observerDef.systemPrompt } : {}),
|
|
1898
1898
|
...(ctx.principal !== undefined ? { principal: ctx.principal } : {}),
|
|
1899
1899
|
...(ctx.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: ctx.getApiKeyAndHeaders } : {}),
|
|
@@ -2133,8 +2133,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2133
2133
|
...(reviveClaim === undefined && ctx.fileHistoryLineage !== undefined ? { fileHistoryLineage: ctx.fileHistoryLineage } : {}),
|
|
2134
2134
|
...(ctx.onSubagentSpawn ? { onSubagentSpawn: ctx.onSubagentSpawn } : {}),
|
|
2135
2135
|
};
|
|
2136
|
-
const
|
|
2137
|
-
|
|
2136
|
+
const childThinking = wantsFork
|
|
2137
|
+
? ctx.thinkingLevel !== undefined ? { thinking: ctx.thinkingLevel } : {}
|
|
2138
|
+
: childThinkingSeat(def?.thinking, deploymentSubagentThinking(runnerRoleMap(opts.runner)), ctx.thinkingLevel);
|
|
2138
2139
|
const provenanceAgentName = agentName ?? def?.name;
|
|
2139
2140
|
const childMemoryPersistenceCapable = ctx.memoryPersistenceCapable === false ? false : (def?.memoryPersistenceCapable ?? ctx.memoryPersistenceCapable);
|
|
2140
2141
|
const childOnAsk = ctx.onAsk !== undefined
|
package/dist/agents/teacher.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_PROBLEM_BYTES, MAX_STRATEGY_TEXT_BYTES } from "../core/strategy-store.js";
|
|
3
3
|
import { createSafeNotifier, observeThenableRejection } from "../core/safe-notify.js";
|
|
4
4
|
import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
|
|
5
|
-
import { isDefineToolProduct, stampDefineToolBrand } from "../core/tools.js";
|
|
5
|
+
import { isDefineToolProduct, rebindDefineToolCtx, stampDefineToolBrand } from "../core/tools.js";
|
|
6
6
|
import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
|
|
7
7
|
import { buildCumulativeStats } from "./cumulative-stats.js";
|
|
8
8
|
export const TEACHER_PROMPT = `You are an expert advisor to a less-capable "student" agent that got stuck.
|
|
@@ -223,7 +223,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
223
223
|
throw e;
|
|
224
224
|
}
|
|
225
225
|
},
|
|
226
|
-
});
|
|
226
|
+
}, (enrich, _mounted) => wrap(rebindDefineToolCtx(product, enrich)));
|
|
227
227
|
}
|
|
228
228
|
return {
|
|
229
229
|
...t,
|
|
@@ -8,9 +8,10 @@ export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
|
|
|
8
8
|
export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2000;
|
|
9
9
|
/**
|
|
10
10
|
* The classifier call's output cap (`maxTokens`), sized from the engine's OWN output contract rather
|
|
11
|
-
* than copied from upstream: the
|
|
12
|
-
* `<block>
|
|
13
|
-
*
|
|
11
|
+
* than copied from upstream: the prompt asks for exactly two shapes, `<block>no</block>` and
|
|
12
|
+
* `<block>yes</block><category>NAME</category><reason>[NAME] one short sentence</reason>` (the parser,
|
|
13
|
+
* `parseAutoModeResponse`, reads the verdict wherever it sits, so a short preamble costs tokens, not
|
|
14
|
+
* the verdict). The longest rule name in the shipped rule sets is 46 characters and the reason
|
|
14
15
|
* is one short sentence naming that rule (≈200 characters at the outside, more when several rules
|
|
15
16
|
* match and the others are named in the reason), so the longest LEGAL reply is ≈350 characters —
|
|
16
17
|
* ≈120 tokens at the conservative 3 chars/token that tag-heavy text tokenizes at, ≈150 with a
|
|
@@ -22,6 +23,17 @@ export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2000;
|
|
|
22
23
|
* wire can say off (`thinkingOffExpressible`); a model that thinks regardless keeps its own budget.
|
|
23
24
|
*/
|
|
24
25
|
export declare const AUTO_MODE_CLASSIFIER_MAX_TOKENS = 256;
|
|
26
|
+
/**
|
|
27
|
+
* CC 2.1.250 `E4`: for a model in the `rejects_disabled_thinking` family (`BHe`) the classifier request
|
|
28
|
+
* adds 2048 output tokens on top of the verdict cap — the thinking ALLOWANCE for a model that reasons
|
|
29
|
+
* whether or not it is told to. The engine has no per-model declaration of that fact (a gateway model
|
|
30
|
+
* with no `reasoning`/`compat` in the catalog reads as "off is expressible"), so it learns it from the
|
|
31
|
+
* reply instead: a capped classify that comes back EMPTY at the cap (the output went to reasoning) is
|
|
32
|
+
* re-asked ONCE with the cap raised by this allowance, thinking still off (CC `cEt` re-asks such a
|
|
33
|
+
* reply up to `maxRetries` = 4 times at the same cap; one re-ask at the allowance-widened cap is the
|
|
34
|
+
* subset the engine spends). A second empty reply is the contract failure it always was.
|
|
35
|
+
*/
|
|
36
|
+
export declare const AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS = 2048;
|
|
25
37
|
/** The sentinel a deployment puts INSIDE a paired rule list to splice the CC default rules back in at
|
|
26
38
|
* that position (CC `XYt = "$defaults"`). It lives here, beside the other defaults, because the #503
|
|
27
39
|
* recipe canonicalizer needs its VALUE and must not load the assembly face (and its SHA-locked assets)
|
|
@@ -3,6 +3,7 @@ export const AUTO_MODE_DEFAULT_FAILURE_THRESHOLD = 3;
|
|
|
3
3
|
export const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
|
|
4
4
|
export const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2_000;
|
|
5
5
|
export const AUTO_MODE_CLASSIFIER_MAX_TOKENS = 256;
|
|
6
|
+
export const AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS = 2048;
|
|
6
7
|
export const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
|
|
7
8
|
export const AUTO_MODE_DENIAL_LIMIT_DEFAULTS = Object.freeze({ maxConsecutive: 3, maxTotal: 20 });
|
|
8
9
|
export const AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS = 120_000;
|
package/dist/core/auto-mode.d.ts
CHANGED
|
@@ -42,30 +42,34 @@ export type AutoModeBreakerCause = (typeof AUTO_MODE_BREAKER_CAUSES)[number];
|
|
|
42
42
|
/** Whether a value is a member of the breaker-cause set (the manifest's value screen over a recorded trip). */
|
|
43
43
|
export declare function isAutoModeBreakerCause(v: unknown): v is AutoModeBreakerCause;
|
|
44
44
|
/**
|
|
45
|
-
* Parse the
|
|
45
|
+
* Parse the classifier's output contract — the CC 2.1.250 verdict reader (`wCe`, its thinking strip
|
|
46
|
+
* `xCe`, its reason reader `yCe`) ported as is:
|
|
46
47
|
* blocked: `<block>yes</block><category>Rule Name</category><reason>[Rule Name] …</reason>`
|
|
47
48
|
* allowed: `<block>no</block>`
|
|
48
|
-
* The contract says the ENTIRE response must begin with `<block>` — we strip `<thinking>` blocks
|
|
49
|
-
* first (CC-anchored: the 206 body's `dJi` removes closed thinking blocks AND a dangling unclosed
|
|
50
|
-
* tail before its own block-scan — a reasoning model's scratch space is not part of the verdict),
|
|
51
|
-
* then tolerate leading whitespace (models routinely emit a leading newline) but nothing else: any
|
|
52
|
-
* other preamble is a `parse_error` (fail-closed; 207 vocabulary: `automode-parsing-error`).
|
|
53
49
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
50
|
+
* The verdict is the set of `<block>yes|no` hits, NOT a prefix match:
|
|
51
|
+
* 1. CONFLICT, read on the RAW text (thinking included): `<block>(yes|no)\b` collected over the whole
|
|
52
|
+
* response — more than one distinct answer ⇒ no verdict (`parse_error`). A scratch-space "yes"
|
|
53
|
+
* contradicting a final "no" is a contradiction, not an allow.
|
|
54
|
+
* 2. HITS, read on the text with `<thinking>` blocks removed (closed ones anywhere, then an unclosed
|
|
55
|
+
* trailing one — a reasoning model's scratch space is not part of the verdict): `<block>(yes|no)\b`
|
|
56
|
+
* with the closing tag OPTIONAL, at ANY position. Zero hits ⇒ `parse_error` (a reply that only
|
|
57
|
+
* answered inside its thinking has no verdict); the one distinct answer ⇒ `no` = allow, `yes` = block.
|
|
58
|
+
* So a preamble before the tag, prose after `<block>no`, a missing `</block>` and a reply that repeats
|
|
59
|
+
* its answer all read as the answer they carry; the tag must be spelled `<block>yes` / `<block>no` with
|
|
60
|
+
* no whitespace inside (the word-boundary form — `<block>yesterday` is not a hit) and case does not matter.
|
|
57
61
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
62
|
+
* A stated BLOCK stands with or without its tail: `<category>` (the BLOCK rule NAME the prompt asked
|
|
63
|
+
* for, read case-insensitively as free text — the deployment authors those names, so there is no
|
|
64
|
+
* fixed vocabulary to screen them against) and `<reason>` (first `<reason>…</reason>` pair on the
|
|
65
|
+
* stripped text, the tag spelled as the prompt spells it, trimmed) are carried when present and `""`
|
|
66
|
+
* when absent; the consumers render their
|
|
67
|
+
* own fallback line from the tool name in that case. The block intent is unambiguous either way, and
|
|
68
|
+
* downgrading a stated block to `parse_error` (⇒ the ask may later auto-deny OR a person may approve)
|
|
69
|
+
* would weaken the classifier's explicit verdict on the exact calls it flagged.
|
|
70
|
+
*
|
|
71
|
+
* `parse_error` is fail-closed: the ask flows the original chain (a person, a durable park, a headless
|
|
72
|
+
* auto-deny) exactly as if auto mode were absent — never a silent allow.
|
|
69
73
|
*/
|
|
70
74
|
export declare function parseAutoModeResponse(text: string): AutoModeVerdict;
|
|
71
75
|
/** What the decider hands the deployment's `classify` hook (the assembled prompt is the hook's job
|
package/dist/core/auto-mode.js
CHANGED
|
@@ -10,20 +10,20 @@ export function isAutoModeBreakerCause(v) {
|
|
|
10
10
|
return AUTO_MODE_BREAKER_CAUSE_SET.has(v);
|
|
11
11
|
}
|
|
12
12
|
export function parseAutoModeResponse(text) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
.replace(/<thinking>[\s\S]*$/, "")
|
|
16
|
-
.trimStart();
|
|
17
|
-
const m = /^<block>\s*(yes|no)\s*<\/block>/i.exec(t);
|
|
18
|
-
if (!m)
|
|
13
|
+
const rawAnswers = new Set([...text.matchAll(/<block>(yes|no)\b/gi)].map((m) => m[1].toLowerCase()));
|
|
14
|
+
if (rawAnswers.size > 1)
|
|
19
15
|
return { kind: "parse_error", raw: text };
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const stripped = text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "").replace(/<thinking>[\s\S]*$/, "");
|
|
17
|
+
const hits = [...stripped.matchAll(/<block>(yes|no)\b(<\/block>)?/gi)];
|
|
18
|
+
if (hits.length === 0)
|
|
19
|
+
return { kind: "parse_error", raw: text };
|
|
20
|
+
const answers = new Set(hits.map((m) => m[1].toLowerCase()));
|
|
21
|
+
if (answers.size !== 1)
|
|
22
|
+
return { kind: "parse_error", raw: text };
|
|
23
|
+
if (!answers.has("yes"))
|
|
23
24
|
return { kind: "allow" };
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const reason = /<reason>([\s\S]*?)<\/reason>/i.exec(t)?.[1]?.trim() ?? "";
|
|
25
|
+
const category = /<category>([\s\S]*?)<\/category>/i.exec(stripped)?.[1]?.trim() ?? "";
|
|
26
|
+
const reason = /<reason>([\s\S]*?)<\/reason>/.exec(stripped)?.[1]?.trim() ?? "";
|
|
27
27
|
return { kind: "block", category, reason };
|
|
28
28
|
}
|
|
29
29
|
export function createAutoModeDecider(opts) {
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -82,7 +82,12 @@ export declare function persistedRuleMandateOf(marks: {
|
|
|
82
82
|
/** #502: the surviving ask's engine-stamped `probeMandated` — see
|
|
83
83
|
* {@link import("./types.js").ReversibilityVerdict.mandated}. */
|
|
84
84
|
probeMandated?: boolean;
|
|
85
|
-
|
|
85
|
+
/** The pass's own record that the probe was consulted and did not answer (`GatePass.probeUnanswered`):
|
|
86
|
+
* the boundary is unknown, which no standing yes may stand in for. The runner's suggestion factory does
|
|
87
|
+
* not see the pass and never passes it — a card may offer a rule the lane then discloses as shadowed
|
|
88
|
+
* (loud, never a silent override), the same drift window the `matchedAskRule` conjunct already has. */
|
|
89
|
+
probeUnanswered?: boolean;
|
|
90
|
+
}): "operator_always" | "tool_marks" | "probe_mandate" | "probe_unanswered" | undefined;
|
|
86
91
|
/**
|
|
87
92
|
* Run the lanes over the pass: the org layer over the fold's verdict, then — for a surviving ask — the rewrite
|
|
88
93
|
* merge, the rule lane, the stamp, the classifier, the sandbox leg, the parks, the content lane, the resolution
|