Skip to content

feat: sticky agent mode — mode locking + permission-based skill scoping#1725

Merged
yanyihan-xiaomi merged 4 commits into
mainfrom
feat/sticky-agent-mode
Jul 14, 2026
Merged

feat: sticky agent mode — mode locking + permission-based skill scoping#1725
yanyihan-xiaomi merged 4 commits into
mainfrom
feat/sticky-agent-mode

Conversation

@yanyihan-xiaomi

Copy link
Copy Markdown
Collaborator

Summary

Lock agent mode after the first message in a session, ensuring a stable system prompt (skills + tools) throughout the conversation. Supporting change: replace hidden: true with a permission-based scoping system for compose skills and plan tools.

Motivation

Mode-switching mid-session causes prefix cache invalidation (the system prompt changes when skills/tools change). This is both a performance cost and a source of confusion — the model's context shifts underneath the user. By locking mode after the first message:

  1. Stable prefix cache — system prompt is fixed for the entire session
  2. Clear UX contract — the user commits to a mode at session start
  3. Clean permission model — future agents auto-inherit deny rules, no per-agent configuration needed

Changes

1. Sticky Mode (TUI)

  • FREE_SWITCH_GROUP = ["build", "plan"] — can freely switch between these
  • All other modes (Compose, etc.) are isolated once entered
  • set() (system) vs userSwitch() (user) separation — only user actions are guarded
  • Reactive derivation from lastUserMessage()/new and /session work correctly without manual lock/unlock
  • Contextual toast messages when blocked

2. Permission-Based Skill and Tool Scoping

  • Compose skills: defaults deny compose:*, compose agent explicitly allows
  • Plan tools: defaults deny plan_enter/plan_exit, build+plan allow (symmetric)
  • Skill.available() relies on permissions, not hidden flag
  • Remove composeSkillsBlock() injection — skills appear naturally in the system prompt

Design Decisions

Decision Rationale
set() vs userSwitch() Only guard user actions; system paths (session restore, CLI) are never blocked
Reactive !!lastUserMessage() No manual lock state; handles all session transitions naturally
move() skips blocked agents Tab cycles within group, doesn't stop at first blocked entry
Symmetric plan tools in build/plan No tool list mutation on build↔plan switch
Permission defaults deny + allow Future agents auto-inherit deny, no per-agent config needed

Breaking Changes (need discussion)

  1. Custom modes: User-defined agents not in FREE_SWITCH_GROUP are treated as isolated. Would need group configuration support.
  2. hidden: true deprecated: Skills relying on hidden must migrate to permission config.
  3. Plan tools hidden from compose: Compose agent no longer sees plan_enter/plan_exit.

Tests

  • bun typecheck — clean
  • bun test test/agent/agent.test.ts — 48/48 pass
  • bun test test/session/prompt-skill-mention.test.ts — 8/8 pass
  • bun test test/permission — 141/141 pass

Open Questions

  • Should FREE_SWITCH_GROUP be configurable (e.g., user-defined agents can declare their group)?
  • Should there be a /unlock escape hatch for power users?
  • Is one-session-one-mode too strict for workflows that genuinely need mode changes mid-conversation?

Supersedes #1719 (experimental branch, closed).

After a session's first message, the mode is locked to the current group:
- Build/Plan form a free-switch group (Tab cycles between them)
- All other modes (Compose, etc.) are isolated once entered
- /new restores free selection; /session respects existing lock

Implementation:
- set() for system/programmatic use (unguarded)
- userSwitch() for user actions (guarded, shows contextual toast)
- move() cycles within allowed group, skips blocked agents
- Reactive derivation from lastUserMessage() — no manual lock state
Replace hidden:true with permission system for compose skills:
- defaults deny compose:* skills; compose agent allows them
- defaults deny plan_enter/plan_exit; build+plan allow (symmetric)
- Skill.available() relies on permissions, not hidden flag
- Remove composeSkillsBlock() from extract.ts and prompt.ts
- Skills appear naturally in system prompt via available(agent)

Result: future agents auto-inherit deny rules, compose gets a clean
tool/skill list, and the system prompt is stable within a session.
@yanyihan-xiaomi

Copy link
Copy Markdown
Collaborator Author

@wqymi @MiMoHardFather Check this

…ary batch()

- Verify build/plan deny compose:* skills, compose allows them
- Verify non-compose skills remain allowed for all agents
- Remove redundant batch() wrapper in move() (single write, no batching needed)
@yanyihan-xiaomi yanyihan-xiaomi merged commit 6f8b2bb into main Jul 14, 2026
9 of 10 checks passed
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