round robin
This commit is contained in:
@@ -108,12 +108,20 @@ Create config files. Both are optional:
|
|||||||
```yaml
|
```yaml
|
||||||
execution:
|
execution:
|
||||||
maxParallel: 3 # ralpi-level concurrency only
|
maxParallel: 3 # ralpi-level concurrency only
|
||||||
|
models: # round-robin in <provider>/<model> format
|
||||||
|
- google/gemini-3.5-flash # 1st and 3rd task in parallel
|
||||||
|
- openai/gpt-5.5 # 2nd task in parallel
|
||||||
prompts:
|
prompts:
|
||||||
projectContext: "Additional context for all tasks"
|
projectContext: "Additional context for all tasks"
|
||||||
```
|
```
|
||||||
|
|
||||||
> ralpi deliberately does **not** set timeouts or retries — those are inherited
|
> ralpi deliberately does **not** set timeouts or retries — those are inherited
|
||||||
> from Pi's own settings. Tasks run until they complete or Pi's own flow stops them.
|
> from Pi's own settings. Tasks run until they complete or Pi's own flow stops them.
|
||||||
|
>
|
||||||
|
> `execution.models` uses slot-aware round-robin: with 3 models and 2 concurrent
|
||||||
|
> tasks, only the first two models are used. The third model is only touched when
|
||||||
|
> a third concurrent task starts. Freed model slots are reused before new ones
|
||||||
|
> are allocated.
|
||||||
|
|
||||||
The keys mirror the nested structure of `RalpiConfig` in `src/types.ts`.
|
The keys mirror the nested structure of `RalpiConfig` in `src/types.ts`.
|
||||||
|
|
||||||
|
|||||||
10
index.ts
10
index.ts
@@ -220,6 +220,16 @@ export default function ralpiLoopExtension(pi: ExtensionAPI): void {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Register the extension's prompts/ directory so Pi discovers @task-manager
|
||||||
|
pi.on("resources_discover", async (_event, _ctx) => {
|
||||||
|
const promptsDir = fs.existsSync(path.resolve(__dirname, "prompts"))
|
||||||
|
? path.resolve(__dirname, "prompts")
|
||||||
|
: path.resolve(__dirname, "..", "prompts");
|
||||||
|
return {
|
||||||
|
promptPaths: [promptsDir],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
pi.registerCommand("ralpi", {
|
pi.registerCommand("ralpi", {
|
||||||
description:
|
description:
|
||||||
"Execute tasks from a task file using DAG-based dependency resolution",
|
"Execute tasks from a task file using DAG-based dependency resolution",
|
||||||
|
|||||||
@@ -34,6 +34,53 @@ const MAX_COLLAPSED = 3;
|
|||||||
|
|
||||||
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||||
|
|
||||||
|
// ─── Model Round-Robin ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round-robin model assignment with slot reuse.
|
||||||
|
*
|
||||||
|
* With models [A, B, C] and 2 concurrent tasks, only A and B are used.
|
||||||
|
* Model C is only touched when a third concurrent task starts.
|
||||||
|
* Freed slots are reused before new slots are allocated.
|
||||||
|
*/
|
||||||
|
class ModelRoundRobin {
|
||||||
|
private models: unknown[];
|
||||||
|
private freeSlots: number[];
|
||||||
|
private nextIndex = 0;
|
||||||
|
private assignments = new Map<string, number>();
|
||||||
|
|
||||||
|
constructor(models: unknown[]) {
|
||||||
|
this.models = models;
|
||||||
|
this.freeSlots = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(taskId: string): unknown {
|
||||||
|
let index: number;
|
||||||
|
if (this.freeSlots.length > 0) {
|
||||||
|
// Reuse a freed model slot first
|
||||||
|
index = this.freeSlots.shift()!;
|
||||||
|
} else if (this.nextIndex < this.models.length) {
|
||||||
|
// Allocate a new slot
|
||||||
|
index = this.nextIndex++;
|
||||||
|
} else {
|
||||||
|
// All models in use — wrap around
|
||||||
|
index = this.nextIndex % this.models.length;
|
||||||
|
this.nextIndex++;
|
||||||
|
}
|
||||||
|
this.assignments.set(taskId, index);
|
||||||
|
return this.models[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
release(taskId: string): void {
|
||||||
|
const index = this.assignments.get(taskId);
|
||||||
|
if (index !== undefined) {
|
||||||
|
this.freeSlots.push(index);
|
||||||
|
this.freeSlots.sort((a, b) => a - b);
|
||||||
|
this.assignments.delete(taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Shared state for parallel-batch widget. Each running task writes its
|
/** Shared state for parallel-batch widget. Each running task writes its
|
||||||
* tool calls and spinner frame; the batch widget reads them in task-ID order. */
|
* tool calls and spinner frame; the batch widget reads them in task-ID order. */
|
||||||
interface ParallelWidgetEntry {
|
interface ParallelWidgetEntry {
|
||||||
@@ -61,6 +108,7 @@ export async function runTask(
|
|||||||
sendChatMessage?: SendChatMessage,
|
sendChatMessage?: SendChatMessage,
|
||||||
projectDir: string = project.sourceDir,
|
projectDir: string = project.sourceDir,
|
||||||
parallelState?: ParallelWidgetState,
|
parallelState?: ParallelWidgetState,
|
||||||
|
assignedModel?: unknown,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
reflection?: Reflection;
|
reflection?: Reflection;
|
||||||
@@ -190,7 +238,7 @@ export async function runTask(
|
|||||||
},
|
},
|
||||||
undefined, // no abort signal
|
undefined, // no abort signal
|
||||||
sessionFilePath, // stream events to file
|
sessionFilePath, // stream events to file
|
||||||
config.model,
|
assignedModel ?? config.model,
|
||||||
config.thinkingLevel,
|
config.thinkingLevel,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -275,6 +323,37 @@ export async function executeBatch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up model round-robin if configured.
|
||||||
|
// Config entries are "<provider>/<model>" strings — resolve via modelRegistry.
|
||||||
|
let roundRobin: ModelRoundRobin | null = null;
|
||||||
|
if (config.execution.models.length > 0) {
|
||||||
|
const resolvedModels: unknown[] = [];
|
||||||
|
for (const entry of config.execution.models) {
|
||||||
|
const slashIdx = entry.indexOf("/");
|
||||||
|
if (slashIdx === -1) {
|
||||||
|
ctx.ui.notify(
|
||||||
|
`ralpi config: skipping model "${entry}" — expected <provider>/<model> format`,
|
||||||
|
"warning",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const provider = entry.slice(0, slashIdx);
|
||||||
|
const modelId = entry.slice(slashIdx + 1);
|
||||||
|
const resolved = ctx.modelRegistry?.find(provider, modelId);
|
||||||
|
if (resolved) {
|
||||||
|
resolvedModels.push(resolved);
|
||||||
|
} else {
|
||||||
|
ctx.ui.notify(
|
||||||
|
`ralpi config: model "${entry}" not found in registry — skipping`,
|
||||||
|
"warning",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resolvedModels.length > 0) {
|
||||||
|
roundRobin = new ModelRoundRobin(resolvedModels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we should run parallel
|
// Check if we should run parallel
|
||||||
const shouldParallel =
|
const shouldParallel =
|
||||||
options?.parallel && tasks.length > 1 && config.execution.maxParallel > 0;
|
options?.parallel && tasks.length > 1 && config.execution.maxParallel > 0;
|
||||||
@@ -288,12 +367,14 @@ export async function executeBatch(
|
|||||||
ctx,
|
ctx,
|
||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
projectDir,
|
projectDir,
|
||||||
|
roundRobin,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute sequentially
|
// Execute sequentially
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
|
const model = roundRobin?.assign(task.id);
|
||||||
await executeTask(
|
await executeTask(
|
||||||
task,
|
task,
|
||||||
project,
|
project,
|
||||||
@@ -302,7 +383,10 @@ export async function executeBatch(
|
|||||||
ctx,
|
ctx,
|
||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
projectDir,
|
projectDir,
|
||||||
|
undefined,
|
||||||
|
model,
|
||||||
);
|
);
|
||||||
|
roundRobin?.release(task.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +401,7 @@ async function executeBatchParallel(
|
|||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
sendChatMessage?: SendChatMessage,
|
sendChatMessage?: SendChatMessage,
|
||||||
projectDir?: string,
|
projectDir?: string,
|
||||||
|
roundRobin?: ModelRoundRobin | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const maxParallel = config.execution.maxParallel;
|
const maxParallel = config.execution.maxParallel;
|
||||||
const sharedState: ParallelWidgetState = new Map();
|
const sharedState: ParallelWidgetState = new Map();
|
||||||
@@ -385,6 +470,7 @@ async function executeBatchParallel(
|
|||||||
const results: Array<{ task: Task; result: Promise<any> }> = [];
|
const results: Array<{ task: Task; result: Promise<any> }> = [];
|
||||||
|
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
|
const assignedModel = roundRobin?.assign(task.id);
|
||||||
results.push({
|
results.push({
|
||||||
task,
|
task,
|
||||||
result: executeTask(
|
result: executeTask(
|
||||||
@@ -396,7 +482,8 @@ async function executeBatchParallel(
|
|||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
projectDir,
|
projectDir,
|
||||||
sharedState,
|
sharedState,
|
||||||
),
|
assignedModel,
|
||||||
|
).finally(() => roundRobin?.release(task.id)),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Limit concurrency
|
// Limit concurrency
|
||||||
@@ -426,6 +513,7 @@ async function executeTask(
|
|||||||
sendChatMessage?: SendChatMessage,
|
sendChatMessage?: SendChatMessage,
|
||||||
projectDir: string = project.sourceDir,
|
projectDir: string = project.sourceDir,
|
||||||
parallelState?: ParallelWidgetState,
|
parallelState?: ParallelWidgetState,
|
||||||
|
assignedModel?: unknown,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const maxRetries = config.execution.maxRetries;
|
const maxRetries = config.execution.maxRetries;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
@@ -450,6 +538,7 @@ async function executeTask(
|
|||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
projectDir,
|
projectDir,
|
||||||
parallelState,
|
parallelState,
|
||||||
|
assignedModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ export interface RalpiConfig {
|
|||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
/** Maximum parallel tasks (0 = unlimited) */
|
/** Maximum parallel tasks (0 = unlimited) */
|
||||||
maxParallel: number;
|
maxParallel: number;
|
||||||
|
/** Round-robin model list for parallel tasks (empty = inherit parent model) */
|
||||||
|
models: string[];
|
||||||
};
|
};
|
||||||
prompts: {
|
prompts: {
|
||||||
/** Additional context injected into every task prompt */
|
/** Additional context injected into every task prompt */
|
||||||
@@ -176,6 +178,7 @@ export const DEFAULT_CONFIG: RalpiConfig = {
|
|||||||
retryDelayMs: 0,
|
retryDelayMs: 0,
|
||||||
timeoutMs: 0, // 0 = inherit Pi's own defaults (no ralpi-level timeout)
|
timeoutMs: 0, // 0 = inherit Pi's own defaults (no ralpi-level timeout)
|
||||||
maxParallel: 3,
|
maxParallel: 3,
|
||||||
|
models: [],
|
||||||
},
|
},
|
||||||
prompts: {
|
prompts: {
|
||||||
projectContext: "",
|
projectContext: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user