mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
feat(opencode): add plan mode, reasoning effort, and status telemetry (#688)
* feat(opencode): support plan mode * feat(opencode): support reasoning effort * feat(opencode): surface context usage in web Bridge OpenCode ACP usage updates into the existing token-count pipeline so the web status bar can show live context and cache information without a separate UI path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(opencode): restrict plan mode to remote, rollback reasoning effort on failure - Block local OpenCode plan startup (tools not enforced in local path) - Allow remote OpenCode plan only (ACP permission handler denies tools) - Guard web /permission-mode endpoint for local OpenCode plan sessions - Rollback session reasoning effort when OpenCode rejects set_config_option - Wire rollback callback through opencodeLoop to runOpencode closure - Add tests: local plan rejected, remote plan allowed, web guard, effort rollback * fix(web): auto-retry OpenCode models query to populate model selector without refresh - Retry early failures (RPC may still be registering on new sessions) - Poll briefly until availableModels is non-empty - Stop polling once model options are discovered - Add tests for retry/poll/stop policy * fix(opencode): cap model discovery polling --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,7 @@ export async function runOpencode(opts: {
|
||||
startingMode?: 'local' | 'remote';
|
||||
permissionMode?: PermissionMode;
|
||||
model?: string;
|
||||
modelReasoningEffort?: string | null;
|
||||
resumeSessionId?: string;
|
||||
existingSessionId?: string;
|
||||
workingDirectory?: string;
|
||||
@@ -33,6 +34,13 @@ export async function runOpencode(opts: {
|
||||
opts.startingMode = 'remote';
|
||||
}
|
||||
|
||||
const startingMode: 'local' | 'remote' = opts.startingMode
|
||||
?? (startedBy === 'runner' ? 'remote' : 'local');
|
||||
|
||||
if (opts.permissionMode === 'plan' && startingMode !== 'remote') {
|
||||
throw new Error('OpenCode plan mode is only supported in remote mode');
|
||||
}
|
||||
|
||||
const initialState: AgentState = {
|
||||
controlledByUser: false
|
||||
};
|
||||
@@ -41,6 +49,7 @@ export async function runOpencode(opts: {
|
||||
// Mid-session selections are persisted by the hub via the set-session-config RPC,
|
||||
// not by this initial bootstrap.
|
||||
const initialModel = opts.model ?? null;
|
||||
const initialModelReasoningEffort = opts.modelReasoningEffort ?? null;
|
||||
|
||||
const bootstrap = opts.existingSessionId
|
||||
? await bootstrapExistingSession({
|
||||
@@ -54,23 +63,23 @@ export async function runOpencode(opts: {
|
||||
startedBy,
|
||||
workingDirectory,
|
||||
agentState: initialState,
|
||||
model: initialModel ?? undefined
|
||||
model: initialModel ?? undefined,
|
||||
modelReasoningEffort: initialModelReasoningEffort ?? undefined
|
||||
});
|
||||
const { api, session } = bootstrap;
|
||||
|
||||
const startingMode: 'local' | 'remote' = opts.startingMode
|
||||
?? (startedBy === 'runner' ? 'remote' : 'local');
|
||||
|
||||
setControlledByUser(session, startingMode);
|
||||
|
||||
const messageQueue = new MessageQueue2<OpencodeMode>((mode) => hashObject({
|
||||
permissionMode: mode.permissionMode,
|
||||
model: mode.model ?? null
|
||||
model: mode.model ?? null,
|
||||
modelReasoningEffort: mode.modelReasoningEffort ?? null
|
||||
}));
|
||||
|
||||
const sessionWrapperRef: { current: OpencodeSession | null } = { current: null };
|
||||
let currentPermissionMode: PermissionMode = opts.permissionMode ?? 'default';
|
||||
let sessionModel: string | null = initialModel;
|
||||
let sessionModelReasoningEffort: string | null = initialModelReasoningEffort;
|
||||
const hookServer = await startOpencodeHookServer({
|
||||
onEvent: (event) => {
|
||||
const currentSession = sessionWrapperRef.current;
|
||||
@@ -102,19 +111,21 @@ export async function runOpencode(opts: {
|
||||
}
|
||||
sessionInstance.setPermissionMode(currentPermissionMode);
|
||||
sessionInstance.setModel(sessionModel);
|
||||
sessionInstance.setModelReasoningEffort(sessionModelReasoningEffort);
|
||||
|
||||
// Notify hub immediately so the UI reflects the change without
|
||||
// waiting for the next 2s keepalive tick.
|
||||
sessionInstance.pushKeepAlive();
|
||||
|
||||
logger.debug(`[opencode] Synced session config for keepalive: permissionMode=${currentPermissionMode}, model=${sessionModel ?? '(default)'}`);
|
||||
logger.debug(`[opencode] Synced session config for keepalive: permissionMode=${currentPermissionMode}, model=${sessionModel ?? '(default)'}, modelReasoningEffort=${sessionModelReasoningEffort ?? '(default)'}`);
|
||||
};
|
||||
|
||||
session.onUserMessage((message, localId) => {
|
||||
const formattedText = formatMessageWithAttachments(message.content.text, message.content.attachments);
|
||||
const mode: OpencodeMode = {
|
||||
permissionMode: currentPermissionMode,
|
||||
model: sessionModel ?? undefined
|
||||
model: sessionModel ?? undefined,
|
||||
modelReasoningEffort: sessionModelReasoningEffort
|
||||
};
|
||||
messageQueue.push(formattedText, mode, localId);
|
||||
});
|
||||
@@ -129,6 +140,7 @@ export async function runOpencode(opts: {
|
||||
rpcHandlerManager: session.rpcHandlerManager,
|
||||
flavor: 'opencode',
|
||||
modelMode: 'nullable',
|
||||
modelReasoningEffortMode: 'nullable',
|
||||
onApply: (config) => {
|
||||
if (config.permissionMode !== undefined) {
|
||||
currentPermissionMode = config.permissionMode;
|
||||
@@ -136,6 +148,9 @@ export async function runOpencode(opts: {
|
||||
if (config.model !== undefined) {
|
||||
sessionModel = config.model;
|
||||
}
|
||||
if (config.modelReasoningEffort !== undefined) {
|
||||
sessionModelReasoningEffort = config.modelReasoningEffort;
|
||||
}
|
||||
},
|
||||
onAfterApply: syncSessionMode
|
||||
});
|
||||
@@ -152,10 +167,14 @@ export async function runOpencode(opts: {
|
||||
api,
|
||||
permissionMode: currentPermissionMode,
|
||||
model: sessionModel ?? undefined,
|
||||
modelReasoningEffort: sessionModelReasoningEffort,
|
||||
resumeSessionId: opts.resumeSessionId,
|
||||
hookServer,
|
||||
hookUrl,
|
||||
onModeChange: createModeChangeHandler(session),
|
||||
onReasoningEffortRollback: (effort) => {
|
||||
sessionModelReasoningEffort = effort;
|
||||
},
|
||||
onSessionReady: (instance) => {
|
||||
sessionWrapperRef.current = instance;
|
||||
syncSessionMode();
|
||||
|
||||
Reference in New Issue
Block a user