review pass: purge stale enforcement language after the axis decoupling
Self-review findings, all prose/metadata (no behavior change): the package description still claimed hard (execution) enforcement; the RFC Problem framing still defined plan mode as a read-only tool policy; integration/mode/snapshot-suite comments still referenced the removed sandbox clamp and cap; the header-fallback explanation said 'front-of-list insertion' where the actual mechanism is a non-tail insertion the append-only tools delta cannot express (exit_plan_mode sits mid-list alphabetically); AGENTS.md's layout line and the RFC's 'policy state/listener' phrasing move to collaboration-state wording.
This commit is contained in:
@@ -23,7 +23,7 @@ packages/ @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
|
|||||||
subagent/ subagent seam + spawn/fork/ACP backends + delegation tool
|
subagent/ subagent seam + spawn/fork/ACP backends + delegation tool
|
||||||
workflow/ workflow seam + worker-thread engine + the workflow tool
|
workflow/ workflow seam + worker-thread engine + the workflow tool
|
||||||
todo/ the todo_write tool
|
todo/ the todo_write tool
|
||||||
mode/ session modes: plan mode as logged per-agent policy state
|
mode/ session modes: plan mode as logged per-agent collaboration state
|
||||||
guard/ loop-hygiene plugins
|
guard/ loop-hygiene plugins
|
||||||
cordis/ self-referential toolset: the agent inspects/mounts plugins in its own runtime
|
cordis/ self-referential toolset: the agent inspects/mounts plugins in its own runtime
|
||||||
hooks/ Claude Code / Codex hook bridges + shared wire-protocol library
|
hooks/ Claude Code / Codex hook bridges + shared wire-protocol library
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ Status: implemented
|
|||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
The harness has no way to put an agent into a reduced-authority working state. The canonical feature that needs one is plan mode — the agent explores and designs under a read-only tool policy, produces a reviewable plan, and crosses back into full authority only through an explicit approval. [The extension cookbook](../../../cookbook/extension-cookbook.md) already reserves the row ("Plan mode — `tools/pre-execute` (deny writes) + a mode prompt section"), and [the ACP feature matrix](../../../../packages/ui/acp/acp-feature-support.md) records session modes as a known gap both reference adapters ship. Neither says where the mode STATE lives, how it survives resume and fork, or how its model-visible consequences stay honest with the session log.
|
The harness has no way to put an agent into a distinct working stance. The canonical feature that needs one is plan mode — the agent explores and designs under a planning stance, produces a reviewable plan, and crosses back through an explicit approval. [The extension cookbook](../../../cookbook/extension-cookbook.md) already reserves the row ("Plan mode — `tools/pre-execute` (deny writes) + a mode prompt section"), and [the ACP feature matrix](../../../../packages/ui/acp/acp-feature-support.md) records session modes as a known gap both reference adapters ship. Neither says where the mode STATE lives, how it survives resume and fork, or how its model-visible consequences stay honest with the session log.
|
||||||
|
|
||||||
Every shipped plan mode decomposes into the same five parts — a low-authority tool policy, a plan artifact, an approval moment, an execution-state switch, and durable state ([Prior art](#prior-art) carries the survey). Four of the five already exist here as gated infrastructure: what the model is TOLD it can do is shaped per step at [`system-prompt/assemble`](../../../../packages/core/system-prompt/README.md) and whatever ships is logged as `request/header*` events ([reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md)); what can RUN is gated at `tools/pre-execute` with typed decisions ([interception seams](../../implemented/feature/2026-06-30-interception-seams.md)); the approval moment is a human answer over the user-interaction seam ([`ctx.userInteraction`](../../../../packages/ui/user-interaction/README.md), the [ask-user precedent](../../implemented/feature/2026-06-25-ask-user-question.md)); durable per-agent facts are `SessionEventMap` members ([the `todo/write` precedent](../../implemented/feature/2026-06-29-todo-write-tool.md)). The missing fifth is the mode itself: a named, durable, per-agent policy state the policy listeners can read.
|
Every shipped plan mode decomposes into the same five parts — a low-authority tool policy, a plan artifact, an approval moment, an execution-state switch, and durable state ([Prior art](#prior-art) carries the survey). Four of the five already exist here as gated infrastructure: what the model is TOLD it can do is shaped per step at [`system-prompt/assemble`](../../../../packages/core/system-prompt/README.md) and whatever ships is logged as `request/header*` events ([reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md)); what can RUN is gated at `tools/pre-execute` with typed decisions ([interception seams](../../implemented/feature/2026-06-30-interception-seams.md)); the approval moment is a human answer over the user-interaction seam ([`ctx.userInteraction`](../../../../packages/ui/user-interaction/README.md), the [ask-user precedent](../../implemented/feature/2026-06-25-ask-user-question.md)); durable per-agent facts are `SessionEventMap` members ([the `todo/write` precedent](../../implemented/feature/2026-06-29-todo-write-tool.md)). The missing fifth is the mode itself: a named, durable, per-agent state its listeners can read.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
The deliverable is **plan mode**. It ships as the first **session mode** — a named, logged, per-agent COLLABORATION state: a mode definition is the guidance section the model sees, deployment config; the mode IN FORCE for an agent is session state, folded from its log. Modes are one axis and the enforcement knobs — the sandbox mode, the approval policy — are others: they never read or write each other, matching how Codex keeps its Plan/Default collaboration presets separate from its sandbox and approval settings. One new product package, `@deepseek-ai/dsh-mode` at `packages/mode/mode/` (a new top-level group, the `packages/approval/` shape), owns the event vocabulary, a thin `ctx.modes` service, and every policy listener; the loop does not change. `plan` is the only shipped definition — the mode-shaped vocabulary exists so a second mode never renames durable event types, not because more modes ship now.
|
The deliverable is **plan mode**. It ships as the first **session mode** — a named, logged, per-agent COLLABORATION state: a mode definition is the guidance section the model sees, deployment config; the mode IN FORCE for an agent is session state, folded from its log. Modes are one axis and the enforcement knobs — the sandbox mode, the approval policy — are others: they never read or write each other, matching how Codex keeps its Plan/Default collaboration presets separate from its sandbox and approval settings. One new product package, `@deepseek-ai/dsh-mode` at `packages/mode/mode/` (a new top-level group, the `packages/approval/` shape), owns the event vocabulary, a thin `ctx.modes` service, and every listener; the loop does not change. `plan` is the only shipped definition — the mode-shaped vocabulary exists so a second mode never renames durable event types, not because more modes ship now.
|
||||||
|
|
||||||
The state is one `SessionEventMap` member: **`mode/set`**, a log-only, non-surface event carrying `{ mode: string }` with whole-value-replace semantics, plus a pure `foldMode(events)` that returns the mode in force — the last `mode/set`, or the default mode when none exists. Because [the log is the fact channel](../../implemented/architecture/2026-06-30-event-domain-semantics.md), resume, fork, and compaction restore the mode with no extra machinery, and UIs read flips off `session/event`. The default mode is the absence of policy — no section, no filtering, no gate — so an agent that never sees a `mode/set` behaves byte-identically to a deployment that never loads `dsh-mode`, which keeps every existing snapshot golden stable and makes the plugin safe to compose unconditionally.
|
The state is one `SessionEventMap` member: **`mode/set`**, a log-only, non-surface event carrying `{ mode: string }` with whole-value-replace semantics, plus a pure `foldMode(events)` that returns the mode in force — the last `mode/set`, or the default mode when none exists. Because [the log is the fact channel](../../implemented/architecture/2026-06-30-event-domain-semantics.md), resume, fork, and compaction restore the mode with no extra machinery, and UIs read flips off `session/event`. The default mode is the absence of policy — no section, no filtering, no gate — so an agent that never sees a `mode/set` behaves byte-identically to a deployment that never loads `dsh-mode`, which keeps every existing snapshot golden stable and makes the plugin safe to compose unconditionally.
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ Contained listeners on the loop's interception seams ([defensive patterns](../..
|
|||||||
|
|
||||||
### The soft layer: a computed section and a narrow post-`next()` filter
|
### The soft layer: a computed section and a narrow post-`next()` filter
|
||||||
|
|
||||||
A `system-prompt/assemble` waterfall listener reads the calling agent's mode (the `AssembleContext` carries `agent`) and, in a non-default mode, appends the mode's guidance section. The loop already renders per step and logs the result: entering or leaving a mode surfaces on the next step as a `request/header-delta` — or as the full `request/header` fallback snapshot when the change is inexpressible in the delta encoding (adding `exit_plan_mode` is a front-of-list insertion with no delta form; removing it on exit is a pure removal and DOES delta) — so every mode transition is an attributable log fact. The section is static per mode and the plan itself stays in the conversation (messages and tool args, already in context), so a mode does not add per-step prompt churn — re-injecting plan state into every request ([Prior art](#prior-art)'s compaction-survival hack) is unnecessary and would only burn prefix cache.
|
A `system-prompt/assemble` waterfall listener reads the calling agent's mode (the `AssembleContext` carries `agent`) and, in a non-default mode, appends the mode's guidance section. The loop already renders per step and logs the result: entering or leaving a mode surfaces on the next step as a `request/header-delta` — or as the full `request/header` fallback snapshot when the change is inexpressible in the delta encoding (adding `exit_plan_mode` at its canonical alphabetical position is a non-tail insertion the append-only tools delta cannot express; removing it on exit is a pure removal and DOES delta) — so every mode transition is an attributable log fact. The section is static per mode and the plan itself stays in the conversation (messages and tool args, already in context), so a mode does not add per-step prompt churn — re-injecting plan state into every request ([Prior art](#prior-art)'s compaction-survival hack) is unnecessary and would only burn prefix cache.
|
||||||
|
|
||||||
The guidance section is an ordinary registered section, `{ name: 'mode:policy', order: 50, text: context => … }` — order 50 sits after the persona (0) and before tool guidance (100–199); it resolves to the folded mode's configured text and to `''` (dropped at render) for the default mode or an agent-less assembly. The same listener carries the ONE tool rule this plugin has, deliberately narrow (`prepend: true`, so it wraps outside every append-registered listener's post-`next()` mutation): `exit_plan_mode` is visible IFF the agent's folded mode is `plan` — which keeps a default-mode assembly byte-identical to a no-`dsh-mode` deployment even though the tool is always registered. Everything else passes through untouched: which tools a mode admits is not this plugin's decision. Code Mode's soft surface is the `tools:sdk` section rather than the wire schemas, and section text resolves in assemble's base, so the same wrapper re-renders that section under the same visibility rule; the default mode re-renders it too, hiding only the exit binding, keeping a default-mode Code Mode assembly byte-identical to a deployment that never loaded `dsh-mode`. There is NO `tools/pre-execute` listener: a mode gates nothing (the exit tool's own folded-mode recheck rejects an out-of-plan call), and the registry's `ask` vocabulary stays free for genuine permission gating — the exit review is a question with options and feedback, not a permission, so it lives inside the tool's own execution over the user-interaction seam.
|
The guidance section is an ordinary registered section, `{ name: 'mode:policy', order: 50, text: context => … }` — order 50 sits after the persona (0) and before tool guidance (100–199); it resolves to the folded mode's configured text and to `''` (dropped at render) for the default mode or an agent-less assembly. The same listener carries the ONE tool rule this plugin has, deliberately narrow (`prepend: true`, so it wraps outside every append-registered listener's post-`next()` mutation): `exit_plan_mode` is visible IFF the agent's folded mode is `plan` — which keeps a default-mode assembly byte-identical to a no-`dsh-mode` deployment even though the tool is always registered. Everything else passes through untouched: which tools a mode admits is not this plugin's decision. Code Mode's soft surface is the `tools:sdk` section rather than the wire schemas, and section text resolves in assemble's base, so the same wrapper re-renders that section under the same visibility rule; the default mode re-renders it too, hiding only the exit binding, keeping a default-mode Code Mode assembly byte-identical to a deployment that never loaded `dsh-mode`. There is NO `tools/pre-execute` listener: a mode gates nothing (the exit tool's own folded-mode recheck rejects an out-of-plan call), and the registry's `ask` vocabulary stays free for genuine permission gating — the exit review is a question with options and feedback, not a permission, so it lives inside the tool's own execution over the user-interaction seam.
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ What holds now, pinned by the unit, protocol, and snapshot tiers:
|
|||||||
- The mode in force is a pure function of the session log: resume and fork restore it with no extra machinery, and a `mode/set` is followed by the matching `request/header` event (delta or fallback snapshot) on the next step with the dev invariant green throughout.
|
- The mode in force is a pure function of the session log: resume and fork restore it with no extra machinery, and a `mode/set` is followed by the matching `request/header` event (delta or fallback snapshot) on the next step with the dev invariant green throughout.
|
||||||
- A user-driven flip narrates exactly once at the next boundary and a net-zero flip sequence narrates nothing; a tool-driven exit narrates only through its tool result.
|
- A user-driven flip narrates exactly once at the next boundary and a net-zero flip sequence narrates nothing; a tool-driven exit narrates only through its tool result.
|
||||||
- In the default mode the plugin is invisible: assemblies are byte-identical with and without `dsh-mode` loaded, and every pre-existing snapshot golden is unchanged.
|
- In the default mode the plugin is invisible: assemblies are byte-identical with and without `dsh-mode` loaded, and every pre-existing snapshot golden is unchanged.
|
||||||
- In plan mode the guidance section and the `exit_plan_mode` tool reach both the wire request and the logged header; entering plan logs the fallback header snapshot (front-of-list insertion), the approved exit logs the pure-removal header-delta.
|
- In plan mode the guidance section and the `exit_plan_mode` tool reach both the wire request and the logged header; entering plan logs the fallback header snapshot (a non-tail tool insertion), the approved exit logs the pure-removal header-delta.
|
||||||
- Plan mode changes nothing on the enforcement axes: the toolset, the sandbox mode, escalation, and the approval policy behave identically in plan and default — pairing the mode with the independent sandbox/approval knobs is how a deployment hardens planning.
|
- Plan mode changes nothing on the enforcement axes: the toolset, the sandbox mode, escalation, and the approval policy behave identically in plan and default — pairing the mode with the independent sandbox/approval knobs is how a deployment hardens planning.
|
||||||
- Mode definitions (section text) are changeable from `cordis.yml` with no code edit; an unknown mode name fails validation loudly at `set()` time, an unknown definition key at load.
|
- Mode definitions (section text) are changeable from `cordis.yml` with no code edit; an unknown mode name fails validation loudly at `set()` time, an unknown definition key at load.
|
||||||
- `exit_plan_mode`'s approve path flips the mode and drops the plan surface on the next step; the keep-planning path returns the corrective `isError` carrying the user's feedback and stays in plan mode; the ACP `session/set_mode` round-trip updates `current_mode_update`, and the exit review prompts through each surface's user-interaction provider.
|
- `exit_plan_mode`'s approve path flips the mode and drops the plan surface on the next step; the keep-planning path returns the corrective `isError` carrying the user's feedback and stays in plan mode; the ACP `session/set_mode` round-trip updates `current_mode_update`, and the exit review prompts through each surface's user-interaction provider.
|
||||||
|
|||||||
@@ -8,17 +8,16 @@ import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from
|
|||||||
* `DSH_SNAPSHOT=replay`).
|
* `DSH_SNAPSHOT=replay`).
|
||||||
*
|
*
|
||||||
* The recorded scenarios re-execute the fs tools AND the sandboxed bash
|
* The recorded scenarios re-execute the fs tools AND the sandboxed bash
|
||||||
* executor for real on replay (the replay overlay swaps only the model), so
|
* executor for real on replay (the replay overlay swaps only the model): the
|
||||||
* the plan-mode arc doubles as a live check of plan's read-only `access`
|
* recorded `cat` runs under the host's actual runner (Seatbelt on macOS,
|
||||||
* cap: the recorded `cat` runs under the host's actual runner (Seatbelt on
|
* bwrap on Linux CI), under the sandbox's own configured mode — plan mode
|
||||||
* macOS, bwrap on Linux CI). The recorded commands stay `cat`-shaped —
|
* does not change it. The recorded commands stay `cat`-shaped —
|
||||||
* byte-identical across those backends and across GNU/BSD userlands — and
|
* byte-identical across those backends and across GNU/BSD userlands — and
|
||||||
* the transcripts carry NO sandbox denial (a denied command's stderr is the
|
* the transcripts carry NO sandbox denial (a denied command's stderr is the
|
||||||
* backend's dialect; the denial→marker path is pinned at dsh-tool-bash's
|
* backend's dialect; the denial→marker path is pinned at dsh-tool-bash's
|
||||||
* unit tier, and the cap's clamp at dsh-mode's). Prompts pin the model to
|
* unit tier). Prompts pin the model to RELATIVE paths, because a recorded
|
||||||
* RELATIVE paths, because a recorded absolute temp path would neither replay
|
* absolute temp path would neither replay on another host nor normalize
|
||||||
* on another host nor normalize (the normalizers scrub the RUN's own cwd,
|
* (the normalizers scrub the RUN's own cwd, not the recording's).
|
||||||
* not the recording's).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function snapshotModeFromEnv(value: string | undefined): SnapshotSuiteOptions['mode'] {
|
function snapshotModeFromEnv(value: string | undefined): SnapshotSuiteOptions['mode'] {
|
||||||
@@ -50,11 +49,11 @@ const SCENARIOS: Scenario[] = [
|
|||||||
// initial) — the full toolset plus exit_plan_mode and the mode section —
|
// initial) — the full toolset plus exit_plan_mode and the mode section —
|
||||||
// and the approved exit narrows it back by exactly that tool and section,
|
// and the approved exit narrows it back by exactly that tool and section,
|
||||||
// a pure removal the delta encoding CAN express (one header-delta; the
|
// a pure removal the delta encoding CAN express (one header-delta; the
|
||||||
// ENTERING flip's front-of-list insertion has no delta form and falls back
|
// ENTERING flip is a non-tail insertion the append-only tools delta cannot
|
||||||
// to a snapshot, pinned at the unit tier). The arc: setMode(plan) → the
|
// express, so it falls back to a snapshot — pinned at the unit tier). The
|
||||||
// model runs a real `cat` through the clamped read-only sandbox and
|
// arc: setMode(plan) → the model runs a real `cat` inside plan and
|
||||||
// presents the plan via exit_plan_mode → the scripted elicitation approves
|
// presents the plan via exit_plan_mode → the scripted elicitation approves
|
||||||
// → the very next step already runs unclamped and edits for real, mid-turn.
|
// → the very next step already edits for real, mid-turn.
|
||||||
{ name: 'plan-mode', hasModelTurn: true, recorded: true, pinsHeader: true, headerClass: 'plan', expectedHeaderDeltas: 1 },
|
{ name: 'plan-mode', hasModelTurn: true, recorded: true, pinsHeader: true, headerClass: 'plan', expectedHeaderDeltas: 1 },
|
||||||
// The keep-planning branch: one presentation, the scripted review answers
|
// The keep-planning branch: one presentation, the scripted review answers
|
||||||
// with free-text feedback (no approval), and the corrective isError carries
|
// with free-text feedback (no approval), and the corrective isError carries
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ The `default` mode is the absence of policy: no section, no extra tool. An agent
|
|||||||
|
|
||||||
## What a mode carries
|
## What a mode carries
|
||||||
|
|
||||||
**The guidance section.** A `system-prompt/assemble` listener renders the mode's `section` text as the `mode:policy` section (order 50) while the mode is in force, and shows the `exit_plan_mode` tool IFF the folded mode is `plan` — on the wire and, under the registry's Code Mode, in the `tools:sdk` section alike. Every transition therefore surfaces as an attributable `request/header` event on the next step (entering plan adds the exit tool, a front-of-list insertion with no delta form → the full fallback snapshot; the approved exit removes exactly that tool and the section, a pure removal → one `request/header-delta`).
|
**The guidance section.** A `system-prompt/assemble` listener renders the mode's `section` text as the `mode:policy` section (order 50) while the mode is in force, and shows the `exit_plan_mode` tool IFF the folded mode is `plan` — on the wire and, under the registry's Code Mode, in the `tools:sdk` section alike. Every transition therefore surfaces as an attributable `request/header` event on the next step (entering plan adds the exit tool at its canonical alphabetical position — a non-tail insertion the append-only tools delta cannot express → the full fallback snapshot; the approved exit removes exactly that tool and the section, a pure removal → one `request/header-delta`).
|
||||||
|
|
||||||
**Deliberately absent: enforcement.** A mode never gates execution, filters the toolset, or touches the sandbox/approval knobs — those are independent axes the user switches separately (a deployment that wants a hard read-only floor while planning flips the sandbox-mode option beside the mode picker, in either order; neither disturbs the other). A per-mode tool allow/deny list is likewise out: which tools a mode admits is an effects question — a per-tool read-only/mutating classification the harness does not yet have — parked until tool definitions declare their effects (the plan-mode RFC's deferred item). The config vocabulary is exactly `{ section }`, and an unknown key (a `tools` list or an `access` cap included) fails loud at load.
|
**Deliberately absent: enforcement.** A mode never gates execution, filters the toolset, or touches the sandbox/approval knobs — those are independent axes the user switches separately (a deployment that wants a hard read-only floor while planning flips the sandbox-mode option beside the mode picker, in either order; neither disturbs the other). A per-mode tool allow/deny list is likewise out: which tools a mode admits is an effects question — a per-tool read-only/mutating classification the harness does not yet have — parked until tool definitions declare their effects (the plan-mode RFC's deferred item). The config vocabulary is exactly `{ section }`, and an unknown key (a `tools` list or an `access` cap included) fails loud at load.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@deepseek-ai/dsh-mode",
|
"name": "@deepseek-ai/dsh-mode",
|
||||||
"description": "Session modes for the DeepSeek Harness: plan mode as logged per-agent policy state with soft (prompt) and hard (execution) enforcement",
|
"description": "Session modes for the DeepSeek Harness: plan mode as a logged per-agent collaboration state with a guidance section and a user-reviewed exit",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ describe('plan mode through the agent loop', () => {
|
|||||||
expect(header.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])
|
expect(header.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])
|
||||||
expect(header.data.header.system).toContain('plan mode')
|
expect(header.data.header.system).toContain('plan mode')
|
||||||
|
|
||||||
// No general tool gate: the write RUNS — plan's non-shell restraint is
|
// No tool gate: the write RUNS — plan restrains by the section's
|
||||||
// the section's guidance (the shell restraint is the sandbox clamp,
|
// guidance alone (enforcement lives on the independent sandbox/approval
|
||||||
// pinned in mode.spec). The mode itself stays plan throughout.
|
// axes). The mode itself stays plan throughout.
|
||||||
const result = findEvent(log, 'tool/result')
|
const result = findEvent(log, 'tool/result')
|
||||||
expect(result.data.isError).toBe(false)
|
expect(result.data.isError).toBe(false)
|
||||||
expect(foldMode(log)).toBe(PLAN_MODE)
|
expect(foldMode(log)).toBe(PLAN_MODE)
|
||||||
@@ -115,8 +115,10 @@ describe('plan mode through the agent loop', () => {
|
|||||||
{ type: 'text', text: 'The user switched this session to plan mode.' },
|
{ type: 'text', text: 'The user switched this session to plan mode.' },
|
||||||
])
|
])
|
||||||
// The header change is logged as a FULL fallback snapshot, not a delta:
|
// The header change is logged as a FULL fallback snapshot, not a delta:
|
||||||
// adding exit_plan_mode reorders the canonical tool list (it sorts
|
// the tools delta can only APPEND additions, while the canonical
|
||||||
// first), and a pure reordering is inexpressible in the delta encoding.
|
// (alphabetical) order places exit_plan_mode before read/write — a
|
||||||
|
// non-tail insertion the writer's round-trip guard rejects, so it
|
||||||
|
// falls back.
|
||||||
const second = findEvent(log, 'request/header', 'last')
|
const second = findEvent(log, 'request/header', 'last')
|
||||||
expect(second.data.reason).toBe('fallback')
|
expect(second.data.reason).toBe('fallback')
|
||||||
expect(second.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])
|
expect(second.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])
|
||||||
|
|||||||
@@ -350,10 +350,10 @@ describe('the soft layer', () => {
|
|||||||
|
|
||||||
it('leaves foreign post-next() additions alone in plan mode (no general tool filtering)', async () => {
|
it('leaves foreign post-next() additions alone in plan mode (no general tool filtering)', async () => {
|
||||||
// A foreign listener that post-processes await next(): the mode filter
|
// A foreign listener that post-processes await next(): the mode filter
|
||||||
// wraps outside it (prepend) but hides only the exit tool outside plan
|
// wraps outside it (prepend) but hides only the exit tool outside plan —
|
||||||
// and the bash trio under an unhonorable cap — a foreign addition
|
// a foreign addition survives, because which tools a mode admits is
|
||||||
// survives, because which tools a mode admits is deliberately not this
|
// deliberately not this plugin's decision (the effects question stays
|
||||||
// plugin's decision (the effects question stays parked; module doc).
|
// parked; module doc).
|
||||||
const ctx = new Context()
|
const ctx = new Context()
|
||||||
await ctx.plugin(SystemPrompt)
|
await ctx.plugin(SystemPrompt)
|
||||||
await ctx.plugin(ToolRegistry)
|
await ctx.plugin(ToolRegistry)
|
||||||
@@ -388,8 +388,8 @@ describe('the soft layer', () => {
|
|||||||
agent.session.append('mode/set', { mode: PLAN_MODE })
|
agent.session.append('mode/set', { mode: PLAN_MODE })
|
||||||
const assembly = await ctx.systemPrompt.assemble({ agent })
|
const assembly = await ctx.systemPrompt.assemble({ agent })
|
||||||
expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code'])
|
expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code'])
|
||||||
// The SDK documents the full binding set plus the exit — plan mode no
|
// The SDK documents the full binding set plus the exit — a mode never
|
||||||
// longer prunes capabilities; its restraint is the section + the sandbox.
|
// prunes capabilities; it restrains by the section's guidance alone.
|
||||||
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
||||||
expect(sdk).toContain('read(args:')
|
expect(sdk).toContain('read(args:')
|
||||||
expect(sdk).toContain('write(args:')
|
expect(sdk).toContain('write(args:')
|
||||||
|
|||||||
Reference in New Issue
Block a user