The safe way to extend Flue is to extend at the seam. Add providers through provider registration. Add transport options through provider configuration. Add tools through tool definitions or MCP. Add execution environments through SandboxApi or SessionEnv. Compose routes in app.ts.
The unsafe way is to tunnel through the rented model loop or bury target-specific behavior inside agent files.
The source pin for this chapter is withastro/flue@dbaa9effa305561c627c6836559f8a0cbce67875.
The safest extension is the smallest seam that solves the problem without bypassing runtime ownership.
Domain Word
An extension seam is a stable boundary where user code can add capability without changing Flue’s core runtime ownership. In this chapter, the seams are provider registry, provider settings, custom tools, MCP tools, sandbox adapters, and app composition.
The invariant is: extend at the seam, not inside the loop.
The twelve-factor pressure is config and backing services. Providers, sandboxes, connectors, stores, and admin apps are attached resources. The runtime has to attach them explicitly without making every agent file target-specific.
Extension Map
app.ts / generated entry
│
▼
configure runtime
│
┌───────────────────┼───────────────────┐
│ │ │
provider registry custom/MCP tools sandbox adapters
│ │ │
▼ ▼ ▼
resolve model model tool list SessionEnv
│ │ │
└──────────────┬────┴──────────────┬────┘
▼ ▼
Session flue() routes
│
▼
pi-agent-core loop
A good extension preserves the diagram. It adds a provider, tool, sandbox, or route composition point without making Session stop owning session semantics.
Provider Extension
packages/runtime/src/runtime/providers.ts exposes the provider registry. The common path is:
- Use
registerApiProvider(...)if the provider API itself is new to pi-ai. - Use
registerProvider(name, registration)to map a Flue model prefix to a provider template. - Use
configureProvider(name, settings)for runtime settings such as API keys, base URLs, headers, orstoreResponses. - Let
resolveRegisteredModel(...)turnname/modelIdinto a pi-aiModel.
The distinction between registration and configuration is useful:
| Operation | Use |
|---|---|
registerProvider(...) | Define how a model prefix resolves. |
configureProvider(...) | Override runtime settings for an existing provider prefix. |
attachModelBinding(...) | Attach platform-specific model binding metadata. |
Provider settings are not session history. They modify model transport. For example, storeResponses eventually becomes store: true only through the session payload override hook for OpenAI Responses APIs.
Tool Extension
types.ts defines ToolDef: name, description, parameter schema, and execute function. Tools can be passed at init(...) for harness-wide availability, or per prompt(...), skill(...), or task(...) call for scoped use.
Custom tools must obey two constraints:
| Constraint | Why |
|---|---|
| No built-in name collisions | The model should not see two meanings for bash, read, or task. |
| No duplicate custom names | Tool-call routing has to be unambiguous. |
The execute function receives params and an optional abort signal. Return text, keep error shapes understandable, and avoid smuggling target-specific assumptions into the model-visible description.
MCP Extension
packages/runtime/src/mcp.ts adapts MCP server tools into Flue ToolDefs. It lists tools from the MCP client, converts their JSON schemas, prefixes tool names as mcp__<server>__<tool>, and calls the server when the model uses the tool.
The prefixing rule matters. MCP servers are external namespaces. Flue needs model-visible names that are unique inside one session tool list.
MCP is therefore not a separate execution model. It is a tool adapter feeding the same Flue tool surface.
Sandbox Extension
For execution environments, choose the narrowest seam:
| Seam | Use when |
|---|---|
SessionEnv | You already have a complete capability object for exec and file operations. |
SandboxApi | You have an external sandbox SDK and want Flue to adapt it. |
BashFactory | You are using just-bash style local execution. |
createCwdSessionEnv(...) | You need child sessions with cwd overrides. |
The most important connector rule is timeout. SessionEnv.exec(...) and SandboxApi.exec(...) both accept timeout as the primary provider-native deadline and signal as the cancellation channel. A remote sandbox should forward timeout to the provider SDK when possible.
App Composition
The flue() public app and admin() inspection app are meant to be mounted by generated or user-authored app code.
Default generation mounts a working app for simple projects. A custom app.ts should own:
- auth middleware
- route prefixes
- CORS policy
- logging middleware
- whether and where
admin()is exposed - extra product routes around
flue()
This is the correct place for operational policy. Agent files should not decide which admin routes are internet-visible.
Production Checklist
Before shipping an extension, check the seam it touches:
| Extension | Production questions |
|---|---|
| Provider | Where do API keys live? What payload options are transport-only? What happens if model resolution fails? |
| Custom tool | Is the name unique? Is the schema honest? Is timeout/cancellation handled? Are side effects idempotent where needed? |
| MCP | Are external tool names namespaced? Are server errors surfaced clearly? Is the MCP server trusted for the data it can reach? |
| Sandbox | Does timeout reach the provider? Are paths scoped? Are stdout/stderr/exit-code semantics stable? |
| App | Is admin() behind auth? Are public and admin OpenAPI surfaces separate? Are run streams allowed by middleware? |
The checklist is intentionally boring. Extension bugs usually come from crossing a seam invisibly, not from missing an exotic abstraction.
What Breaks If This Boundary Drifts
| Drift | Failure |
|---|---|
Provider code patches Session directly | Every provider-specific change risks session semantics. |
| Tool adapters bypass name validation | Model-visible tool names become ambiguous. |
| MCP tools skip namespacing | Two servers can expose the same name with different behavior. |
| Sandbox connectors ignore timeout | The bash schema lies under load. |
| Admin routes are mounted casually | Runtime inspection leaks beyond the intended trust boundary. |
| Agent files own app composition | Work declaration and deployment policy become tangled. |
What To Copy
The copyable principle is boring on purpose: add behavior at the smallest explicit seam that already exists. Provider transport goes through provider registration/configuration. Tool behavior goes through ToolDef or MCP. Host execution goes through SessionEnv or SandboxApi. HTTP policy goes through app composition.
If an extension requires modifying the model loop, first ask whether it is really provider behavior or harness behavior. Most production extensions are not model-loop changes.
Verify In Source
providers.tsexportsregisterProvider(...),configureProvider(...),registerApiProvider,attachModelBinding(...), andresolveRegisteredModel(...).types.tsdefinesToolDef,ProviderSettings,SessionEnv, andSandboxFactory.mcp.tsconverts MCP tools into namespacedToolDefs.sandbox.tsdefinesSandboxApiand adapters intoSessionEnv.agent.tsvalidates custom tool names against built-ins.flue-app.tsandadmin-app.tsare separate app surfaces.- Build plugins support optional user
app.tscomposition.