/** * types.ts — Type definitions for the subagent system. */ import type { ModelThinkingLevel } from "@earendil-works/pi-ai"; import type { AgentSession } from "@earendil-works/pi-coding-agent"; import type { LifetimeUsage } from "./usage.js"; /** Pi's full model thinking domain, including explicit `off`. */ export type ThinkingLevel = ModelThinkingLevel; /** Agent type: any string name (built-in defaults or user-defined). */ export type SubagentType = string; /** Names of the three embedded default agents. */ export const DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as const; /** Memory scope for persistent agent memory. */ export type MemoryScope = "user" | "project" | "local"; /** * Isolation mode accepted at invocation boundaries. * `"off"` is an explicit no-worktree value and is collapsed to normal execution * before the run starts; `"worktree"` is the only runtime isolation mode. */ export type IsolationMode = "off" | "worktree"; /** Unified agent configuration — used for both default and user-defined agents. */ export interface AgentConfig { name: string; displayName?: string; description: string; builtinToolNames?: string[]; /** Raw `ext:` selector entries from the `tools:` CSV, e.g. ["ext:foo", "ext:bar/x"]. * Presence of any entry flips extension tools to an explicit allowlist. */ extSelectors?: string[]; /** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */ disallowedTools?: string[]; /** true = inherit all, string[] = only listed, false = none */ extensions: true | string[] | false; /** Extension-name denylist applied after the `extensions:` include set. Exclude wins. * Plain canonical names only (case-insensitive); no paths, no wildcard. */ excludeExtensions?: string[]; /** true = inherit all, string[] = only listed, false = none */ skills: true | string[] | false; model?: string; thinking?: ThinkingLevel; maxTurns?: number; /** Persist this subagent as a normal pi session instead of keeping it in memory only. */ persistSession?: boolean; /** Write the subagent's .output transcript. Defaults to true; false suppresses only that transcript. */ outputTranscript?: boolean; /** Optional session directory used when persistSession is true. Omitted = pi's normal session location. */ sessionDir?: string; systemPrompt: string; promptMode: "replace" | "append"; /** Default for spawn: fork parent conversation. undefined = caller decides. */ inheritContext?: boolean; /** Default for spawn: run in background. undefined = caller decides. */ runInBackground?: boolean; /** Default for spawn: no extension tools. undefined = caller decides. */ isolated?: boolean; /** Persistent memory scope — agents with memory get a persistent directory and MEMORY.md */ memory?: MemoryScope; /** * Isolation mode — "worktree" requests a temporary git worktree; "off" * vetoes a caller's worktree request because agent config has precedence. */ isolation?: IsolationMode; /** true = this is an embedded default agent (informational) */ isDefault?: boolean; /** false = agent is hidden from the registry */ enabled?: boolean; /** Where this agent was loaded from */ source?: "default" | "project" | "global"; } /** * Complete caller-supplied agent definition for cross-extension orchestration. * Model, thinking, context inheritance, isolation, worktree mode, and output * transcript stay spawn-level choices. Registry/discovery metadata is excluded * so an inline role cannot be mistaken for a persisted project/global agent. */ export type InlineAgentConfig = Omit< AgentConfig, | "model" | "thinking" | "source" | "isDefault" | "enabled" | "inheritContext" | "runInBackground" | "isolated" | "isolation" | "outputTranscript" >; /** Who owns delivery of the terminal result to the parent conversation. */ export type CompletionOwner = "runtime" | "caller"; /** Stable, serializable model identity for lifecycle events. */ export interface AgentModelIdentity { provider: string; modelId: string; } export type JoinMode = 'async' | 'group' | 'smart'; /** How a completed background agent is delivered into the parent agent loop. */ export type CompletionDelivery = "steer" | "followUp"; /** * Display mode for the persistent above-editor agent widget. * - `all`: show every agent (foreground + background). * - `background`: hide foreground agents (they already render inline as the * Agent tool result, #118); show background/queued/scheduled/RPC. * - `off`: hide the widget entirely. */ export type WidgetMode = 'all' | 'background' | 'off'; /** Versioned construction recipe; credentials and live objects are never persisted. */ export interface AgentResumeSnapshot { version: 1; config: AgentConfig; systemPrompt: string; cwd: string; configCwd: string; model: AgentModelIdentity; thinkingLevel: ThinkingLevel; isolated: boolean; maxTurns?: number; graceTurns?: number; } export interface AgentRecord { id: string; type: SubagentType; description: string; status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error"; result?: string; error?: string; toolUses: number; startedAt: number; completedAt?: number; session?: AgentSession; abortController?: AbortController; promise?: Promise; groupId?: string; joinMode?: JoinMode; /** Completion delivery policy fixed when the record is created. */ completionDelivery: CompletionDelivery; /** Set when result was already consumed via get_subagent_result — suppresses completion notification. */ resultConsumed?: boolean; /** Monotonic execution generation; fences held notifications on resume. */ runGeneration?: number; /** Last completed report, kept separately so failed continuations cannot erase it. */ previousResult?: string; /** Construction recipe required for fail-closed disk recovery. */ resumeSnapshot?: AgentResumeSnapshot; /** Steering messages queued before the session was ready. */ pendingSteers?: string[]; /** Worktree info if the agent is running in an isolated worktree. */ worktree?: { path: string; branch: string; baseSha: string; workPath: string }; /** Worktree cleanup result after agent completion. */ worktreeResult?: { hasChanges: boolean; branch?: string }; /** The tool_use_id from the original Agent tool call. */ toolCallId?: string; /** Path to the streaming output transcript file. */ outputFile?: string; /** Persisted Pi session file, when this agent is not running in memory. */ sessionFile?: string; /** Cleanup function for the output file stream subscription. */ outputCleanup?: () => void; /** * Lifetime usage breakdown, accumulated via assistant `message_end` events. * Survives compaction and retains input/output/cacheRead/cacheWrite plus cost * when available. The legacy compact total remains input + output + * cacheWrite; cacheRead is breakdown-only (issue #38). */ lifetimeUsage: LifetimeUsage; /** Number of times this agent's session has compacted. Initialized to 0 at spawn. */ compactionCount: number; /** * Whether this agent was spawned to run in the background. Tri-state, set at * spawn from `SpawnOptions.isBackground`: `true` = background, `false` = * foreground (has an inline Agent tool-result surface), `undefined` = the * caller never declared it (e.g. a cross-extension RPC spawn, which is detached * and has no inline surface). The widget's background-only filter keys off this * — and excludes only explicit `false`, so `undefined` agents stay visible. * Reliable across ALL spawn paths, unlike the UI-only `invocation` snapshot, * which only the Agent-tool path populates. */ isBackground?: boolean; /** Resolved spawn params, captured for UI display. Fixed at spawn time. */ invocation?: AgentInvocation; /** Inline role display metadata; absent for ordinary registry-backed agents. */ inlineDisplayName?: string; inlinePromptMode?: "replace" | "append"; /** Optional cross-extension correlation key, never sourced from model output. */ correlationId?: string; /** Defaults to runtime when omitted, preserving ordinary Agent notifications. */ completionOwner?: CompletionOwner; /** Requested route, captured only for correlated cross-extension runs. */ requestedModel?: AgentModelIdentity; requestedThinkingLevel?: ThinkingLevel; /** Effective route after the child session has been constructed. */ effectiveModel?: AgentModelIdentity; effectiveThinkingLevel?: ThinkingLevel; } export interface AgentInvocation { /** Canonical effective route for detailed viewers and persisted recovery UI. */ modelIdentity?: AgentModelIdentity; /** Effective short model label when known (e.g. "haiku"), including parent-inherited. */ modelName?: string; /** True when the effective model is the parent session model (for TUI "(inherit)" chips). */ modelInherited?: boolean; thinking?: ThinkingLevel; maxTurns?: number; isolated?: boolean; inheritContext?: boolean; runInBackground?: boolean; isolation?: IsolationMode; } /** Details attached to custom notification messages for visual rendering. */ export interface NotificationDetails { id: string; description: string; status: string; toolUses: number; turnCount: number; maxTurns?: number; totalTokens: number; durationMs: number; outputFile?: string; error?: string; resultPreview: string; /** Additional agents in a group notification. */ others?: NotificationDetails[]; } export interface EnvInfo { isGitRepo: boolean; branch: string; platform: string; } /** * A subagent spawn registered to fire on a schedule. * * Stored at `/.pi/subagent-schedules/.json`. Session-scoped: * survives `/resume` but resets on `/new`, mirroring pi-chonky-tasks. */ export interface ScheduledSubagent { id: string; /** Unique within store. Defaults to `description`. */ name: string; description: string; /** Raw user input — cron expr | "+10m" | ISO | "5m". */ schedule: string; scheduleType: "cron" | "once" | "interval"; /** Computed at create time for interval/once. */ intervalMs?: number; // spawn params (subset of Agent tool params; no inherit_context, no resume) subagent_type: SubagentType; prompt: string; model?: string; thinking?: ThinkingLevel; max_turns?: number; isolated?: boolean; isolation?: IsolationMode; // state enabled: boolean; /** ISO timestamp. */ createdAt: string; lastRun?: string; lastStatus?: "success" | "error" | "running"; /** Refreshed on every fire and on store load. */ nextRun?: string; runCount: number; } export interface ScheduleStoreData { /** For future migrations. */ version: 1; jobs: ScheduledSubagent[]; }