import type { PathMetadata } from "@earendil-works/pi-coding-agent"; import { type ContextUsage, type CreateAgentSessionOptions, type LoadExtensionsResult, ModelRegistry, type PromptTemplate, type ResourceDiagnostic, type ResourceLoader, type Skill, type Theme, 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 ModelRoleConfig, 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?: (pendingPrompt?: string) => void; }, lastText: (messages: unknown[]) => string): Promise; /** * Resolve which concrete model spec a subagent should use. Precedence, most * specific first (see issue #142): * 1. options.model — an explicit per-agent model (also carries agentType / * phase model, which the workflow layer folds into options.model). * 2. options.modelRole + optional options.modelRoute — the semantic routing * role (worker/conductor/advisor/security) resolved from the roles block * of the model routing config. A named route is an exact specialist * selection, never a fallback ladder: an unknown/absent route does NOT * fall back to the role default; it surfaces an error upstream. * 3. options.tier — DEPRECATED size tier (small/medium/big), resolved via the * legacy tiers block, falling back to the session's main model when the * tier has no configured entry. Kept for migration of saved workflows and * journals. * 4. DEFAULT ROLE — when none of the above are set but a config exists, * untagged agents default to the `conductor` role (the old `medium` tier's * successor) so a configured profile affects the whole workflow, not just * agents the script tagged. Fresh-install conductor == the session model, * so this is a no-op until the user customizes via `/workflows-models`. * Returns undefined when nothing applies, so the session default is used. * * Policy guards (issue #142): the security-only model (Fable) is rejected * outside the `security` role — after every source above, including an * explicit `model`. API-billed (google-ai-studio) models are rejected from * role defaults/routes; an exact `model` use requires `googleBillingOptIn`. * These throw rather than silently drop to the session model, because a * silent downgrade of a required security gate is unsafe. Callers that want * the legacy non-throwing behavior (e.g. old tests) can pass `{ enforcePolicy: false }`. * * `loadConfig` is injectable for testing; it defaults to reading from disk. */ export interface ResolveAgentModelSpecOptions { model?: string; /** Semantic routing role (worker/conductor/advisor/security). */ modelRole?: string; /** Named specialist route within the role (e.g. escalation, long-context). */ modelRoute?: string; /** DEPRECATED size tier (small/medium/big). Migration input only. */ tier?: string; modelTierConfig?: ModelRoleConfig | null; /** Visible operator opt-in to use an API-billed (google-ai-studio) exact model. */ googleBillingOptIn?: boolean; /** The role the caller is running under (for security-model enforcement). Defaults to modelRole. */ securityRoleContext?: string; /** When false, skip the security/billing policy guards (legacy/test path). */ enforcePolicy?: boolean; } export declare function resolveAgentModelSpec(options: ResolveAgentModelSpecOptions | { model?: string; tier?: string; modelTierConfig?: ModelTierConfig | null; }, mainModel: string | undefined, loadConfig?: () => ModelRoleConfig | null): string | undefined; export interface WorkflowAgentOptions { cwd?: string; /** Extra tools available to the subagent in addition to the structured output tool. */ tools?: ToolDefinition[]; /** * Base createAgentSession options. When a tool-authority fence is active, * `tools`, `customTools`, and `resourceLoader` are re-resolved after this layer * so session defaults cannot widen the fence. */ 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; } export declare function listAvailableModelSpecs(registry?: ModelRegistry): string[]; /** * Structural mirror of the SDK's `ResourceExtensionPaths` (not exported from the * package index). Used only by `ImmutableResourceLoader.extendResources`. */ interface ResourceExtensionPaths { skillPaths?: Array<{ path: string; metadata: PathMetadata; }>; promptPaths?: Array<{ path: string; metadata: PathMetadata; }>; themePaths?: Array<{ path: string; metadata: PathMetadata; }>; } /** * Clear the shared discovery cache. Exposed for tests and for an explicit * extension-reload handoff (issue #135 scope item 4): the host calls this when * the extension set changes (e.g. after `/n` reload) so the next fan-out * rediscovers against the new set instead of replaying a stale snapshot. The * handoff is a single function call; the next `ensureSharedDiscoveryLoader` * rebuilds one active set. */ export declare function invalidateSharedDiscoveryLoaders(): void; /** * A read-only, per-agent facade over the shared discovery loader. Applies the * context-mode + skills-allowlist filters at READ time without mutating the * shared base, so N subagents share one discovery reload. `reload()` is a no-op * (the base is already loaded; `createAgentSession` never reloads an injected * loader). `extendResources()` is a no-op with an observable warning — the * workflow subagent path never calls it (no `bindExtensions`), so this is purely * defense-in-depth against a future `resources_discover` hook. */ export declare class ImmutableResourceLoader implements ResourceLoader { private readonly base; private readonly noContextFiles; private readonly noSkills; private readonly systemPromptOverride; private readonly appendSystemPromptOverride; private readonly skillsFilter; constructor(options: { base: ResourceLoader; noContextFiles: boolean; noSkills: boolean; systemPrompt: string | undefined; appendSystemPrompt: string[] | undefined; skillsFilter?: (base: { skills: Skill[]; diagnostics: ResourceDiagnostic[]; }) => { skills: Skill[]; diagnostics: ResourceDiagnostic[]; }; }); getExtensions(): LoadExtensionsResult; getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[]; }; getPrompts(): { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[]; }; getThemes(): { themes: Theme[]; diagnostics: ResourceDiagnostic[]; }; getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string; }>; }; getSystemPrompt(): string | undefined; getSystemPromptSource(): { path: string; } | undefined; getAppendSystemPrompt(): string[]; getAppendSystemPromptSources(): Array<{ path: string; }>; /** * No-op (defense-in-depth). The workflow subagent path never calls this: a * session built via `createAgentSession` does not invoke `bindExtensions`, so * `extendResourcesFromExtensions` (the only caller of `extendResources`) never * runs, and no installed extension registers a `resources_discover` hook * today. If a future extension DOES register one and the host binds extensions * into a subagent session, per-session discovery cannot be served by the shared * immutable base — emit an observable warning rather than silently dropping * the paths or silently contaminating the shared base. */ extendResources(paths: ResourceExtensionPaths): void; /** No-op: the shared base is already loaded. `createAgentSession` never reloads * an injected loader, and re-loading the shared base would defeat the cache * and risk cross-agent mutation. A no-op keeps the facade immutable. */ reload(): Promise; } /** 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; /** * Semantic routing role (worker | conductor | advisor | security). Resolved * from the `roles` block of the model routing config (see /workflows-models). * An optional `modelRoute` selects a named specialist route within the role. * Precedence: explicit `model` > agentType model > `modelRole`(+`modelRoute`) * > deprecated `tier` > untagged conductor default > session model. */ modelRole?: string; /** Named specialist route within `modelRole` (e.g. escalation, long-context). */ modelRoute?: string; /** * Visible operator opt-in to use an API-billed (google-ai-studio) exact * `model`. Required when `model` is a google-ai-studio spec; otherwise the * call is rejected (API-billed models are never role defaults/routes). */ googleBillingOptIn?: boolean; /** * DEPRECATED size tier ("small" | "medium" | "big"), resolved from the legacy * `tiers` block of the model routing config (see /workflows-models). Kept only * as a migration input for existing saved workflows and journals; new authoring * guidance uses `modelRole`. An explicit `model` always takes priority. * @deprecated Use `modelRole` (small→worker, medium→conductor, big→advisor). */ tier?: string; /** * Model routing config snapshotted by runWorkflow. When present (including * null), run() must not re-read model-tiers.json mid-run. */ modelTierConfig?: ModelRoleConfig | 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; /** Whether the default tool universe should include resource-loader extension tools. */ private readonly includeResourceLoaderTools; 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?; /** Registry facade for an explicitly supplied session runtime. */ private sessionRegistry?; constructor(options?: WorkflowAgentOptions); /** * Extract the ModelRuntime from a ModelRegistry. * The runtime field is private in the type declaration, but the JS runtime * exposes it as a plain property. Isolated in one narrow helper so the * structural access is contained and reviewed. */ private static extractRuntime; /** * Resolve the registry used for model lookup. Precedence: per-run registry, * shared host registry, explicit session runtime, then disk fallback. */ private getRegistry; /** * Resolve the runtime passed to createAgentSession with the same precedence as * model lookup. Sharing the host runtime preserves dynamically registered * providers; a missing future SDK bridge fails clearly in extractRuntime(). */ private getRuntime; /** * 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; } export {};