Your Agent on a Companion
A companion is a third model source alongside built-in AI and user keys - what it can run, the one call it cannot, and the single constraint your own agent tools must satisfy to reach a device.
A paired companion is a third model source, resolved by the same decision point as built-in AI and user API keys (resolveChatRun in packages/api/src/ai/byok.ts). The user picks their CLI in the model picker and the run streams from their own machine on their own subscription.
| Source | Whose credential | Draws credits |
|---|---|---|
| Built-in AI | Your operator key | Yes - the only source that bills |
| User API key (BYOK) | The user's provider key | No - they pay their provider directly |
| Companion | The user's own coding-CLI subscription | No - their subscription executes the run |
That a companion is never metered is not a setting anyone has to remember: it resolves to an unwrapped model at zero rates, and an unwrapped model has no path to the ledger at all. See metering is structural.
It behaves like any other provider, too - per-run knobs arrive on providerOptions.companion, and the CLI's resumed session id comes back on the result's provider metadata, so you wire it exactly as you wire OpenAI or Anthropic.
What a companion model can run
| Call | On a companion |
|---|---|
streamText | Works - the adapter maps the daemon's frames to stream parts as they arrive |
generateText | Works - drains that same stream into a one-shot result |
generateObject | Throws UnsupportedFunctionalityError, naming responseFormat.type = json |
generateObject is a permanent capability gap, not a TODO: a coding CLI answers in prose and offers no constrained decoding, so there is nothing to hand a JSON schema to. The throw rejects the call itself rather than leaving an agent loop waiting on a stream that will never produce valid JSON. Route structured output to a built-in or user-keyed model.
Your own agent tools reach the device
Tools you declare on the assistant agent (new Agent({ tools: { myTool } })) work on a companion run, not only on built-in AI and BYOK. The run advertises them to the CLI over the device's loopback MCP; when the CLI calls one, your backend resolves it server-side under the verified user, so the tool's secrets never leave your server. An app capability wins a name collision (and a collision is refused outright at dispatch time), and a run may only call what its own dispatch advertised.
execute must be reconstructible from the agent definition. The device posts the call back over HTTP long after the dispatching request ended, possibly into a different serverless instance, so the closure the tool was declared in is gone and the agent's tools are rebuilt from scratch. Whatever execute needs has to come from its own arguments plus the run's persisted scope (owner, surface, org).
This works - everything it needs arrives in its arguments:
const saveAudit = tool({
description: "Save a completed site audit.",
inputSchema: z.object({ url: z.string(), score: z.number() }),
execute: async ({ url, score }) => db.insert(audits).values({ url, score })
});This does not - and it works fine on built-in AI and BYOK, where execute runs inside the very request that declared it. The asymmetry is the transport, not a bug:
new Agent({
// A dynamic `tools` function is re-resolved on the device's callback with a FRESH,
// EMPTY request context - so `tenantId` is undefined and the captured client is gone.
tools: ({ requestContext }) => {
const client = clientFor(requestContext.get("tenantId"));
return { saveAudit: tool({ /* ... */ execute: async (args) => client.save(args) }) };
}
});Two limits worth knowing:
- The default assistant agent only. Mastra never tells a model which agent invoked it, so a companion run cannot name a second agent - put tools that must reach a device on the assistant.
- Memory-managed tools are not agent tools. With Mastra working memory on,
updateWorkingMemoryis advertised to the device and answeredUnknown tool, which the CLI recovers from.
Agents
Author the assistant agent the tools above hang off - persona, tools, MCP clients, and skills.
Dispatch from Your Code
Send device-side work from product code - the per-device grant, statuses, and results via capabilities.
AI
The AI product a companion plugs into: chat, scheduled runs, capabilities, and credit billing.