Skip to content

fix(subagents): carry the delegation policy in the run's prompt - #1389

Open
brynne8 wants to merge 1 commit into
agegr:mainfrom
brynne8:fix/subagent-delegation-policy
Open

brynne8 wants to merge 1 commit into
agegr:mainfrom
brynne8:fix/subagent-delegation-policy

Conversation

@brynne8

@brynne8 brynne8 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

What it does

The delegation policy — when to delegate, and which subagent to pick — now reaches the model in a form it reads.

  • lib/subagent-extension.ts gains subagentDelegationBlock(profiles): a <delegation> block, one rule per line, built from the enabled profiles. An intro line (Agent runs a whole task in a fresh session and returns only the result, so this conversation stays small), a rule for explore (any question about existing code: where a symbol lives, how a flow works, which files touch a feature), a rule for plan (before an edit that spans several files or steps: steps, files, risks, verification), the shared rules, and any other enabled profile listed by name.
  • withSubagentDelegationPrompt(prompt, profiles) appends that block to the prompt text.
  • A before_agent_start handler returns { systemPrompt: withSubagentDelegationPrompt(event.systemPrompt, profiles) }, so the block rides on every run's prompt, and a prompt another extension forced earlier is carried along rather than overwritten (event.systemPrompt is rendered when it is read).
  • The Agent tool's promptSnippet now says to delegate whenever possible and points at the <delegation> section of the system prompt.

The three existing promptGuidelines on the Agent tool are removed. Reason: the block says the same three things, in the place the model actually reads; keeping both would state each of them twice, and the guidelines' version is the one the model tends to skip. Nothing is lost — the three upstream rules are covered one-for-one by the block:

upstream promptGuidelines now in the block
"Use Agent for a focused task that benefits from an isolated context." the intro line ("runs a whole task in a fresh session … so this conversation stays small")
"Use multiple background Agent calls in the same response for independent parallel work." "Use several background Agent calls in one response for independent parallel work; their scopes may overlap, and an isolated run can use a worktree."
"Do not duplicate work already delegated to a running subagent." the same rule, verbatim in substance

One consequence, for the record: the Tool Definitions panel shows a tool's promptGuidelines (it hides the section when there are none), so the Agent entry no longer lists a "Guidelines" section. The policy is in the run's prompt instead.

Deliberately not part of this change: the fork's rewording of the built-in explore / plan profile descriptions in lib/subagents.ts (the block carries what each profile is for, so the descriptions need no change), and the fork's lib/rpc-manager.ts change (upstream already reports the per-run prompt).

Why it matters

On main the policy that says when to delegate never reaches the model in a form it reads.

