CodeVetter · Repo Unpack · Data flow
earendil-works/pi
A prompt enters via CLI args, piped stdin, or the TUI editor; AgentSession wraps it as a user AgentMessage and hands it to Agent.prompt → runAgentLoop. Per turn the loop transforms AgentMessage[] → LLM Message[] (convertToLlm), streams via the model's provider streamFn, executes toolCall content blocks through tools/extensions, and appends ToolResultMessages. AgentSession subscribes to agent events and persists each message_end to the JSONL session file via SessionManager, while the active mode renders events (TUI components, text, or JSON lines). Credentials come from auth.json (locked, 0600) resolved per-request by ModelRuntime; model catalogs come from generated data + models.json + a remote catalog refresh.
- streamAssistantResponse is the LLM boundary: it applies transformContext (AgentMessage[]→AgentMessage[]), convertToLlm (AgentMessage[]→Message[]), normalizeContext, resolves a fresh API key via getApiKey(provider), then calls streamFn(model, llmContext, options) and folds stream events into a final AssistantMessage.
- runLoop has an outer loop for follow-up messages and an inner loop that alternates: drain steering queue → emit turn_start → prepareRequest hook → stream assistant response → execute tool calls → finishTurn hook → turn_end; the loop ends on agent_end.
- Tool calls from an assistant message run in parallel by default, sequentially if config.toolExecution or any tool's executionMode requires it; each call goes through prepareToolCall (argument validation via validateToolArguments), execution with progress callbacks, and finalizeExecutedToolCall producing a ToolResultMessage pushed into context.
- Persistence path: AgentSession subscribes to agent events; on message_end it calls sessionManager.appendMessage(event.message) which appends a SessionMessageEntry to the JSONL file; model changes, thinking-level changes, usage, compaction, labels, and custom entries get their own entry types.
- Agent state is owned by the Agent class: messages/tools arrays with copy-on-assign setters, pendingToolCalls set, steeringQueue and followUpQueue (PendingMessageQueue with 'all' | 'one-at-a-time' drain modes) polled by the loop via getSteeringMessages/getFollowUpMessages.
- Credential flow: FileAuthStorageBackend reads/writes ~/.pi/agent/auth.json under a proper-lockfile lock (sync retry loop, async 30s stale timeout), always writing with mode 0600; ModelRuntime composes stored credentials, env vars (env-api-keys.ts), and runtime overrides into provider auth.
- Model catalog flow: static MODELS catalog (generated) + per-provider dynamic refresh + user models.json + remote catalog provider compose into ModelRuntime; resolveModelScope expands --models/settings enabledModels patterns into ScopedModel list used for selection and Ctrl+P cycling.
- Settings are layered: global ~/.pi/agent/settings.json deep-merged with project .pi/settings.json (deepMergeSettings recurses into nested objects); project settings only load when the project is trusted.
- Context files flow into the system prompt: loadContextFileFromDir checks AGENTS.override.md, AGENTS.md, AGENTS.MD, CLAUDE.md, CLAUDE.MD per directory; loadProjectContextFiles walks ancestors from cwd plus the global agent dir, with git-worktree shadowing handled by findShadowedContextFile.
- Extension events flow through ExtensionRunner (built on an EventBus): AgentSession emits lifecycle events (session_start, turn_start, message_*, tool_*, context, session_compact, ...) which extension handlers observe or mutate; handler errors route to onError listeners rather than crashing the session.
- Output boundaries: interactive mode renders via pi-tui components through InteractiveMode; print mode writes final text or toJsonEvent JSON lines via writeRawStdout (stdout is taken over so extension noise doesn't corrupt the stream); RPC mode serializes responses/events as JSONL on stdout.
- Session reload after compaction or /reload rebuilds state from disk: SessionManager.buildSessionProjection folds entries into messages (skipping compacted ranges via firstKeptEntryId), then the agent context is replaced — this is why CompactionEntry stores the full systemMessage snapshot.
- Piped stdin and @file arguments are merged into the initial message by buildInitialMessage/processFileArguments (images auto-resized later once the model is known); in RPC mode @file args are rejected because stdin carries the protocol.