Skip to content

feat: let agents declare a plan_persona instruction#3

Draft
trungutt wants to merge 1 commit into
plan-modefrom
plan-persona
Draft

feat: let agents declare a plan_persona instruction#3
trungutt wants to merge 1 commit into
plan-modefrom
plan-persona

Conversation

@trungutt

Copy link
Copy Markdown
Owner

Why

Plan mode (docker#3140) is the host-facing primitive: a session flag a host (CLI, TUI, embedder) flips to put the agent into a research-and-draft posture instead of an act-now one. Two layers enforce it: the runtime strips every tool without Annotations.ReadOnlyHint, and a per-turn <system-reminder> tells the model to plan rather than act.

That works as a host primitive — but it leaves a real prompt conflict when the agent's canonical instruction is heavily tuned for execution. Many real-world coding-agent YAMLs read like the example below:

instruction: |
  You are an executor. Fix files immediately. Never ask clarifying
  questions. Tool call discipline before FIRST response. …

Layering a "no edits, no shell, no state changes, ask the user" reminder on top of that gives the model two contradictory specs in the same context. Even with the mutating tools hidden, the action-oriented framing leaks into behaviour: the model still pushes to a one-turn conclusion, refuses to iterate, and drops the plan in execution-completion shape. The PR title and the <system-reminder> envelope alone aren't enough — the upstream instruction needs to change too.

This PR adds the missing knob without disturbing the host-level primitive: agents can declare a plan_persona whose instruction the runtime swaps in for the plan-mode reminder.

What

flowchart LR
    A[session.Mode = plan] --> B{agent.PlanInstruction set?}
    B -- "no, today's behaviour" --> C["canned plan-mode reminder<br/>(no edits / no shell / etc.)"]
    B -- "yes (new)" --> D["&lt;system-reminder&gt;<br/>guardrail line +<br/>persona instruction"]
    C --> E[GetMessages extras]
    D --> E
Loading
  • latest.AgentConfig gains an optional PlanPersona *PlanPersonaConfig. The only field today is instruction. The block is intentionally minimal so follow-ups can add model, toolsets, etc. additively without locking the shape.
  • agent.Agent carries a planInstruction string and exposes PlanInstruction() / WithPlanInstruction(...). The canonical Instruction() is untouched — the persona is a per-mode override, not a replacement.
  • runtime/plan_mode.go's planModeReminderMessages takes the active agent. When the agent has declared a persona, the runtime wraps it in the existing <system-reminder> envelope and prefixes a short guardrail line stating that only read-only tools are available, so persona authors own the workflow framing and tone while the read-only contract stays runtime-owned and impossible for the persona author to drop. When no persona is declared, the canned reminder is used unchanged — preserving today's behaviour for every agent that hasn't opted in.
  • teamloader expands ${env.X} placeholders in the persona instruction the same way it does for Instruction.
  • agent-schema.json adds plan_persona to the agent properties and a PlanPersonaConfig definition.

Example:

agents:
  root:
    instruction: |
      You are an executor. Act now. Never ask clarifying questions.
    plan_persona:
      instruction: |
        You plan. You do not execute. Ask clarifying questions and
        iterate with the user. Produce a numbered, file-specific plan.

In build mode the agent runs the executor instruction. In plan mode the runtime sees PlanInstruction() is non-empty, emits the persona-wrapped reminder, and the executor instruction is no longer the most recent system context — the persona is.

Scope

In: schema field, runtime swap, teamloader wiring, agent-schema.json entry, unit tests at every layer (agent opt round-trip, runtime reminder selection incl. nil-agent / whitespace-only persona / canned-fallback, end-to-end teamloader YAML→Agent.PlanInstruction()).

Out (left to follow-ups):

  • plan_persona.model for per-mode model overrides.
  • plan_persona.toolsets for per-mode toolset overrides.
  • A plan-file artifact + plan_exit tool — orthogonal, on top of this.

Dependencies

Stacked on docker#3140 (session-scoped plan/build mode). The new branch is based on that PR's commit; once docker#3140 lands this can be rebased on main and reviewed as a single commit.

In plan mode (added in PR docker#3140), the runtime filters the agent's
toolset to read-only tools and injects a per-turn system reminder.
For agents whose canonical instruction is heavily tuned for execution
("act now", "never ask clarifying questions", "fix files
immediately"), layering a 'plan, don't act' reminder on top leaves
two contradictory specs in the same context: the model can still
push to an action-oriented conclusion even with the mutating tools
hidden.

Add an optional plan_persona block on AgentConfig that lets the
agent author replace the per-turn plan-mode system reminder with a
persona instruction tailored for planning. The runtime wraps the
persona in the existing <system-reminder> envelope and prefixes a
short guardrail line stating that only read-only tools are
available, so persona authors own the workflow framing and tone
while the read-only contract stays runtime-owned.

Agents that don't declare plan_persona fall back to the canned
reminder, preserving today's behaviour.

  agents:
    root:
      instruction: |
        You are an executor. Act now.
      plan_persona:
        instruction: |
          You plan. You do not execute. Iterate with the user.
@trungutt trungutt marked this pull request as ready for review June 19, 2026 08:28
@trungutt trungutt marked this pull request as draft June 22, 2026 08:53
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