mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
203
ccw/frontend/src/lib/unifiedExecutionDispatcher.ts
Normal file
203
ccw/frontend/src/lib/unifiedExecutionDispatcher.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user