Skip to content

fix(coding-agent): move the dynamic env block to the end of the Step system prompt - #169

Closed
jasonkneen wants to merge 1 commit into
stepfun-ai:mainfrom
jasonkneen:fix/harness-stable-prompt-prefix
Closed

jasonkneen wants to merge 1 commit into
stepfun-ai:mainfrom
jasonkneen:fix/harness-stable-prompt-prefix

Conversation

@jasonkneen

Copy link
Copy Markdown

Problem

packages/coding-agent/src/step/system-prompt.ts built the Step appendix's
<env> block (working directory, platform, today's date, git branch,
"Uncommitted changes: N", operating mode — buildEnvironmentSection,
~L95-115) and placed it as the 4th entry of the sections array (~L305),
right after the three opening contract lines and ahead of nearly all the
static contract text (Priorities, Communication, Security, Destructive
actions, Workflow, Code conventions, Git, Tool usage, plus all the
conditional sections).

The appendix is rebuilt on every tool-set change
(core/agent-session.ts _rebuildSystemPrompt, called from
setActiveToolsByName/_refreshToolRegistry at ~L1083 and on extension
resource changes at ~L2620), and each rebuild re-reads git status (cached
with a 5s TTL, but still time- and edit-state-dependent). Because the env
block sat near the front, any rebuild — including ones triggered mid-session
by newly discovered deferred tools — changed content that appears very early
in the composed system prompt, which busts the provider's prompt cache for
the entire prompt on every rebuild, and guarantees the prompt differs
across sessions/days.

Fix

Moved the buildEnvironmentSection(...) call from position 4 in the
sections array to the very last entry, pushed after the tool-selection
rules block, immediately before sections.join("\n\n"). No wording was
changed — the env block's own trailing sentences ("The working directory
above is also the default base...", "(see Current working directory
below)") still read correctly because the env block is now directly before
core/system-prompt.ts's trailing Current working directory: ... line,
which was already the very last line of the fully composed prompt in both
the customPrompt and default branches of buildSystemPrompt. No changes
were needed in core/system-prompt.ts — it already appends Current working directory at the end of the assembled prompt in both branches, so
it does not put date/cwd early.

Everything else in the appendix — all static contract sections, tool rules,
and conditional sections — is now byte-identical across rebuilds and
sessions for the same tool set, and only the trailing env block (plus core's
final cwd line) varies.

Test

Added to packages/coding-agent/test/step-system-prompt.test.ts:

  • "places the dynamic env block last so the static prefix is stable across
    date/cwd/git state": builds the appendix twice with the same tool set but
    differing cwd/platform/date, asserts the env block's trailing
    sentence is the very last thing in the prompt, and asserts the text
    before the first <env> tag (the static prefix) is identical between the
    two builds.
  • "pins a hash of the static prefix for a fixed tool set": computes a
    sha256 of the static prefix for a fixed tool set and asserts it against a
    pinned value, with a comment that intentional prompt-wording changes must
    recompute and update the hash.

Verified both regression tests fail against the pre-fix code (env block was
mid-prompt, so the "trailing sentence" assertion failed and the pinned hash
did not match) and pass after the fix. Ran the pre-fix source via a
byte-for-byte revert of only system-prompt.ts (test file kept as the
fixed version) to confirm:

places the dynamic env block last ... : FAIL (expected false to be true)
pins a hash of the static prefix ...   : FAIL (508b146... !== 68f93aa9...)

Full suite after the fix: 24/24 pass in step-system-prompt.test.ts.

No existing test pinned the old env-block position (all existing assertions
use toContain, not index/order checks), so no other tests needed updating.

Risk / behaviour change

Purely a section-ordering change inside buildStepSystemPromptAppendix; the
env block's content and wording are unchanged, so model-visible semantics
are unchanged (same facts, same sentences). The only observable difference
is where <env>...</env> appears in the prompt text — now at the very end,
immediately before the harness's own trailing Current working directory:
line, instead of near the top. This is the intended fix: it makes the
prompt's static prefix cacheable by providers that support prompt caching,
which should reduce cost/latency for sessions with tool-set churn (e.g.
deferred MCP tool discovery) without changing any instruction the model
receives.

https://claude.ai/code/session_01GUdnnHEaDThHUATSwXBpV9

Invariant: the cacheable prompt prefix must be byte-identical across
turns and sessions, with dynamic content appended after it.

Cause: buildStepSystemPromptAppendix placed the <env> block (cwd,
platform, date, git branch, uncommitted-change count, operating mode)
as the 4th section, ahead of nearly all static contract text. The
appendix rebuilds on every tool-set change (agent-session.ts
_rebuildSystemPrompt via setActiveToolsByName/_refreshToolRegistry and
on extension resource changes), so after any tool discovery the env
block's near-front position busted the provider prompt cache for the
whole system prompt, and made the prompt differ across sessions/days.

Fix: push buildEnvironmentSection(...) as the last entry of the
sections array instead of the 4th, so all static sections precede it
unconditionally. core/system-prompt.ts already appends its own
"Current working directory" line at the very end of the composed
prompt in both branches, so no change was needed there. Wording is
unchanged; only ordering moved.

Tests: added to step-system-prompt.test.ts — one asserting the env
block (and its trailing sentences) is the last thing in the prompt and
that the static prefix is identical across differing cwd/platform/date
contexts, and one pinning a sha256 hash of the static prefix for a
fixed tool set (comment notes intentional prompt edits must update the
hash). Verified both fail against the pre-fix ordering and pass after.
Copilot AI lite review requested due to automatic review settings September 23, 2026 11:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The scoped change has regression coverage, and the remaining comment is a non-blocking documentation nit.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Moves the dynamic environment block to the end of the Step system prompt appendix to improve prompt-cache stability.

Changes:

  • Relocates environment-section generation after static and tool-specific sections.
  • Adds regression tests for ordering and static-prefix hashing.
File Summary Review note
packages/​coding-agent/​test/​step-system-prompt.test.ts Verifies environment ordering and prefix stability.
packages/​coding-agent/​src/​step/​system-prompt.ts Moves environment prompt generation to the appendix tail. Nit (2 votes): Qualify the cache-stability comment for fixed tool sets and other dynamic inputs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +403 to +405
// every static section above, means the static prefix stays byte-identical
// across rebuilds and sessions so provider prompt caches on that prefix
// survive; only this trailing block busts on rebuild.
@ZouR-Ma ZouR-Ma closed this Sep 23, 2026
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.

3 participants