import { type ContextUsage, type CreateAgentSessionOptions, ModelRegistry, type ToolDefinition } from "@earendil-works/pi-coding-agent"; import type { Static, TSchema } from "typebox"; import { type AgentHistoryEntry } from "./agent-history.js"; import { type WorkflowCompactionPolicyName } from "./compaction-policy.js"; import { type SystemPromptMode } from "./context-mode.js"; import { type GuardCtxReadOptions } from "./lean-ctx-guardrail.js"; import { type ModelTierConfig } from "./model-tier-config.js"; import { type StructuredOutputCapture } from "./structured-output.js"; /** * Last-resort structured-output recovery: extract a JSON block from prose, coerce * it toward the schema, and accept it only if it then validates. Never fabricates * — returns undefined unless the parsed value genuinely satisfies the schema. */ export declare function extractValidated(text: string, schema: TSchema): T | undefined; /** * The last assistant message's terminal metadata (stopReason/errorMessage). The pi * SDK does NOT throw provider usage/quota limits — it records them as an assistant * message with stopReason "error" and an errorMessage. This is the only place that * metadata is observable to the workflow layer. */ export declare function lastAssistantError(messages: unknown[]): { stopReason?: string; errorMessage?: string; } | undefined; /** * If the subagent's turn ended in a provider usage/quota/rate-limit error, throw a * PROVIDER_USAGE_LIMIT WorkflowError carrying the real provider message + reset hint. * Gated on stopReason === "error" so a successful turn whose text merely mentions * "rate limit" is never misclassified. recoverable:false so the run checkpoints * (paused) rather than being retried into the same wall or collapsed to a silent null. */ export declare function throwIfProviderLimit(messages: unknown[], label?: string): void; /** Minimal session surface resolveStructuredOutput needs (real session or a test double). */ export interface StructuredSession { prompt(text: string): Promise; setActiveToolsByName?(names: string[]): void; messages: unknown[]; } /** * Resolve a schema agent's result. If the tool was called, return the captured * value. Otherwise re-prompt up to maxSchemaRetries (tools restricted to * structured_output), then try strict schema-validated prose extraction, else * throw SCHEMA_NONCOMPLIANCE (non-recoverable — surfaced, never a silent null). * Module-level with an injected `lastText` so it is unit-testable. */ export declare function resolveStructuredOutput(session: StructuredSession, capture: StructuredOutputCapture, schema: TSchema, options: { maxSchemaRetries?: number; signal?: AbortSignal; label?: string; checkContextCap?: () => void; }, lastText: (messages: unknown[]) => string): Promise; /** * Resolve which concrete model spec a subagent should use. Precedence, most * specific first: * 1. options.model — an explicit per-agent model (also carries agentType / * phase model, which the workflow layer folds into options.model). * 2. options.tier — resolved via the model-tiers config, falling back to the * session's main model when the tier has no configured entry. * 3. DEFAULT TIER — when neither is set but the user has a model-tiers config, * untagged agents default to the "medium" tier so a configured tier set * actually affects the whole workflow (not just agents the script tagged). * Fresh-install medium == the session model, so this is a no-op until the * user customizes tiers via /workflows-models. * Returns undefined when nothing applies, so the session default is used. * * `loadConfig` is injectable for testing; it defaults to reading from disk. */ export declare function resolveAgentModelSpec(options: { model?: string; tier?: string; modelTierConfig?: ModelTierConfig | null; }, mainModel: string | undefined, loadConfig?: () => ModelTierConfig | null): string | undefined; export interface WorkflowAgentOptions { cwd?: string; /** Extra tools available to the subagent in addition to the structured output tool. */ tools?: ToolDefinition[]; /** Override any createAgentSession option (model, authStorage, resourceLoader, etc.). */ session?: Partial; /** Extra system guidance prepended to every subagent task. */ instructions?: string; /** * The session's main model (`provider/modelId`). Used as a fallback when * resolving opts.tier and no model-tiers.json config exists. Without this, * a workflow using `{ tier: "small" }` would log a warning and fall through * to the session default when no config is saved yet. */ mainModel?: string; /** * Host session ModelRegistry shared with subagents (upstream #49 port). When * set, tier/phase/model routing resolves against the same registry as the * main Pi session — including providers registered dynamically by extensions * — instead of an isolated disk registry. Absent → disk fallback. */ modelRegistry?: ModelRegistry; } /** * List the user's currently available models (those with auth configured) as * `provider/modelId` specs. Used to tell the workflow author which models it may * route agents to. Best-effort: returns [] if the registry can't be built. */ export declare function listAvailableModelSpecs(registry?: ModelRegistry): string[]; /** Real token/cost usage for a single subagent run, read from the SDK session. */ export interface AgentUsage { input: number; output: number; cacheRead: number; cacheWrite: number; total: number; cost: number; } export type AgentContextWindowLevel = "ok" | "warn" | "critical" | "over" | "unknown"; export interface AgentContextWindowStats { /** Tokens currently occupying the model context, usually provider input tokens for the completed turn. */ contextTokens: number; /** Model/runtime context window, when known. */ runtimeContextWindow?: number; /** Reserved response/scratch tokens subtracted from runtimeContextWindow, when known. */ reserve?: number; /** Runtime window minus reserve. */ effectiveWindow?: number; /** contextTokens / effectiveWindow, when effectiveWindow is known. */ occupancy?: number; /** Highest threshold crossed by this measurement. */ threshold?: number; /** Human-readable severity for UI/telemetry. */ level: AgentContextWindowLevel; /** Optional hard cap supplied by workflow policy. */ maxContextTokens?: number; /** True when contextTokens exceeded maxContextTokens. */ exceededMaxContextTokens?: boolean; /** Human-readable warning for UI/log/telemetry. */ warning?: string; } export type WorkflowCtxReadGuardrailOptions = Omit; export interface AgentRunOptions { label?: string; schema?: TSchemaDef; tools?: ToolDefinition[]; instructions?: string; signal?: AbortSignal; /** * Called once with this subagent's real usage, read from the session right * before disposal. Fires on both the success and error paths so partial * usage is never lost. `total === 0` means the provider reported no usage. */ onUsage?: (usage: AgentUsage) => void; /** * Model spec for this subagent: either `provider/modelId` (unambiguous) or a * bare `modelId`. When it can't be resolved, the session default is used and * a warning is logged. When omitted, the session default applies. */ model?: string; /** * Model tier name (e.g. "small", "medium", "big"). When set (and no explicit * `model` is given), the model is resolved from the user's model-tiers.json * config before `run()` starts, falling back to the session's main model when * the tier has no configured entry. An explicit `model` always takes priority, * so workflow scripts can use `{ tier: "small" }` for coarse routing without * caring which concrete model backs that tier. */ tier?: string; /** * Model-tier config snapshotted by runWorkflow. When present (including * null), run() must not re-read model-tiers.json mid-run. */ modelTierConfig?: ModelTierConfig | null; /** * Per-run ModelRegistry override (wins over the agent's shared registry and * the disk fallback). Lets a host thread its live registry into a single run. */ modelRegistry?: ModelRegistry; /** Called with the resolved model id once known (for display/telemetry). */ onModelResolved?: (modelId: string) => void; /** Called when `model`/`tier`/phase resolved to a spec that wasn't found (fell back to session default). */ onModelFallback?: (requestedSpec: string) => void; /** Called with a compact snapshot of this subagent's message/tool history. */ onHistory?: (history: AgentHistoryEntry[]) => void; /** Called with model-window occupancy stats for this subagent, when measurable. */ onContextWindow?: (stats: AgentContextWindowStats) => void; /** Per-subagent compaction policy. "auto" makes local/no-cache models compact earlier. */ compactionPolicy?: WorkflowCompactionPolicyName | null; /** Workflow run id/phase used to scope compaction telemetry emitted by this subagent. */ workflowRunId?: string; phase?: string; /** Hard cap for provider input/context tokens for this subagent. */ maxContextTokens?: number; /** Override the model output/reserve tokens used to compute effective context window. */ contextReserveTokens?: number; /** Run this agent in a different working directory (e.g. an isolated worktree). */ cwd?: string; /** * Directory to persist this subagent's NDJSON transcript into. When set, * a real (file-backed) SessionManager is used so the full subagent message * stream survives session disposal — matching Claude Code's per-subagent * `agent-.jsonl` transcript. When omitted, an in-memory session is used * (ad-hoc `agent()` with no run context) and nothing is written to disk. */ transcriptDir?: string; /** * Restrict the subagent's coding tools to these names (an agentType * definition's `tools` allowlist). Undefined = all coding tools. The * structured_output tool is always added after this filter, so a schema * still works under a restrictive allowlist. */ toolNames?: string[]; /** Remove these coding-tool names after the allowlist (an agentType `disallowedTools` denylist). */ disallowedToolNames?: string[]; /** Optional read-path guardrail options supplied by a harness_config expansion. */ ctxReadGuardrail?: WorkflowCtxReadGuardrailOptions; /** * With `schema`: how many extra repair turns to allow if the model finishes * without calling structured_output. Each retry re-prompts (tools restricted to * structured_output) before falling back to strict prose extraction. Default 2. */ maxSchemaRetries?: number; /** * Context-inheritance posture for this subagent (expands to the three * primitives below). When omitted, the explicit fields — else `inherit` — * apply. See context-mode.ts. Default `inherit` == today's behavior. */ contextMode?: string; /** Load project AGENTS.md / context files into the subagent session. Default true. */ inheritProjectContext?: boolean; /** "append": base prompt intact, role-as-task (default); "replace": role IS the base system prompt. */ systemPromptMode?: SystemPromptMode; /** Load skills into the subagent session. Default true. */ inheritSkills?: boolean; /** * Per-agent **skills allowlist**: when set, the subagent loads ONLY the named * skills (matched by skill `name`), regardless of `inheritSkills`/`contextMode` * skill posture. An empty array is a fence that yields ZERO skills (equivalent * to `inheritSkills:false`); `undefined` preserves today's behavior (the * resolved context posture decides whether skills load at all). * * Precedence: `skills` wins over `inheritSkills`/`contextMode` for the skills * channel — when set, a custom resource loader is always constructed (even * under `legacy`) and a `skillsOverride` filter keeps only the named skills. * Names that match no discovered skill warn (console) and are skipped; the run * never fails on an unknown name. */ skills?: string[]; /** * Inherit the main-agent append channel (`.pi/APPEND_SYSTEM.md`) into this * subagent. Default false: the main session's orchestration-only rules do not * leak into subagents (OpenCode-style). Set true (or use the `legacy` mode) to * restore the pre-feature behavior where subagents inherited them. */ inheritMainRules?: boolean; /** * The agentType role prompt to install AS the system prompt when the resolved * `systemPromptMode` is "replace". The workflow layer passes the agent `.md` * body here (and omits it from the task to avoid duplication). Ignored unless * the resolved mode is "replace". */ systemPromptText?: string; /** * Read-only fence: when true, the subagent never receives write tools * (edit, bash, write). The fence is the last filter step so it cannot be * bypassed by an allowlist from `harness_config` or `agentType`. */ readOnly?: boolean; } export type AgentRunResult = TSchemaDef extends TSchema ? Static : string; export declare function createGuardedReadOperations(cwd: string, guardrail: WorkflowCtxReadGuardrailOptions): { access(absolutePath: string): Promise; readFile(absolutePath: string): Promise; detectImageMimeType(absolutePath: string): Promise; }; export declare function applyCtxReadGuardrailToTools(baseTools: ToolDefinition[], cwd: string, guardrail: WorkflowCtxReadGuardrailOptions): ToolDefinition[]; export declare function buildAgentContextWindowStats(usage: Pick, options?: { runtimeContextWindow?: number; reserve?: number; maxContextTokens?: number; }): AgentContextWindowStats; export declare function buildContextWindowStatsForSession(usage: Pick, contextUsage: ContextUsage | undefined, options?: { runtimeContextWindow?: number; reserve?: number; maxContextTokens?: number; }): AgentContextWindowStats; export declare class WorkflowAgent { private readonly cwd; private readonly baseTools; private readonly sessionOptions; private readonly instructions?; private readonly mainModel?; /** Host session registry shared across all runs of this agent (upstream #49 port). */ private readonly sharedRegistry?; /** Lazily built once; shares the SDK's agentDir/auth so resolved models are authed. */ private registry?; constructor(options?: WorkflowAgentOptions); /** Registry precedence: per-run override > shared host registry > lazy disk fallback. */ private getRegistry; /** * Resolve a model spec to a Model. Accepts `provider/modelId` (unambiguous) * or a bare `modelId` (prefers auth-configured models, then any known model). * Returns undefined when nothing matches. */ private resolveModel; private resolveSettingsDefaultModel; run(prompt: string, options?: AgentRunOptions): Promise>; private buildPrompt; private lastAssistantText; }