import type { StreamFn } from "@earendil-works/pi-agent-core"; import type { Message } from "@earendil-works/pi-ai"; import { type AgentSessionEvent, type ExtensionFactory, type ModelRegistry as ModelRegistryType, type ToolDefinition } from "@earendil-works/pi-coding-agent"; import { type TailLogEntry } from "./viewport/events.js"; export interface ForgePersona { name: string; description: string; model?: string; tools?: string[]; systemPrompt: string; filePath: string; } export interface UsageStats { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; contextTokens: number; turns: number; } export interface SubagentResult { exitCode: 0 | 1; messages: Message[]; usage: UsageStats; stopReason?: string; errorMessage?: string; model?: string; provider?: string; /** * Absolute path of the auto-exported subagent transcript * (.forge/transcripts//____.json). Unset only * when the export failed (non-fatal). Orchestrators thread this into the * phase-end event's `subagentTranscriptPath` and the transcript archive. */ subagentTranscriptPath?: string; } export interface RunSubagentOptions { persona: ForgePersona; task: string; cwd?: string; signal?: AbortSignal; /** * Forge root directory. When provided, FORGE_ROOT is set in the subagent's * process environment so that $FORGE_ROOT in workflow tool paths resolves * correctly. Without this, subagent bash calls to store-cli etc. will fail * with "Cannot find module '/tools/store-cli.cjs'". */ forgeRoot?: string; /** * Per-phase model routing (Plan 16 Slice 2). When set, forge-cli calls * `session.agent.setModel(registry.find(provider, model))` after session * creation. If the model isn't in the registry, or setModel throws, the * phase falls through to inherit pi's current model — no crash. * * result.model / result.provider always reflect what the stream returned * (not this field) — IL10 emit path reads runtime telemetry, not the request. */ requestedModel?: { provider: string; model: string; }; /** * FORGE-BUG-001 followup. The host pi session's ModelRegistry — the only * registry that contains extension-registered providers (e.g. * `ollama-cloud`). When omitted, runForgeSubagent builds a fresh * `ModelRuntime.create()` which only sees the * baseline providers wired up at process start, and every per-persona * `setModel` call will MISS — silently falling back to pi's current * model (typically Anthropic). Callers running inside an extension * command context MUST pass `ctx.modelRegistry` here. * * Internally converted to a ModelRuntime for the subagent session; * extension-registered providers are copied across. */ modelRegistry?: ModelRegistryType; onEvent?: (event: AgentSessionEvent) => void; /** * Optional tag included in the auto-exported transcript filename for * greppability — e.g. `${taskId}__${phaseRole}`. Stripped to a safe * filename slug. See forge-cli#8. */ exportTag?: string; /** * LIVE reference to the viewport observer's tail-line record * (observer.state.tailLog). Read at export time — both on the normal * path and in the process-exit hook — and persisted next to the phase * transcript as `.tail.jsonl` so transcript replay * shows the exact lines the dashboard rendered, even after /quit. */ tailLog?: TailLogEntry[]; /** * Optional prompt-cache session identifier forwarded to the underlying pi * Agent (which forwards it to the LLM provider as a cache namespace key). * * Forge sets this to a sprint-scoped value (e.g. `forge:FORGE-S21`) so * that every persona spawned within a sprint shares a cache prefix on * Anthropic (system prompt + persona + skills + tools all stay warm) and * a stable `prompt_cache_key` on OpenAI. This captures the majority of * cacheable mass within a ~10-minute phase. Note: the cache *retention* * window is provider-default ("short") unless the operator opts into * `PI_CACHE_RETENTION=long` — see the rationale in bin/forge.ts. * * When omitted, no sessionId is set and providers fall back to their * default cache behaviour (Anthropic: implicit prefix match within the * stream; OpenAI: in-memory, request-scoped). */ cacheSessionId?: string; /** * Test-only seam: when set, replaces the underlying pi Agent's `streamFn` * after `createAgentSession`. The agent loop still executes real tool calls * — only the LLM provider is replaced with a scripted fake. Used by * `test/helpers/scripted-subagent.ts` to drive ceremony / pipeline tests * without mocking `forge-subagent` itself. See forge-cli#17. * * MUST be `undefined` in production code. Setting this in production code * would silently route a real subagent dispatch through a fake provider. */ streamFn?: StreamFn; /** * Forge tool definitions to inject into the subagent session via * `createAgentSession({ customTools })`. When provided, the subagent can * call `forge_store`, `forge_store_describe`, etc. as named MCP tools * instead of shelling out to `store-cli.cjs` via bash. * * Use `getSubagentTools(forgeToolDefs, persona.name)` from forge-tools.ts * to build the appropriate subset for each persona. */ customTools?: ToolDefinition[]; /** * When true, suppress extension and skill discovery in the subagent session. * Default false — subagents inherit the parent's extensions (lean-ctx, * ollama-cloud, etc.) so they get the same tool surface. * * Test harnesses should set this to true to avoid loading globally-installed * extensions (which adds latency and creates environment-dependent behavior). */ noExtensions?: boolean; /** * Extension factories to inject into the subagent session via * DefaultResourceLoader({ extensionFactories }). When provided, the * factories are registered on the subagent's ExtensionRunner. * Used by Mechanism E (T09) to wire the Forge-aware compaction handler * into specific subagent sessions. * * These factories fire even when noExtensions=true — explicitly-passed * factories are separate from globally-discovered extensions and are not * suppressed by the noExtensions flag. This matches the pattern proven in * spike-r-cg3 (T08) and is the correct production behavior: callers that * want the governor's compaction handler can pass it explicitly regardless * of the global extension discovery flag. * * Best-effort: factory errors must be caught by the factory itself * (pi requirement). Callers must not pass factories that can throw at * registration time. */ extensionFactories?: ExtensionFactory[]; } /** * Load a Forge persona from `.forge/personas/.md`. * * Frontmatter (optional, all keys may be missing): * description: short role summary * model: pi model id (default: project default) * tools: comma-separated tool list (default: all coding tools) * * If frontmatter absent, name derives from filename and the entire file body * is used as the system prompt. */ export declare function loadForgePersona(name: string, cwd: string): ForgePersona; /** * Load a Forge persona from an explicit personas directory (`/.md`). * * Same parsing contract as {@link loadForgePersona}, but the caller supplies the * directory. Used by `/forge:init`, whose orchestration personas ship in the * bundle's `.base-pack/personas/` and must load *before* Phase 3 materializes * `.forge/personas/` (which may be absent entirely on a fresh or reset project). */ export declare function loadForgePersonaFromDir(name: string, personasDir: string): ForgePersona; /** * Spawn a Forge subagent in-process via pi SDK and run a task to completion. * * Returns a SubagentResult after `session.prompt()` resolves. If `signal` * fires, the session is aborted and the call returns with exitCode=1 and * stopReason="aborted". * * Usage events are aggregated from `turn_end` (per-turn assistant message * usage). contextTokens is the PEAK per-turn totalTokens (high-water context * size), not a cumulative sum and not merely the latest turn. */ export declare function runForgeSubagent(opts: RunSubagentOptions): Promise; interface WriteTranscriptOptions { cwd: string; persona: string; tag?: string; result: SubagentResult; startedAt: Date; /** * The task body sent to session.prompt(). Captured because it never * appears in result.messages (the capture records assistant/toolResult * only) — without it, transcript replay has no prompt to show. */ prompt?: string; } /** * Format a Date as a compact UTC timestamp for transcript filenames. * Example: 2026-05-28T13:35:00.123Z → "20260528T133500Z" (no dashes, * colons, or fractional seconds). Lexicographic sort matches chronological * order, which is the property that makes `ls` show transcripts in run order. */ export declare function formatTranscriptTimestamp(d: Date): string; /** Derive the subdirectory and filename from the export tag. * * exportTag format for tasks/bugs/sprints: "__" * → dir = (e.g. "FORGE-S21-T04") * → file = ____.json * * Untagged / partial tag: "general" as dir, "__.json" filename. */ export declare function computeTranscriptPath(cwd: string, persona: string, tag: string | undefined, startedAt: Date): string; export declare function writeSubagentTranscript(opts: WriteTranscriptOptions): string; /** Extract the final assistant text from a SubagentResult. */ export declare function getFinalOutput(messages: Message[]): string; export {};