createAgentSession Wiring
createAgentSession is the core entry point of the SDK. It takes external services — AuthStorage, ModelRegistry, SettingsManager, SessionManager, ResourceLoader — plus tool definitions and an extension runner, and assembles them into a usable AgentSession. main.ts doesn't call it directly; it goes through createAgentSessionRuntime → createAgentSessionServices. But SDK callers (embedding pi into other programs) use this function directly.
Responsibilities
- Service resolution: each external service can be passed in or auto-created; default paths are based on
agentDir(~/.pi/agent). Seepackages/coding-agent/src/core/sdk.ts:193-211. - Model recovery: when session data exists, the
modelis read from the session header; otherwise the default is taken from settings. If the model can't be recovered, amodelFallbackMessageis produced. Seepackages/coding-agent/src/core/sdk.ts:214-248. - Thinking level clamp: reads thinking level from session or settings, then
clampThinkingLevelrestricts it to the model's capability range. Seepackages/coding-agent/src/core/sdk.ts:250-269. - Stream function injection: the
Agent'sstreamFndoesn't callstreamSimpledirectly. It first callsmodelRegistry.getApiKeyAndHeadersto get auth, then merges in telemetry headers fromgetAttributionHeaders. Seepackages/coding-agent/src/core/sdk.ts:328-346. - convertToLlm wrapper: when
blockImagesis on, all ImageContent is replaced with placeholder text; settings are read dynamically so mid-session changes take effect. Seepackages/coding-agent/src/core/sdk.ts:282-316. - Attribution headers: OpenRouter and Cloudflare get provider-specific HTTP headers for attribution. See
packages/coding-agent/src/core/sdk.ts:128-156.
Design Motivation
Why not have callers new Agent and then new AgentSession themselves? The wiring steps involve cross-dependencies among a dozen services: auth determines whether a model is usable, the model determines the thinking level range, settings determine retry/transport/steering, the resource loader loads extensions and injects into extensionRunnerRef. Exposing all of this to callers would make everyone re-step on the same mines. createAgentSession centralizes the wiring rules into one function; SDK callers only need to pass cwd and (optionally) model.
getAttributionHeaders is extracted separately because different providers need different headers: OpenRouter looks at HTTP-Referer + X-OpenRouter-*, Cloudflare looks at User-Agent. Centralizing this avoids scatter across the stream path.
Key Files
packages/coding-agent/src/core/sdk.ts:33-80—CreateAgentSessionOptions, all wiring parameters (cwd, authStorage, modelRegistry, model, tools, scopedModels, customTools, resourceLoader, sessionManager, settingsManager).packages/coding-agent/src/core/sdk.ts:82-90—CreateAgentSessionResult:session+extensionsResult+ optionalmodelFallbackMessage.packages/coding-agent/src/core/sdk.ts:124-126—getDefaultAgentDir, the default global config directory.packages/coding-agent/src/core/sdk.ts:128-156—getAttributionHeaders, provider-specific telemetry headers.packages/coding-agent/src/core/sdk.ts:193-211— start ofcreateAgentSession: service resolution and defaults.packages/coding-agent/src/core/sdk.ts:320-376—new Agent({...})construction, injecting all runtime params:streamFn,onPayload,onResponse,transformContext,steeringMode,transport, etc.packages/coding-agent/src/core/sdk.ts:392-413— finalnew AgentSession({...})and returningCreateAgentSessionResult.packages/coding-agent/src/core/sdk.ts:108-120— re-export of tool factories, so SDK callers can pick upcreateCodingToolsand friends directly.
The streamFn injection shows how auth failure short-circuits and how attribution headers are merged:
// packages/coding-agent/src/core/sdk.ts:328-346
streamFn: async (model, context, options) => {
const auth = await modelRegistry.getApiKeyAndHeaders(model);
if (!auth.ok) {
throw new Error(auth.error);
}
const providerRetrySettings = settingsManager.getProviderRetrySettings();
const attributionHeaders = getAttributionHeaders(model, settingsManager);
return streamSimple(model, context, {
...options,
apiKey: auth.apiKey,
timeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,
// ...
headers:
attributionHeaders || auth.headers || options?.headers
? { ...attributionHeaders, ...auth.headers, ...options?.headers }
: undefined,
});
},The convertToLlm wrapper, when blockImages is on, replaces images with text placeholders and dedupes consecutive placeholders:
// packages/coding-agent/src/core/sdk.ts:282-298
const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
const converted = convertToLlm(messages);
if (!settingsManager.getBlockImages()) {
return converted;
}
return converted.map((msg) => {
if (msg.role === "user" || msg.role === "toolResult") {
const content = msg.content;
if (Array.isArray(content)) {
const hasImages = content.some((c) => c.type === "image");
if (hasImages) {
const filteredContent = content
.map((c) =>
c.type === "image" ? { type: "text" as const, text: "Image reading is disabled." } : c,
)
// ... dedupe ...Data Flow
Wiring order:
Edges and Failures
- Model can't be recovered:
modelFallbackMessagecovers both "saved model can't be restored" and "no configured models at all"; both cases return it, and the UI decides how to show it. Seepackages/coding-agent/src/core/sdk.ts:243-247. - Thinking off when no model:
if (!model) thinkingLevel = "off", to avoid sending thinking params for an empty model. - Session exists but no thinking entry: on recovery, an
appendThinkingLevelChangeis appended so subsequent fork/resume can pick up state. Seepackages/coding-agent/src/core/sdk.ts:378-390. - Extension transformContext: if
extensionRunnerRef.currentdoesn't exist, the original messages are returned directly instead of throwing, so the flow works even when extensions aren't bound.
Summary
createAgentSession centralizes the wiring rules for a dozen services into one function; SDK callers only pass cwd and an optional model. For the assembly internals, see AgentSession Orchestration Layer; for tool factories, see Tools: read/bash/edit/write/grep/find/ls; for message conversion, see Message Types and convertToLlm.