add persisted pi workflow extension

This commit is contained in:
jb
2026-08-25 16:07:44 -04:00
parent bfb07282d6
commit c5c3316e49
46 changed files with 20642 additions and 94 deletions
+14 -77
View File
@@ -19,10 +19,7 @@ import type {
} from "@earendil-works/pi-coding-agent";
import {
createAgentSession,
DefaultResourceLoader,
getAgentDir,
SessionManager,
SettingsManager,
} from "@earendil-works/pi-coding-agent";
import type { Cause, Scope } from "effect";
import { Effect, Queue, Stream } from "effect";
@@ -34,21 +31,15 @@ import type {
TranscriptPart,
} from "../domain.ts";
import { SendError, SpawnError } from "../domain.ts";
import {
bindChildSessionExtensions,
childToolPolicy,
createChildResources,
shutdownAndDisposeChildSession,
waitForChildOperation,
} from "../../../shared/child-session.ts";
import { createToolCallTimeoutGuard } from "../../../shared/tool-call-timeout.ts";
const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
/** Tools that headless children must not receive. Everything else stays enabled. */
const CHILD_EXCLUDED_TOOL_NAMES = [
"subagent_spawn",
"subagent_wait",
"subagent_cancel",
"subagent_check",
"subagent_list",
"workflow",
"ask_user",
] as const;
// --- Model + effort resolution -----------------------------------------------
type ThinkingLevel = NonNullable<
@@ -92,60 +83,6 @@ function resolvePiModel(
throw new Error(`Unknown model "${hint}".`);
}
// --- Child session helpers (ported from v1 shared/child-session.ts) -----------
/** Load normal global/package resources and trust-gated project resources. */
async function createChildResources(cwd: string, projectTrusted: boolean) {
const agentDir = getAgentDir();
const settingsManager = SettingsManager.create(cwd, agentDir, {
projectTrusted,
});
const loader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });
await loader.reload();
return { loader, settingsManager };
}
function waitBounded(operation: Promise<unknown>, timeoutMs: number) {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<void>((resolve) => {
timer = setTimeout(resolve, timeoutMs);
});
return Promise.race([
operation.then(
() => undefined,
() => undefined,
),
timeout,
])
.catch(() => {})
.finally(() => {
if (timer) clearTimeout(timer);
});
}
/** Emit child session_shutdown (bounded), then dispose. Never throws. */
async function shutdownAndDisposeChildSession(session: AgentSession) {
try {
if (session.extensionRunner.hasHandlers("session_shutdown")) {
await waitBounded(
session.extensionRunner.emit({
type: "session_shutdown",
reason: "quit",
}),
CHILD_SHUTDOWN_TIMEOUT_MS,
);
}
} catch {
// Extension runner inspection/emission is best-effort during teardown.
} finally {
try {
session.dispose();
} catch {
// Disposal is terminal and must remain idempotent for callers.
}
}
}
// --- Event translation ----------------------------------------------------------
function messageRole(msg: unknown): Message["role"] | undefined {
@@ -281,10 +218,10 @@ const makePiSession = (
const session = yield* Effect.tryPromise({
try: async () => {
const { loader, settingsManager } = await createChildResources(
task.cwd,
task.parent.projectTrusted,
);
const { loader, settingsManager } = await createChildResources({
cwd: task.cwd,
projectTrusted: task.parent.projectTrusted,
});
const { session } = await createAgentSession({
cwd: task.cwd,
sessionManager: SessionManager.create(task.cwd),
@@ -292,13 +229,13 @@ const makePiSession = (
resourceLoader: loader,
model,
thinkingLevel,
excludeTools: [...CHILD_EXCLUDED_TOOL_NAMES],
...childToolPolicy(),
});
// Start child extension session hooks/resources in headless mode.
// A rejection here would otherwise leak the freshly created session:
// the scope finalizer that owns cleanup is only registered later.
try {
await session.bindExtensions({ mode: "print" });
await bindChildSessionExtensions(session);
} catch (error) {
await shutdownAndDisposeChildSession(session);
throw error;
@@ -493,7 +430,7 @@ const makePiSession = (
} catch {
// Continue with abort/dispose.
}
await waitBounded(session.abort(), CHILD_SHUTDOWN_TIMEOUT_MS);
await waitForChildOperation(session.abort());
await shutdownAndDisposeChildSession(session);
Queue.endUnsafe(events);
}),