lib/subagent-extension.ts:183-194 (on main) registers the Agent tool with the policy as three promptGuidelines:

      pi.registerTool(defineTool({
        name: "Agent",
        label: "Agent",
        description: `Delegate a focused task to a configured subagent. Each subagent runs as a full, inspectable Pi session. Use background mode for independent work and foreground mode when the result is needed immediately.\n\nAvailable agent types:\n${agentTypeDescription(profiles)}`,
        promptSnippet: "Delegate a focused task to an inspectable subagent session",
        exposure: SUBAGENT_TOOL_EXPOSURE,
        promptGuidelines: [
          "Use Agent for a focused task that benefits from an isolated context.",
          "Use multiple background Agent calls in the same response for independent parallel work.",
          "Do not duplicate work already delegated to a running subagent.",
        ],

pi flattens a tool's promptGuidelines into the one shared <rules> list (@earendil-works/pi-coding-agent/dist/core/system-prompt.js:31-64, buildRules()), so the model receives this:

<rules>
- Use bash for file operations like ls, rg, find
- Use read to examine files instead of cat or sed.
...
- Use Agent for a focused task that benefits from an isolated context.
- Use multiple background Agent calls in the same response for independent parallel work.
- Do not duplicate work already delegated to a running subagent.
- Be concise in your responses
- Show file paths clearly when working with files
</rules>

A subagent policy reads there as generic advice next to "be concise". Nothing tells the model that explore is for questions about existing code, that plan is for a multi-file change, or that delegating is cheaper than doing the work inline: that exists only in the tool's description string and in the profile list. So with subagents enabled the model has no policy for delegation, and picks a profile — or does the work itself — without a reason.

How to reproduce (on main, before the fix)

With the built-in subagents enabled, start a session and look at the system prompt the provider receives (the integration test below does exactly this against a real AgentSession):

node --experimental-strip-types --test lib/subagent-extension.integration.test.mjs

On main the prompt's <rules> list holds the three delegation bullets among pi's own rules, and there is no <delegation> block anywhere in it. After the fix, the prompt ends with:

<delegation>
- `Agent` runs a whole task in a fresh session and returns only the result, so this conversation stays small. Delegate as soon as the goal is clear; a goal and known paths are enough.
- `explore` for any question about existing code: where a symbol lives, how a flow works, which files touch a feature. It only reads and reports paths, so ask it first instead of reading the neighbourhood yourself.
- `plan` before an edit that spans several files or steps: it reads the repository and returns steps, files, risks and verification, without changing anything.
- Use several background `Agent` calls in one response for independent parallel work; their scopes may overlap, and an isolated run can use a worktree.
- Do not duplicate work already delegated to a running subagent.
</delegation>

Testing

The new tests fail on main (the base commit e533d9b3) and pass with the fix.

On the base commit, with only the new tests added:

$ node --experimental-strip-types --test lib/subagent-extension.test.mjs lib/subagent-extension.integration.test.mjs
✖ the prompt the model receives carries the delegation block, not the flat rules list (8.957084ms)
  AssertionError [ERR_ASSERTION]: The input did not match the regular expression /<delegation>\n- `Agent` runs a whole task in a fresh session/. Input:
  '...<rules>\n- Use Agent for a focused task that benefits from an isolated context.\n- Use multiple background Agent calls in the same response for independent parallel work.\n- Do not duplicate work already delegated to a running subagent.\n- Be concise in your responses\n...</rules>...'
✖ a prompt another extension forced earlier is kept, and the block still rides on it (10.698042ms)
✖ every run's prompt carries the delegation block, and the tool's rules do not (0.704375ms)
  AssertionError [ERR_ASSERTION]: the policy is no longer a rule beside "be concise"
  + actual
  + [
  +   'Use Agent for a focused task that benefits from an isolated context.',
  +   'Use multiple background Agent calls in the same response for independent parallel work.',
  +   'Do not duplicate work already delegated to a running subagent.'
  + ]
  - undefined
✖ the delegation block names the enabled profiles and leaves the disabled ones out (0.19075ms)
  TypeError: handlers.get(...) is not a function
ℹ tests 30
ℹ pass 26
ℹ fail 4

With the fix (Node 26.7.0):

$ npm test
ℹ tests 2874
ℹ suites 16
ℹ pass 2873
ℹ fail 0
ℹ cancelled 0
ℹ skipped 1
ℹ todo 0
ℹ duration_ms 19181.084166

$ node_modules/.bin/tsc --noEmit
(no output, exit 0)

$ npm run lint
> eslint .
(no output, exit 0)

New tests:

  • lib/subagent-extension.test.mjs: "every run's prompt carries the delegation block, and the tool's rules do not" and "the delegation block names the enabled profiles and leaves the disabled ones out".
  • lib/subagent-extension.integration.test.mjs: "the prompt the model receives carries the delegation block, not the flat rules list" and "a prompt another extension forced earlier is kept, and the block still rides on it" — both against a real AgentSession, reading the system message the provider receives.

Provenance

Ported from the Bun-running fork https://github.com/brynne8/pi-web-bun, commit 5f2ef78e ("fix(agent): carry the delegation policy in the prompt, and steer the model to it"). Ported by hand onto upstream's current lib/subagent-extension.ts (the fork's file has been rewritten since), with upstream's structure, naming and comment style kept. The fork's lib/rpc-manager.ts change and its rewording of the built-in profile descriptions are not part of this PR.

A subagent policy only works where the model reads it. Upstream ships the
delegation advice as the `Agent` tool's `promptGuidelines`, and pi flattens
those into the one flat `<rules>` list that every tool and pi's own "be
concise" share, so the policy arrives as generic advice next to advice about
file paths. Nothing told the model that `explore` is for questions about
existing code, that `plan` is for a multi-file change, or that delegating is
cheaper than doing the work inline: that lived only in the tool's description
and in the profile list.

- The policy now ends every run's prompt as a `<delegation>` block, built from
  the enabled profiles: `explore` and `plan` get a rule of their own, the
  other enabled subagents are listed by name.
- The block rides on the text of the prompt, through a `before_agent_start`
  handler that returns `systemPrompt` (the shape `createExactSystemPromptExtension()`
  already uses), so a prompt another extension forced earlier is carried along
  rather than overwritten.
- The `Agent` tool's prompt snippet says to delegate whenever possible and
  points at the delegation section.
- The three `promptGuidelines` are gone: the block says the same things in the
  place the model reads, and keeping both would say each of them twice.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant