Add orchestrator types and error handling configurations

- Introduced new TypeScript types for orchestrator functionality, including `SessionStrategy`, `ErrorHandlingStrategy`, and `OrchestrationStep`.
- Defined interfaces for `OrchestrationPlan` and `ManualOrchestrationParams` to facilitate orchestration management.
- Added a new PNG image file for visual representation.
- Created a placeholder file named 'nul' for future use.
This commit is contained in:
catlog22
2026-02-14 12:54:08 +08:00
parent cdb240d2c2
commit 4d22ae4b2f
56 changed files with 4767 additions and 425 deletions

View File

@@ -2039,6 +2039,14 @@ export interface NormalizedTask extends TaskData {
_raw?: unknown;
}
/**
* Normalize files field: handles both old string[] format and new {path}[] format.
*/
function normalizeFilesField(files: unknown): Array<{ path: string; name?: string }> | undefined {
if (!Array.isArray(files) || files.length === 0) return undefined;
return files.map((f: unknown) => typeof f === 'string' ? { path: f } : f) as Array<{ path: string; name?: string }>;
}
/**
* Normalize a raw task object (old 6-field or new unified flat) into NormalizedTask.
* Reads new flat fields first, falls back to old nested paths.
@@ -2049,18 +2057,23 @@ export function normalizeTask(raw: Record<string, unknown>): NormalizedTask {
return { task_id: 'N/A', status: 'pending', _raw: raw } as NormalizedTask;
}
// Type-safe access helpers
const rawContext = raw.context as LiteTask['context'] | undefined;
// Type-safe access helpers (use intersection for broad compat with old/new schemas)
const rawContext = raw.context as (LiteTask['context'] & { requirements?: string[] }) | undefined;
const rawFlowControl = raw.flow_control as FlowControl | undefined;
const rawMeta = raw.meta as LiteTask['meta'] | undefined;
const rawConvergence = raw.convergence as NormalizedTask['convergence'] | undefined;
// Description: new flat field first, then join old context.requirements
// Description: new flat field first, then join old context.requirements, then old details/scope
const rawRequirements = rawContext?.requirements;
const rawDetails = raw.details as string[] | undefined;
const description = (raw.description as string | undefined)
|| (Array.isArray(rawRequirements) && rawRequirements.length > 0
? rawRequirements.join('; ')
: undefined);
: undefined)
|| (Array.isArray(rawDetails) && rawDetails.length > 0
? rawDetails.join('; ')
: undefined)
|| (raw.scope as string | undefined);
return {
// Identity
@@ -2084,7 +2097,7 @@ export function normalizeTask(raw: Record<string, unknown>): NormalizedTask {
// Promoted from flow_control (new first, old fallback)
pre_analysis: (raw.pre_analysis as PreAnalysisStep[]) || rawFlowControl?.pre_analysis,
implementation: (raw.implementation as (ImplementationStep | string)[]) || rawFlowControl?.implementation_approach,
files: (raw.files as Array<{ path: string; name?: string }>) || rawFlowControl?.target_files,
files: normalizeFilesField(raw.files) || rawFlowControl?.target_files,
// Promoted from meta (new first, old fallback)
type: (raw.type as string) || rawMeta?.type,
@@ -5964,8 +5977,23 @@ export async function fetchCcwTools(): Promise<CcwToolInfo[]> {
// ========== Team API ==========
export async function fetchTeams(): Promise<{ teams: Array<{ name: string; messageCount: number; lastActivity: string }> }> {
return fetchApi('/api/teams');
export async function fetchTeams(location?: string): Promise<{ teams: Array<{ name: string; messageCount: number; lastActivity: string; status: string; created_at: string; updated_at: string; archived_at?: string; pipeline_mode?: string; memberCount: number; members?: string[] }> }> {
const params = new URLSearchParams();
if (location) params.set('location', location);
const qs = params.toString();
return fetchApi(`/api/teams${qs ? `?${qs}` : ''}`);
}
export async function archiveTeam(teamName: string): Promise<{ success: boolean; team: string; status: string }> {
return fetchApi(`/api/teams/${encodeURIComponent(teamName)}/archive`, { method: 'POST' });
}
export async function unarchiveTeam(teamName: string): Promise<{ success: boolean; team: string; status: string }> {
return fetchApi(`/api/teams/${encodeURIComponent(teamName)}/unarchive`, { method: 'POST' });
}
export async function deleteTeam(teamName: string): Promise<void> {
return fetchApi<void>(`/api/teams/${encodeURIComponent(teamName)}`, { method: 'DELETE' });
}
export async function fetchTeamMessages(

View File

@@ -92,6 +92,7 @@ export const workspaceQueryKeys = {
prompts: (projectPath: string) => [...workspaceQueryKeys.all(projectPath), 'prompts'] as const,
promptsList: (projectPath: string) => [...workspaceQueryKeys.prompts(projectPath), 'list'] as const,
promptsInsights: (projectPath: string) => [...workspaceQueryKeys.prompts(projectPath), 'insights'] as const,
promptsInsightsHistory: (projectPath: string) => [...workspaceQueryKeys.prompts(projectPath), 'insightsHistory'] as const,
// ========== Index ==========
index: (projectPath: string) => [...workspaceQueryKeys.all(projectPath), 'index'] as const,

View File

@@ -0,0 +1,203 @@
// ========================================
// Unified Execution Dispatcher
// ========================================
// Stateless dispatcher that resolves session strategy and dispatches
// OrchestrationStep execution to the CLI session API.
import type { OrchestrationStep, SessionStrategy } from '../types/orchestrator';
import { createCliSession, executeInCliSession } from './api';
import type { ExecuteInCliSessionInput } from './api';
// ========== Types ==========
/**
* Options for dispatch execution.
* These supplement the step's own configuration with runtime context.
*/
export interface DispatchOptions {
/** Working directory for the CLI session (used when creating new sessions). */
workingDir?: string;
/** Execution category for tracking/filtering. */
category?: ExecuteInCliSessionInput['category'];
/** Resume key for session continuity. */
resumeKey?: string;
/** Resume strategy for the CLI execution. */
resumeStrategy?: ExecuteInCliSessionInput['resumeStrategy'];
/** Project path for API routing. */
projectPath?: string;
}
/**
* Result of a dispatched execution.
* Provides the execution ID for callback registration and the resolved session key.
*/
export interface DispatchResult {
/** Unique execution ID returned by the API, used for tracking and callback chains. */
executionId: string;
/** The session key used for execution (may differ from input if strategy created a new session). */
sessionKey: string;
/** Whether a new CLI session was created for this dispatch. */
isNewSession: boolean;
}
// ========== Session Strategy Resolution ==========
interface ResolvedSession {
sessionKey: string;
isNewSession: boolean;
}
/**
* Resolve the session key based on the step's session strategy.
*
* - 'reuse_default': Use the provided defaultSessionKey directly.
* - 'new_session': Create a new PTY session via the API.
* - 'specific_session': Use the step's targetSessionKey (must be provided).
*/
async function resolveSessionKey(
strategy: SessionStrategy,
defaultSessionKey: string,
step: OrchestrationStep,
options: DispatchOptions
): Promise<ResolvedSession> {
switch (strategy) {
case 'reuse_default':
return { sessionKey: defaultSessionKey, isNewSession: false };
case 'new_session': {
const result = await createCliSession(
{
workingDir: options.workingDir,
tool: step.tool,
},
options.projectPath
);
return { sessionKey: result.session.sessionKey, isNewSession: true };
}
case 'specific_session': {
const targetKey = step.targetSessionKey;
if (!targetKey) {
throw new DispatchError(
`Step "${step.id}" uses 'specific_session' strategy but no targetSessionKey is provided.`,
'MISSING_TARGET_SESSION_KEY'
);
}
return { sessionKey: targetKey, isNewSession: false };
}
default:
throw new DispatchError(
`Unknown session strategy: "${strategy}" on step "${step.id}".`,
'UNKNOWN_SESSION_STRATEGY'
);
}
}
// ========== Error Type ==========
/**
* Typed error for dispatch failures with an error code for programmatic handling.
*/
export class DispatchError extends Error {
constructor(
message: string,
public readonly code: DispatchErrorCode
) {
super(message);
this.name = 'DispatchError';
}
}
export type DispatchErrorCode =
| 'MISSING_TARGET_SESSION_KEY'
| 'UNKNOWN_SESSION_STRATEGY'
| 'SESSION_CREATION_FAILED'
| 'EXECUTION_FAILED';
// ========== Dispatcher ==========
/**
* Dispatch an orchestration step for execution in a CLI session.
*
* This is a stateless utility function that:
* 1. Resolves the session key based on the step's sessionStrategy.
* 2. Calls executeInCliSession() with the resolved session and step parameters.
* 3. Returns the executionId for callback chain registration.
*
* @param step - The orchestration step to execute.
* @param sessionKey - The default session key (used when strategy is 'reuse_default').
* @param options - Additional dispatch options.
* @returns The dispatch result containing executionId and resolved sessionKey.
* @throws {DispatchError} When session resolution or execution fails.
*/
export async function dispatch(
step: OrchestrationStep,
sessionKey: string,
options: DispatchOptions = {}
): Promise<DispatchResult> {
const strategy: SessionStrategy = step.sessionStrategy ?? 'reuse_default';
// Step 1: Resolve session key
let resolved: ResolvedSession;
try {
resolved = await resolveSessionKey(strategy, sessionKey, step, options);
} catch (err) {
if (err instanceof DispatchError) throw err;
throw new DispatchError(
`Failed to resolve session for step "${step.id}": ${err instanceof Error ? err.message : String(err)}`,
'SESSION_CREATION_FAILED'
);
}
// Step 2: Build execution input from step + options
const executionInput: ExecuteInCliSessionInput = {
tool: step.tool ?? 'gemini',
prompt: step.instruction,
mode: mapExecutionMode(step.mode),
workingDir: options.workingDir,
category: options.category,
resumeKey: options.resumeKey ?? step.resumeKey,
resumeStrategy: options.resumeStrategy,
};
// Step 3: Execute in the resolved session
try {
const result = await executeInCliSession(
resolved.sessionKey,
executionInput,
options.projectPath
);
return {
executionId: result.executionId,
sessionKey: resolved.sessionKey,
isNewSession: resolved.isNewSession,
};
} catch (err) {
throw new DispatchError(
`Execution failed for step "${step.id}" in session "${resolved.sessionKey}": ${err instanceof Error ? err.message : String(err)}`,
'EXECUTION_FAILED'
);
}
}
/**
* Map the orchestrator's ExecutionMode to the API's mode parameter.
* The API accepts 'analysis' | 'write' | 'auto', while the orchestrator
* uses a broader set including 'mainprocess' and 'async'.
*/
function mapExecutionMode(
mode?: OrchestrationStep['mode']
): ExecuteInCliSessionInput['mode'] {
if (!mode) return undefined;
switch (mode) {
case 'analysis':
return 'analysis';
case 'write':
return 'write';
default:
// 'mainprocess', 'async', and any future modes default to 'auto'
return 'auto';
}
}