Skip to content

feat(core): add agent instructions and use them for Plan mode - #63669

Open
rekram1-node wants to merge 3 commits into
devfrom
agent-instructions-field
Open

rekram1-node wants to merge 3 commits into
devfrom
agent-instructions-field

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Issue for this PR

Closes #62628. That loop came from Plan re-adding its reminder as a synthetic after each compaction, which woke the session again. This PR removes that code path, and after compaction the rules sit in the system prompt with no new message. The review bot ran the issue's mock-model repro with a 3,000-token context limit: on dev it never exits (1,873 requests in 40s), and with this PR it exits after 3 requests (title, one compaction, the answer).

Related: #62554 (this removes the one-off Plan reminder it lists; the other one-off prompts are unchanged).

The revert case below relies on #63667, which is merged.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Agents get an instructions field: text that's added to the default prompt while the agent is selected, without replacing the prompt like system does. It's either a string or { active, inactive? }:

item.instructions = "You are in Plan mode..."  // generic note on leaving
item.instructions = { active: "You are in Plan mode...", inactive: "You are NO LONGER in Plan mode..." }

active is a state: it's shown whenever the agent is selected, including in the system prompt after compaction. inactive is a transition: it's sent once when the session switches away and never again, so after compaction on another agent neither text appears. Plan mode now uses it in place of its own reminder machinery.

Today a plugin can't add rules for one agent without trouble. Plan injects "you are in Plan mode" reminders as synthetic messages and repairs them on every request after compaction or a revert (#42652). The internal orchestrator agent adds its rules to the system prompt in a context hook, so every agent switch rewrites the start of the prompt and loses the cache, and the rules aren't stored with the session.

Core already has the right mechanism: instruction sources, which are stored per session, shown once in the system prompt, and sent as an update at the next step when they change. This adds one source, core/agent, built from the selected agent:

agent has instructions → value { id, name, text }
agent has none         → removed

first step             → text in the system prompt
switch in              → text as an update before the next prompt
switch away            → the agent's inactive text, or "The Plan agent
                          is no longer selected. Its instructions from
                          earlier in this conversation no longer apply."
switch between two     → one update: inactive text + new agent's text
plugin edits the text  → "...instructions changed. These replace..."

Compaction, reverts, forks and storage all come from the existing instruction state. The source is last in the list because it changes most often.

plan.ts now sets instructions and drops the event subscription, the per-request repair hook and the reminder bookkeeping (about 60 lines). Its permissions and the error rewrite for blocked edits stay.

What changes for Plan, beyond the plumbing:

  • Both texts are the same as before, without the <system-reminder> wrapper, since they're now system content.
  • The rules arrive as a system message (or in the system prompt), not as a user-role synthetic. In the timeline that shows as the standard "Instructions updated: core/agent" row.
  • Switching away and back before the next step now sends nothing. Plan used to send both reminders.
  • Sessions that are already in Plan when this ships still have the old reminder in history and get the rules once more as an update. That happens once per session.

The field is plugin-only for now. Agent config files have their own schema, and config already uses instructions for a list of file paths, so exposing this there needs a naming decision first.

How did you verify your code works?

New runner scenarios under agent instructions in session-runner.test.ts use real agents and switchAgent: starting in an agent, switching in, switching to an agent without instructions, switching away and back, switching between two agents, a plugin editing the text, a custom inactive text (including that it isn't shown after compaction), editing only the inactive text (sends nothing), compaction, reverting past the switch in, and reverting between switching in and out. All pass, and the system prompt stays byte-identical across switches.

  • bun run check from the root.
  • Ran every core test file that touches history, compaction, forks, reverts, instructions, agents or Plan (179 files): 3,141 pass. The 33 failures are in shell, integration, provider and image test files and fail the same way on unchanged dev on my machine.
  • Regenerated the client (AgentInfo.instructions).

Screenshots / recordings

Not a UI change; the timeline row is the existing instruction notice.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Fixes #62628

Related: #62554

Public API: adds optional instructions (string or { active, inactive? }) to Agent.Info (schema, plugin agent editor, client AgentInfo, OpenAPI); backwards compatible

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works as described. In the TUI with the simulated model, switching build → Plan → build now sends the Plan rules once before the next prompt and the generic "no longer selected" note once after leaving. The system prompt stays byte-identical across switches. On the base, the old <system-reminder> user messages are sent instead. plan.test.ts and session-runner.test.ts pass (267/267), and tsc -b passes for core and its test config.

Public API: adds an optional instructions?: string to Agent.Info. This is in the schema, the plugin agent editor, the generated client AgentInfo and the HTTP agent responses. It is backwards compatible: existing plugins and clients are unaffected, and agent config files don't accept it yet. The Plan reminders change from user-role <system-reminder> synthetics to instruction updates; anything matching the old reminder text will no longer find it.

One thing to fix before merging: packages/protocol/openapi.json wasn't regenerated, so bun run check:generated fails (see inline).

I couldn't run the mock-model repro from #62628 here, because the source CLI's run doesn't start in this sandbox. The context hook that caused that loop is removed by this change, though.

Fixes #62628
Related: #62554

Before

After

Comment thread packages/schema/src/agent.ts Outdated
request: Provider.Request,
system: Schema.String.pipe(optional),
/** Added to the default prompt while the agent is selected, without replacing it like `system`. */
instructions: Schema.String.pipe(optional),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field changes Agent.Info in the HTTP API, but packages/protocol/openapi.json wasn't regenerated: bun run check:generated in packages/protocol fails with "Generated OpenAPI document is stale" (it passes on dev). Running bun run generate there adds the 3-line instructions property.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit regenerates openapi.json, so the stale-spec point is fixed: check:generated now passes.

I also ran the #62628 mock-model repro from source. On the base, opencode run --agent plan with a 3,000-token context limit never exits (1,873 requests in 40s, alternating compaction and answer). With this PR it exits after 3 requests: title, one compaction, the answer.

Public API impact is unchanged since my last review: it adds an optional instructions field to Agent.Info (schema, plugin agent editor, client AgentInfo, OpenAPI spec), and that is backwards compatible. No new concerns.

@rekram1-node
rekram1-node force-pushed the agent-instructions-field branch from 822403d to 52681c3 Compare November 13, 2026 06:08

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new push is a rebase onto dev now that #63667 is merged; the PR's own commits are unchanged. The openapi.json point was already fixed by the regenerated spec, and check:generated passes.

On the rebased head I ran the #62628 mock-model repro again: the base still loops between compaction and answer, and this PR exits after 3 requests. Plan and runner tests pass (267/267), and the core typecheck passes.

Public API impact is unchanged: it adds an optional instructions field to Agent.Info (schema, plugin agent editor, client AgentInfo, OpenAPI spec), and that is backwards compatible. No new concerns.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit works as described. In a TUI run with the simulated model (build → Plan → build), Plan's rules are sent once before the next prompt and its own "You are NO LONGER in Plan mode…" text once after switching away. The #62628 repro still exits after one compaction. check:generated, the Plan and runner tests (269/269) and the core typecheck all pass.

Public API: Agent.Info.instructions (new in this PR) changes from string to string | { active: string; inactive?: string } in the schema, the plugin agent editor, the client AgentInfo and the OpenAPI spec. That is backwards compatible, since the field hasn't shipped yet. Anything reading the field now has to handle both shapes.

No new concerns.

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.

Plan agent loops forever after auto-compaction (opencode run never exits)

1 participant