Compare commits
4 Commits
ead5d9be3a
...
ab1e2eb430
| Author | SHA1 | Date | |
|---|---|---|---|
| ab1e2eb430 | |||
| d2ef124369 | |||
| 925e37938b | |||
| 8e2e24d0e3 |
42
README.md
42
README.md
@@ -98,16 +98,46 @@ tasks:
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Create `.ralpi/config.yaml`:
|
Create config files. Both are optional:
|
||||||
|
|
||||||
|
| Scope | Path |
|
||||||
|
|-------|------|
|
||||||
|
| **Global** | `~/.pi/ralpi/config.yaml` |
|
||||||
|
| **Project** | `./.ralpi/config.yaml` |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
maxRetries: 3
|
execution:
|
||||||
retryDelayMs: 5000
|
maxParallel: 3 # ralpi-level concurrency only
|
||||||
timeoutMs: 1800000
|
models: # round-robin in <provider>/<model> format
|
||||||
maxParallel: 3
|
- google/gemini-3.5-flash # 1st and 3rd task in parallel
|
||||||
projectContext: "Additional context for all tasks"
|
- openai/gpt-5.5 # 2nd task in parallel
|
||||||
|
prompts:
|
||||||
|
projectContext: "Additional context for all tasks"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 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.
|
||||||
|
>
|
||||||
|
> `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.
|
||||||
|
>
|
||||||
|
> **Automatic failover**: if a provider/API is unreachable (rate limit, 503, etc.),
|
||||||
|
> the task automatically cycles to the next model in the list without counting it
|
||||||
|
> as a task failure. Each model is tried once before the task is marked as failed.
|
||||||
|
|
||||||
|
The keys mirror the nested structure of `RalpiConfig` in `src/types.ts`.
|
||||||
|
|
||||||
|
### Precedence (highest wins)
|
||||||
|
|
||||||
|
| Priority | Source |
|
||||||
|
|----------|--------|
|
||||||
|
| **1st** | In-memory overrides (`model`, `thinkingLevel` from parent Pi session) |
|
||||||
|
| **2nd** | `./.ralpi/config.yaml` — project-level |
|
||||||
|
| **3rd** | `~/.pi/ralpi/config.yaml` — global, shared across projects |
|
||||||
|
| **4th** | `DEFAULT_CONFIG` in `src/types.ts` |
|
||||||
|
|
||||||
### Task-Level Timeout
|
### Task-Level Timeout
|
||||||
|
|
||||||
You can set a timeout for individual tasks using a meta block in the task file:
|
You can set a timeout for individual tasks using a meta block in the task file:
|
||||||
|
|||||||
88
index.ts
88
index.ts
@@ -66,9 +66,10 @@ async function selectExecutionMode(
|
|||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
project: import("./src/types").Project,
|
project: import("./src/types").Project,
|
||||||
taskFile: string,
|
taskFile: string,
|
||||||
|
config: import("./src/types").RalpiConfig,
|
||||||
): Promise<ExecutionMode> {
|
): Promise<ExecutionMode> {
|
||||||
const mode = await ctx.ui.select("Execution mode for this run?", [
|
const mode = await ctx.ui.select("Execution mode for this run?", [
|
||||||
"Parallel (where dependencies allow)",
|
`Parallel (where dependencies allow)-[${config.execution.maxParallel} max]`,
|
||||||
"Sequential (one at a time)",
|
"Sequential (one at a time)",
|
||||||
]);
|
]);
|
||||||
const isParallel = mode?.startsWith("Parallel") ?? false;
|
const isParallel = mode?.startsWith("Parallel") ?? false;
|
||||||
@@ -117,7 +118,7 @@ async function executePlanBatches(
|
|||||||
plan: ReturnType<typeof buildPlanByMode>,
|
plan: ReturnType<typeof buildPlanByMode>,
|
||||||
project: Parameters<typeof buildExecutionPlan>[0],
|
project: Parameters<typeof buildExecutionPlan>[0],
|
||||||
taskFile: string,
|
taskFile: string,
|
||||||
config: import("./src/types").ralpiConfig,
|
config: import("./src/types").RalpiConfig,
|
||||||
progress: ProgressTracker,
|
progress: ProgressTracker,
|
||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
mode: ExecutionMode,
|
mode: ExecutionMode,
|
||||||
@@ -220,6 +221,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",
|
||||||
@@ -248,21 +259,45 @@ export default function ralpiLoopExtension(pi: ExtensionAPI): void {
|
|||||||
return handlePlan(ctx, parts);
|
return handlePlan(ctx, parts);
|
||||||
}
|
}
|
||||||
if (looksLikePath(parts[0])) {
|
if (looksLikePath(parts[0])) {
|
||||||
return handleRun(ctx, parts, sendProgress);
|
return handleRun(
|
||||||
|
ctx,
|
||||||
|
parts,
|
||||||
|
sendProgress,
|
||||||
|
ctx.model,
|
||||||
|
pi.getThinkingLevel(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = parts[0];
|
const command = parts[0];
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "run":
|
case "run":
|
||||||
return handleRun(ctx, parts.slice(1), sendProgress);
|
return handleRun(
|
||||||
|
ctx,
|
||||||
|
parts.slice(1),
|
||||||
|
sendProgress,
|
||||||
|
ctx.model,
|
||||||
|
pi.getThinkingLevel(),
|
||||||
|
);
|
||||||
case "plan":
|
case "plan":
|
||||||
return handlePlan(ctx, parts.slice(1));
|
return handlePlan(ctx, parts.slice(1));
|
||||||
case "status":
|
case "status":
|
||||||
return handleStatus(ctx, parts.slice(1));
|
return handleStatus(ctx, parts.slice(1));
|
||||||
case "resume":
|
case "resume":
|
||||||
return handleResume(ctx, parts.slice(1), sendProgress);
|
return handleResume(
|
||||||
|
ctx,
|
||||||
|
parts.slice(1),
|
||||||
|
sendProgress,
|
||||||
|
ctx.model,
|
||||||
|
pi.getThinkingLevel(),
|
||||||
|
);
|
||||||
case "next":
|
case "next":
|
||||||
return handleNext(ctx, parts.slice(1), sendProgress);
|
return handleNext(
|
||||||
|
ctx,
|
||||||
|
parts.slice(1),
|
||||||
|
sendProgress,
|
||||||
|
ctx.model,
|
||||||
|
pi.getThinkingLevel(),
|
||||||
|
);
|
||||||
case "reset":
|
case "reset":
|
||||||
return handleReset(ctx, parts.slice(1));
|
return handleReset(ctx, parts.slice(1));
|
||||||
default: {
|
default: {
|
||||||
@@ -316,6 +351,8 @@ async function handleRun(
|
|||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
args: string[],
|
args: string[],
|
||||||
sendChatMessage?: SendChatMessage,
|
sendChatMessage?: SendChatMessage,
|
||||||
|
parentModel?: unknown,
|
||||||
|
parentThinkingLevel?: unknown,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const taskFile = resolveTaskArg(args[0] || "README.md", process.cwd());
|
const taskFile = resolveTaskArg(args[0] || "README.md", process.cwd());
|
||||||
|
|
||||||
@@ -323,7 +360,13 @@ async function handleRun(
|
|||||||
// auto-resume instead of starting fresh
|
// auto-resume instead of starting fresh
|
||||||
const existingProgress = findProgressFile(process.cwd(), taskFile);
|
const existingProgress = findProgressFile(process.cwd(), taskFile);
|
||||||
if (existingProgress) {
|
if (existingProgress) {
|
||||||
return handleResume(ctx, args.slice(0, 1), sendChatMessage);
|
return handleResume(
|
||||||
|
ctx,
|
||||||
|
args.slice(0, 1),
|
||||||
|
sendChatMessage,
|
||||||
|
parentModel,
|
||||||
|
parentThinkingLevel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No existing progress for this task — check for any progress at all
|
// No existing progress for this task — check for any progress at all
|
||||||
@@ -336,7 +379,13 @@ async function handleRun(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (shouldResume?.startsWith("Yes")) {
|
if (shouldResume?.startsWith("Yes")) {
|
||||||
return handleResume(ctx, [], sendChatMessage);
|
return handleResume(
|
||||||
|
ctx,
|
||||||
|
[],
|
||||||
|
sendChatMessage,
|
||||||
|
parentModel,
|
||||||
|
parentThinkingLevel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,16 +395,12 @@ async function handleRun(
|
|||||||
|
|
||||||
const project = parseTaskFile(taskFile);
|
const project = parseTaskFile(taskFile);
|
||||||
const config = loadConfig(projectDir);
|
const config = loadConfig(projectDir);
|
||||||
|
config.model = parentModel ?? ctx.model;
|
||||||
|
config.thinkingLevel = parentThinkingLevel;
|
||||||
const progress = new ProgressTracker(projectDir, taskFile);
|
const progress = new ProgressTracker(projectDir, taskFile);
|
||||||
|
|
||||||
// Set initial status
|
|
||||||
ctx.ui.setStatus(
|
|
||||||
"ralpi",
|
|
||||||
`Starting ${project.tasks.length} tasks from ${path.basename(taskFile)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const completed = buildCompletedSet(progress, project);
|
const completed = buildCompletedSet(progress, project);
|
||||||
const mode = await selectExecutionMode(ctx, project, taskFile);
|
const mode = await selectExecutionMode(ctx, project, taskFile, config);
|
||||||
const plan = buildPlanByMode(mode, project, completed);
|
const plan = buildPlanByMode(mode, project, completed);
|
||||||
|
|
||||||
// Show execution plan before starting so user can see batch breakdown
|
// Show execution plan before starting so user can see batch breakdown
|
||||||
@@ -434,6 +479,8 @@ async function handleResume(
|
|||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
args: string[],
|
args: string[],
|
||||||
sendChatMessage?: SendChatMessage,
|
sendChatMessage?: SendChatMessage,
|
||||||
|
parentModel?: unknown,
|
||||||
|
parentThinkingLevel?: unknown,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let taskFile: string;
|
let taskFile: string;
|
||||||
let projectDir: string;
|
let projectDir: string;
|
||||||
@@ -473,15 +520,14 @@ async function handleResume(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const config = loadConfig(projectDir);
|
const config = loadConfig(projectDir);
|
||||||
|
config.model = parentModel ?? ctx.model;
|
||||||
|
config.thinkingLevel = parentThinkingLevel;
|
||||||
const progress = new ProgressTracker(projectDir, taskFile, found.prdKey);
|
const progress = new ProgressTracker(projectDir, taskFile, found.prdKey);
|
||||||
|
|
||||||
progress.setPaused(false);
|
progress.setPaused(false);
|
||||||
|
|
||||||
// Set resume status
|
|
||||||
ctx.ui.setStatus("ralpi", `Resuming from ${path.basename(taskFile)}`);
|
|
||||||
|
|
||||||
const completed = buildCompletedSet(progress, project);
|
const completed = buildCompletedSet(progress, project);
|
||||||
const mode = await selectExecutionMode(ctx, project, taskFile);
|
const mode = await selectExecutionMode(ctx, project, taskFile, config);
|
||||||
const plan = buildPlanByMode(mode, project, completed);
|
const plan = buildPlanByMode(mode, project, completed);
|
||||||
|
|
||||||
await executePlanBatches(
|
await executePlanBatches(
|
||||||
@@ -505,6 +551,8 @@ async function handleNext(
|
|||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
args: string[],
|
args: string[],
|
||||||
sendChatMessage?: SendChatMessage,
|
sendChatMessage?: SendChatMessage,
|
||||||
|
parentModel?: unknown,
|
||||||
|
parentThinkingLevel?: unknown,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let taskFile: string;
|
let taskFile: string;
|
||||||
let projectDir: string;
|
let projectDir: string;
|
||||||
@@ -540,6 +588,8 @@ async function handleNext(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const config = loadConfig(projectDir);
|
const config = loadConfig(projectDir);
|
||||||
|
config.model = parentModel ?? ctx.model;
|
||||||
|
config.thinkingLevel = parentThinkingLevel;
|
||||||
const progress = new ProgressTracker(projectDir, taskFile, found?.prdKey);
|
const progress = new ProgressTracker(projectDir, taskFile, found?.prdKey);
|
||||||
|
|
||||||
const completed = buildCompletedSet(progress, project);
|
const completed = buildCompletedSet(progress, project);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ralpi-loop",
|
"name": "ralpi",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"description": "Execute tasks from task files using DAG-based dependency resolution with persistent progress tracking",
|
"description": "Execute tasks from task files/PRD's using DAG-based dependency resolution with persistent progress tracking",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"pi-package",
|
"pi-package",
|
||||||
|
|||||||
908
src/executor.ts
908
src/executor.ts
File diff suppressed because it is too large
Load Diff
13
src/types.ts
13
src/types.ts
@@ -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 */
|
||||||
@@ -160,6 +162,10 @@ export interface RalpiConfig {
|
|||||||
/** Custom prompt suffix for reflection extraction */
|
/** Custom prompt suffix for reflection extraction */
|
||||||
reflectionPrompt: string;
|
reflectionPrompt: string;
|
||||||
};
|
};
|
||||||
|
/** Parent session model to inherit in child agent sessions */
|
||||||
|
model?: unknown;
|
||||||
|
/** Parent session thinking level to inherit in child agent sessions */
|
||||||
|
thinkingLevel?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: RalpiConfig = {
|
export const DEFAULT_CONFIG: RalpiConfig = {
|
||||||
@@ -168,10 +174,11 @@ export const DEFAULT_CONFIG: RalpiConfig = {
|
|||||||
reflectionsDir: ".ralpi/reflections",
|
reflectionsDir: ".ralpi/reflections",
|
||||||
},
|
},
|
||||||
execution: {
|
execution: {
|
||||||
maxRetries: 3,
|
maxRetries: 0,
|
||||||
retryDelayMs: 5000,
|
retryDelayMs: 0,
|
||||||
timeoutMs: 30 * 60 * 1000, // 30 minutes
|
timeoutMs: 0, // 0 = inherit Pi's own defaults (no ralpi-level timeout)
|
||||||
maxParallel: 3,
|
maxParallel: 3,
|
||||||
|
models: [],
|
||||||
},
|
},
|
||||||
prompts: {
|
prompts: {
|
||||||
projectContext: "",
|
projectContext: "",
|
||||||
|
|||||||
119
src/utils.ts
119
src/utils.ts
@@ -82,32 +82,35 @@ export function findProgressFile(
|
|||||||
|
|
||||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function parseSimpleYaml(content: string): Record<string, any> {
|
/** Try to use the `yaml` package (real dependency in package.json).
|
||||||
const result: Record<string, any> = {};
|
* Falls back to a flat key:value parser when unavailable. */
|
||||||
const lines = content.split("\n");
|
const parseSimpleYaml: (content: string) => Record<string, any> = (() => {
|
||||||
|
try {
|
||||||
for (const line of lines) {
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const trimmed = line.trim();
|
const { parse } = require("yaml");
|
||||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
return (content: string) => parse(content) ?? {};
|
||||||
|
} catch {
|
||||||
const match = trimmed.match(/^([^:]+):\s*(.+)$/);
|
return (content: string) => {
|
||||||
if (match) {
|
const result: Record<string, any> = {};
|
||||||
const key = match[1].trim();
|
for (const line of content.split("\n")) {
|
||||||
let value: string | boolean | number = match[2].trim();
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||||
// Parse booleans
|
const match = trimmed.match(/^([^:]+):\s*(.*)$/);
|
||||||
if (value === "true") value = true;
|
if (match) {
|
||||||
else if (value === "false") value = false;
|
const value = match[2].trim();
|
||||||
// Parse numbers
|
if (value === "true") result[match[1].trim()] = true;
|
||||||
else if (/^\d+$/.test(value)) value = parseInt(value, 10);
|
else if (value === "false") result[match[1].trim()] = false;
|
||||||
else if (/^\d+\.\d+$/.test(value)) value = parseFloat(value);
|
else if (/^\d+$/.test(value))
|
||||||
|
result[match[1].trim()] = parseInt(value, 10);
|
||||||
result[key] = value;
|
else if (/^\d+\.\d+$/.test(value))
|
||||||
}
|
result[match[1].trim()] = parseFloat(value);
|
||||||
|
else result[match[1].trim()] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deep merge configuration objects
|
* Deep merge configuration objects
|
||||||
@@ -129,25 +132,44 @@ function mergeConfig(
|
|||||||
return result as RalpiConfig;
|
return result as RalpiConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Path to the global ralpi config under the user's Pi home directory. */
|
||||||
|
const GLOBAL_CONFIG_PATH = path.join(
|
||||||
|
process.env.HOME || "/tmp",
|
||||||
|
".pi",
|
||||||
|
"ralpi",
|
||||||
|
"config.yaml",
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load configuration from .ralpi/config.yaml or return defaults
|
* Load and merge config from global and project sources.
|
||||||
|
*
|
||||||
|
* Precedence (highest wins):
|
||||||
|
* 1. Project-level: `<projectDir>/.ralpi/config.yaml`
|
||||||
|
* 2. Global: `~/.pi/ralpi/config.yaml`
|
||||||
|
* 3. `DEFAULT_CONFIG` in `src/types.ts`
|
||||||
*/
|
*/
|
||||||
export function loadConfig(projectDir: string): RalpiConfig {
|
export function loadConfig(projectDir: string): RalpiConfig {
|
||||||
const configPath = path.join(projectDir, ".ralpi", "config.yaml");
|
// Start with defaults
|
||||||
|
const merged: RalpiConfig = { ...DEFAULT_CONFIG };
|
||||||
|
|
||||||
// Return defaults silently when config file does not exist
|
// Layer 1: global config (~/.pi/ralpi/config.yaml)
|
||||||
if (!fs.existsSync(configPath)) {
|
tryLoadConfigFile(GLOBAL_CONFIG_PATH, merged);
|
||||||
return { ...DEFAULT_CONFIG };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Layer 2: project config (.ralpi/config.yaml) — overrides global
|
||||||
const content = fs.readFileSync(configPath, "utf-8");
|
tryLoadConfigFile(path.join(projectDir, ".ralpi", "config.yaml"), merged);
|
||||||
// Simple YAML parsing (key: value format)
|
|
||||||
const config = parseSimpleYaml(content);
|
return merged;
|
||||||
return mergeConfig(DEFAULT_CONFIG, config);
|
|
||||||
} catch {
|
/** Attempt to load a single config file and merge into `acc` in place. */
|
||||||
// Malformed config — fall back to defaults silently
|
function tryLoadConfigFile(filePath: string, acc: RalpiConfig): void {
|
||||||
return { ...DEFAULT_CONFIG };
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const parsed = parseSimpleYaml(content);
|
||||||
|
Object.assign(acc, mergeConfig(acc, parsed));
|
||||||
|
} catch {
|
||||||
|
// Malformed config — skip silently
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +361,8 @@ export async function runAgentSession(
|
|||||||
onEvent?: (event: AgentSessionEvent) => void,
|
onEvent?: (event: AgentSessionEvent) => void,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
sessionFile?: string,
|
sessionFile?: string,
|
||||||
|
model?: unknown,
|
||||||
|
thinkingLevel?: unknown,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -361,10 +385,13 @@ export async function runAgentSession(
|
|||||||
? fs.createWriteStream(sessionFile, { flags: "a" })
|
? fs.createWriteStream(sessionFile, { flags: "a" })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Wire timeout via abort signal
|
// Wire timeout via abort signal (only when set; 0 means inherit Pi's defaults)
|
||||||
const timeoutHandle = setTimeout(() => {
|
let timeoutHandle: NodeJS.Timeout | null = null;
|
||||||
if (sessionRef?.session) sessionRef.session.agent.abort();
|
if (timeoutMs > 0) {
|
||||||
}, timeoutMs);
|
timeoutHandle = setTimeout(() => {
|
||||||
|
if (sessionRef?.session) sessionRef.session.agent.abort();
|
||||||
|
}, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
const sessionRef: {
|
const sessionRef: {
|
||||||
session?: Awaited<ReturnType<typeof createAgentSession>>["session"];
|
session?: Awaited<ReturnType<typeof createAgentSession>>["session"];
|
||||||
@@ -387,6 +414,8 @@ export async function runAgentSession(
|
|||||||
sessionManager: SessionManager.inMemory(),
|
sessionManager: SessionManager.inMemory(),
|
||||||
resourceLoader: loader,
|
resourceLoader: loader,
|
||||||
tools: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
tools: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
||||||
|
model: model as any,
|
||||||
|
thinkingLevel: thinkingLevel as any,
|
||||||
});
|
});
|
||||||
sessionRef.session = result.session;
|
sessionRef.session = result.session;
|
||||||
|
|
||||||
@@ -437,7 +466,7 @@ export async function runAgentSession(
|
|||||||
unsubscribe();
|
unsubscribe();
|
||||||
result.session.dispose();
|
result.session.dispose();
|
||||||
signal?.removeEventListener("abort", abortHandler);
|
signal?.removeEventListener("abort", abortHandler);
|
||||||
clearTimeout(timeoutHandle);
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
||||||
|
|
||||||
// Flush and close the event stream before returning
|
// Flush and close the event stream before returning
|
||||||
if (eventStream) {
|
if (eventStream) {
|
||||||
@@ -463,7 +492,7 @@ export async function runAgentSession(
|
|||||||
events: [], // streamed to file
|
events: [], // streamed to file
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutHandle);
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
||||||
if (eventStream && !eventStream.destroyed) {
|
if (eventStream && !eventStream.destroyed) {
|
||||||
eventStream.end();
|
eventStream.end();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user