import { randomBytes } from "node:crypto" import { rm } from "node:fs/promises" import type { AgentSessionEvent, ExtensionAPI, ExtensionContext, ScopedModel } from "@earendil-works/pi-coding-agent" import { Container, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui" import { Type } from "typebox" import { type ChildSessionHandle, childPromptOptions, createChildSession, resolveChildSessionSelection, resolveChildToolPolicy } from "./child-session.js" import type { AgentsConfig } from "./config.js" import { resolveConfiguredModels } from "./config.js" import { getAgentCoordinator, type ResidentAgent, type ResidentInputOptions, type ResidentInputResult } from "./coordinator.js" import { type AgentDefinition, discoverAgentDefinitions, projectResourcesTrusted } from "./definitions.js" import { discardTask, stopOwnedTaskTree, stopTask } from "./lifecycle.js" import { appendTaskNotification, deliverTaskNotifications, prepareTaskNotification } from "./notifications.js" import { renderExpandableResult } from "./rendering.js" import { type AgentTaskMetadata, acquireParentLease, appendHistoryLog, displayWorkspacePath, ensureParentStorage, initializeRetainedLogs, MAX_AGENT_INPUT_BYTES, MAX_AGENT_LABEL_BYTES, MAX_QUEUED_FOLLOWUPS, mutateTaskMetadata, RETAINED_OUTPUT_MAX_WAIT_MS, readRetainedOutput, readTaskMetadata, reserveTaskStorage, retainedPaths, TASK_METADATA_VERSION, TASK_REFERENCE_PATTERN, type TaskMetadata, type TaskStoragePaths, taskStoragePaths, writeTaskMetadata, writeTaskProgress } from "./state.js" import { deferred } from "./utils.js" const ThinkingLevel = Type.Union([ Type.Literal("off"), Type.Literal("minimal"), Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh"), Type.Literal("max") ]) export type AgentCreationResult = { id: string run: number label: string definition: string state: TaskMetadata["state"] latestOutcome: TaskMetadata["latestOutcome"] model: string thinking: AgentTaskMetadata["thinking"] depth: number allowAgents: boolean detached: boolean queuedFollowUps: number output: Awaited> } export type TaskInputResult = { id: string run: number requestedDelivery: "followup" | "steer" | undefined effectiveDelivery: "followup" | "steer" | "stdin" conversionReason?: string queuePosition: number | null state: TaskMetadata["state"] latestOutcome: TaskMetadata["latestOutcome"] queuedFollowUps: number } export type AgentToolOptions = { getConfig: () => AgentsConfig createChild?: typeof createChildSession getAgentDir?: () => string } /** Schemas and descriptions are static; config and depth limits are enforced at execution. */ export function registerAgentTool(pi: ExtensionAPI, options: AgentToolOptions): void { pi.registerTool({ name: "agent", label: "Agent", description: "Create a durable Lovely Agent session and start its initial run. The call waits up to waitMs for the result, then detaches; a detached run notifies you when it ends. Cancellation before detachment stops the accepted work.", promptSnippet: "Create or delegate work to a durable agent", promptGuidelines: ["Call agent_roster before creating an agent and use task tools for existing work."], parameters: Type.Object( { definition: Type.String({ minLength: 1, description: "Agent Definition name" }), label: Type.String({ minLength: 1, description: "Short task label" }), prompt: Type.String({ minLength: 1, description: "Initial task prompt" }), waitMs: Type.Optional( Type.Integer({ minimum: 0, maximum: RETAINED_OUTPUT_MAX_WAIT_MS, description: "Wait before detaching; 0 detaches immediately" }) ), model: Type.Optional( Type.String({ minLength: 1, description: "Configured provider/model ID or an enabled alias from agent_roster" }) ), thinking: Type.Optional(ThinkingLevel), allowAgents: Type.Optional(Type.Boolean({ description: "Allow this child to create descendants, depth permitting" })) }, { additionalProperties: false } ), renderCall(args, theme, context) { return { render(width) { // The result supplies the ID after renderCall; read shared state at paint time. const header = `${theme.fg("toolTitle", theme.bold("agent"))}${args.definition ? ` ${theme.fg("muted", args.definition)}` : ""}${args.label ? ` ${theme.fg("dim", `label=${JSON.stringify(args.label)}`)}` : ""}` const suffix = context.state.taskRef ? `${theme.fg("muted", " -> ")}${theme.fg("accent", context.state.taskRef)}` : "" if (context.expanded) { const lines = new Text(header + suffix, 0, 0).render(width) if (args.prompt) lines.push(...new Text(`${theme.fg("muted", "Input: ")}${args.prompt}`, 0, 0).render(width)) return lines } const available = Math.max(0, width - visibleWidth(suffix)) const promptWidth = available - visibleWidth(header) - visibleWidth(' prompt=""') const preview = args.prompt && promptWidth > 0 ? `${header}${theme.fg("muted", ' prompt="')}${truncateToWidth(JSON.stringify(args.prompt.replace(/\s+/g, " ").trim()).slice(1, -1), promptWidth)}${theme.fg("muted", '"')}` : truncateToWidth(header, available) // Pi's truncation emits full resets; preserve the surrounding tool background. return [truncateToWidth(preview + suffix, width).replaceAll("\x1b[0m", "\x1b[22;39m")] }, invalidate() {} } }, renderResult(result, { expanded }, theme, context) { const details = result.details as Partial | undefined if (typeof details?.id === "string") context.state.taskRef = details.id const output = new Container() if (expanded || context.isError) { output.addChild(new Text(theme.fg("muted", "── Result ──"), 0, 0)) output.addChild(renderExpandableResult(result, true, theme)) } return output }, async execute(_toolCallId, params, signal, _onUpdate, ctx) { if (signal?.aborted) throw abortError(signal) const config = options.getConfig() const allowAgents = params.allowAgents === true validateInput(params.label, "label", MAX_AGENT_LABEL_BYTES) validateInput(params.prompt, "prompt", MAX_AGENT_INPUT_BYTES) const parentSessionId = ctx.sessionManager.getSessionId() const coordinator = getAgentCoordinator(config.maxConcurrency) const parentContext = coordinator.getSessionContext(parentSessionId) if (parentContext && !parentContext.allowAgents) throw new Error("This parent agent is not allowed to delegate") const parentDepth = parentContext?.depth ?? 0 if (allowAgents && parentDepth + 1 >= config.maxDepth) throw new Error("This child cannot delegate at the current maximum depth") resolveChildToolPolicy({ parentDepth, maximumDepth: config.maxDepth, allowAgents }) const discovered = discoverAgentDefinitions({ cwd: ctx.cwd, projectTrusted: projectResourcesTrusted(ctx), toolNames: pi.getAllTools().map(tool => tool.name), models: ctx.modelRegistry.getAll(), ...(options.getAgentDir ? { agentDir: options.getAgentDir() } : {}) }) const definition = discovered.definitions.find(candidate => candidate.name === params.definition) if (!definition) throw new Error(`Unknown or invalid Agent Definition: ${params.definition}`) const configuredModels = await resolveConfiguredModels(config, ctx) const selection = resolveChildSessionSelection({ ...(params.model ? { callModel: params.model } : {}), ...(params.thinking ? { callThinking: params.thinking } : {}), definition, configuredModels: configuredModels.models, aliases: configuredModels.aliases, availableModels: ctx.modelRegistry.getAvailable(), parentModel: ctx.model, parentThinking: ctx.thinkingLevel ?? "medium" }) await acquireParentLease(ctx.cwd, parentSessionId) const paths = await reserveTaskStorage(await ensureParentStorage(ctx.cwd, parentSessionId)) let child: ChildSessionHandle | undefined let accepted = false try { await initializeRetainedLogs(paths) child = await (options.createChild ?? createChildSession)({ cwd: ctx.cwd, paths, definition, selection, scopedModels: configuredModels.models, parentDepth, maximumDepth: config.maxDepth, allowAgents, projectTrusted: ctx.isProjectTrusted() }) if (signal?.aborted) throw abortError(signal) const acceptedAt = Date.now() const runId = createRunId() const acceptanceOrder = coordinator.nextAcceptanceOrder() const metadata: TaskMetadata = { version: TASK_METADATA_VERSION, kind: "agent", taskRef: paths.taskRef, parentSessionId, childSessionId: child.session.sessionId, definitionName: definition.name, label: params.label.trim(), model: { provider: child.session.model?.provider ?? selection.model.provider, id: child.session.model?.id ?? selection.model.id }, thinking: child.session.thinkingLevel, depth: child.depth, allowAgents: child.allowAgents, sessionConfig: { systemPrompt: definition.systemPrompt, tools: definition.tools ?? null, excludeAgentsMd: definition.excludeAgentsMd ?? false, scopedModels: configuredModels.models.map(choice => ({ provider: choice.model.provider, id: choice.model.id, ...(choice.thinkingLevel ? { thinkingLevel: choice.thinkingLevel } : {}) })) }, state: "queued", latestOutcome: null, latestReply: null, lastActivity: { at: acceptedAt, action: "queued" }, lastRunSequence: 1, activeRun: { id: runId, sequence: 1, acceptanceOrder, kind: "initial", state: "queued", input: params.prompt, acceptedAt }, queuedFollowUps: [], notifications: [], discardedAt: null, createdAt: acceptedAt, updatedAt: acceptedAt } await writeTaskMetadata(paths, metadata) accepted = true const runtime = new AgentRuntime(paths, child, metadata, config.expandPromptTemplates) child = undefined runtime.start() const waitMs = params.waitMs ?? config.waitMs const wait = () => runtime.wait(waitMs, signal) const detached = waitMs === 0 ? await wait() : await coordinator.withLentPermit(wait, signal) const loaded = await readTaskMetadata(paths) if (loaded.status !== "ok") throw new Error(`Could not read accepted task ${paths.taskRef}`) return buildAgentCreationToolResult(loaded.metadata, detached, await readRetainedOutput(paths, { run: 1 })) } finally { child?.dispose() if (!accepted) await rm(paths.taskDirectory, { recursive: true, force: true }) } } }) } const TaskReference = Type.String({ pattern: TASK_REFERENCE_PATTERN.source, description: "Task Reference" }) export function registerTaskInputTool(pi: ExtensionAPI, options: AgentToolOptions): void { pi.registerTool({ name: "task_input", label: "Task Input", description: "Send a durable Follow-up or live Steer to an owned Lovely Agent task, or write literal stdin to a running owned Bash task; eof closes stdin. Empty content requires eof:true.", promptSnippet: "Send Follow-up work or a live Steer, or literal Bash stdin, to a durable task", promptGuidelines: [ "Use task_input Follow-up for a new run; Steer queues input for a live streaming run. Without a live steering target, Steer becomes a Follow-up. Acceptance does not guarantee observation before stop.", "For Bash, omit delivery and write literal stdin; eof closes stdin without restarting the command." ], parameters: Type.Object( { id: Type.String({ pattern: TASK_REFERENCE_PATTERN.source, description: "Task Reference" }), content: Type.String({ description: "Input text" }), delivery: Type.Optional(Type.Union([Type.Literal("followup"), Type.Literal("steer")], { description: "Agent tasks only" })), eof: Type.Optional(Type.Boolean({ description: "Bash only: close stdin after writing content" })) }, { additionalProperties: false } ), renderCall(args, theme) { const delivery = args.delivery ?? (args.id?.startsWith("b_") ? "stdin" : "followup") const content = args.content ?? "" const preview = content.length > 60 ? `${content.slice(0, 57)}...` : content return new Text( `${theme.fg("toolTitle", theme.bold("task_input"))}${args.id ? ` ${theme.fg("muted", args.id)}` : ""} ${theme.fg("dim", `${delivery}${preview ? ` ${JSON.stringify(preview)}` : ""}`)}`, 0, 0 ) }, async execute(_toolCallId, params, signal, _onUpdate, ctx) { const result = await sendTaskInput( ctx, options, params.id, params.content, params.delivery, signal, params.eof !== undefined ? { eof: params.eof } : {} ) return { content: [ { type: "text", text: result.effectiveDelivery === "stdin" ? `${result.id} run=${result.run}: stdin delivered${params.eof ? " (EOF)" : ""}` : result.effectiveDelivery === "steer" ? `${result.id} run=${result.run}: steer queued for current run` : `${result.id} run=${result.run}: ${result.conversionReason ? `requested steer -> ` : ""}followup accepted${result.conversionReason ? `; ${result.conversionReason}` : ""} (position ${result.queuePosition}; task=${result.state}; ${result.queuedFollowUps} queued)` } ], details: result } } }) for (const action of ["stop", "discard"] as const) { pi.registerTool({ name: `task_${action}`, label: action === "stop" ? "Task Stop" : "Task Discard", description: action === "stop" ? "Stop active or queued work for owned Agent or Bash tasks while preserving their files; id accepts one Task Reference or a list." : "Stop and permanently discard owned task subtrees from active work; id accepts one Task Reference or a list. Files stay at their original paths for read-only inspection.", promptSnippet: action === "stop" ? "Stop work for a durable task" : "Discard a durable task", promptGuidelines: action === "discard" ? [ "Use task_discard after dependent work is integrated and no Follow-up is expected. Keep reusable specialists and tasks whose results are still needed; idle agents already unload." ] : [], parameters: Type.Object( { id: Type.Union([TaskReference, Type.Array(TaskReference, { minItems: 1, maxItems: 64, uniqueItems: true })], { description: "One Task Reference, or a list to handle in one call" }) }, { additionalProperties: false } ), renderCall(args, theme) { const ids = [args.id ?? []].flat().join(" ") return new Text(`${theme.fg("toolTitle", theme.bold(`task_${action}`))}${ids ? ` ${theme.fg("muted", ids)}` : ""}`, 0, 0) }, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const describe = (details: Awaited>) => action === "discard" ? `${details.id}: discarded (files retained at ${details.taskDirectory})` : `${details.id}: stop complete (${details.state}; outcome ${details.latestOutcome ?? "none"}; ${details.queuedFollowUps} queued)` if (typeof params.id === "string") { const details = await controlTaskLifecycle(ctx, params.id, action) return { content: [{ type: "text", text: describe(details) }], details } } // One bad reference must not abandon the rest of a batch; only a total failure is a tool error. const tasks: Array>> = [] const errors: Array<{ id: string; message: string }> = [] for (const id of params.id) { try { tasks.push(await controlTaskLifecycle(ctx, id, action)) } catch (error) { errors.push({ id, message: error instanceof Error ? error.message : String(error) }) } } const text = [...tasks.map(describe), ...errors.map(error => `${error.id}: failed: ${error.message}`)].join("\n") if (tasks.length === 0) throw new Error(text) return { content: [{ type: "text", text }], details: { tasks, errors } } } }) } } export async function sendTaskInput( ctx: ExtensionContext, options: AgentToolOptions, id: string, content: string, requestedDelivery: "followup" | "steer" | undefined = undefined, signal?: AbortSignal, inputOptions: { eof?: boolean } = {} ): Promise { if (signal?.aborted) throw abortError(signal) if (!id.startsWith("b_")) validateInput(content, "content", MAX_AGENT_INPUT_BYTES) const lease = await acquireParentLease(ctx.cwd, ctx.sessionManager.getSessionId()) const paths = taskStoragePaths(lease.paths, id) const loaded = await readTaskMetadata(paths) if (loaded.status === "missing") throw new Error(`Unknown Task Reference: ${id}`) if (loaded.status === "invalid") throw new Error(loaded.diagnostic.message) if (loaded.metadata.discardedAt !== null) throw new Error(`Task ${id} has been discarded`) if (loaded.metadata.kind === "bash") { if (requestedDelivery !== undefined) throw new Error("Bash input does not accept delivery; omit it to write stdin") if (Buffer.byteLength(content, "utf8") > MAX_AGENT_INPUT_BYTES) throw new Error(`content must be at most ${MAX_AGENT_INPUT_BYTES} UTF-8 bytes`) if (content.length === 0 && !inputOptions.eof) throw new Error("Empty Bash input requires eof") const resident = getAgentCoordinator().getResident(paths.taskDirectory) if (loaded.metadata.state !== "running" || !resident?.input) throw new Error(`Bash task ${id} requires a live running resident for stdin`) const accepted = await resident.input(content, "stdin", { ...inputOptions, ...(signal ? { signal } : {}) }) const current = await readTaskMetadata(paths) if (current.status !== "ok") throw new Error(`Could not read accepted task ${id}`) return { id, run: accepted.run, requestedDelivery, effectiveDelivery: accepted.delivery, queuePosition: accepted.queuePosition, state: current.metadata.state, latestOutcome: current.metadata.latestOutcome, queuedFollowUps: accepted.queuedFollowUps } } if (inputOptions.eof !== undefined) throw new Error("eof is only supported for Bash stdin") const delivery = requestedDelivery ?? "followup" let accepted: ResidentInputResult | undefined while (!accepted) { if (signal?.aborted) throw abortError(signal) const runtime = await controllableRuntime(ctx, paths, options) try { accepted = await runtime.input(content, delivery, signal ? { signal } : {}) } catch (error) { if (!isRuntimeClosingError(error)) throw error await new Promise(resolve => setTimeout(resolve, 0)) } } const current = await readTaskMetadata(paths) if (current.status !== "ok") throw new Error(`Could not read accepted task ${id}`) return { id, run: accepted.run, ...(accepted.conversionReason ? { conversionReason: accepted.conversionReason } : {}), requestedDelivery: delivery, effectiveDelivery: accepted.delivery, queuePosition: accepted.queuePosition, state: current.metadata.state, latestOutcome: current.metadata.latestOutcome, queuedFollowUps: current.metadata.queuedFollowUps.length } } export async function controlTaskLifecycle(ctx: ExtensionContext, id: string, action: "stop" | "discard") { const lease = await acquireParentLease(ctx.cwd, ctx.sessionManager.getSessionId()) const paths = taskStoragePaths(lease.paths, id) if (action === "discard") { await discardTask(paths) return { id, action, state: "discarded", latestOutcome: null, discarded: true, queuedFollowUps: 0, taskDirectory: displayWorkspacePath(ctx.cwd, paths.taskDirectory) } } let loaded = await readTaskMetadata(paths) if (loaded.status === "missing") throw new Error(`Unknown Task Reference: ${id}`) if (loaded.status === "invalid") throw new Error(loaded.diagnostic.message) if (loaded.metadata.discardedAt !== null) throw new Error(`Task ${id} has been discarded`) await stopTask(paths) if (loaded.metadata.kind === "agent") await stopOwnedTaskTree(ctx.cwd, loaded.metadata.childSessionId) loaded = await readTaskMetadata(paths) if (loaded.status !== "ok") throw new Error(`Could not read task ${id}`) const metadata = loaded.metadata return { id: metadata.taskRef, action, state: metadata.state, latestOutcome: metadata.latestOutcome, discarded: metadata.discardedAt !== null, queuedFollowUps: metadata.queuedFollowUps.length, taskDirectory: displayWorkspacePath(ctx.cwd, paths.taskDirectory), paths: retainedPaths(paths) } } class AgentRuntime implements ResidentAgent { readonly #paths: TaskStoragePaths readonly #child: ChildSessionHandle readonly #expandPromptTemplates: boolean readonly #scheduleAbort = new AbortController() readonly #initialRunId: string | undefined readonly #initialCompletion = deferred() readonly #runtimeCompletion = deferred() readonly #toolArguments = new Map() #unbindResident: (() => void) | undefined #unsubscribe: (() => void) | undefined #eventWrites: Promise = Promise.resolve() #recordingRunId: string | undefined #pendingProgress: ({ runId: string } & Partial>) | undefined #lastHeartbeatAt = 0 #progressWriteQueued = false #progressTimer: ReturnType | undefined #lastProgressWriteAt = 0 #started = false #stopRequested = false #accepting = true #disposed = false #sawInput = false #lastAssistantOutcome: NonNullable | undefined constructor(paths: TaskStoragePaths, child: ChildSessionHandle, metadata: AgentTaskMetadata, expandPromptTemplates: boolean) { this.#paths = paths this.#child = child this.#initialRunId = metadata.activeRun?.kind === "initial" ? metadata.activeRun.id : undefined this.#expandPromptTemplates = expandPromptTemplates this.#unbindResident = getAgentCoordinator().bindResident(paths.taskDirectory, this) this.#unsubscribe = child.session.subscribe(event => this.recordEvent(event)) } start(): void { if (this.#started) return this.#started = true void this.run().catch(() => {}) } async wait(waitMs: number, signal?: AbortSignal): Promise { if (waitMs === 0) { await this.detach() return true } if (signal?.aborted) await this.stopTree() let timer: ReturnType | undefined let onAbort: (() => void) | undefined const timeout = new Promise<"timeout">(resolve => { timer = setTimeout(() => resolve("timeout"), waitMs) timer.unref() }) const aborted = signal ? new Promise<"aborted">(resolve => { onAbort = () => resolve("aborted") signal.addEventListener("abort", onAbort, { once: true }) }) : undefined const result = await Promise.race([ this.#initialCompletion.promise.then(() => "completed" as const), timeout, ...(aborted ? [aborted] : []) ]) if (timer) clearTimeout(timer) if (signal && onAbort) signal.removeEventListener("abort", onAbort) if (result === "aborted") { await this.stopTree() return false } if (result === "timeout") { await this.detach() return true } return false } /** Cancellation owns the accepted run and any descendants it already detached. */ private async stopTree(): Promise { await this.stop() await stopOwnedTaskTree(this.#paths.workspace, this.#child.session.sessionId) } async stop(): Promise { if (this.#stopRequested) return this.#runtimeCompletion.promise this.#stopRequested = true this.#accepting = false this.#scheduleAbort.abort(new Error("Agent run stopped")) await this.#child.session.abort() const loaded = await readTaskMetadata(this.#paths) if (loaded.status === "ok" && loaded.metadata.activeRun) { await this.settle(loaded.metadata.activeRun, "stopped", true) } else if (loaded.status === "ok" && loaded.metadata.queuedFollowUps.length > 0) { await mutateTaskMetadata(this.#paths, metadata => ({ ...metadata, queuedFollowUps: [], updatedAt: Date.now() })) } if (!this.#started) { this.dispose() this.#runtimeCompletion.resolve(undefined) this.#initialCompletion.resolve(undefined) } await this.#runtimeCompletion.promise } dispose(): void { if (this.#disposed) return this.#disposed = true this.#unsubscribe?.() this.#unsubscribe = undefined this.#unbindResident?.() this.#unbindResident = undefined this.#child.dispose() } async input(content: string, delivery: "followup" | "steer" | "stdin", options: ResidentInputOptions = {}): Promise { if (delivery === "stdin" || options.eof !== undefined) throw new Error("Agent tasks do not accept stdin or eof") if (!this.#accepting || this.#disposed) throw new RuntimeClosingError() let result: ResidentInputResult | undefined try { await mutateTaskMetadata(this.#paths, async metadata => { if (options.signal?.aborted) throw abortError(options.signal) if (!this.#accepting || this.#disposed) throw new RuntimeClosingError() if (metadata.discardedAt !== null) throw new Error(`Task ${metadata.taskRef} has been discarded`) if (delivery === "steer" && metadata.state === "running" && metadata.activeRun && this.#child.session.isStreaming) { // prompt() can yield in input hooks and start a new run after streaming ends. if (this.#expandPromptTemplates) await this.#child.session.steer(content) else this.#child.session.agent.steer({ role: "user", content, timestamp: Date.now() }) result = { run: metadata.activeRun.sequence, delivery: "steer", queuePosition: null, queuedFollowUps: metadata.queuedFollowUps.length } return metadata } if (metadata.queuedFollowUps.length >= MAX_QUEUED_FOLLOWUPS) { throw new Error(`Task ${metadata.taskRef} already has ${MAX_QUEUED_FOLLOWUPS} queued Follow-ups`) } const acceptedAt = Date.now() const sequence = metadata.lastRunSequence + 1 const runId = createRunId() const acceptanceOrder = getAgentCoordinator().nextAcceptanceOrder() const queuePosition = metadata.activeRun ? metadata.queuedFollowUps.length + 1 : 1 result = { run: sequence, delivery: "followup", queuePosition, queuedFollowUps: metadata.queuedFollowUps.length + 1, ...(delivery === "steer" ? { conversionReason: `task ${metadata.state}, no live stream to steer` } : {}) } const acceptedRun: NonNullable = { id: runId, sequence, acceptanceOrder, kind: "followup", state: "queued", input: content, acceptedAt } if (!metadata.activeRun) { result.queuedFollowUps = 0 return { ...metadata, state: "queued", activeRun: acceptedRun, lastRunSequence: sequence, updatedAt: acceptedAt } } return { ...metadata, lastRunSequence: sequence, queuedFollowUps: [...metadata.queuedFollowUps, { id: runId, sequence, acceptanceOrder, content, acceptedAt }], updatedAt: acceptedAt } }) } catch (error) { if (!this.#started) { this.#accepting = false this.dispose() this.#runtimeCompletion.resolve(undefined) this.#initialCompletion.resolve(undefined) } throw error } if (!result) throw new Error("Task input was not accepted") this.start() return result } private async run(): Promise { try { while (!this.#stopRequested) { const activeRun = await this.nextRun() if (!activeRun) break await this.executeRun(activeRun) } } finally { this.dispose() this.#initialCompletion.resolve(undefined) this.#runtimeCompletion.resolve(undefined) } } private async nextRun(): Promise | null> { const selected: { run: NonNullable | null } = { run: null } await mutateTaskMetadata(this.#paths, metadata => { if (metadata.activeRun) { selected.run = metadata.activeRun return metadata } const [next, ...remaining] = metadata.queuedFollowUps if (!next) { this.#accepting = false return metadata } selected.run = { id: next.id, sequence: next.sequence, ...(next.acceptanceOrder ? { acceptanceOrder: next.acceptanceOrder } : {}), kind: "followup", state: "queued", input: next.content, acceptedAt: next.acceptedAt } return { ...metadata, state: "queued", activeRun: selected.run, queuedFollowUps: remaining, updatedAt: Date.now() } }) return selected.run } private async executeRun(run: NonNullable): Promise { this.#recordingRunId = run.id this.#lastHeartbeatAt = 0 let outcome: NonNullable = "failed" this.#lastAssistantOutcome = undefined let settled = false try { // The stored acceptance order puts a promoted Follow-up ahead of later-accepted work. const schedule = { signal: this.#scheduleAbort.signal, ...(run.acceptanceOrder ? { acceptanceOrder: run.acceptanceOrder } : {}) } await getAgentCoordinator().run(schedule, async () => { if (this.#stopRequested) return try { let won = false const startedAt = Date.now() await mutateTaskMetadata(this.#paths, metadata => { const activeRun = metadata.activeRun if (!activeRun || activeRun.id !== run.id) return metadata won = true return { ...metadata, state: "running", activeRun: { ...activeRun, state: "running", startedAt }, lastActivity: { at: startedAt, action: "started" }, updatedAt: startedAt } }) if (!won) return // Inputs are logged as the child session observes them: the first is the run's // prompt, later ones are delivered Steers. this.#sawInput = false this.queueEventWrite(() => appendHistoryLog(this.#paths, { type: "run-start", sequence: run.sequence, kind: run.kind })) await this.#child.prompt(run.id, run.input, childPromptOptions(this.#expandPromptTemplates)) await this.#eventWrites outcome = this.#stopRequested ? "stopped" : (this.#lastAssistantOutcome ?? "failed") } catch { await this.#eventWrites outcome = this.#stopRequested ? "stopped" : "failed" } await this.settle(run, outcome, this.#stopRequested) settled = true }) } catch { await this.#eventWrites outcome = this.#stopRequested ? "stopped" : "failed" } this.#child.session.clearQueue() if (!settled) await this.settle(run, outcome, this.#stopRequested) } private async detach(): Promise { const detachedAt = Date.now() await mutateTaskMetadata(this.#paths, metadata => { const activeRun = metadata.activeRun if (!activeRun || activeRun.id !== this.#initialRunId || activeRun.detachedAt !== undefined) return metadata return { ...metadata, activeRun: { ...activeRun, detachedAt }, updatedAt: detachedAt } }) } private async settle( run: NonNullable, outcome: NonNullable, clearFollowUps = false ): Promise { let won = false let notification: TaskMetadata["notifications"][number] | undefined const timestamp = Date.now() await mutateTaskMetadata(this.#paths, async metadata => { const activeRun = metadata.activeRun if (activeRun?.id !== run.id) return clearFollowUps ? { ...metadata, queuedFollowUps: [], updatedAt: timestamp } : metadata won = true if ((outcome === "succeeded" || outcome === "failed") && (run.kind === "followup" || activeRun.detachedAt !== undefined)) { notification = await prepareTaskNotification(this.#paths, metadata, activeRun, "completion", outcome) } const [next, ...remaining] = clearFollowUps ? [] : metadata.queuedFollowUps if (next) { // Promote atomically: an idle gap would let new input jump the queue. const promoted: NonNullable = { id: next.id, sequence: next.sequence, ...(next.acceptanceOrder ? { acceptanceOrder: next.acceptanceOrder } : {}), kind: "followup", state: "queued", input: next.content, acceptedAt: next.acceptedAt } return { ...metadata, state: "queued", latestOutcome: outcome, activeRun: promoted, queuedFollowUps: remaining, ...(notification ? { notifications: appendTaskNotification(metadata.notifications, notification) } : {}), updatedAt: timestamp } } return { ...metadata, state: "idle", latestOutcome: outcome, activeRun: null, ...(clearFollowUps ? { queuedFollowUps: [] } : {}), ...(notification ? { notifications: appendTaskNotification(metadata.notifications, notification) } : {}), updatedAt: timestamp } }) // Observability only: a failed append must not skip notification delivery below. if (won) await appendHistoryLog(this.#paths, { type: "run-end", sequence: run.sequence, outcome }).catch(() => {}) if (won && notification) await deliverTaskNotifications(this.#paths).catch(() => {}) if (run.id === this.#initialRunId) this.#initialCompletion.resolve(undefined) } private recordEvent(event: AgentSessionEvent): void { const at = Date.now() const runId = this.#recordingRunId if (runId && event.type === "agent_start") { // before_agent_start hooks have finished; capture now, not when the queued write runs. const effectiveSystemPrompt = this.#child.session.systemPrompt this.queueEventWrite(() => writeTaskProgress(this.#paths, runId, { effectiveSystemPrompt })) } let action: string | undefined let latestReply: TaskMetadata["latestReply"] | undefined if ( runId && ((event.type === "message_update" && event.assistantMessageEvent.type === "thinking_delta") || event.type === "tool_execution_update") && at - this.#lastHeartbeatAt >= 1_000 ) { // Non-reply heartbeats are event-driven and capped at one durable write per second. this.#lastHeartbeatAt = at action = event.type === "tool_execution_update" ? `tool ${event.toolName.slice(0, 160)}` : "thinking" } if (runId && (event.type === "tool_execution_start" || event.type === "tool_execution_end")) { action = `tool ${event.toolName.slice(0, 160)}${event.type === "tool_execution_end" ? (event.isError ? " failed" : " complete") : ""}` } if ( (event.type === "message_start" || event.type === "message_update" || event.type === "message_end") && event.message.role === "assistant" && (event.type !== "message_update" || event.assistantMessageEvent.type === "text_delta") ) { latestReply = { text: messageText(event.message.content), streaming: event.type !== "message_end" } action = latestReply.streaming ? "responding" : "reply complete" } if (runId && action) { this.#pendingProgress = { ...this.#pendingProgress, runId, ...(latestReply ? { latestReply } : {}), lastActivity: { at, action } } this.scheduleProgressWrite(latestReply?.streaming === true) } if (event.type === "tool_execution_start") { this.#toolArguments.set(event.toolCallId, { name: event.toolName, arguments: renderUnknown(event.args) }) return } if (event.type === "message_end" && event.message.role === "assistant") { this.#lastAssistantOutcome = event.message.stopReason === "error" || event.message.stopReason === "aborted" ? "failed" : "succeeded" const content = messageText(event.message.content) if (content) this.queueEventWrite(() => appendHistoryLog(this.#paths, { type: "assistant", content })) return } if (event.type === "message_start" && event.message.role === "user") { const entry = { type: this.#sawInput ? ("steer" as const) : ("user" as const), content: messageText(event.message.content) } this.#sawInput = true this.queueEventWrite(() => appendHistoryLog(this.#paths, entry)) return } if (event.type === "tool_execution_end") { const started = this.#toolArguments.get(event.toolCallId) this.#toolArguments.delete(event.toolCallId) this.queueEventWrite(() => appendHistoryLog(this.#paths, { type: "tool", tool: started?.name ?? event.toolName, arguments: started?.arguments ?? "(unavailable)", result: renderUnknown(event.result), isError: event.isError }) ) } } /** * Each write rewrites and fsyncs the whole snapshot, so streaming deltas are * capped at two per second; completed replies and tool activity flush at once. */ private scheduleProgressWrite(deferrable: boolean): void { const wait = deferrable ? this.#lastProgressWriteAt + 500 - Date.now() : 0 if (wait > 0) { this.#progressTimer ??= setTimeout(() => this.scheduleProgressWrite(false), wait) this.#progressTimer.unref() return } clearTimeout(this.#progressTimer) this.#progressTimer = undefined if (this.#progressWriteQueued) return this.#progressWriteQueued = true this.queueEventWrite(async () => { try { // Coalesce replies and activity together so bursts cannot reorder their last action. // Drain inside this one chain link: settlement awaits the chain it saw, so a // terminal reply arriving mid-write must not be deferred to a later link. while (this.#pendingProgress) { if (this.#pendingProgress.latestReply?.streaming === true && this.#lastProgressWriteAt + 500 > Date.now()) break const { runId, ...progress } = this.#pendingProgress this.#pendingProgress = undefined this.#lastProgressWriteAt = Date.now() await writeTaskProgress(this.#paths, runId, progress) } } finally { this.#progressWriteQueued = false // Only a throttled streaming snapshot can remain. if (this.#pendingProgress) this.scheduleProgressWrite(true) } }) } private queueEventWrite(write: () => Promise): void { // history.md and progress snapshots are observability; their I/O errors never decide a run's outcome. this.#eventWrites = this.#eventWrites.then(write).catch(() => {}) } } class RuntimeClosingError extends Error { constructor() { super("Agent runtime is closing") this.name = "LovelyAgentRuntimeClosingError" } } function isRuntimeClosingError(error: unknown): boolean { return error instanceof RuntimeClosingError || (error instanceof Error && error.name === "LovelyAgentRuntimeClosingError") } type ControllableResident = ResidentAgent & { input(content: string, delivery: "followup" | "steer", options?: ResidentInputOptions): Promise } const COLD_RUNTIME_LOADS = Symbol.for("@xl0/pi-lovely-agents/cold-runtime-loads/v1") async function controllableRuntime( ctx: ExtensionContext, paths: TaskStoragePaths, options: AgentToolOptions ): Promise { const resident = getAgentCoordinator().getResident(paths.taskDirectory) if (resident) { if (!resident.input) throw new Error(`Task ${paths.taskRef} does not accept input`) return resident as ControllableResident } const loads = coldRuntimeLoads() let pending = loads.get(paths.taskDirectory) if (pending) return pending pending = openColdRuntime(ctx, paths, options) loads.set(paths.taskDirectory, pending) try { return await pending } finally { if (loads.get(paths.taskDirectory) === pending) loads.delete(paths.taskDirectory) } } async function openColdRuntime(ctx: ExtensionContext, paths: TaskStoragePaths, options: AgentToolOptions): Promise { const loaded = await readTaskMetadata(paths) if (loaded.status !== "ok") throw new Error(`Could not load task ${paths.taskRef}`) const metadata = loaded.metadata if (metadata.kind !== "agent") throw new Error("Bash tasks cannot be cold reopened") if (metadata.discardedAt !== null) throw new Error(`Task ${paths.taskRef} has been discarded`) if (metadata.state !== "idle" && metadata.state !== "interrupted") { throw new Error(`Task ${paths.taskRef} has active state ${metadata.state} but no resident runtime`) } const definition: AgentDefinition = { name: metadata.definitionName, description: `Retained configuration for ${metadata.definitionName}`, systemPrompt: metadata.sessionConfig.systemPrompt, source: "project", filePath: paths.metadata, displayPath: paths.metadata, ...(metadata.sessionConfig.tools ? { tools: [...metadata.sessionConfig.tools] } : {}), excludeAgentsMd: metadata.sessionConfig.excludeAgentsMd } const model = ctx.modelRegistry .getAvailable() .find(candidate => candidate.provider === metadata.model.provider && candidate.id === metadata.model.id) if (!model) throw new Error(`Task model "${metadata.model.provider}/${metadata.model.id}" is not authenticated`) const config = options.getConfig() const scopedModels: ScopedModel[] = metadata.sessionConfig.scopedModels.map(saved => { const savedModel = ctx.modelRegistry.getAll().find(candidate => candidate.provider === saved.provider && candidate.id === saved.id) if (!savedModel) throw new Error(`Retained scoped model "${saved.provider}/${saved.id}" is no longer available`) return { model: savedModel, ...(saved.thinkingLevel ? { thinkingLevel: saved.thinkingLevel } : {}) } }) const child = await (options.createChild ?? createChildSession)({ cwd: ctx.cwd, paths, definition, selection: { model, thinking: metadata.thinking }, scopedModels, parentDepth: metadata.depth - 1, maximumDepth: metadata.depth + (metadata.allowAgents ? 1 : 0), allowAgents: metadata.allowAgents, projectTrusted: ctx.isProjectTrusted(), expectedSessionId: metadata.childSessionId, ...(options.getAgentDir ? { agentDir: options.getAgentDir() } : {}) }) return new AgentRuntime(paths, child, metadata, config.expandPromptTemplates) } function coldRuntimeLoads(): Map> { const global = globalThis as typeof globalThis & { [COLD_RUNTIME_LOADS]?: Map> } global[COLD_RUNTIME_LOADS] ??= new Map() return global[COLD_RUNTIME_LOADS] } function buildAgentCreationToolResult( metadata: TaskMetadata, detached: boolean, output: Awaited> ): { content: [{ type: "text"; text: string }]; details: AgentCreationResult } { if (metadata.kind !== "agent") throw new Error("Expected an Agent task") const result: AgentCreationResult = { id: metadata.taskRef, run: 1, label: metadata.label, definition: metadata.definitionName, state: output.state, latestOutcome: output.latestOutcome, model: `${metadata.model.provider}/${metadata.model.id}`, thinking: metadata.thinking, depth: metadata.depth, allowAgents: metadata.allowAgents, detached, queuedFollowUps: metadata.queuedFollowUps.length, output } const pending = detached && (result.state === "queued" || result.state === "running") return { content: [ { type: "text", text: [ `task: ${result.id}`, `run: ${result.run}`, `state: ${result.state}`, ...(output.queueReason ? [`waiting: ${output.queueReason} (${output.capacity.active}/${output.capacity.limit} execution permits)`] : []), `outcome: ${result.latestOutcome ?? "none"}`, `detached: ${result.detached}`, `queued_followups: ${result.queuedFollowUps}`, "output:", pending ? "(pending; use task_output)" : result.output.text || "(no output)" ].join("\n") } ], details: result } } function validateInput(value: string, name: string, maximumBytes: number): void { if (!value.trim()) throw new Error(`${name} must be nonblank`) if (Buffer.byteLength(value, "utf8") > maximumBytes) throw new Error(`${name} must be at most ${maximumBytes} UTF-8 bytes`) } function createRunId(): string { return `r_${randomBytes(8).toString("hex")}` } function messageText(content: string | ReadonlyArray<{ type: string; text?: string }>): string { return typeof content === "string" ? content : content.map(part => (part.type === "text" ? (part.text ?? "") : "")).join("") } function renderUnknown(value: unknown): string { if (typeof value === "string") return value try { return JSON.stringify(value) ?? String(value) } catch { return String(value) } } function abortError(signal: AbortSignal): Error { return signal.reason instanceof Error ? signal.reason : new Error("Agent creation aborted") }