/** * agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results. */ import type { Model } from "@earendil-works/pi-ai"; import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { type AgentSession, DefaultResourceLoader, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { SubagentType, ThinkingLevel } from "./types.js"; /** * Tool names registered by THIS extension. Single source of truth so the * registration sites (index.ts) and the subagent exclusion list below can't * drift apart. These are our own tools, not pi built-ins, so they can't be * derived from pi — but they only need defining once. */ export declare const SUBAGENT_TOOL_NAMES: { readonly AGENT: "agent"; readonly GET_RESULT: "get_subagent_result"; readonly STEER: "steer_subagent"; }; /** * Canonical name of an extension for `extensions: [...]` allowlist matching. * Lowercased — extension names match case-insensitively so `extensions: [Mcp]` * resolves the same as `[mcp]`. Tool names within `ext:foo/bar` are not affected. * Directory extensions (`foo/index.ts`) resolve to the parent directory name; * single-file extensions to the basename minus `.ts`/`.js`. */ export declare function extensionCanonicalName(extPath: string): string; /** * All names an extension answers to for allowlist matching (lowercased): its * path-derived {@link extensionCanonicalName} plus, when a pi package manifest * declares this entry, that package's unscoped short name (`@scope/foo` → `foo`). * #143: an extension installed via `pi.extensions: ["./src/index.ts"]` would * otherwise only ever match as `src` (the source directory), never by its * package name. The path-derived name is preserved, so it keeps matching too. */ export declare function extensionCanonicalNames(extPath: string): string[]; /** * Classify `extensions: string[]` frontmatter entries for the loader-level filter. * * An entry is a PATH iff it contains a path separator or starts with `~`; otherwise * it is a NAME. `"*"` sets the wildcard flag (keep all default-discovered extensions). * * Path entries are resolved (`~` expanded, made absolute against `cwd`) into `paths` * — and their canonical name is also added to `names`. The loader override matches * everything by canonical name, so path-loaded extensions are matched via their name * rather than their post-staging `Extension.path`. */ export declare function parseExtensionsSpec(entries: string[], cwd: string): { names: Set; paths: string[]; wildcard: boolean; }; /** * Parse raw `ext:` selector strings (from the `tools:` CSV) into the set of * extension names to keep loaded and a per-extension tool-narrowing map. * * `ext:foo` → `extNames` has `foo`, no narrowing entry (all of foo's tools). * `ext:foo/bar` → `extNames` has `foo`, `narrowing.foo` has `bar` (only `bar`). * A name lands in `narrowing` only when a `/tool` form is seen, so a bare * `ext:foo` alongside `ext:foo/bar` leaves narrowing in effect (narrowing wins). * The split is on the first `/`; extension canonical names never contain `/`. */ export declare function parseExtSelectors(entries: string[]): { extNames: Set; narrowing: Map>; }; /** * Keep a subagent's tool scope correct as extensions register tools over time. * * Extensions may call `registerTool` long after load — pi-mcp from `session_start`, * context-mode from `before_agent_start` — so scope has to be re-derived rather than * snapshotted. `registerTool` writes into the very `extension.tools` maps this reads, * so `inScope()` sees late arrivals on the next call. * * Two enforcement points, because neither covers the whole picture: * * - `turn_end` re-narrows the ACTIVE set. pi emits `turn_end` immediately before * `prepareNextTurn` re-snapshots `agent.state.tools`, and session listeners run * synchronously, so the narrow lands in time for turns 2..N. * - `beforeToolCall` blocks out-of-scope calls. Turn 1 cannot be narrowed at all: * `before_agent_start` fires INSIDE `prompt()` and may widen the tool set, but * `createContextSnapshot()` freezes that turn's tools immediately after — there * is no hook in between. A call-time check is the only correct guard there. * * Both are installed on the session and deliberately NOT unsubscribed: they must * outlive the `runAgent` call so resumed/steered turns stay scoped. pi's `dispose()` * clears `_eventListeners`, so they die with the session rather than leaking. * * Only meaningful when extensions are loaded — under `noExtensions`/`isolated` the * static `allowedToolNames` allowlist already gates the registry itself. */ export declare function installExtensionToolScope(session: AgentSession, ctx: { loader: DefaultResourceLoader; toolNames: string[]; disallowedSet: Set | undefined; extNames: Set; narrowing: Map>; extensionToolNames?: ReadonlySet; }): void; /** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */ export declare function normalizeMaxTurns(n: number | undefined): number | undefined; /** Get the default max turns value. undefined = unlimited. */ export declare function getDefaultMaxTurns(): number | undefined; /** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */ export declare function setDefaultMaxTurns(n: number | undefined): void; /** Get the grace turns value. */ export declare function getGraceTurns(): number; /** Set the grace turns value (minimum 1). */ export declare function setGraceTurns(n: number): void; /** Info about a tool event in the subagent. */ export interface ToolActivity { type: "start" | "end"; toolName: string; } export interface RunOptions { /** ExtensionAPI instance — used for pi.exec() instead of execSync. */ pi: ExtensionAPI; /** Explicit execution contract for non-interactive callers such as Magic Context. */ profile?: AgentExecutionProfile; /** Manager-assigned id; suffixes session name to disambiguate parallel spawns (e.g. `Explore#a1b2c3d4`). */ agentId?: string; model?: Model; maxTurns?: number; signal?: AbortSignal; isolated?: boolean; inheritContext?: boolean; thinkingLevel?: ThinkingLevel; /** Override working directory (e.g. for worktree isolation). */ cwd?: string; /** * Where .pi config is discovered (project extensions, skills, pi settings, * agent memory). Default: same as the working directory. The manager sets * this to the parent session's cwd when `SpawnOptions.cwd` points the * working directory elsewhere — the agent works *there* but carries the * parent project's config (the target's `.pi` extensions never execute). * * WARNING for future callers: if you pass `cwd` pointing at a directory the * user didn't open, you almost certainly must pass `configCwd` too — * omitting it makes the target's `.pi` extensions execute in this process. * (Worktree isolation is the one intentional exception: its copy IS the * parent's repo, so config resolving inside it is correct.) */ configCwd?: string; /** Called on tool start/end with activity info. */ onToolActivity?: (activity: ToolActivity) => void; /** Called on streaming text deltas from the assistant response. */ onTextDelta?: (delta: string, fullText: string) => void; onSessionCreated?: (session: AgentSession) => void; /** Called at the end of each agentic turn with the cumulative count. */ onTurnEnd?: (turnCount: number) => void; /** * Called once per assistant message_end with that message's usage delta. * Lets callers maintain a lifetime accumulator that survives compaction * (which replaces session.state.messages and resets stats-derived sums). */ onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number; }) => void; /** * Called when the session successfully compacts. `tokensBefore` is upstream's * pre-compaction context size estimate. Aborted compactions don't fire. */ onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number; }) => void; } /** * A non-interactive agent contract. It bypasses `.pi/agents` configuration while * retaining the same public Pi AgentSession execution path as normal subagents. */ export interface AgentExecutionProfile { /** Session-name prefix used to identify the child and its capability set. */ readonly name: string; /** Complete child system prompt; no parent prompt or agent template is appended. */ readonly systemPrompt: string; /** Optional provider/model reference resolved through the parent model registry. */ readonly model?: string; /** Optional explicit thinking level. */ readonly thinkingLevel?: ThinkingLevel; /** Built-in tools available to the child. */ readonly toolNames: readonly string[]; /** Extensions to load: true for the host set, false for none, or package/path allowlist. */ readonly extensions: true | readonly string[] | false; /** Extension tools explicitly allowed after extensions load. Omit to allow all loaded tools. */ readonly extensionToolNames?: readonly string[]; /** Tools denied even when listed above. */ readonly disallowedTools?: readonly string[]; /** Skills to preload. Defaults to false. */ readonly skills?: readonly string[] | false; /** Persist the child session. Defaults to false. */ readonly persistSession?: boolean; /** Explicit child capabilities consumed by cooperating extension entries. */ readonly capabilities?: readonly string[]; } declare global { var __hepiSubagentCapabilitiesBySessionName: Map | undefined; } export interface RunResult { responseText: string; session: AgentSession; /** Tool calls requested during this execution. */ toolCallCount: number; /** True if the agent was hard-aborted (max_turns + grace exceeded). */ aborted: boolean; /** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */ steered: boolean; /** * A failure message for the run's FINAL assistant turn, when that turn failed: * a provider error (stopReason "error"), or a "length" stop that produced no * text (a silent max-token death). pi resolves an exhausted-retries failure * normally instead of rejecting, so without this the manager would report such * a run as completed — with an empty result, or worse, an earlier turn's text * presented as the answer (#144). Undefined for a clean stop, or a "length" * stop that produced text (a legitimate truncated answer). */ failure?: string; } export declare function parseThinkingLevel(value: string | undefined): ThinkingLevel | undefined; /** Execute an explicit non-interactive profile without AgentManager UI state. */ export declare function executeAgentProfile(ctx: ExtensionContext, profile: AgentExecutionProfile, prompt: string, options: Omit): Promise; export declare function runAgent(ctx: ExtensionContext, type: SubagentType, prompt: string, options: RunOptions): Promise; /** * Send a new prompt to an existing session (resume). */ export declare function resumeAgent(session: AgentSession, prompt: string, options?: { onToolActivity?: (activity: ToolActivity) => void; onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number; }) => void; onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number; }) => void; signal?: AbortSignal; }): Promise<{ text: string; failure?: string; }>; /** * Send a steering message to a running subagent. * The message will interrupt the agent after its current tool execution. */ export declare function steerAgent(session: AgentSession, message: string): Promise; /** * Get the subagent's conversation messages as formatted text. */ export declare function getAgentConversation(session: AgentSession): string;