Files
oh-my-pi/packages/coding-agent/src/task/output-manager.ts
T
can1357 f82a5d121f feat(coding-agent/task): added task recursion depth control and hierarchical subagent naming
- Added task.maxRecursionDepth setting to control how many levels deep subagents can spawn their own subagents (0=none, 1=one level, 2=two levels, -1=unlimited).
- Added nested task artifact naming with parent task prefixes (e.g., '0-Auth.1-Subtask') to support hierarchical task identification.
- Added taskDepth and parentTaskPrefix options to CreateAgentSessionOptions for supporting nested/subagent sessions.
- Changed task tool spawns configuration from 'explore' to '*' to allow spawning any type of subagent.
- Updated system prompt to unconditionally include parallel delegation guidance for all agent types instead of only coordinators.
- Implemented automatic task tool disabling at maximum recursion depth to prevent excessive task nesting.
2026-02-05 02:18:10 +01:00

108 lines
3.0 KiB
TypeScript

/**
* Session-scoped manager for agent output IDs.
*
* Ensures unique output IDs across task tool invocations within a session.
* Prefixes each ID with a sequential number (e.g., "0-AuthProvider", "1-AuthApi").
* If a parent prefix is provided, IDs are nested (e.g., "0-Auth.1-Subtask").
*
* This enables reliable agent:// URL resolution and prevents artifact collisions.
*/
import * as fs from "node:fs/promises";
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Manages agent output ID allocation to ensure uniqueness.
*
* Each allocated ID gets a numeric prefix based on allocation order.
* If configured with a parent prefix, the numeric prefix is appended after
* the parent (e.g., "0-Parent.0-Child").
* On resume, scans existing files to find the next available index.
*/
export class AgentOutputManager {
#nextId = 0;
#initialized = false;
readonly #getArtifactsDir: () => string | null;
readonly #parentPrefix: string | undefined;
constructor(getArtifactsDir: () => string | null, options?: { parentPrefix?: string }) {
this.#getArtifactsDir = getArtifactsDir;
this.#parentPrefix = options?.parentPrefix;
}
/**
* Scan existing agent output files to find the next available ID.
* This ensures we don't overwrite outputs when resuming a session.
*/
async #ensureInitialized(): Promise<void> {
if (this.#initialized) return;
this.#initialized = true;
const dir = this.#getArtifactsDir();
if (!dir) return;
let files: string[];
try {
files = await fs.readdir(dir);
} catch {
return; // Directory doesn't exist yet
}
const pattern = this.#parentPrefix
? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)-.*\\.md$`)
: /^(\d+)-.*\.md$/;
let maxId = -1;
for (const file of files) {
const match = file.match(pattern);
if (match) {
const id = Number.parseInt(match[1], 10);
if (id > maxId) maxId = id;
}
}
this.#nextId = maxId + 1;
}
/**
* Allocate a unique ID with numeric prefix.
*
* @param id Requested ID (e.g., "AuthProvider")
* @returns Unique ID with prefix (e.g., "0-AuthProvider")
*/
async allocate(id: string): Promise<string> {
await this.#ensureInitialized();
const prefix = this.#parentPrefix ? `${this.#parentPrefix}.` : "";
return `${prefix}${this.#nextId++}-${id}`;
}
/**
* Allocate unique IDs for a batch of tasks.
*
* @param ids Array of requested IDs
* @returns Array of unique IDs in same order
*/
async allocateBatch(ids: string[]): Promise<string[]> {
await this.#ensureInitialized();
const prefix = this.#parentPrefix ? `${this.#parentPrefix}.` : "";
return ids.map(id => `${prefix}${this.#nextId++}-${id}`);
}
/**
* Get the next ID that would be allocated (without allocating).
*/
async peekNextIndex(): Promise<number> {
await this.#ensureInitialized();
return this.#nextId;
}
/**
* Reset state (primarily for testing).
*/
reset(): void {
this.#nextId = 0;
this.#initialized = false;
}
}