Skip to content

exp: sticky agent mode — mode locking + permission-based skill scoping#1719

Closed
yanyihan-xiaomi wants to merge 10 commits into
mainfrom
exp/sticky-agent-mode
Closed

exp: sticky agent mode — mode locking + permission-based skill scoping#1719
yanyihan-xiaomi wants to merge 10 commits into
mainfrom
exp/sticky-agent-mode

Conversation

@yanyihan-xiaomi

@yanyihan-xiaomi yanyihan-xiaomi commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Overview

Experimental branch exploring "mode is immutable within a session" interaction model. Will not be merged to main — for trying out and discussion.

Key Changes

1. Sticky Mode

After the first message in a session, the mode is locked:

  • Build / Plan form a group — can freely switch between them (Tab cycles within group)
  • Compose and other modes are isolated — cannot switch once entered
  • /new creates a fresh session and restores free selection

Contextual toasts:

  • Build/Plan user tries to switch to compose → "In this session, you can only switch between build, plan"
  • Compose user presses Tab → "Cannot switch mode mid-session after entering compose mode"

2. Permission Replaces Hidden

  • Removed hidden: true from all compose skills
  • Configured via agent permission: defaults deny compose:*, compose agent explicitly allows
  • Skill.available() no longer filters by hidden — relies entirely on permission system
  • Compose skills naturally appear in compose agent's system prompt skill listing

3. Plan Tools Scoped to Build/Plan

  • Defaults deny plan_enter/plan_exit, build and plan explicitly allow both (symmetric)
  • No registry.ts changes needed — llm.ts:resolveTools() already uses Permission.disabled() to strip denied tools
  • Compose and future agents get a clean tool list automatically
  • Differs from pre-feat(tool): make plan_enter/plan_exit always visible across modes #1207 (asymmetric per-agent, caused cache issues) — here both build/plan see both tools

4. Removed composeSkillsBlock() Injection

  • No more hardcoded <available_skills> XML block injection
  • compose.txt cleaned: removed {{compose_skills}} placeholder
  • Subagent guidance updated to "distill instructions" model

Design Tradeoffs

Decision Rationale
set() vs userSwitch() separation Only guard user actions, don't block system behavior (session restore, plan tools)
Reactive !!lastUserMessage() No manual lock/unlock — /new and /session work correctly by nature
move() skips blocked agents Tab cycles within group, doesn't stop at first blocked entry
Self-switch always allowed current === target → no-op, no false toast
Permission defaults deny + specific allow Future agents auto-inherit deny, no per-agent config needed
Plan tools: symmetric in build/plan No tool list mutation on build↔plan switch, stable prefix cache

⚠️ Breaking Changes

  1. Custom modes affected: User-defined agents not in FREE_SWITCH_GROUP are treated as isolated (sticky). Would need group configuration support.
  2. hidden: true no longer effective: Skills relying on hidden must migrate to permission config
  3. Subagents can no longer receive <available_skills> blocks: Must distill instructions directly into prompts
  4. Plan tools hidden from compose: Compose agent no longer sees plan_enter/plan_exit (denied via permission)

Try It

git checkout exp/sticky-agent-mode
bun install
bun dev

Test:

  • Build mode → send message → Tab only switches to Plan
  • Build mode → /agents → try compose → toast: "only switch between build, plan"
  • Compose mode → send message → Tab shows toast
  • /new → free switching restored
  • /session into history → auto-locked

Stability Note

Theoretically more stable due to constrained behavior — the model's system prompt (skills + tools) is fixed for the entire session (no prefix cache invalidation from mode switches). However, this is a breaking change that affects custom mode workflows and is not planned to be merged.

Tests

  • bun test test/agent/agent.test.ts — 48/48 pass (includes new plan tool permission tests)
  • bun test test/session/prompt-skill-mention.test.ts — 8/8 pass
  • bun test test/permission — 141/141 pass

Related Files

  • Report: docs/compose/reports/sticky-agent-mode.md
  • TUI core: packages/opencode/src/cli/cmd/tui/context/local.tsx
  • Permission: packages/opencode/src/agent/agent.ts
  • Compose cleanup: packages/opencode/src/session/prompt.ts, compose.txt

@yanyihan-xiaomi yanyihan-xiaomi changed the title exp: 实验性 sticky agent mode — 模式锁定 + permission 替代 hidden exp: sticky agent mode — mode locking + permission-based skill scoping Jul 14, 2026
@yanyihan-xiaomi yanyihan-xiaomi force-pushed the exp/sticky-agent-mode branch 2 times, most recently from 4012f85 to 177b840 Compare July 14, 2026 11:44
…coping

- TUI: lock agent after first message, Build/Plan can switch freely
- Show toast when switch blocked, lock on session restore
- Remove hidden:true from compose skills, use permission deny/allow
- defaults deny compose:*, compose agent allows compose:*
- Remove hidden filter from Skill.available()
- Remove composeSkillsBlock() call from prompt.ts (skills now in normal listing)
- Clean compose.txt: remove {{compose_skills}} placeholder
- Update subagent guidance: distill instructions, don't pass skill lists
lock() was broken with /new because the boolean persisted.
Now uses session content as the source of truth — /new creates
an empty session, naturally allowing mode switching again.
Tab now finds the next valid agent in the group instead of
stopping at the first blocked one. Toast only shows when no
valid agent exists to switch to (e.g., compose is the only option).
Simple reactive effect: sessionHasMessages = !!lastUserMessage().
No manual lock/unlock needed — works correctly with /new, /session,
and message submission automatically.
set() is unguarded (for system use: session restore, plan tools, CLI).
userSwitch() has the sticky guard (for user actions: dialog, voice).
move() already has its own guard internally.
- plan_enter/plan_exit: deny in compose agent permission (agent.ts)
- registry.ts reads permission to filter, no hardcoded agent names
- switchBlockedToast: shows subset message (build/plan) or locked message (compose)
- i18n: add tui.agent.locked.subset in all locales
@yanyihan-xiaomi

Copy link
Copy Markdown
Collaborator Author

Superseded by #1725 — same implementation, clean commit history, ready for discussion.

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