diff --git a/docs/rpc.md b/docs/rpc.md index f1f27db30..eb324074c 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -124,6 +124,7 @@ Important edge behavior from runtime: ### State - `{ id?, type: "get_state" }` +- `{ id?, type: "set_fast_mode", enabled: boolean }` - `{ id?, type: "get_available_commands" }` - `{ id?, type: "set_todos", phases: TodoPhase[] }` - `{ id?, type: "set_host_tools", tools: RpcHostToolDefinition[] }` @@ -231,6 +232,10 @@ Local-only slash commands may emit `command_output` frames before completing via ### `get_state` payload +`tokensPerSecond` is a number when output throughput is available and `null` +otherwise. `fastModeEnabled` reports the session setting, while +`fastModeActive` reports whether the current model realizes fast mode. + ```json { "model": { "provider": "...", "id": "..." }, @@ -243,6 +248,9 @@ Local-only slash commands may emit `command_output` frames before completing via "sessionFile": "...", "sessionId": "...", "sessionName": "...", + "fastModeEnabled": false, + "tokensPerSecond": null, + "fastModeActive": false, "autoCompactionEnabled": true, "messageCount": 0, "queuedMessageCount": 0, @@ -275,6 +283,56 @@ Local-only slash commands may emit `command_output` frames before completing via } ``` +### `set_fast_mode` payload + +`set_fast_mode` changes whether fast mode is enabled for the session. The +request is: + +```json +{ "id": "req_fast_on", "type": "set_fast_mode", "enabled": true } +``` + +On success, `data` always contains both `enabled` and `active`. `enabled` +reports the session setting; `active` reports whether fast mode is realized by +the current model: + +```json +{ + "id": "req_fast_on", + "type": "response", + "command": "set_fast_mode", + "success": true, + "data": { "enabled": true, "active": true } +} +``` + +Enabling fast mode on a model without a service-tier family fails with the +exact error below: + +```json +{ + "id": "req_fast_on", + "type": "response", + "command": "set_fast_mode", + "success": false, + "error": "Fast mode is unavailable for the current model." +} +``` + +Disabling fast mode is idempotent, including on an unsupported model. It +succeeds as an off/no-op result and leaves `get_state.fastModeEnabled` and +`get_state.fastModeActive` false: + +```json +{ + "id": "req_fast_off", + "type": "response", + "command": "set_fast_mode", + "success": true, + "data": { "enabled": false, "active": false } +} +``` + ### `set_todos` payload Replaces the in-memory todo state for the current session and returns the normalized phase list: diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1953032df..630029a34 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - Added tokens-per-second throughput to RPC `get_state` responses for non-TUI clients. +- Added the RPC `set_fast_mode` command for live fast-mode control. +- Added `fastModeEnabled` and `fastModeActive` to RPC `get_state` responses. ## [17.1.8] - 2026-07-28 diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 66d567a53..952079365 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -1104,7 +1104,8 @@ export async function runRpcMode( } case "set_fast_mode": { - if (!session.setFastMode(command.enabled)) { + const supported = session.setFastMode(command.enabled); + if (command.enabled && !supported) { return error(id, "set_fast_mode", "Fast mode is unavailable for the current model."); } return success(id, "set_fast_mode", { diff --git a/packages/coding-agent/test/rpc.test.ts b/packages/coding-agent/test/rpc.test.ts index 817c6de75..8cc867afb 100644 --- a/packages/coding-agent/test/rpc.test.ts +++ b/packages/coding-agent/test/rpc.test.ts @@ -315,3 +315,44 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("RPC mode", () => { expect(text).toContain("test123"); }, 90000); }); + +describe("RPC fast mode with unsupported model", () => { + let client: RpcClient; + let sessionDir: string; + + beforeEach(() => { + sessionDir = path.join(os.tmpdir(), `omp-rpc-fast-mode-test-${Snowflake.next()}`); + client = new RpcClient({ + cliPath: path.join(import.meta.dir, "..", "src", "cli.ts"), + cwd: path.join(import.meta.dir, ".."), + env: { + PI_CODING_AGENT_DIR: sessionDir, + FIREWORKS_API_KEY: "test-fireworks-key", + }, + provider: "fireworks", + model: "deepseek-v4-flash", + }); + }); + + afterEach(async () => { + await client.stop(); + if (sessionDir && fs.existsSync(sessionDir)) { + removeSyncWithRetries(sessionDir); + } + }); + + test("rejects enable but idempotently accepts disable", async () => { + await client.start(); + + await expect(client.setFastMode(true)).rejects.toMatchObject({ + message: "Fast mode is unavailable for the current model.", + }); + + const disabled = await client.setFastMode(false); + expect(disabled).toEqual({ enabled: false, active: false }); + + const state = await client.getState(); + expect(state.fastModeEnabled).toBe(false); + expect(state.fastModeActive).toBe(false); + }, 30000); +});