Skip to content

cordis-plugin-instruction-scope

Verified

@argszero/cordis-plugin-instruction-scope Β· v0.1.0 Β· MIT

Per-agent scoping for the dsh workspace instruction chain (discussion #7361). @deepseek-ai/dsh-agent-instructions injects the workspace instruction chain into every agent in the tree; its Config governs which files are discovered, globally, and nothing ex

Install

dsh plugin add @argszero/cordis-plugin-instruction-scope

Confirm the layer applied with dsh --profile default --dump-config β€” see the install guide.

Source

Tags

Readme

@argszero/cordis-plugin-instruction-scope

Per-agent scoping for the workspace instruction chain β€” the missing axis in discussion #7361.

@deepseek-ai/dsh-agent-instructions injects the workspace instruction chain into every agent in the tree. Its Config governs which files are discovered, globally; nothing in the harness expresses which agents receive them. For a delegated child whose task needs zero project context β€” a bounded review, a source check, a numeric recheck, a verdict β€” that is more than overhead:

  • the chain arrives as a user-role message, i.e. with the same authority as the dispatcher's own prompt;
  • it carries the user-global file ($DSH_HOME/AGENTS.md) β€” personal workflow conventions, machine-local paths, credential-handling notes β€” that has nothing to do with the child's task;
  • it is appended to the child's durable log, so every later request in that child's session re-derives it, and a child that follows a startup protocol written for the main agent spends its budget loading files the dispatcher explicitly excluded.

This plugin supplies the missing axis without changing the harness: it decides, per agent, whether that agent receives the chain at all.

Install

npm install @argszero/cordis-plugin-instruction-scope

Mount it through a bundle patch (cordis.patch.yml ships with the package) or as a profile layer:

- insert:
    - id: instruction-scope
      name: '@argszero/cordis-plugin-instruction-scope'
      config:
        rules:
          - match:
              origin: subagent
              delegationDepth: { gte: 1 }
            policy: omit
            reason: bounded child, no project context needed

With no config the plugin changes nothing: defaultPolicy is keep.

Measure first

report is a real dry run β€” nothing moves, and the plugin still records exactly what it would have omitted, including the rendered character count of the chain:

config:
  defaultPolicy: report

Each verdict is logged once per session (log: first, the default) and retained on the service API. It reads like this:

instruction-scope: agent=child-1 policy=report rule=default chars=16089 removed=0

That number is the measurement #7361 lists under "What I did not verify".

Selecting an agent

The report's central point is that the decision belongs to the dispatcher, not to the child β€” "the child does not know the lane it was dispatched into". A plugin cannot add a field to the subagent tool, so this one reads the lane from the surfaces that already carry it, all synchronously:

fact source
origin, delegationDepth, cwd, parentSession, isSeeded, agentPreset the child's durable session header β€” a frozen metadata read, not an event-log scan
label the child's creation label, from the subagent session projection when it is registered

label is the most dispatcher-controlled key available: the dispatcher literally names the child. It is read through the projection registry, which is the sanctioned post-resume seam; the repository prohibits new synchronous scans of arbitrary event history. When dsh-subagent is not mounted the key is absent and the plugin degrades to the header facts.

String fields accept * and ? globs. rules is ordered and first match wins, so a narrow keep can carve a lane back out of a broad omit. A rule with no match accepts every agent, which makes an ordered list end in a catch-all.

config:
  defaultPolicy: keep
  rules:
    # Only the children dispatched for it.
    - match: { label: 'bounded-*' }
      policy: omit
      reason: named for it
    # Everything else delegated, of any depth.
    - match: { origin: subagent, delegationDepth: { gte: 1 } }
      policy: omit
    # Except children working from a scratch tree, which keep their chain.
    - match: { cwdPrefix: /tmp/scratch }
      policy: keep

Policies

policy effect
keep (default) the agent receives the chain exactly as it would without this plugin
report nothing is touched; the verdict and the measured size are recorded
omit the agent receives no workspace instruction chain at all

How omission works

One listener: agent/pre-step, registered with { prepend: true }.

prepend is load-bearing. Waterfall entries run outermost-first and every listener's next() continuation runs after all entries, so this plugin's await next() resolves with the decision agent-instructions has already composed its message into. That is the only position from which the injected message is both visible and still removable.

The decision is not a display concern: the agent loop's Agent.step() appends exactly decision.messages to the durable log as user/message events, and the next request is derived from that log. Removing a message from the decision removes it from the child's context and from the child's durable transcript.

The loop explicitly supports the resulting shape β€” its turn loop documents that "an enter decision rewritten to empty still owns the initial turn boundary, but it spends no model call". Rewriting the batch is a designed seam, not a hack against the loop.

A second action keeps the omission stable. agent-instructions stages its composed message into the pending inbox before it enters the decision, and it decides "already supplied" by scanning the claimed batch and the durable log for a message whose source is agent-instructions with baseline: true. This plugin therefore also removes a staged instruction message from inbox.nextStep / inbox.nextTurn, so no later claim can pick it up.

API

The plugin provides instructionScope:

ctx.instructionScope.verdicts()   // ScopeVerdict[], bounded by maxRecords
ctx.instructionScope.counts()     // { keep, report, omit }
ctx.instructionScope.reset()
ctx.instructionScope.decide(agent)  // resolve a verdict without recording or applying

Honest boundaries

  • This is omission, not narrowing. A matched agent receives the whole chain or none of it. Dropping only the user-global file while keeping the project chain is not implemented: it would mean rewriting the text of a message whose source contract (baseline, baselineIdentity, changes) belongs to agent-instructions, and that package reconciles later file edits for every scope it rendered β€” a rewrite that removes a scope from the text would still let an edit to that file surface through the reconciliation channel. Narrowing is a core change, not a wrapper.
  • Omission costs a re-composition per step. agent-instructions treats a visible durable baseline as proof the chain was already supplied. With nothing durable to find it re-discovers and re-reads the instruction files on every pre-step, and this plugin discards each result. That repeated I/O is the price of never writing the chain to the child's log β€” which is the point of the policy, and exactly what makes the omission airtight.
  • It cannot name the dispatcher. Rules match on lane facts, so a rule can apply to every subagent or to a named class of child, but a dispatcher that wants a one-off opt-out still has to pick a label or cwd the rule can see.
  • It fails open. Any error while resolving a verdict leaves the decision untouched. An instruction-scope guard must never be the reason a turn dies.

Tests

npm test

Behaviour tests run against real Cordis, the real agent loop, real production agents with their real durable inbox, the real dsh-agent-instructions plugin, the real filesystem, and the loop's own agent/pre-step waterfall. The control arm is the same fixture with the plugin absent, and it proves the reported behaviour is real before the guarded arms prove it is gone. test/packaging.spec.mjs asserts the packaging invariants that only installing the packed artifact can witness.

Licence

MIT