Add Ultraplan Feature for Advanced Multi-Agent Planning (#232)

* feat: add ultraplan feature for advanced multi-agent planning

Implement ultraplan command with web-based planning interface,
supporting multiple prompt modes and interactive plan approval.

* chore: add semi

* chore: add semi
This commit is contained in:
sobird
2026-04-10 22:17:00 +08:00
committed by GitHub
parent 8137b66a46
commit 9da7345f8e
11 changed files with 292 additions and 144 deletions

View File

@@ -25,15 +25,40 @@ import { ALL_MODEL_CONFIGS } from '../utils/model/configs.js';
import { updateTaskState } from '../utils/task/framework.js';
import { archiveRemoteSession, teleportToRemote } from '../utils/teleport.js';
import { pollForApprovedExitPlanMode, UltraplanPollError } from '../utils/ultraplan/ccrSession.js';
import {
getPromptText,
getDialogConfig,
getPromptIdentifier,
type PromptIdentifier
} from '../utils/ultraplan/prompt.js';
import { registerCleanup } from '../utils/cleanupRegistry.js';
// TODO(prod-hardening): OAuth token may go stale over the 30min poll;
// consider refresh.
// Multi-agent exploration is slow; 30min timeout.
/**
* Multi-agent exploration is slow; 30min timeout.
*
* @deprecated use getUltraplanTimeoutMs()
*/
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000;
export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the-web';
export function getUltraplanTimeoutMs(): number {
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_ultraplan_timeout_seconds', 1800) * 1000
}
/**
* 是否启用 ultraplan, 默认启用
*
* @returns
*/
export function isUltraplanEnabled(): boolean {
return getFeatureValue_CACHED_MAY_BE_STALE<{enabled: boolean} | null>('tengu_ultraplan_config', { enabled: true })?.enabled === true
}
// CCR runs against the first-party API — use the canonical ID, not the
// provider-specific string getModelStrings() would return (which may be a
// Bedrock ARN or Vertex ID on the local CLI). Read at call time, not module
@@ -62,6 +87,7 @@ const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPromp
// so the override path is DCE'd from external builds).
// Shell-set env only, so top-level process.env read is fine
// — settings.env never injects this.
// @deprecated use buildUltraplanPrompt()
/* eslint-disable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs -- ant-only dev override; eager top-level read is the point (crash at startup, not silently inside the slash-command try/catch) */
const ULTRAPLAN_INSTRUCTIONS: string =
process.env.USER_TYPE === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE
@@ -73,12 +99,14 @@ const ULTRAPLAN_INSTRUCTIONS: string =
* Assemble the initial CCR user message. seedPlan and blurb stay outside the
* system-reminder so the browser renders them; scaffolding is hidden.
*/
export function buildUltraplanPrompt(blurb: string, seedPlan?: string): string {
export function buildUltraplanPrompt(blurb: string, seedPlan?: string, promptId?: PromptIdentifier): string {
const parts: string[] = [];
if (seedPlan) {
parts.push('Here is a draft plan to refine:', '', seedPlan, '');
}
parts.push(ULTRAPLAN_INSTRUCTIONS);
// parts.push(ULTRAPLAN_INSTRUCTIONS)
parts.push(getPromptText(promptId!));
if (blurb) {
parts.push('', blurb);
}
@@ -98,7 +126,7 @@ function startDetachedPoll(
try {
const { plan, rejectCount, executionTarget } = await pollForApprovedExitPlanMode(
sessionId,
ULTRAPLAN_TIMEOUT_MS,
getUltraplanTimeoutMs(),
phase => {
if (phase === 'needs_input') logEvent('tengu_ultraplan_awaiting_input', {});
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => {
@@ -258,6 +286,7 @@ export async function stopUltraplan(
export async function launchUltraplan(opts: {
blurb: string;
seedPlan?: string;
promptIdentifier?: PromptIdentifier;
getAppState: () => AppState;
setAppState: (f: (prev: AppState) => AppState) => void;
signal: AbortSignal;
@@ -272,7 +301,7 @@ export async function launchUltraplan(opts: {
*/
onSessionReady?: (msg: string) => void;
}): Promise<string> {
const { blurb, seedPlan, getAppState, setAppState, signal, disconnectedBridge, onSessionReady } = opts;
const { blurb, seedPlan, promptIdentifier, getAppState, setAppState, signal, disconnectedBridge, onSessionReady } = opts;
const { ultraplanSessionUrl: active, ultraplanLaunching } = getAppState();
if (active || ultraplanLaunching) {
@@ -292,11 +321,12 @@ export async function launchUltraplan(opts: {
'Usage: /ultraplan \\<prompt\\>, or include "ultraplan" anywhere',
'in your prompt',
'',
'Advanced multi-agent plan mode with our most powerful model',
'(Opus). Runs in Claude Code on the web. When the plan is ready,',
'you can execute it in the web session or send it back here.',
'Terminal stays free while the remote plans.',
'Requires /login.',
// 'Advanced multi-agent plan mode with our most powerful model',
// '(Opus). Runs in Claude Code on the web. When the plan is ready,',
// 'you can execute it in the web session or send it back here.',
// 'Terminal stays free while the remote plans.',
// 'Requires /login.',
...getDialogConfig().usageBlurb,
'',
`Terms: ${CCR_TERMS_URL}`,
].join('\n');
@@ -304,10 +334,11 @@ export async function launchUltraplan(opts: {
// Set synchronously before the detached flow to prevent duplicate launches
// during the teleportToRemote window.
setAppState(prev => (prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true }));
setAppState(prev => prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true });
void launchDetached({
blurb,
seedPlan,
promptIdentifier,
getAppState,
setAppState,
signal,
@@ -319,27 +350,28 @@ export async function launchUltraplan(opts: {
async function launchDetached(opts: {
blurb: string;
seedPlan?: string;
promptIdentifier?: PromptIdentifier;
getAppState: () => AppState;
setAppState: (f: (prev: AppState) => AppState) => void;
signal: AbortSignal;
onSessionReady?: (msg: string) => void;
}): Promise<void> {
const { blurb, seedPlan, getAppState, setAppState, signal, onSessionReady } = opts;
const { blurb, seedPlan, promptIdentifier = getPromptIdentifier(), getAppState, setAppState, signal, onSessionReady } = opts;
// Hoisted so the catch block can archive the remote session if an error
// occurs after teleportToRemote succeeds (avoids 30min orphan).
let sessionId: string | undefined;
try {
const model = getUltraplanModel();
// const model = getUltraplanModel()
const eligibility = await checkRemoteAgentEligibility();
if (!eligibility.eligible) {
logEvent('tengu_ultraplan_create_failed', {
reason: 'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
precondition_errors: (eligibility as { errors: Array<{ type: string }> }).errors
precondition_errors: eligibility.errors
.map(e => e.type)
.join(',') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
const reasons = (eligibility as { errors: Array<{ type: string }> }).errors.map(formatPreconditionError).join('\n');
const reasons = eligibility.errors.map(formatPreconditionError).join('\n');
enqueuePendingNotification({
value: `ultraplan: cannot launch remote session —\n${reasons}`,
mode: 'task-notification',
@@ -347,12 +379,13 @@ async function launchDetached(opts: {
return;
}
const prompt = buildUltraplanPrompt(blurb, seedPlan);
const prompt = buildUltraplanPrompt(blurb, seedPlan, promptIdentifier);
let bundleFailMsg: string | undefined;
let createFailMsg: string | undefined;
const session = await teleportToRemote({
initialMessage: prompt,
description: blurb || 'Refine local plan',
model,
// model,
permissionMode: 'plan',
ultraplan: true,
signal,
@@ -360,15 +393,19 @@ async function launchDetached(opts: {
onBundleFail: msg => {
bundleFailMsg = msg;
},
});
onCreateFail: msg => {
createFailMsg = msg;
},
})
if (!session) {
let failMsg = bundleFailMsg ?? createFailMsg;
logEvent('tengu_ultraplan_create_failed', {
reason: (bundleFailMsg
? 'bundle_fail'
: 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
: createFailMsg ? 'create_api_fail' : 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
enqueuePendingNotification({
value: `ultraplan: session creation failed${bundleFailMsg ? `${bundleFailMsg}` : ''}. See --debug for details.`,
value: `ultraplan: session creation failed${failMsg ? `${failMsg}` : ''}. See --debug for details.`,
mode: 'task-notification',
});
return;
@@ -384,7 +421,8 @@ async function launchDetached(opts: {
onSessionReady?.(buildSessionReadyMessage(url));
logEvent('tengu_ultraplan_launched', {
has_seed_plan: Boolean(seedPlan),
model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
prompt_identifier: promptIdentifier as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
// model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
// TODO(#23985): replace registerRemoteAgentTask + startDetachedPoll with
// ExitPlanModeScanner inside startRemoteSessionPolling.
@@ -400,6 +438,11 @@ async function launchDetached(opts: {
isUltraplan: true,
});
startDetachedPoll(taskId, session.id, url, getAppState, setAppState);
registerCleanup(async()=>{
if(getAppState().ultraplanSessionUrl === url) {
await archiveRemoteSession(session.id, 1500)
}
});
} catch (e) {
logError(e);
logEvent('tengu_ultraplan_create_failed', {
@@ -409,6 +452,13 @@ async function launchDetached(opts: {
value: `ultraplan: unexpected error — ${errorMessage(e)}`,
mode: 'task-notification',
});
enqueuePendingNotification({
value: `Ultraplan hit an unexpected error during launch. Wait for the user's next instructions.`,
mode: 'task-notification',
isMeta: true
});
if (sessionId) {
// Error after teleport succeeded — archive so the remote doesn't sit
// running for 30min with nobody polling it.
@@ -417,11 +467,11 @@ async function launchDetached(opts: {
);
// ultraplanSessionUrl may have been set before the throw; clear it so
// the "already polling" guard doesn't block future launches.
setAppState(prev => (prev.ultraplanSessionUrl ? { ...prev, ultraplanSessionUrl: undefined } : prev));
setAppState(prev => prev.ultraplanSessionUrl ? { ...prev, ultraplanSessionUrl: undefined } : prev);
}
} finally {
// No-op on success: the url-setting setAppState already cleared this.
setAppState(prev => (prev.ultraplanLaunching ? { ...prev, ultraplanLaunching: undefined } : prev));
setAppState(prev => prev.ultraplanLaunching ? { ...prev, ultraplanLaunching: undefined } : prev);
}
}
@@ -469,6 +519,7 @@ export default {
name: 'ultraplan',
description: `~1030 min · Claude Code on the web drafts an advanced plan you can edit and approve. See ${CCR_TERMS_URL}`,
argumentHint: '<prompt>',
isEnabled: () => true,
// isEnabled: () => process.env.USER_TYPE === 'ant',
isEnabled: () => isUltraplanEnabled(),
load: () => Promise.resolve({ call }),
} satisfies Command;