mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
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:
@@ -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: `~10–30 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;
|
||||
|
||||
Reference in New Issue
Block a user