Skip to content

Commit 9da7345

Browse files
authored
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
1 parent 8137b66 commit 9da7345

11 files changed

Lines changed: 293 additions & 145 deletions

File tree

src/commands/ultraplan.tsx

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,40 @@ import { ALL_MODEL_CONFIGS } from '../utils/model/configs.js';
2525
import { updateTaskState } from '../utils/task/framework.js';
2626
import { archiveRemoteSession, teleportToRemote } from '../utils/teleport.js';
2727
import { pollForApprovedExitPlanMode, UltraplanPollError } from '../utils/ultraplan/ccrSession.js';
28+
import {
29+
getPromptText,
30+
getDialogConfig,
31+
getPromptIdentifier,
32+
type PromptIdentifier
33+
} from '../utils/ultraplan/prompt.js';
34+
import { registerCleanup } from '../utils/cleanupRegistry.js';
35+
2836

2937
// TODO(prod-hardening): OAuth token may go stale over the 30min poll;
3038
// consider refresh.
3139

32-
// Multi-agent exploration is slow; 30min timeout.
40+
/**
41+
* Multi-agent exploration is slow; 30min timeout.
42+
*
43+
* @deprecated use getUltraplanTimeoutMs()
44+
*/
3345
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000;
3446

3547
export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the-web';
3648

49+
export function getUltraplanTimeoutMs(): number {
50+
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_ultraplan_timeout_seconds', 1800) * 1000
51+
}
52+
53+
/**
54+
* 是否启用 ultraplan, 默认启用
55+
*
56+
* @returns
57+
*/
58+
export function isUltraplanEnabled(): boolean {
59+
return getFeatureValue_CACHED_MAY_BE_STALE<{enabled: boolean} | null>('tengu_ultraplan_config', { enabled: true })?.enabled === true
60+
}
61+
3762
// CCR runs against the first-party API — use the canonical ID, not the
3863
// provider-specific string getModelStrings() would return (which may be a
3964
// 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
6287
// so the override path is DCE'd from external builds).
6388
// Shell-set env only, so top-level process.env read is fine
6489
// — settings.env never injects this.
90+
// @deprecated use buildUltraplanPrompt()
6591
/* 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) */
6692
const ULTRAPLAN_INSTRUCTIONS: string =
6793
process.env.USER_TYPE === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE
@@ -73,12 +99,14 @@ const ULTRAPLAN_INSTRUCTIONS: string =
7399
* Assemble the initial CCR user message. seedPlan and blurb stay outside the
74100
* system-reminder so the browser renders them; scaffolding is hidden.
75101
*/
76-
export function buildUltraplanPrompt(blurb: string, seedPlan?: string): string {
102+
export function buildUltraplanPrompt(blurb: string, seedPlan?: string, promptId?: PromptIdentifier): string {
77103
const parts: string[] = [];
78104
if (seedPlan) {
79105
parts.push('Here is a draft plan to refine:', '', seedPlan, '');
80106
}
81-
parts.push(ULTRAPLAN_INSTRUCTIONS);
107+
// parts.push(ULTRAPLAN_INSTRUCTIONS)
108+
parts.push(getPromptText(promptId!));
109+
82110
if (blurb) {
83111
parts.push('', blurb);
84112
}
@@ -98,7 +126,7 @@ function startDetachedPoll(
98126
try {
99127
const { plan, rejectCount, executionTarget } = await pollForApprovedExitPlanMode(
100128
sessionId,
101-
ULTRAPLAN_TIMEOUT_MS,
129+
getUltraplanTimeoutMs(),
102130
phase => {
103131
if (phase === 'needs_input') logEvent('tengu_ultraplan_awaiting_input', {});
104132
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => {
@@ -258,6 +286,7 @@ export async function stopUltraplan(
258286
export async function launchUltraplan(opts: {
259287
blurb: string;
260288
seedPlan?: string;
289+
promptIdentifier?: PromptIdentifier;
261290
getAppState: () => AppState;
262291
setAppState: (f: (prev: AppState) => AppState) => void;
263292
signal: AbortSignal;
@@ -272,7 +301,7 @@ export async function launchUltraplan(opts: {
272301
*/
273302
onSessionReady?: (msg: string) => void;
274303
}): Promise<string> {
275-
const { blurb, seedPlan, getAppState, setAppState, signal, disconnectedBridge, onSessionReady } = opts;
304+
const { blurb, seedPlan, promptIdentifier, getAppState, setAppState, signal, disconnectedBridge, onSessionReady } = opts;
276305

277306
const { ultraplanSessionUrl: active, ultraplanLaunching } = getAppState();
278307
if (active || ultraplanLaunching) {
@@ -292,22 +321,24 @@ export async function launchUltraplan(opts: {
292321
'Usage: /ultraplan \\<prompt\\>, or include "ultraplan" anywhere',
293322
'in your prompt',
294323
'',
295-
'Advanced multi-agent plan mode with our most powerful model',
296-
'(Opus). Runs in Claude Code on the web. When the plan is ready,',
297-
'you can execute it in the web session or send it back here.',
298-
'Terminal stays free while the remote plans.',
299-
'Requires /login.',
324+
// 'Advanced multi-agent plan mode with our most powerful model',
325+
// '(Opus). Runs in Claude Code on the web. When the plan is ready,',
326+
// 'you can execute it in the web session or send it back here.',
327+
// 'Terminal stays free while the remote plans.',
328+
// 'Requires /login.',
329+
...getDialogConfig().usageBlurb,
300330
'',
301331
`Terms: ${CCR_TERMS_URL}`,
302332
].join('\n');
303333
}
304334

305335
// Set synchronously before the detached flow to prevent duplicate launches
306336
// during the teleportToRemote window.
307-
setAppState(prev => (prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true }));
337+
setAppState(prev => prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true });
308338
void launchDetached({
309339
blurb,
310340
seedPlan,
341+
promptIdentifier,
311342
getAppState,
312343
setAppState,
313344
signal,
@@ -319,56 +350,62 @@ export async function launchUltraplan(opts: {
319350
async function launchDetached(opts: {
320351
blurb: string;
321352
seedPlan?: string;
353+
promptIdentifier?: PromptIdentifier;
322354
getAppState: () => AppState;
323355
setAppState: (f: (prev: AppState) => AppState) => void;
324356
signal: AbortSignal;
325357
onSessionReady?: (msg: string) => void;
326358
}): Promise<void> {
327-
const { blurb, seedPlan, getAppState, setAppState, signal, onSessionReady } = opts;
359+
const { blurb, seedPlan, promptIdentifier = getPromptIdentifier(), getAppState, setAppState, signal, onSessionReady } = opts;
328360
// Hoisted so the catch block can archive the remote session if an error
329361
// occurs after teleportToRemote succeeds (avoids 30min orphan).
330362
let sessionId: string | undefined;
331363
try {
332-
const model = getUltraplanModel();
364+
// const model = getUltraplanModel()
333365

334366
const eligibility = await checkRemoteAgentEligibility();
335367
if (!eligibility.eligible) {
336368
logEvent('tengu_ultraplan_create_failed', {
337369
reason: 'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
338-
precondition_errors: (eligibility as { errors: Array<{ type: string }> }).errors
370+
precondition_errors: eligibility.errors
339371
.map(e => e.type)
340372
.join(',') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
341373
});
342-
const reasons = (eligibility as { errors: Array<{ type: string }> }).errors.map(formatPreconditionError).join('\n');
374+
const reasons = eligibility.errors.map(formatPreconditionError).join('\n');
343375
enqueuePendingNotification({
344376
value: `ultraplan: cannot launch remote session —\n${reasons}`,
345377
mode: 'task-notification',
346378
});
347379
return;
348380
}
349381

350-
const prompt = buildUltraplanPrompt(blurb, seedPlan);
382+
const prompt = buildUltraplanPrompt(blurb, seedPlan, promptIdentifier);
351383
let bundleFailMsg: string | undefined;
384+
let createFailMsg: string | undefined;
352385
const session = await teleportToRemote({
353386
initialMessage: prompt,
354387
description: blurb || 'Refine local plan',
355-
model,
388+
// model,
356389
permissionMode: 'plan',
357390
ultraplan: true,
358391
signal,
359392
useDefaultEnvironment: true,
360393
onBundleFail: msg => {
361394
bundleFailMsg = msg;
362395
},
363-
});
396+
onCreateFail: msg => {
397+
createFailMsg = msg;
398+
},
399+
})
364400
if (!session) {
401+
let failMsg = bundleFailMsg ?? createFailMsg;
365402
logEvent('tengu_ultraplan_create_failed', {
366403
reason: (bundleFailMsg
367404
? 'bundle_fail'
368-
: 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
405+
: createFailMsg ? 'create_api_fail' : 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
369406
});
370407
enqueuePendingNotification({
371-
value: `ultraplan: session creation failed${bundleFailMsg ? ` — ${bundleFailMsg}` : ''}. See --debug for details.`,
408+
value: `ultraplan: session creation failed${failMsg ? ` — ${failMsg}` : ''}. See --debug for details.`,
372409
mode: 'task-notification',
373410
});
374411
return;
@@ -384,7 +421,8 @@ async function launchDetached(opts: {
384421
onSessionReady?.(buildSessionReadyMessage(url));
385422
logEvent('tengu_ultraplan_launched', {
386423
has_seed_plan: Boolean(seedPlan),
387-
model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
424+
prompt_identifier: promptIdentifier as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
425+
// model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
388426
});
389427
// TODO(#23985): replace registerRemoteAgentTask + startDetachedPoll with
390428
// ExitPlanModeScanner inside startRemoteSessionPolling.
@@ -400,6 +438,11 @@ async function launchDetached(opts: {
400438
isUltraplan: true,
401439
});
402440
startDetachedPoll(taskId, session.id, url, getAppState, setAppState);
441+
registerCleanup(async()=>{
442+
if(getAppState().ultraplanSessionUrl === url) {
443+
await archiveRemoteSession(session.id, 1500)
444+
}
445+
});
403446
} catch (e) {
404447
logError(e);
405448
logEvent('tengu_ultraplan_create_failed', {
@@ -409,6 +452,13 @@ async function launchDetached(opts: {
409452
value: `ultraplan: unexpected error — ${errorMessage(e)}`,
410453
mode: 'task-notification',
411454
});
455+
456+
enqueuePendingNotification({
457+
value: `Ultraplan hit an unexpected error during launch. Wait for the user's next instructions.`,
458+
mode: 'task-notification',
459+
isMeta: true
460+
});
461+
412462
if (sessionId) {
413463
// Error after teleport succeeded — archive so the remote doesn't sit
414464
// running for 30min with nobody polling it.
@@ -417,11 +467,11 @@ async function launchDetached(opts: {
417467
);
418468
// ultraplanSessionUrl may have been set before the throw; clear it so
419469
// the "already polling" guard doesn't block future launches.
420-
setAppState(prev => (prev.ultraplanSessionUrl ? { ...prev, ultraplanSessionUrl: undefined } : prev));
470+
setAppState(prev => prev.ultraplanSessionUrl ? { ...prev, ultraplanSessionUrl: undefined } : prev);
421471
}
422472
} finally {
423473
// No-op on success: the url-setting setAppState already cleared this.
424-
setAppState(prev => (prev.ultraplanLaunching ? { ...prev, ultraplanLaunching: undefined } : prev));
474+
setAppState(prev => prev.ultraplanLaunching ? { ...prev, ultraplanLaunching: undefined } : prev);
425475
}
426476
}
427477

@@ -469,6 +519,7 @@ export default {
469519
name: 'ultraplan',
470520
description: `~10–30 min · Claude Code on the web drafts an advanced plan you can edit and approve. See ${CCR_TERMS_URL}`,
471521
argumentHint: '<prompt>',
472-
isEnabled: () => true,
522+
// isEnabled: () => process.env.USER_TYPE === 'ant',
523+
isEnabled: () => isUltraplanEnabled(),
473524
load: () => Promise.resolve({ call }),
474525
} satisfies Command;

0 commit comments

Comments
 (0)