<!-- Generated by scripts/gen-docs.ts from the TSDoc in src/. Do not edit by hand. -->

# `session`

Source: [`src/session.ts`](https://github.com/AI-for-dev/combo/blob/main/src/session.ts)

The whole pi API lives here, and nowhere else.

The rest of the library only talks to {@link SessionPort}, one turn at a
time, and {@link sessionPort} is the adapter over pi's `AgentSession`. Two
consequences: when pi moves, only this file moves; and tests inject a fake
session with no network, no disk and no `~/.pi`.

combo is written against pi 1.0 and later.

## `AgentMessage`

*type*

```typescript
export type AgentMessage = AgentSession["messages"][number];
```

Alias to pi's message type, without depending on a transitive package.

## `checkModel`

*function*

```typescript
export async function checkModel(pattern: string): Promise<void> { /* … */ }
```

Checks that a model pattern resolves in this pi, without opening a session.

For whoever takes a `--model` argument: `/interview` asks the user for
minutes before the first spawn and `/build` runs unwatched, and a typo must
cost a second, not a conversation or a run found stopped. It reads the real
model catalogue, so a fake cannot stand in for it: only a real pi run proves
it end to end.

## `createDefaultSession`

*const*

```typescript
export const createDefaultSession: CreateSession = async (agent, options) => {
	/* … */
};
```

Creates a real, isolated pi session for an agent.

The system prompt goes through a {@link StaticResourceLoader}: the subagent
inherits neither the user's extensions, nor their context files, nor any
skill it did not name. It only sees what its own definition gives it - which
is what makes it reproducible. Its settings are held in memory and seeded by
{@link subagentSettings}, so pi's settings files reach it only through the
keys that function names, each from the layer it names.

## `CreateSession`

*type*

```typescript
export type CreateSession = (agent: Agent, options: CreateSessionOptions) => Promise<SessionPort>;
```

Session factory. The injection point for tests.

## `CreateSessionOptions`

*type*

```typescript
export type CreateSessionOptions = {
	/** Working directory of the session. Defaults to the process's own. */
	cwd?: string;
	/**
	 * Session directory dedicated to this run. Absent means an in-memory
	 * session: not exportable, and leaving no trace in `~/.pi`. That is the
	 * default, and it is intentional.
	 */
	sessionDir?: string;
	/**
	 * Model pattern for this session, already resolved against the precedence
	 * ladder by `spawn()` - see `SpawnOptions.model`. Absent means pi's own
	 * settings decide, which is the last resort, never a choice made here.
	 */
	model?: string;
	/**
	 * Tools combo itself defines, offered to this session.
	 *
	 * Offering is not granting: `tools` is an allowlist and it covers these too,
	 * so a tool the agent's `tools:` does not name is not enabled. That leaves
	 * the guarantee of {@link StaticResourceLoader} intact - a subagent still
	 * inherits nothing from the user's environment, and what it can do is
	 * readable in its own definition.
	 */
	customTools?: ToolDefinition[];
	/**
	 * `model` is the caller's override, not the agent's own. A thinking level
	 * its suffix names is then the session's for every turn, over the one a
	 * turn asks for: the run's `--model` outranks a flow node's `thinking:`.
	 */
	overrides?: boolean;
};
```

Session creation settings, passed through by `spawn()`.

## `HostProviders`

*type*

```typescript
export type HostProviders = Pick<ModelRegistry, "getRegisteredProviderIds" | "getRegisteredNativeProvider" | "getRegisteredProviderConfig">;
```

Where pi keeps the providers its extensions registered: `ctx.modelRegistry` reads them.

## `MainSession`

*type*

```typescript
export type MainSession = Pick<SessionManager, "getSessionFile" | "getHeader" | "getEntries">;
```

What the library reads of the parent pi session, the one a run is launched
from: pi's `ctx.sessionManager` fits it as it is.

The file's path alone is not enough. pi creates a session's file with its
first message, so a command typed first in a fresh pi runs in a
session whose file does not exist yet, and `--no-session` never writes one.
The header and the entries are in memory from the start, and the file is
those and nothing else, one JSON object per line.

## `PiSession`

*type*

```typescript
export type PiSession = {
	/** One turn. Resolves when the model stops asking for tools, or once `abort()` cut it. */
	prompt(text: string): Promise<void>;
	/** Every event, the turn's and any other. Returns the unsubscribe function. */
	subscribe(listener: (event: SessionEvent) => void): () => void;
	/** **Cumulative** over the session, compactions and failed attempts included. */
	getSessionStats(): SessionStats;
	/** `prompt()` takes no signal, so this is the bridge. */
	abort(): Promise<void>;
	/** What pi resolves with, `"handled"` or `"queued"`, does not tell an idle session from a busy one, so it is not read. */
	steer(text: string): Promise<unknown>;
	/** Whether a turn is in flight - the one moment a steer is safe. */
	readonly isStreaming: boolean;
	/** Releases the session; stats and exports are read before it. */
	dispose(): void;
	/** pi's HTML export. It refuses an in-memory session. */
	exportToHtml?(outputPath?: string): Promise<string>;
	/** pi's JSONL export of the current branch. */
	exportToJsonl?(outputPath?: string): string;
	/** The transcript the model is shown next, rebuilt shorter by a compaction. */
	readonly messages: AgentMessage[];
	/** The model pi resolved, once it has. */
	readonly model?: { provider?: string; id?: string };
	/** The level the next prompt thinks at, clamped to what the model supports. */
	readonly thinkingLevel: ThinkingLevel;
	/** Clamps `level` to the model's, and writes the change into the transcript. */
	setThinkingLevel(level: ThinkingLevel): void;
};
```

The part of pi's `AgentSession` that {@link sessionPort} reads.

`AgentSession` satisfies it structurally, and so does a test's pi-shaped
double, which is how the adapter is tested without pi.

## `READ_ONLY_TOOLS`

*const*

```typescript
export const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"] as const;
```

Tools of an exploration agent: read, never write. This is the default.

## `SessionEvent`

*type*

```typescript
export type SessionEvent =
	| { type: "message_update"; assistantMessageEvent: { type: string; delta?: string } }
	| { type: "tool_execution_start"; toolCallId?: string; toolName: string; args: unknown }
	| { type: "tool_execution_end"; toolCallId?: string; toolName: string; result?: { content?: readonly { type: string; text?: string }[] }; isError?: boolean }
	| { type: "message_end"; message: AgentMessage }
	| { type: "turn_end" }
	| { type: string };
```

pi's session events, as far as we read them: the adapter here, and the
mirror's pane, which draws them with pi's own components.

Deliberately loose on `type`: pi emits many more, and we ignore them. This
type describes what we know how to read, not what pi can produce.

## `sessionPort`

*function*

```typescript
export function sessionPort(pi: PiSession, pinned = false): SessionPort { /* … */ }
```

A {@link SessionPort} over pi's session.

A turn's usage is the difference of `getSessionStats()` around the prompt,
not the sum of the turn's own messages. Measured on pi 1.0.2, the two agree
on every turn, aborted ones included, but a compaction's summary request is
billed to the compaction entry and emits no message, and pi compacts inside
`prompt()`. Only the stats see everything pi billed.

## `SessionPort`

*type*

```typescript
export type SessionPort = {
	/**
	 * `provider/id` as pi resolved it, absent when it could not say.
	 *
	 * Read, never set: an agent declares a *pattern* (`"anthropic/claude-sonnet-5"`,
	 * or nothing at all), and only the session knows what that became.
	 */
	readonly model?: string;
	/**
	 * Runs one turn, until the model stops asking for tools or the signal
	 * fires. Never rejects: a turn that throws, is cut short or ends on a
	 * failing `stopReason` comes back with `error` set, and with what it cost.
	 */
	ask(text: string, options: TurnControl): Promise<Turn>;
	/**
	 * Queues a word for the turn in flight, delivered after the tool call the
	 * model is in, or answers `"idle"` and queues nothing when no turn is in
	 * flight. Measured: a steer queued on an idle pi session is delivered with
	 * the next prompt and answered in place of it, which silently changes what
	 * a workflow reads back from its own task.
	 */
	steer(text: string): Promise<"queued" | "idle">;
	/**
	 * pi's own events, as pi emits them, for the whole life of the session.
	 * The mirror's: a pane draws them with pi's own components, so they are not
	 * read here. Returns the unsubscribe function.
	 */
	watch(listener: (event: SessionEvent) => void): () => void;
	/**
	 * What the model is shown next. It grows with every turn, until pi compacts:
	 * then it is rebuilt shorter, mid-run included, so a turn's own messages are
	 * {@link Turn.messages}, never a slice of this.
	 */
	transcript(): readonly AgentMessage[];
	/**
	 * Writes the session as a readable HTML page. **Before `close()`.**
	 *
	 * Optional because it is not always available: pi refuses to export an
	 * in-memory session ("Cannot export in-memory session to HTML"), which is
	 * exactly what a subagent gets unless it was spawned with a `sessionDir`.
	 */
	exportToHtml?(outputPath: string): Promise<string>;
	/** Writes the current branch as replayable JSONL. **Before `close()`.** */
	exportToJsonl?(outputPath: string): string;
	/** Releases the session. An unreleased session leaks; exports come first. */
	close(): void;
};
```

What combo needs of a session: one turn at a time, read for it.

Shaped by combo's callers rather than by pi's `AgentSession`. A turn comes
back with its own messages, its answer, why it failed and what it cost, so
nothing outside this file subtracts counters, parses pi's events or bridges
a signal to an abort. {@link sessionPort} builds one over pi's session; a
test or a dry run writes its own.

## `situate`

*function*

```typescript
export function situate(systemPrompt: string, cwd: string): string { /* … */ }
```

The agent's prompt, plus the one fact it cannot do its job without: where it is.

A subagent inherits nothing from the user's environment, deliberately - but
its own working directory is not inherited context, it is the ground every
tool call stands on. Without it a model guesses, and a real run showed exactly
what that costs: a scout called `ls /Users/loic/gouarin/…` - the user's name
with a dot turned into a slash - got "no such path", and gave up without
trying a relative one. One branch of three, wasted on a fabricated path.

## `StaticResourceLoader`

*class*

```typescript
export class StaticResourceLoader implements ResourceLoader {
	readonly #systemPrompt: string;
	readonly #skills: Skill[];
	getExtensions() { /* … */ }
	getSkills() { /* … */ }
	getPrompts() { /* … */ }
	getThemes() { /* … */ }
	getAgentsFiles() { /* … */ }
	getSystemPrompt() { /* … */ }
	getSystemPromptSource() { /* … */ }
	getAppendSystemPrompt() { /* … */ }
	getAppendSystemPromptSources() { /* … */ }
	extendResources() { /* … */ }
	async reload() { /* … */ }
}
```

A `ResourceLoader` that discovers nothing: it returns the agent's system
prompt, the skills it was handed, and empty lists for everything else.

`DefaultResourceLoader` would re-read the disk on every spawn, load the
user's extensions and trigger the project trust logic. For a subagent that
is non-deterministic context nobody asked for. Skills are the one thing that
comes from outside the definition, and even then only by name: they are
resolved by `resolveSkills` before we get here, never found by this loader.

## `Streamed`

*type*

```typescript
export type Streamed =
	| { type: "text"; delta: string }
	| { type: "tool"; name: string; args: unknown; call?: string }
	| { type: "tool_error"; name: string; error: string; call?: string };
```

What a streamed event means to a listener: a piece of the answer, a tool
being called, or a call that came back an error. `call` is pi's id for the
call, which tells two calls of one tool apart when they run together.

## `Turn`

*type*

```typescript
export type Turn = TurnReading & {
	/** The messages the turn added, in order: the prompt, each answer, each tool result, a steer once delivered. */
	messages: AgentMessage[];
	/**
	 * What pi billed for the turn, the tool calls it counted and the context
	 * it left. Time is the caller's: `wallMs`, `busyMs` and `turns` are `0` here.
	 */
	usage: Usage;
};
```

One turn, as the session read it.

## `TurnControl`

*type*

```typescript
export type TurnControl = {
	/**
	 * Cuts the turn short. Not yet aborted when the turn starts: refusing a
	 * turn that was stopped before it began is the caller's call, since no
	 * request was made.
	 */
	signal: AbortSignal;
	/** Each event of the turn a listener has a use for, already read. */
	onStreamed?: (event: Streamed) => void;
	/**
	 * The level this turn thinks at. Absent: the session's own, so a level one
	 * turn asked for never leaks into the next. A session pinned by its
	 * caller's model keeps its own whatever a turn asks.
	 */
	thinking?: ThinkingLevel;
};
```

What governs one turn of a {@link SessionPort}.

## `useHostProviders`

*function*

```typescript
export function useHostProviders(providers: HostProviders): void { /* … */ }
```

Makes the providers the host pi's extensions registered resolvable by every
subagent and every `checkModel` in this process.

A subagent's `ModelRuntime` is a fresh one: it reads pi's built-in providers
and `models.json`, and loads no extension (invariant 5), so a provider that
an extension such as `@ai-for-dev/providers` registers is unknown to it, and
`--model` naming one is refused while pi itself runs on it. The extension
hands pi's registry here as a session starts. It is one per process, as pi
keeps it, and a subagent still loads no extension: it gets the providers,
whose models it can name, and nothing else.
