pipeline executor improvements, proxy refresh, task runner step list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,7 @@ type PipelineStep = {
|
||||
inputs?: Record<string, string>;
|
||||
foreach?: 'subdirectory';
|
||||
skip_if?: string;
|
||||
concurrency?: string;
|
||||
concurrency?: string | boolean;
|
||||
};
|
||||
|
||||
type PipelineConfig = {
|
||||
@@ -70,17 +70,29 @@ type RunStepParams = {
|
||||
taskDirName: string;
|
||||
prompt: string;
|
||||
cwd: string;
|
||||
model: string;
|
||||
abortSignal: AbortSignal;
|
||||
emit: EmitEvent;
|
||||
stepIndex: number;
|
||||
iterationLabel?: string;
|
||||
};
|
||||
|
||||
async function runAgenticStep({ userId, email, username, role, taskDirName, prompt, cwd, abortSignal, emit, stepIndex, iterationLabel }: RunStepParams): Promise<MessageCost> {
|
||||
async function refreshProxyToken(): Promise<void> {
|
||||
const port = process.env.ANTHROPIC_PROXY_PORT ?? '5051';
|
||||
try {
|
||||
await fetch(`http://127.0.0.1:${port}/refresh`, { method: 'POST' });
|
||||
} catch {
|
||||
// Best effort — proxy may not be running (e.g. using API key directly)
|
||||
}
|
||||
}
|
||||
|
||||
async function runAgenticStep({ userId, email, username, role, taskDirName, prompt, cwd, model, abortSignal, emit, stepIndex, iterationLabel }: RunStepParams): Promise<MessageCost> {
|
||||
const sessionId = randomUUID();
|
||||
const model = await resolveModel(userId);
|
||||
const isClaudeCode = model.startsWith('claude-code');
|
||||
|
||||
// Ensure fresh OAuth token before spawning Claude Code
|
||||
if (isClaudeCode) await refreshProxyToken();
|
||||
|
||||
console.log(`[pipeline] starting step ${stepIndex} for session ${sessionId} (model=${model})${iterationLabel ? ` [${iterationLabel}]` : ''}`);
|
||||
|
||||
return new Promise<MessageCost>(async (resolve, reject) => {
|
||||
@@ -312,7 +324,7 @@ async function runScriptStep({ email, role, task, inputs, cwd, abortSignal, emit
|
||||
|
||||
// ── Parallel foreach ──
|
||||
|
||||
type ParallelForeachParams = {
|
||||
type ForeachParams = {
|
||||
userId: number;
|
||||
email: string;
|
||||
username: string;
|
||||
@@ -327,13 +339,14 @@ type ParallelForeachParams = {
|
||||
abortSignal: AbortSignal;
|
||||
totalCost: MessageCost;
|
||||
emit: EmitEvent;
|
||||
concurrency: number;
|
||||
model: string;
|
||||
};
|
||||
|
||||
async function runParallelForeach({
|
||||
async function runForeach({
|
||||
userId, email, username, role, stepIdx, step, stepTask, subdirs, baseCwd,
|
||||
inputs, cwd, abortSignal, totalCost, emit,
|
||||
}: ParallelForeachParams) {
|
||||
const concurrency = Math.max(1, parseInt(step.concurrency ?? '1', 10));
|
||||
inputs, cwd, abortSignal, totalCost, emit, concurrency, model,
|
||||
}: ForeachParams) {
|
||||
|
||||
// Determine skip vs run
|
||||
const toSkip: string[] = [];
|
||||
@@ -389,6 +402,7 @@ async function runParallelForeach({
|
||||
taskDirName: step.task,
|
||||
prompt,
|
||||
cwd: resolvedCwd,
|
||||
model,
|
||||
abortSignal,
|
||||
emit,
|
||||
stepIndex: stepIdx,
|
||||
@@ -428,11 +442,13 @@ export type ExecutePipelineParams = {
|
||||
taskDirName: string;
|
||||
inputs: Record<string, string>;
|
||||
cwd?: string;
|
||||
model?: string;
|
||||
startAt?: number;
|
||||
abortSignal: AbortSignal;
|
||||
emit: EmitEvent;
|
||||
};
|
||||
|
||||
export async function executePipeline({ userId, email, username, role, taskDirName, inputs, cwd, abortSignal, emit }: ExecutePipelineParams): Promise<void> {
|
||||
export async function executePipeline({ userId, email, username, role, taskDirName, inputs, cwd, model: modelOverride, startAt, abortSignal, emit }: ExecutePipelineParams): Promise<void> {
|
||||
const pipelineTask = await getTaskByDirName(taskDirName, userId);
|
||||
if (!pipelineTask) {
|
||||
emit({ type: 'error', message: `Task not found: ${taskDirName}` });
|
||||
@@ -450,20 +466,30 @@ export async function executePipeline({ userId, email, username, role, taskDirNa
|
||||
}
|
||||
|
||||
const baseCwd = resolveBaseCwd(email, role, cwd);
|
||||
const model = modelOverride || (await resolveModel(userId));
|
||||
const totalCost: MessageCost = { inputTokens: 0, outputTokens: 0, totalUSD: 0 };
|
||||
|
||||
// Resolve concurrency from user input (default 1)
|
||||
const runtimeConcurrency = Math.max(1, parseInt(inputs._concurrency ?? '1', 10));
|
||||
|
||||
emit({
|
||||
type: 'pipeline:init',
|
||||
steps: config.steps.map((s) => ({
|
||||
task: s.task,
|
||||
foreach: s.foreach,
|
||||
concurrency: s.concurrency ? parseInt(s.concurrency, 10) : undefined,
|
||||
concurrency: s.concurrency ? runtimeConcurrency : undefined,
|
||||
})),
|
||||
});
|
||||
|
||||
for (let stepIdx = 0; stepIdx < config.steps.length; stepIdx++) {
|
||||
if (abortSignal.aborted) break;
|
||||
|
||||
// Skip steps before startAt
|
||||
if (startAt && stepIdx < startAt) {
|
||||
emit({ type: 'step:skip', stepIndex: stepIdx, label: config.steps[stepIdx]!.task, reason: 'skipped (start at)' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const step = config.steps[stepIdx]!;
|
||||
|
||||
const stepTask = await getTaskByDirName(step.task, userId);
|
||||
@@ -527,65 +553,13 @@ export async function executePipeline({ userId, email, username, role, taskDirNa
|
||||
return;
|
||||
}
|
||||
|
||||
const concurrency = parseInt(step.concurrency ?? '1', 10);
|
||||
const concurrency = step.concurrency ? runtimeConcurrency : 1;
|
||||
|
||||
if (concurrency > 1) {
|
||||
await runParallelForeach({
|
||||
userId, email, username, role,
|
||||
stepIdx, step, stepTask: { name: stepTask.name, body: stepTask.body! },
|
||||
subdirs, baseCwd, inputs, cwd, abortSignal, totalCost, emit,
|
||||
});
|
||||
} else {
|
||||
// Sequential
|
||||
for (let i = 0; i < subdirs.length; i++) {
|
||||
if (abortSignal.aborted) break;
|
||||
|
||||
const subdir = subdirs[i]!;
|
||||
const subdirPath = join(baseCwd, subdir);
|
||||
|
||||
if (step.skip_if && existsSync(join(subdirPath, step.skip_if))) {
|
||||
emit({ type: 'step:skip', stepIndex: stepIdx, label: subdir, reason: `${step.skip_if} already exists` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const iterVars = { ...inputs, folder_name: subdir };
|
||||
const iterInputs: Record<string, string> = {};
|
||||
if (step.inputs) {
|
||||
for (const [key, template] of Object.entries(step.inputs)) {
|
||||
iterInputs[key] = resolveInputTemplate(template, iterVars);
|
||||
}
|
||||
}
|
||||
|
||||
emit({
|
||||
type: 'step:start',
|
||||
stepIndex: stepIdx,
|
||||
taskName: stepTask.name,
|
||||
iteration: { current: i + 1, total: subdirs.length, label: subdir },
|
||||
});
|
||||
|
||||
const cwdRelative = cwd ? `${cwd}/${subdir}` : subdir;
|
||||
const resolvedCwd = resolveBaseCwd(email, role, cwdRelative);
|
||||
const targetDir = toAgentPath(resolvedCwd, email, role);
|
||||
const prompt = buildStepPrompt(stepTask.body!,iterInputs, targetDir);
|
||||
|
||||
const cost = await runAgenticStep({
|
||||
userId, email, username, role,
|
||||
taskDirName: step.task,
|
||||
prompt,
|
||||
cwd: resolvedCwd,
|
||||
abortSignal,
|
||||
emit,
|
||||
stepIndex: stepIdx,
|
||||
iterationLabel: subdir,
|
||||
});
|
||||
|
||||
totalCost.inputTokens += cost.inputTokens;
|
||||
totalCost.outputTokens += cost.outputTokens;
|
||||
totalCost.totalUSD += cost.totalUSD;
|
||||
|
||||
emit({ type: 'step:complete', stepIndex: stepIdx, cost });
|
||||
}
|
||||
}
|
||||
await runForeach({
|
||||
userId, email, username, role,
|
||||
stepIdx, step, stepTask: { name: stepTask.name, body: stepTask.body! },
|
||||
subdirs, baseCwd, inputs, cwd, abortSignal, totalCost, emit, concurrency, model,
|
||||
});
|
||||
} else {
|
||||
// Single execution step
|
||||
emit({ type: 'step:start', stepIndex: stepIdx, taskName: stepTask.name });
|
||||
@@ -598,6 +572,7 @@ export async function executePipeline({ userId, email, username, role, taskDirNa
|
||||
taskDirName: step.task,
|
||||
prompt,
|
||||
cwd: baseCwd,
|
||||
model,
|
||||
abortSignal,
|
||||
emit,
|
||||
stepIndex: stepIdx,
|
||||
@@ -629,7 +604,7 @@ type WSData = {
|
||||
};
|
||||
|
||||
type ClientMessage =
|
||||
| { type: 'run'; taskDirName: string; inputs: Record<string, string>; cwd?: string }
|
||||
| { type: 'run'; taskDirName: string; inputs: Record<string, string>; cwd?: string; model?: string; startAt?: number }
|
||||
| { type: 'stop'; jobId: string }
|
||||
| { type: 'attach'; jobId: string }
|
||||
| { type: 'list' };
|
||||
@@ -667,6 +642,8 @@ export async function message(ws: ServerWebSocket<WSData>, raw: string | Buffer)
|
||||
inputs: msg.inputs,
|
||||
cwd: msg.cwd,
|
||||
config: task.config,
|
||||
model: msg.model,
|
||||
startAt: msg.startAt,
|
||||
});
|
||||
|
||||
send(ws, { type: 'job:created', jobId });
|
||||
|
||||
@@ -67,6 +67,8 @@ type StartJobParams = {
|
||||
inputs: Record<string, string>;
|
||||
cwd?: string;
|
||||
config: unknown;
|
||||
model?: string;
|
||||
startAt?: number;
|
||||
};
|
||||
|
||||
export async function startJob(params: StartJobParams): Promise<string> {
|
||||
@@ -146,6 +148,8 @@ export async function startJob(params: StartJobParams): Promise<string> {
|
||||
taskDirName: params.taskDirName,
|
||||
inputs: params.inputs,
|
||||
cwd: params.cwd,
|
||||
model: params.model,
|
||||
startAt: params.startAt,
|
||||
abortSignal: job.abortSignal,
|
||||
emit,
|
||||
}).then(async () => {
|
||||
|
||||
Reference in New Issue
Block a user