Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SCIENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Everything else is upstream's code and behavior.
| Next message | `predict_next_message` gives an RPC host the prediction of the user's next message that OMP's terminal composer shows after a turn: OMP's prompt and parsing, on the session's model and context, without changing its history. It runs outside the command queue; a newer request replaces one in flight, and a turn or compaction that starts cancels it. |
| Usage-limit reset | A `prompt_result` for a turn stopped by a usage limit carries `error.resetAt`, the time the provider said the limit resets (epoch ms): from its retry hint, the account's usage report, or an earlier stated block, whichever is latest. It is left out when the provider gave no time; the 30-minute guess for a hintless error is never reported. A host can resume the turn at that time. |
| Prompt cache | An assistant reply's `message_end` carries `promptCache` (`tier`, `touchedAt`, `ttlMs`): the prompt-cache entry its response confirmed, while the next request would still read it. It is left out for a cache that never expires and for an entry the agent only inferred. A host can compact an idle conversation just before a one-hour entry expires, so the summary reads a warm cache. The agent's own idle compaction stays a terminal feature and never runs over RPC. |
| Plan mode | Oh My Pi plans only in its terminal and for ACP clients. Over RPC, the `plan` command (`enter`, `exit`, `get`) turns plan mode on: the agent reads and drafts a plan file but changes nothing else. When it proposes the plan, a `plan_proposed` frame carries the plan, and a select request asks the host to approve it while the run waits. Only an explicit approval leaves plan mode, and the same run then carries out the plan. Refine asks the host what to change, and that answer, like any typed answer, reaches the model as feedback; planning goes on. An answer that comes after plan mode ended never approves, and a new, switched or forked conversation starts outside plan mode. Each time plan mode turns on or off, a `plan_updated` frame tells the host. Planning is refused while a goal is set. |
| Failure kinds | A `prompt_result` for a failed turn carries `error.kinds`, the error classifier's labels for that failure (`auth-failed`, `context-overflow`, `transient`, `usage-limit`, ...), so a host can report why turns fail. It is left out when the classifier recognized no kind; the labels carry no provider text. |
| macOS Accessibility prompt | The agent sends no Accessibility message while macOS doesn't trust it, so a computer call that refuses for lack of a permission never makes macOS ask. Only the host asks for permissions. |
| Live voice delegation | `live_connect` with `hostDelegation` sends each request the voice model delegates to the host as a `live_delegation` frame instead of starting a turn, so a host that owns its turns (Scient Desktop) sends it as the user's message. The live session still reads that turn's answer. A host that runs the turn elsewhere, such as in the conversation's own agent while the call runs in a separate one, reports its progress and answer with `live_report`. Without the flag nothing changes. |
Expand Down
21 changes: 21 additions & 0 deletions packages/coding-agent/src/modes/rpc/rpc-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ import { RpcNextMessagePredictor } from "../../scient/next-message";
// scient(SA-PROMPT-CACHE): begin - A host compacts an idle conversation before its prompt cache expires.
import { withPromptCache } from "../../scient/prompt-cache";
// scient(SA-PROMPT-CACHE): end
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
import { RpcPlanController } from "../../scient/rpc-plan";
// scient(SA-PLAN-MODE): end
import { pageRpcMessages, RPC_MESSAGES_PAGE_BUSY_ERROR, RpcMessagesPageError } from "./rpc-messages";
import { RpcBtwController } from "./rpc-btw";
import { RpcGoalController } from "./rpc-goal";
Expand Down Expand Up @@ -1754,6 +1757,9 @@ export async function runRpcMode(session: AgentSession, options: RpcModeOptions
// A single shared instance routes all responses received on stdin to the
// correct waiting promise regardless of which code path created the request.
const rpcUiContext = new RpcExtensionUIContext(pendingExtensionRequests, output);
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
const planController = new RpcPlanController(session, rpcUiContext, output);
// scient(SA-PLAN-MODE): end
setToolUIContext?.(rpcUiContext, true);
const setAskDialog = (command: RpcCommand & { type: "set_ask_dialog" }): RpcResponse => {
rpcUiContext.askDialogEnabled = command.enabled === true;
Expand Down Expand Up @@ -1820,6 +1826,9 @@ export async function runRpcMode(session: AgentSession, options: RpcModeOptions
// close the prompts it was answering. Branch and navigation leave a live
// run streaming to its normal yield.
if (detachesRun) promptResults.abortOpen();
// scient(SA-PLAN-MODE): begin - A plan never carries into another conversation.
if (detachesRun) await planController.handle({ op: "exit" });
// scient(SA-PLAN-MODE): end
void settleWatcher.check();
}
}
Expand Down Expand Up @@ -2200,6 +2209,9 @@ export async function runRpcMode(session: AgentSession, options: RpcModeOptions
inputGate.commitSessionChange(command);
// `branch` leaves a live run streaming to its normal yield; new/switch detach it.
if (command.type !== "branch" && command.type !== "fork") promptResults.abortOpen();
// scient(SA-PLAN-MODE): begin - A plan never carries into another conversation.
if (command.type !== "branch") await planController.handle({ op: "exit" });
// scient(SA-PLAN-MODE): end
// The detached run publishes no terminal agent_end to settle on.
void settleWatcher.check();
await emitAvailableCommandsUpdate();
Expand Down Expand Up @@ -2976,6 +2988,15 @@ export async function runRpcMode(session: AgentSession, options: RpcModeOptions
case "predict_next_message":
return success(id, "predict_next_message", { text: await nextMessagePredictor.predict() });
// scient(SA-NEXT-MESSAGE): end
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.

case "plan":
try {
return success(id, "plan", await planController.handle(command));
} catch (planError) {
return error(id, "plan", planError instanceof Error ? planError.message : String(planError));
}
// scient(SA-PLAN-MODE): end

default: {
const unknownCommand = command as { type: string };
Expand Down
12 changes: 12 additions & 0 deletions packages/coding-agent/src/modes/rpc/rpc-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import type { LivePhase } from "@oh-my-pi/pi-tui/apps/live-visualizer";
import type { RpcMessagesPage } from "./rpc-messages";
import type { GoalModeState } from "../../goals/state";
import type { RpcGoalOp, RpcGoalResult } from "./rpc-goal";
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
import type { RpcPlanOp, RpcPlanResult } from "../../scient/rpc-plan";
// scient(SA-PLAN-MODE): end
import type { BtwHistoryRecord } from "../../session/btw-history";
// scient(SA-AUTH): begin - A host sees and signs out stored sign-ins.
import type { RpcLoginProvider } from "../../scient/sign-in-providers";
Expand Down Expand Up @@ -177,6 +180,9 @@ export type RpcCommand =
// scient(SA-NEXT-MESSAGE): begin - A host shows the agent's prediction of the user's next message.
| { id?: string; type: "predict_next_message" }
// scient(SA-NEXT-MESSAGE): end
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
| { id?: string; type: "plan"; op: RpcPlanOp }
// scient(SA-PLAN-MODE): end
| {
id?: string;
type: "predict_word_feedback";
Expand Down Expand Up @@ -352,6 +358,9 @@ export type RpcLiveFrame =
| RpcLiveEndFrame
| RpcLiveDelegationFrame;
// scient(SA-LIVE-HOST-DELEGATION): end
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
export type { RpcPlanProposedFrame, RpcPlanUpdatedFrame } from "../../scient/rpc-plan";
// scient(SA-PLAN-MODE): end

/** Draft-only dictation frames bypass the session event filter. */
export interface RpcDictationStateFrame {
Expand Down Expand Up @@ -719,6 +728,9 @@ export type RpcResponse =
// scient(SA-NEXT-MESSAGE): begin - A host shows the agent's prediction of the user's next message.
| { id?: string; type: "response"; command: "predict_next_message"; success: true; data: { text: string | null } }
// scient(SA-NEXT-MESSAGE): end
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
| { id?: string; type: "response"; command: "plan"; success: true; data: RpcPlanResult }
// scient(SA-PLAN-MODE): end

// Side questions (/btw)
| { id?: string; type: "response"; command: "btw"; success: true; data: { record: RpcBtwHistoryRecord } }
Expand Down
8 changes: 8 additions & 0 deletions packages/coding-agent/src/modes/rpc/wire/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,4 +452,12 @@ export const rpcCommands: readonly RpcCommandSpec[] = [
timeoutMs: 30_000,
},
// scient(SA-NEXT-MESSAGE): end
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
{
name: "plan",
doc: "Read, enter or leave plan mode: the agent plans without changing anything until the host approves the plan it proposes (`plan_proposed`, then a select request).",
params: { op: "PlanOp" },
result: "PlanResult",
},
// scient(SA-PLAN-MODE): end
];
10 changes: 10 additions & 0 deletions packages/coding-agent/src/modes/rpc/wire/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ export const frameDefs = {
"A spoken request for the host to send as the user's message; sent only to a `live_connect` with `hostDelegation`.",
),
// scient(SA-LIVE-HOST-DELEGATION): end
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
PlanProposedEvent: doc(
{ type: "'plan_proposed'", title: "string", planFilePath: "string", markdown: "string" },
"The plan the agent proposed in plan mode, sent just before the select request that asks to approve it.",
),
PlanUpdatedEvent: doc(
{ type: "'plan_updated'", enabled: "boolean", "planFilePath?": "string" },
"Plan mode turned on or off: by the `plan` command, by approving a plan, or by a change of conversation.",
),
// scient(SA-PLAN-MODE): end
DictationStateEvent: doc(
{ type: "'dictation_state'", captureId: "string", state: "'starting' | 'recording' | 'transcribing'" },
"State of one draft-only microphone capture.",
Expand Down
4 changes: 4 additions & 0 deletions packages/coding-agent/src/modes/rpc/wire/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ const notificationDefs = {
// scient(SA-LIVE-HOST-DELEGATION): begin - A host that owns its turns sends spoken requests itself.
"LiveDelegationEvent",
// scient(SA-LIVE-HOST-DELEGATION): end
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
"PlanProposedEvent",
"PlanUpdatedEvent",
// scient(SA-PLAN-MODE): end
"DictationStateEvent",
"DictationTranscriptEvent",
"DictationEndEvent",
Expand Down
31 changes: 30 additions & 1 deletion packages/coding-agent/src/modes/rpc/wire/rpc-wire.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,15 @@ export type MessageUpdates = "full" | "delta";

export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned" | "blocked";

export type PlanOp = "get" | "enter" | "exit";

export interface PlanResult {
/** Plan mode is on: the agent plans and changes nothing until a plan is approved. */
enabled: boolean;
/** The plan file the agent drafts, while plan mode is on. */
planFilePath?: string;
}

export type GoalOp = "get" | "create" | "resume" | "pause" | "drop";

export type GoalStatus = "active" | "paused" | "budget-limited" | "complete" | "dropped";
Expand Down Expand Up @@ -1223,6 +1232,21 @@ export interface LiveDelegationEvent {
text: string;
}

/** The plan the agent proposed in plan mode, sent just before the select request that asks to approve it. */
export interface PlanProposedEvent {
type: "plan_proposed";
title: string;
planFilePath: string;
markdown: string;
}

/** Plan mode turned on or off: by the `plan` command, by approving a plan, or by a change of conversation. */
export interface PlanUpdatedEvent {
type: "plan_updated";
enabled: boolean;
planFilePath?: string;
}

/** State of one draft-only microphone capture. */
export interface DictationStateEvent {
type: "dictation_state";
Expand Down Expand Up @@ -1580,7 +1604,7 @@ export interface HostUriSchemeDefinition {
}

/** Unsolicited outbound frame (everything except responses and host tool/URI requests), discriminated by `type`. */
export type RpcNotification = ReadyEvent | PromptResultEvent | SessionSettledEvent | ExtensionError | ExtensionUiRequest | AvailableCommandsUpdateEvent | SubagentLifecycleEvent | SubagentProgressEvent | SubagentEvent | LivePhaseEvent | LiveLevelsEvent | LiveTranscriptEvent | LiveEndEvent | LiveDelegationEvent | DictationStateEvent | DictationTranscriptEvent | DictationEndEvent | BtwDeltaEvent | BtwRecordEvent | CommandOutputEvent | SessionInfoUpdateEvent | ConfigUpdateEvent | RpcFrameErrorEvent | RpcAgentEvent;
export type RpcNotification = ReadyEvent | PromptResultEvent | SessionSettledEvent | ExtensionError | ExtensionUiRequest | AvailableCommandsUpdateEvent | SubagentLifecycleEvent | SubagentProgressEvent | SubagentEvent | LivePhaseEvent | LiveLevelsEvent | LiveTranscriptEvent | LiveEndEvent | LiveDelegationEvent | PlanProposedEvent | PlanUpdatedEvent | DictationStateEvent | DictationTranscriptEvent | DictationEndEvent | BtwDeltaEvent | BtwRecordEvent | CommandOutputEvent | SessionInfoUpdateEvent | ConfigUpdateEvent | RpcFrameErrorEvent | RpcAgentEvent;

/** Any frame the server writes to stdout (after reassembling `rpc_chunk` sequences), discriminated by `type`. */
export type RpcServerFrame = RpcResponse | RpcHostRequest | RpcNotification;
Expand Down Expand Up @@ -1980,6 +2004,10 @@ export interface PredictNextMessageResult {
text: string | null;
}

export interface PlanParams {
op: PlanOp;
}

/** Every RPC command's parameters and successful response `data`. */
export interface RpcWireCommands {
negotiate_protocol: { params: NegotiateProtocolParams; result: NegotiateProtocolResult };
Expand Down Expand Up @@ -2059,4 +2087,5 @@ export interface RpcWireCommands {
btw_cancel: { params: BtwCancelParams; result: BtwCancelResult };
get_btw_history: { params: undefined; result: GetBtwHistoryResult };
predict_next_message: { params: undefined; result: PredictNextMessageResult };
plan: { params: PlanParams; result: PlanResult };
}
95 changes: 95 additions & 0 deletions packages/coding-agent/src/modes/rpc/wire/rpc-wire.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,17 @@
"timeoutMs": 30000,
"completion": null,
"clientOmit": []
},
{
"name": "plan",
"doc": "Read, enter or leave plan mode: the agent plans without changing anything until the host approves the plan it proposes (`plan_proposed`, then a select request).",
"params": "PlanParams",
"result": "PlanResult",
"nullable": false,
"unwrap": null,
"timeoutMs": null,
"completion": null,
"clientOmit": []
}
]
},
Expand Down Expand Up @@ -2334,6 +2345,30 @@
],
"type": "string"
},
"PlanOp": {
"enum": [
"get",
"enter",
"exit"
],
"type": "string"
},
"PlanResult": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Plan mode is on: the agent plans and changes nothing until a plan is approved."
},
"planFilePath": {
"type": "string",
"description": "The plan file the agent drafts, while plan mode is on."
}
},
"required": [
"enabled"
]
},
"GoalOp": {
"enum": [
"get",
Expand Down Expand Up @@ -5047,6 +5082,49 @@
],
"description": "A spoken request for the host to send as the user's message; sent only to a `live_connect` with `hostDelegation`."
},
"PlanProposedEvent": {
"type": "object",
"properties": {
"type": {
"const": "plan_proposed"
},
"title": {
"type": "string"
},
"planFilePath": {
"type": "string"
},
"markdown": {
"type": "string"
}
},
"required": [
"type",
"title",
"planFilePath",
"markdown"
],
"description": "The plan the agent proposed in plan mode, sent just before the select request that asks to approve it."
},
"PlanUpdatedEvent": {
"type": "object",
"properties": {
"type": {
"const": "plan_updated"
},
"enabled": {
"type": "boolean"
},
"planFilePath": {
"type": "string"
}
},
"required": [
"type",
"enabled"
],
"description": "Plan mode turned on or off: by the `plan` command, by approving a plan, or by a change of conversation."
},
"DictationStateEvent": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -6258,6 +6336,12 @@
{
"$ref": "#/$defs/LiveDelegationEvent"
},
{
"$ref": "#/$defs/PlanProposedEvent"
},
{
"$ref": "#/$defs/PlanUpdatedEvent"
},
{
"$ref": "#/$defs/DictationStateEvent"
},
Expand Down Expand Up @@ -7469,6 +7553,17 @@
"required": [
"text"
]
},
"PlanParams": {
"type": "object",
"properties": {
"op": {
"$ref": "#/$defs/PlanOp"
}
},
"required": [
"op"
]
}
}
}
7 changes: 7 additions & 0 deletions packages/coding-agent/src/modes/rpc/wire/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export const stateDefs = {
"`set_event_filter` projection: `delta` replaces the accumulated snapshots with the block a start/end event touches.",
),
TodoStatus: "'pending' | 'in_progress' | 'completed' | 'abandoned' | 'blocked'",
// scient(SA-PLAN-MODE): begin - A host plans with the agent before it acts, and approves the plan.
PlanOp: "'get' | 'enter' | 'exit'",
PlanResult: {
enabled: doc("boolean", "Plan mode is on: the agent plans and changes nothing until a plan is approved."),
"planFilePath?": doc("string", "The plan file the agent drafts, while plan mode is on."),
},
// scient(SA-PLAN-MODE): end
GoalOp: "'get' | 'create' | 'resume' | 'pause' | 'drop'",
GoalStatus: "'active' | 'paused' | 'budget-limited' | 'complete' | 'dropped'",
SlashCommandSource: "'builtin' | 'skill' | 'extension' | 'custom' | 'mcp_prompt' | 'file'",
Expand Down
Loading
Loading