# Matilda documentation > Matilda is an AI coding agent and assistant built by Maincode in Melbourne and served from Australian infrastructure. It ships as a CLI (Matilda Code), a desktop app, a mobile chat app, and SDKs for TypeScript and Python over an onshore API. This file is the complete Matilda documentation, 39 pages, in reading order. For a linked index instead, see https://maincode.com/llms.txt. For a single page, append `.md` to its URL. ## The pieces - **Matilda** — the model. It reads code, reasons about it, and proposes edits and commands. It never acts on a machine by itself. - **Matilda Code** — the CLI coding agent. It drives the model in a loop against your working tree: read, plan, act, verify. - **Matilda Desktop** — the same agent in a native window for macOS, Windows, and Linux. - **Matilda Chat** — the conversational surface, on the web and on iOS and Android. - **Client SDK** — TypeScript and Python access to chat, conversations, files, feedback, devices, and auth. - **Agent SDK** — TypeScript and Python agents and runners with client-side tool execution on top of the same API. ## Quick facts - Install the CLI: `npm install -g @maincode-ai/matilda-code@latest` (Node.js 22 or newer). It installs `matilda` and `matilda-code`, which are the same binary. - Sign in interactively: `matilda auth login`. The refresh token is written to `~/.matilda/matilda-auth.json`. - Authenticate a pipeline: set `MATILDA_API_KEY`. Keys are `mc_live_` tokens, created with the `matilda-key` CLI or the client SDK. - API base URL: `https://matilda.maincode.com/api`. - Install the TypeScript SDKs: `npm install @maincode-ai/matilda-client-sdk` or `npm install @maincode-ai/matilda-agent-sdk` (Node.js 20 or newer). - Install the Python SDKs: `pip install matilda-client` or `pip install matilda-agent-sdk` (Python 3.12 or newer). - Run headless: `matilda -p ""`, with `-o text|json|stream-json` for the output shape. - Approval modes, cycled mid-session with Shift+Tab: `plan` → `default` → `auto-edit` → `auto` → `yolo`. - Project memory: `MATILDA.md` in the repo root is shared with the team, `~/.matilda/MATILDA.md` follows you across projects, `.matilda/MATILDA.local.md` is personal to one project. - Model and provider selection are not exposed. Matilda core owns routing, safety, server-side tools, and policy. ## Reading this site as an agent - Any docs page is available as markdown at the same URL with `.md` appended: https://maincode.com/docs/installation.md - Every Client SDK and Agent SDK page also exists in Python under the `python-` prefixed URL: https://maincode.com/docs/python-client-sdk-overview.md - https://maincode.com/llms-full.txt is every docs page concatenated into one file. - https://maincode.com/sitemap.xml lists every URL on the site. --- # Install Matilda Code > Install the CLI, sign in, and open your first session. Section: Matilda Code · Source: https://maincode.com/docs/installation ## Prerequisites Matilda Code runs anywhere Node runs. Before you install, make sure you have: - Node.js 22 or newer, on macOS, Linux, or Windows - A Maincode account - A terminal open inside a project you want to work in ## Install **Install with npm** — The fastest path on any platform ```bash npm install -g @maincode-ai/matilda-code@latest ``` The package installs two binaries, `matilda` and `matilda-code`. They are the same program. After a global install, npm prints the exact directory it installed into. That is not always on your `PATH`, so if the shell cannot find `matilda`, add the npm prefix and try again: ```bash export PATH="$(npm prefix -g)/bin:$PATH" ``` ## Sign in Authenticate once per machine. `matilda auth login` opens your browser and starts a temporary callback listener on `127.0.0.1`. Finish the sign-in there. If the browser does not open by itself, paste the URL the CLI prints. ```bash matilda auth login ``` On success the CLI writes a refresh token to `~/.matilda/matilda-auth.json` with user-only file permissions, and refreshes access tokens for you from then on. > **Note** — For interactive use you do not need an API key. Keys are for CI and other > non-interactive jobs, covered in [Headless and CI](https://maincode.com/docs/headless) — mint one > in the [dashboard](https://maincode.com/dashboard/keys) or with the `matilda-key` CLI. ## Run it Change into a project and start Matilda Code from its root. It reads your working tree in place; it does not copy your code anywhere. ```bash cd your-project matilda ``` You land in an interactive session. Describe a change in plain language and Matilda Code reads the relevant files, plans the edits, and runs commands, pausing for your approval before anything is written or executed. Type `/help` to see the commands available inside a session. ## Verify To confirm the CLI is installed and signed in: ```bash matilda --version matilda auth status ``` `auth status` prints your sign-in state; `whoami` is an alias for it. If you need to sign out of this device, `matilda auth logout` clears the local credentials. It does not revoke sessions on your other machines. ## Next steps - Read [Core concepts](https://maincode.com/docs/core-concepts) to see how the model, the agent, and the action layer fit together. - Learn [how an agent run works](https://maincode.com/docs/agent-runs), including the five approval modes, before you hand it a large change. - Run it in CI with [Headless and CI](https://maincode.com/docs/headless). --- # Core concepts > The model, the agent, and the action layer. Section: Matilda Code · Source: https://maincode.com/docs/core-concepts ## The pieces Matilda is a model, an agent, and the action layer between them. Name the three and the rest of the product is just how they connect. **Matilda** — The model Matilda is Maincode's assistant, served from Melbourne. It reads your code, reasons about it, and produces edits and commands. It never touches your machine on its own. **Matilda Code** — The agent The coding agent that drives the model in a loop: read the repo, plan, act, check the result, repeat until the task is done or it needs you. **Action layer** — The tools The typed tools the agent is allowed to call: read a file, write an edit, run a shell command, search the tree. Every call is visible, and in the default approval mode every write waits for you. **Context** — What it sees The working set the model reasons over: the files in play, the conversation so far, and any notes the agent has kept about your project. ## How a task flows through A request you type becomes a sequence of tool calls before a single line of your code changes. 1. **Read.** The agent pulls the files it needs from the working tree into context. It reads them in place; it does not copy them. 2. **Plan.** The model proposes a sequence of edits and commands, and shows you that plan for anything non-trivial. 3. **Act.** Each step runs through the action layer. Edits appear as diffs; commands appear as the exact line to be run. 4. **Verify.** The agent runs your tests or build, reads the output, and either moves on or corrects course. ## The model, in one paragraph Matilda is a single model with a long context window, not a router over smaller ones. Maincode serves it from infrastructure we operate in Melbourne. You give it the problem and the relevant slice of your codebase; it returns edits, commands, and an explanation of why. It has no standing access to your machine. The action layer does, and in the default mode the agent only reaches it with your approval. That separation is the whole safety story: the model can be wrong, but it cannot act on being wrong without a human or an explicit permission in the loop. > The point isn't a model that writes code. It's an agent that can change a > real repository and still be safe to leave running. ## What we deliberately don't do Matilda Code doesn't run as a background service, doesn't watch your filesystem, and doesn't phone home with your source. It's a foreground process you start, that acts only while you're there, and that stops when you close it. If you ever wonder what it's doing when you're not looking, the answer is nothing; it isn't running. > **Note** — This is the design decision teams question first and rely on most: the agent > has no life of its own outside the session you started. ## Where to go next - [Headless and CI](https://maincode.com/docs/headless), running Matilda Code from scripts and pipelines. - [Context and memory](https://maincode.com/docs/retention), what the agent keeps and what it forgets. - [How an agent run works](https://maincode.com/docs/agent-runs), the loop and the five approval modes in detail. --- # How an agent run works > Read, plan, act, verify, and the five approval modes. Section: Matilda Code · Source: https://maincode.com/docs/agent-runs ## What a run is When you give Matilda Code a task, it does not answer in one shot. It runs a loop, read, plan, act, verify, and pauses for you at the points that matter. In the default mode nothing is written to your files or run in your shell without you seeing it first. > **Note** — The loop is not there to look busy. It is there so a wrong step gets caught by > the next one, a failing test or a bad diff, instead of shipping. ## The loop **Read** The agent pulls the files it needs into context from your working tree. It reads what is relevant to the task, and you can see every file it opens. **Plan** The model turns your request into a concrete plan: the files it intends to change, the commands it intends to run, and why. **Approve** Your gate. Edits are shown as diffs and commands as the exact line to run. How often you are asked depends on the approval mode. **Act** On approval the change goes through the action layer: the edit is written, or the command runs in your shell. One step at a time, so a bad step stops the run instead of compounding. **Verify** The agent runs your tests or build, reads the output, and decides whether the step worked. If it did not, it loops back to plan with the failure in context. ## The five approval modes Approval is a mode, not a single setting. Cycle through them mid-session with **Shift+Tab**, or **Tab** on Windows. The status bar always shows where you are. | Field | Type | Description | | - | - | - | | `plan` | `read-only` | Analysis only. No edits, no commands. Best for exploring a codebase or planning a change. | | `default` | `ask permissions` | Edits and commands both need approval. The balanced choice, and the one to use on unfamiliar code. | | `auto-edit` | `edits auto` | Edits are auto-approved; shell commands still ask. Good for a run of safe refactors. | | `auto` | `classifier` | A classifier evaluates each edit and command. Fewer interruptions than auto-edit, more caution than yolo. | | `yolo` | `everything auto` | Edits and commands both auto-approved. Trusted personal projects and controlled automation only. | The cycle order is `plan → default → auto-edit → auto → yolo` and back to `plan`. > **Caution** — `yolo` approves shell commands as well as edits. Pair it with `--sandbox` when > the prompt is not entirely under your control. Set a mode for one run from the command line, or make it the default for a project in settings: ```bash matilda --approval-mode auto-edit ``` ```json title=".matilda/settings.json" { "tools": { "approvalMode": "default" } } ``` > **Note** — The mode once called **Default** is now **Ask Permissions** in the UI. The > configuration value stayed `default` for backward compatibility, which is why > the setting and the label do not match. ## Steering a run You steer while the run is happening, not just at the start. Interrupt at any time and redirect; the agent folds your correction into the plan and keeps its place. To make guidance stick across runs, tell the agent to remember it and it goes into project memory: > remember: always use pnpm in this repo, never npm See [Context and memory](https://maincode.com/docs/retention) for where that gets written and who else can see it. ## Guarantees - In `plan` and `default`, **nothing is written or run without approval**. The diff or the command is shown first, every time. - The agent **has no life outside your session**. It acts only while you are there and stops when you close it. - Loosening approval is **always an explicit act**, a flag or a mode change, never a default that drifts. --- # Context and memory > What Matilda holds in a session, and what carries across them. Section: Matilda Code · Source: https://maincode.com/docs/retention ## Two kinds of memory Every session starts with a fresh context window. Anything Matilda knows at a given moment is either **in context**, meaning this session, or in a **memory file** it reads from your repo at startup. Nothing else persists. **In context** — This session The files, conversation, and command output the model is reasoning over right now. Large, but finite. When it fills, older turns are summarised rather than silently dropped. **Memory files** — Across sessions Plain-text `MATILDA.md` files that Matilda reads at the start of every session. They live on your machine and in your repo, in your git history. **Not kept** — After the call Everything else. Your code is not uploaded as a corpus and not used for training. ## Where memory files live You can use any combination of these. Matilda loads all of them at startup. | Field | Scope | Committed | Notes | | - | - | - | - | | `MATILDA.md` | Your whole team | Yes, project root | Shared conventions, build and test commands, architecture decisions. | | `~/.matilda/MATILDA.md` | You, every project | No, your machine | Personal preferences that follow you between repositories. | | `.matilda/MATILDA.local.md` | You, this project | No, gitignore it yourself | Project-specific but personal. Loads after the shared file, so it can override. | | `AGENTS.md` | Your whole team | Yes, project root | Read automatically if your repo already has one for other tools. No need to duplicate. | > **Caution** — `.matilda/` is not gitignored for you, and some projects deliberately commit > `.matilda/settings.json`. If you use `MATILDA.local.md`, add it to your > `.gitignore` yourself. Run `/init` in a project without one and Matilda will draft a `MATILDA.md` for you from what it finds in the repo. ## What belongs in a memory file Things you would otherwise repeat every session: - Build and test commands, `npm run test`, `make build` - Conventions your team follows, "every new file needs JSDoc" - Architectural decisions, "never call the database from a controller" - Personal preferences, "always pnpm, never npm" Leave out anything Matilda can work out by reading the code. These files work best short and specific. The longer one gets, the less reliably it is followed. ## Managing context **Add** Pull directories into the workspace yourself instead of waiting for the agent to find them. ```bash matilda --add-dir ../shared-lib ``` Inside a session, reference a path with `@` to pull a specific file in. **Inspect** See what is loaded and roughly how much of the window it uses. ``` /context ``` **Compress** Summarise the conversation so far and keep working with the room it frees. ``` /compress ``` **Clear** Drop the working set and start fresh. Memory files are untouched, since they are files rather than part of the session. ``` /clear ``` ## What never leaves - **Your source code** is read locally and sent per request only as needed. It is not retained after the response and not used for training. - **Your memory files** live in your git history and on your machines, not ours. - **Your credentials** sit in `~/.matilda/matilda-auth.json` with user-only file permissions. > **Caution** — A project `MATILDA.md` is committed with your code and readable by everyone > with repository access. Never ask Matilda to remember a secret there. --- # Headless and CI > Run Matilda Code from scripts, pipelines, and other automation. Section: Matilda Code · Source: https://maincode.com/docs/headless ## One-shot runs Matilda Code has no separate `run` command. Pass a prompt and it executes once, prints the result, and exits. Use the positional form, or `-p` / `--prompt`: ```bash matilda "explain the changed files" matilda -p "explain the changed files" ``` To run a prompt and then stay in the session, use `-i` / `--prompt-interactive` instead. ## Piping Matilda Code reads stdin, so it composes with the rest of your shell: ```bash echo "explain this code" | matilda git diff | matilda -p "review this diff and list risks" ``` > **Note** — When `-p` is passed explicitly, ambient piped stdin is ignored. That is > deliberate, so a job that inherits a pipe cannot hang waiting on input. ## Authenticating a pipeline Browser sign-in is for humans. CI, containers, and cron jobs use a Matilda API token from the environment: ```bash export MATILDA_API_KEY="" matilda -p "summarise the failing tests" ``` ## Structured output `-o` / `--output-format` selects the shape of what comes back: `text` by default, `json` for a single object, or `stream-json` for events as they happen. ```bash matilda -p "list every TODO in src/" -o json ``` For output your own code can rely on, pass a JSON Schema. The run registers a `structured_output` tool and ends on the first valid call, so you get exactly the shape you asked for or nothing. ```bash matilda -p "extract the failing test names" --json-schema @schema.json ``` ## Approvals in automation A non-interactive run has nobody to approve anything. Choose a mode explicitly with `--approval-mode`, or use `-b` / `--bogan` / `--yolo` to auto-approve every tool call. ```bash matilda -p "fix the lint errors" --approval-mode auto-edit ``` > **Caution** — `yolo` approves shell commands as well as edits. Reserve it for trusted > automation in a controlled environment, and prefer `--sandbox` when the prompt > is not fully under your control. ## Useful flags | Field | Type | Description | | - | - | - | | `--prompt, -p` | `string` | Run one prompt non-interactively and exit. | | `--prompt-interactive, -i` | `string` | Run a prompt, then continue in the interactive session. | | `--output-format, -o` | `text \| json \| stream-json` | Shape of the CLI output. Defaults to text. | | `--json-schema` | `string` | JSON Schema the final output must satisfy. Accepts a literal or @path/to/schema.json. | | `--approval-mode` | `plan \| default \| auto-edit \| auto \| yolo` | How much the agent may do without asking. | | `--model, -m` | `string` | Override the model for this invocation. | | `--include-directories, --add-dir` | `array` | Extra directories to include in the workspace. | | `--sandbox, -s` | `boolean` | Run tool calls inside the sandbox. | | `--fresh, --no-resume` | `boolean` | Start a clean one-shot session without resuming or recording history. | ## Where to go next - [CLI reference](https://maincode.com/docs/cli-reference) for the full command and flag surface. - [How an agent run works](https://maincode.com/docs/agent-runs) for what each approval mode permits. --- # CLI reference > Commands, flags, and settings for the matilda CLI. Section: Matilda Code · Source: https://maincode.com/docs/cli-reference ## Install and verify ```bash npm install -g @maincode-ai/matilda-code@latest matilda --version ``` The package installs `matilda` and `matilda-code`. They are the same binary. ## Commands Running `matilda` with no subcommand starts an interactive session in the current directory. Anything else you type as a positional argument is treated as a prompt. | Field | Type | Description | | - | - | - | | `matilda` | `command` | Start an interactive session, or run a positional prompt one-shot. | | `auth` | `command` | Sign in, sign out, or check status. Subcommands: login, logout, status (alias whoami). | | `sessions` | `command` | List and manage recorded sessions. | | `mcp` | `command` | Manage MCP servers available to the agent. | | `extensions` | `command` | Install and manage Matilda extensions. | | `hooks` | `command` | Manage hooks. Also available as /hooks in a session. | | `channel` | `command` | Manage messaging channels such as Telegram and Discord. | | `serve` | `command` | Run Matilda as a local HTTP daemon. Experimental. | | `update` | `command` | Update Matilda Code in place. | > **Note** — There is no `matilda run` or `matilda diff`. To run a single task, pass the > prompt directly. To see pending changes, use `/diff` inside a session. ## Commands you will actually use **matilda** Start an interactive session in the current directory. The agent reads the working tree, waits for a task, and pauses for approval before it writes or runs anything. ```bash matilda ``` **matilda auth** Sign in once per machine. The token lands in `~/.matilda/matilda-auth.json` with user-only permissions. ```bash matilda auth login matilda auth status matilda auth logout ``` **One-shot prompt** Run a single task and exit, for scripts and CI. Combine with `-o json` when something downstream has to parse it. ```bash matilda "add a --json flag to the export command" matilda -p "list every TODO in src/" -o json ``` **matilda sessions** Sessions are recorded per project. List them, then resume the one you want. ```bash matilda sessions ``` Use `--continue` or `--resume` on a new run to pick up where you left off. `--fresh` starts a clean one-shot that neither resumes nor records. ## Global flags These apply to any invocation: | Field | Type | Description | | - | - | - | | `--model, -m` | `string` | Model for this invocation. | | `--prompt, -p` | `string` | Run one prompt non-interactively. Ambient piped stdin is ignored when set. | | `--prompt-interactive, -i` | `string` | Run a prompt, then stay in the interactive session. | | `--output-format, -o` | `text \| json \| stream-json` | Shape of the CLI output. Defaults to text. | | `--approval-mode` | `plan \| default \| auto-edit \| auto \| yolo` | How much the agent may do without asking. | | `--bogan, --yolo, -b` | `boolean` | Auto-approve every action. Shorthand for the yolo mode. | | `--sandbox, -s` | `boolean` | Run tool calls inside the sandbox. | | `--include-directories, --add-dir` | `array` | Additional directories to include in the workspace. | | `--allowed-tools` | `array` | Tools that bypass confirmation for this run. | | `--mcp-config` | `string` | MCP server config as inline JSON or a path to a JSON file. | | `--fresh, --no-resume` | `boolean` | Start a fresh one-shot session without resuming or recording history. | | `--debug, -d` | `boolean` | Run in debug mode. | ## Slash commands Inside a session, `/help` lists everything. The ones worth knowing early: | Field | Type | Description | | - | - | - | | `/help` | `command` | List every slash command. | | `/auth` | `command` | Switch account, API token, or provider. | | `/model` | `command` | Pick the model for this session. | | `/approval-mode` | `command` | Change what the agent may do without asking. | | `/context` | `command` | Show what is loaded and how much of the window it uses. | | `/compress` | `command` | Summarise the conversation to free up context. | | `/clear` | `command` | Drop the working set and start fresh. | | `/diff` | `command` | Show pending changes. | | `/memory` | `command` | Inspect and edit what Matilda remembers. | | `/init` | `command` | Create a MATILDA.md for this project. | | `/mcp` | `command` | Inspect connected MCP servers. | | `/doctor` | `command` | Diagnose a broken setup. | | `/aussie` | `command` | Hop a Matilda easter egg across the terminal. Takes an optional animal. | > **Note** — `/aussie` takes `kangaroo`, `koala`, `wombat`, `emu`, or `quokka`, and picks > one at random if you leave it off. Interactive sessions only, and `Esc` clears > it early. ## Settings Settings are JSON, not TOML, and they layer. Later layers win: | Field | Type | Description | | - | - | - | | `~/.matilda/settings.json` | `user` | Applies to every session for this user. | | `.matilda/settings.json` | `project` | Applies only in this repository. Overrides user settings. | | `System settings` | `system` | Machine-wide, for administrators. Overrides user and project. Path varies by OS. | Environment variables override the files, and command-line flags override everything. ```json title=".matilda/settings.json" { "tools": { "approvalMode": "default" } } ``` > **Note** — String values in `settings.json` can reference environment variables with > `$VAR_NAME` or `${VAR_NAME}`, so credentials never have to be written into the > file itself. ## Environment | Field | Type | Description | | - | - | - | | `MATILDA_API_KEY` | `string` | Matilda API token for CI and other non-interactive runs. Not needed for interactive use. | --- # Client SDK quickstart > Install the client SDK, send your first message, and stream a response. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-overview A small, self-contained TypeScript SDK for building Matilda clients. Ships a dual ESM + CommonJS build with bundled type definitions and zero `@matilda/*` runtime dependencies. Requires Node.js 20 or later. This guide covers SDK version 0.2.0. The SDK follows the OpenAI client shape where it helps: constructor config, resource groups, request options, typed API errors, and async-iterable streaming. It does not expose model or provider selection — Matilda core owns routing, safety, resumable SSE, server-side tool execution, and policy. ## What's included - **Chat** — non-streaming, full-event streaming, text-only streaming, schema-constrained structured output, and durable stream resume - **Conversations** — list, retrieve, rename, and set message feedback - **Files** — upload (single and parallel), retrieve metadata - **Feedback** — report harmful content and submit response feedback - **Devices** — register, list, and unregister push notification devices - **Auth** — managed PKCE browser login, RFC 8628 device flow, token refresh, and persistent token storage - **API keys** — create, list, and revoke `mc_live_` API keys via SDK methods, the `matilda-key` CLI, or the [dashboard](https://maincode.com/dashboard/keys) ## What's not included - **Local tool-execution loop** — for client-side tool execution (`clientTools`, local approval/sandbox loops, tool-result continuation), use the [agent SDK](https://maincode.com/docs/agent-sdk-agent) - **Session class** — multi-turn conversations are managed via `conversationId`; see [Multi-turn conversations](https://maincode.com/docs/client-sdk-multi-turn) ## Installation **npm** ```bash npm install @maincode-ai/matilda-client-sdk ``` **pnpm** ```bash pnpm add @maincode-ai/matilda-client-sdk ``` **yarn** ```bash yarn add @maincode-ai/matilda-client-sdk ``` `zod` (v3.25+) is a required peer dependency — install it alongside the SDK. It is used by the structured-output helpers ([Structured output](https://maincode.com/docs/client-sdk-structured-output)): ```bash npm install zod ``` ### ESM import ```ts title="index.ts" import Matilda from '@maincode-ai/matilda-client-sdk'; ``` ### CommonJS require ```js title="index.js" const { Matilda } = require('@maincode-ai/matilda-client-sdk'); ``` ### Auth subpath (Node-only) The standalone auth helpers are available via a subpath import: ```ts title="node.ts" import { loginWithBrowser, loginWithDeviceFlow } from '@maincode-ai/matilda-client-sdk/auth'; ``` For the full Node auth surface (token manager, file store, loopback receiver, login flow controller): ```ts title="node.ts" import { createLoginFlow, createTokenManager, createFileTokenStore, fetchAuthServerMetadata, memoryStorage, } from '@maincode-ai/matilda-client-sdk/auth/node'; ``` ## Quick start ### Send a message ```ts title="chat.ts" import Matilda from '@maincode-ai/matilda-client-sdk'; const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api', accessToken: process.env.MATILDA_ACCESS_TOKEN!, }); const response = await client.chat.create({ input: 'Summarize this thread.' }); console.log(response.outputText); ``` > **Note** — The `accessToken` option above is fine for quick testing, but for production use we recommend the managed auth flows (`loginWithBrowser` or `loginWithDeviceFlow`), which auto-wire a `TokenManager` with automatic token refresh. See [Authentication](https://maincode.com/docs/client-sdk-authentication). ### Stream a response ```ts title="stream.ts" for await (const event of client.chat.stream({ input: 'Write a short plan.' })) { if (event.type === 'response.output_text.delta') { process.stdout.write(event.delta); } } ``` ### Authenticated: device flow and chat ```ts title="device-flow.ts" import Matilda from '@maincode-ai/matilda-client-sdk'; const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); // Authenticate via RFC 8628 device flow — prints a code to stderr await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); // Token is now managed automatically — no manual header wiring const response = await client.chat.create({ input: 'Hello, Matilda!' }); console.log(response.outputText); ``` ## Environment URLs | Environment | Base URL | | - | - | | Production | `https://matilda.maincode.com/api` | --- # Configuration > Constructor options, trusted base URLs, and per-instance isolation. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-configuration ## `MatildaClientOptions` Extends `ClientConfig`. All fields are optional except `baseUrl`. | Field | Type | Description | | - | - | - | | `baseUrl` | `string` | The Matilda API base URL. Must be absolute for auth flows. Defaults to '/api'. | | `accessToken` | `string` | A static access token. Use this for simple setups, or use getToken for managed refresh. | | `getToken` | `GetToken` | Dynamic token provider. Called on every request. The SDK's TokenManager implements this. | | `apiVersion` | `string \| null` | API version sent via the X-Matilda-API-Version header. Omit to use the current version. | | `urlPolicy` | `TrustedApiBaseUrlPolicy` | URL validation policy for trustApiBaseUrl(). | | `publicConfigEndpoint` | `PublicConfigEndpoint` | Which public runtime config endpoint to use. | | `getCsrfToken` | `() => string \| null` | CSRF token provider for web BFF cookie auth. | | `reportedRequestMetadata` | `ReportedRequestMetadataConfig \| null` | SDK identification metadata. Auto-set to { sdkName: 'matilda-client', version }. | ### `TrustedApiBaseUrlPolicy` | Field | Type | Description | | - | - | - | | `allowRelative` | `boolean` | Allow relative URLs (e.g. /api). | | `allowedHosts` | `readonly string[]` | Allowlist of hostnames. | | `allowLocalHttp` | `boolean` | Allow http\://localhost / 127.0.0.1 (development). | | `requiredPathPrefix` | `string` | Require a specific path prefix (e.g. /api). | | `requireHttps` | `boolean` | Enforce HTTPS (loopback exempt). | ### `PublicConfigEndpoint` ```ts type PublicConfigEndpoint = | 'web-bff' | 'core-api-relative' | 'core-api-localhost' | 'core-api-localhost-3000' | 'core-api-android-emulator' | 'core-api-production'; ``` ## Environment URLs | Environment | Base URL | | - | - | | Production | `https://matilda.maincode.com/api` | | Staging | `https://staging.matilda.maincode.com/api` | ## Instance isolation Each `Matilda` instance holds its own independent config. Multiple instances in the same process are fully isolated — constructor options and `configure()` writes are scoped to that instance. ```ts title="environments.ts" const staging = new Matilda({ baseUrl: 'https://staging.matilda.maincode.com/api' }); const prod = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); console.log(staging.config.baseUrl); // https://staging.matilda.maincode.com/api console.log(prod.config.baseUrl); // https://matilda.maincode.com/api // Reconfiguring one never affects the other: staging.configure({ baseUrl: 'https://override.example/api' }); console.log(staging.config.baseUrl); // https://override.example/api console.log(prod.config.baseUrl); // https://matilda.maincode.com/api (unchanged) ``` ## `configure(options)` Updates the instance config in place. Returns `this` for chaining. ```ts client.configure({ accessToken: newToken }).chat.create(/* … */); ``` | Field | Type | Description | | - | - | - | | `options` | `MatildaClientOptions` | New config to merge. | ## `config` (getter) Returns the current `ClientConfig`. ```ts const cfg = client.config; console.log(cfg.baseUrl, cfg.accessToken); ``` ## `trustApiBaseUrl(rawUrl, policy?)` Validates and brands a URL as a trusted API base URL. | Field | Type | Description | | - | - | - | | `rawUrl` | `string` | The URL to validate. | | `policy` | `TrustedApiBaseUrlPolicy` | Optional override policy. | Returns a `TrustedApiBaseUrl` (a branded string). --- # Authentication > Browser PKCE, device flow, token persistence, and API keys for the client SDK. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-authentication The SDK provides a managed auth tier: on successful login, a `TokenManager` is auto-configured on the client instance. Every subsequent request automatically carries a managed access token with single-flight, skew-aware auto-refresh. ## OAuth client IDs The following OAuth client ID is currently available for public integrations: | Client ID | Use case | Notes | | - | - | - | | `matilda-code` | Public CLI / SDK / agent integrations | PKCE + device flow. The default choice for most integrations. | `matilda-code` is the only publicly accessible client ID at the moment. Additional client IDs will be documented here as access opens up. ## `auth.loginWithBrowser(opts)` Managed PKCE browser login (RFC 8252 loopback). Starts a temporary local server, opens the browser, receives the auth code, exchanges it for tokens, and auto-wires a `TokenManager`. ```ts title="login.ts" const tokens = await client.auth.loginWithBrowser({ clientId: 'matilda-code', openBrowser: (url) => console.log(`Open: ${url}`), }); console.log(tokens.accessToken); ``` ### `BrowserLoginOptions` | Field | Type | Description | | - | - | - | | `clientId` | `string` | OAuth client alias (e.g. 'matilda-code'). | | `scope` | `string` | Space-separated OAuth scopes. Defaults to 'openid offline\_access'. | | `identityProviderId` | `string` | Route straight to a federated IdP (e.g. Google SSO) instead of the hosted login page. | | `callbackPort` | `number` | Fixed loopback port. Recommended for FusionAuth redirect validation. Defaults to a random ephemeral port. | | `timeoutMs` | `number` | How long to wait for the browser callback. Defaults to 300\_000 ms (5 min). | | `openBrowser` | `(url: string) => void \| Promise` | Called with the authorize URL. If omitted, caller handles browser opening. | | `successRedirect` | `string` | URL the browser is 302-redirected to on success. Defaults to 'https\://matilda.maincode.com/cli/signed-in'. | | `errorRedirect` | `string` | URL for the error case; otherwise a bare 400 text response. | | `fetchImpl` | `typeof fetch` | Override fetch (testing, custom transport). Defaults to global fetch. | | `tokenStore` | `StorageAdapter` | Custom token persistence. Defaults to memoryStorage(). | | `tokenLock` | `(fn: () => Promise) => Promise` | Cross-process critical-section lock for token refresh (e.g. from createFileTokenStore). | | `onEvent` | `(e: LoginFlowEvent) => void` | Subscribe to login flow state events. | ### Returns `Promise` — the token set from the login flow. The `TokenManager` is also auto-configured on the client instance. ## `auth.loginWithDeviceFlow(opts)` Managed RFC 8628 device flow. Requests a device code, prints the user code and verification URL to stderr (by default), and polls until the user authorises. ```ts title="device.ts" const tokens = await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code', onEvent: (e) => { if (e.type === 'user_code') { console.log(`Visit ${e.verificationUri} and enter code: ${e.userCode}`); } }, }); ``` If no `onEvent` handler is provided, the SDK prints the user code and verification URL to stderr automatically. ### `DeviceLoginOptions` | Field | Type | Description | | - | - | - | | `clientId` | `string` | OAuth client alias. | | `scope` | `string` | Space-separated OAuth scopes. Defaults to 'openid offline\_access'. | | `timeoutMs` | `number` | Polling timeout. Defaults to 300\_000 ms (5 min). | | `signal` | `AbortSignal` | Abort the polling loop. | | `fetchImpl` | `typeof fetch` | Override fetch. Defaults to global fetch. | | `tokenStore` | `StorageAdapter` | Custom token persistence. Defaults to memoryStorage(). | | `tokenLock` | `(fn: () => Promise) => Promise` | Cross-process lock for token refresh. | | `onEvent` | `(e: LoginFlowEvent) => void` | Subscribe to login flow events. If omitted, prints user code to stderr. Defaults to defaultDeviceOnEvent. | ### Returns `Promise` ## `auth.refreshToken(refreshToken, clientId)` Manually refresh an access token using a refresh token. This bypasses the `TokenManager` — use it only when you need raw token exchange. ```ts const tokens = await client.auth.refreshToken(oldRefreshToken, 'matilda-code'); ``` | Field | Type | Description | | - | - | - | | `refreshToken` | `string` | The refresh token to exchange. | | `clientId` | `string` | OAuth client alias. | Returns `Promise`. ## `auth.getTokens()` Returns the current token set from the managed `TokenManager`, or `null` if not authenticated. ```ts const tokens = await client.auth.getTokens(); if (tokens) { console.log(`Token expires at: ${new Date(tokens.expiresAt).toISOString()}`); } ``` Returns `Promise`. ## `auth.logout()` Clears the token store, destroys the `TokenManager`, and disconnects the client's `getToken` provider. ```ts await client.auth.logout(); ``` Returns `Promise`. ## Token persistence By default, tokens are stored in memory (`memoryStorage()`). For cross-process persistence (e.g. CLI sessions), use `createFileTokenStore` from the `/auth/node` subpath: ```ts title="persist.ts" import Matilda from '@maincode-ai/matilda-client-sdk'; import { createFileTokenStore } from '@maincode-ai/matilda-client-sdk/auth/node'; import { homedir } from 'node:os'; import { join } from 'node:path'; const tokenPath = join(homedir(), '.matilda', 'tokens.json'); const { store, lock } = createFileTokenStore(tokenPath); const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); await client.auth.loginWithBrowser({ clientId: 'matilda-code', tokenStore: store, tokenLock: lock, }); ``` The file store uses a `0600` JSON file with a lockfile-based single-writer lock to prevent cross-process refresh races. ## `StorageAdapter` interface ```ts interface StorageAdapter { get(key: string): string | null | Promise; set(key: string, value: string): void | Promise; remove(key: string): void | Promise; } ``` Implement this to store tokens in a database, keychain, or any custom backend. ## `TokenManager` interface ```ts interface TokenManager { getAccessToken(opts?: { forceRefresh?: boolean }): Promise; getTokens(): Promise; setTokens(tokens: TokenSet): Promise; clear(): Promise; } ``` Created via `createTokenManager(deps)` from `/auth/node`. The SDK auto-creates one on login. ## `TokenSet` interface ```ts interface TokenSet { accessToken: string; refreshToken?: string; idToken?: string; // OIDC id_token when 'openid' scope is granted expiresAt: number; // epoch milliseconds } ``` ## `AuthError` class ```ts class AuthError extends Error { readonly code: string; // OAuth error code (e.g. 'invalid_grant', 'authorization_pending') readonly retryable: boolean; // true for transient 5xx/network; false for revoked tokens } ``` ## `LoginFlowEvent` type ```ts type LoginFlowEvent = | { type: 'state'; status: 'idle' | 'awaiting_user' | 'exchanging' | 'authenticated' | 'error' } | { type: 'authorize_url'; url: string } | { type: 'user_code'; userCode: string; verificationUri: string; verificationUriComplete?: string }; ``` ## Standalone auth subpath (`/auth`) The `/auth` subpath provides the raw PKCE and device-flow helpers without the managed `TokenManager`. These are **deprecated** in favour of the managed `client.auth.*` methods, but remain available for integrators who need direct protocol access: | Export | Description | | - | - | | `loginWithBrowser(coreAuthUrl, opts)` | Raw PKCE browser login. Returns `TokenSet`. | | `loginWithDeviceFlow(coreAuthUrl, opts)` | Raw device flow. Returns `TokenSet`. | | `refreshToken(coreAuthUrl, clientId, refreshToken)` | Raw token refresh. Returns `TokenSet`. | | `createPkcePair()` | Generate PKCE `code_verifier` + `code_challenge` (S256). | | `buildAuthorizeUrl(coreAuthUrl, opts)` | Construct the authorize URL. | The `/auth/node` subpath adds the Node-only adapters on top of the isomorphic core: | Export | Description | | - | - | | `createLoginFlow(opts)` | Headless login controller for loopback + device transports. | | `createTokenManager(deps)` | Per-session token manager with single-flight refresh. | | `createFileTokenStore(filePath)` | `0600` JSON file store with cross-process lock. | | `createLoopbackReceiver(opts)` | RFC 8252 loopback redirect receiver. | | `fetchAuthServerMetadata(issuer, fetchImpl?, opts?)` | RFC 8414 metadata discovery. | | `memoryStorage()` | In-memory `StorageAdapter`. | | `beginLogin(authorizationEndpoint, params)` | Stateless "begin" half of a redirect/BFF login. | | `completeLogin(tokenEndpoint, params, fetchImpl?)` | Stateless "complete" half. | ## API Key Management The SDK provides convenience methods for managing `mc_live_` API keys. These hit the same `JwtGuard`-protected endpoints the developer dashboard uses — the session JWT from a prior `loginWithBrowser()` or `loginWithDeviceFlow()` call is carried automatically by the `TokenManager`. > **Note** — Prefer a browser? The [dashboard](https://maincode.com/dashboard/keys) mints, lists, and revokes > the same keys — sign in with your Matilda account, no code required. ### `auth.createApiKey(opts)` Mints a new API key. The secret is returned **only at creation time** — store it immediately. ```ts title="api-key.ts" await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); const key = await client.auth.createApiKey({ name: 'ci-runner', // scopes: ['api:code'], // omit → server default // expiresAt: '2026-12-31T23:59:59Z', // omit → never expires }); console.log(key.secret); // mc_live_... — shown only once console.log(key.keyPrefix); // mc_live_abcd console.log(key.id); // UUID for revocation ``` #### `CreateApiKeyOptions` | Field | Type | Description | | - | - | - | | `name` | `string` | Human-readable key name (1–80 chars). | | `scopes` | `string[]` | Permission scopes (e.g. \['api:code']). Defaults to the server default. | | `expiresAt` | `string` | ISO 8601 expiry timestamp. Omit for no expiry. | Returns `Promise`. ### `auth.listApiKeys()` Lists all non-revoked API keys for the authenticated user. Secrets are never included — only the `keyPrefix` for identification. ```ts const keys = await client.auth.listApiKeys(); for (const key of keys) { console.log(`${key.keyPrefix} ${key.name} ${key.revokedAt ? 'REVOKED' : 'ACTIVE'}`); } ``` Returns `Promise`. ### `auth.revokeApiKey(id)` Revokes a key by ID. The key immediately stops working for authentication. ```ts await client.auth.revokeApiKey(key.id); ``` | Field | Type | Description | | - | - | - | | `id` | `string` | The key UUID (from createApiKey or listApiKeys). | Returns `Promise` (the revoked key with `revokedAt` set). ### `ApiKey` interface ```ts interface ApiKey { id: string; name: string; keyPrefix: string; // e.g. 'mc_live_abcd1234' scopes: string[]; createdAt: string; // ISO 8601 lastUsedAt: string | null; revokedAt: string | null; expiresAt: string | null; } ``` ### `ApiKeyWithSecret` interface Extends `ApiKey` with the one-time secret: ```ts interface ApiKeyWithSecret extends ApiKey { secret: string; // full key, e.g. 'mc_live_...' — shown only at creation } ``` ## CLI — `matilda-key` The package ships a `matilda-key` CLI binary that mints API keys via device-flow login. This is the easiest way for social-login (Google/Apple) users to get an API key without writing code. ### Installation The binary is available via `npx` (no global install required) or after installing the package: ```bash npx matilda-key create-api-key --name "my-key" # or, if installed globally: matilda-key create-api-key --name "my-key" ``` ### Usage ```bash matilda-key create-api-key --name [--scopes ...] [--expires-at ...] [--api-base-url ...] [--client-id ...] ``` | Field | Type | Description | | - | - | - | | `--name` | `string` | API key name (1–80 chars). | | `--scopes` | `string` | Comma-separated scopes (e.g. api:code,api:chat). Defaults to the server default. | | `--expires-at` | `string` | ISO 8601 expiry date. Defaults to never expiring. | | `--api-base-url` | `string` | API base URL. Defaults to https\://matilda.maincode.com/api. | | `--client-id` | `string` | OAuth client ID / alias. Defaults to matilda-code. | The `MATILDA_API_BASE_URL` environment variable is also honoured as a fallback for `--api-base-url`. ### What happens when you run it 1. **Device-flow login** — a verification URL and code are printed to **stderr**. Open the URL, sign in with Google or Apple, enter the code. 2. **Key minting** — once authenticated, an API key is created from the session JWT. 3. **Output** — key metadata (ID, name, prefix, scopes, expiry) is printed to **stderr**. The secret is printed to **stdout**. The stdout/stderr separation is deliberate: the secret on stdout is clean and pipeable, while the login flow and metadata remain visible on the terminal via stderr. ### Piping the secret ```bash # Capture into an env var (login flow still visible on terminal): MATILDA_API_KEY=$(matilda-key create-api-key --name "ci-runner") # Pipe to a file: matilda-key create-api-key --name "ci-runner" > /tmp/key.txt # Use in CI: export MATILDA_API_KEY="$(cat /tmp/key.txt)" ``` ### Full example ```bash $ matilda-key create-api-key --name "ci-runner" --scopes api:code Starting device-flow login... Open https://matilda.maincode.com/device and enter code: ABCD-1234 [state] awaiting_user_verification [state] token_received Login successful! Minting API key "ci-runner"... API key created successfully. ID: 8f3a2b1c-... Name: ci-runner Prefix: mc_live_abcd1234 Scopes: api:code Expires: never Created at: 2026-08-18T10:30:00.000Z Secret printed to stdout. Store it securely — it won't be shown again. ``` The secret (`mc_live_...`) is on stdout; everything else is on stderr. --- # Chat > Send messages and get complete responses from the chat API. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-chat ## `chat.create(params, options?)` Sends a chat message and returns the complete response. Internally this runs the stream and collects all events. ```ts title="chat.ts" const response = await client.chat.create({ input: 'What is the capital of Australia?', conversationId: 'conv-123', responseMode: 'instant', }); console.log(response.outputText); console.log(response.usage); ``` ### `ChatCreateParams` | Field | Type | Description | | - | - | - | | `input` | `string` | The user's message. Required if messages is not provided. | | `messages` | `ApiMessage[]` | Explicit message array. Overrides input. Each message: { role: 'user' \| 'assistant', content: string }. | | `conversationId` | `string` | Associates this message with a conversation thread for multi-turn chat. | | `fileIds` | `string[]` | File IDs to attach (from files.upload()). | | `responseMode` | `ChatResponseMode` | Response depth: 'auto', 'instant', or 'deep'. Defaults to 'auto'. | | `responseSchema` | `string` | Raw JSON Schema (as a string) to grammar-constrain the response to. Prefer chat.streamObject / chat.createObject, which convert a zod schema for you (see Structured output). | ### `MatildaRequestOptions` Extends `RequestOptions`. All fields optional. | Field | Type | Description | | - | - | - | | `fingerprint` | `string \| null` | Device fingerprint for rate limiting. | | `accessToken` | `string \| null` | Override the client-level access token for this request. | | `signal` | `AbortSignal` | Abort the request. | | `stallTimeoutMs` | `number` | SSE stall watchdog timeout in ms. Defaults to 45\_000. Pass 0 to disable. | | `onEvent` | `(event: MatildaChatStreamEvent) => void` | Catch-all stream event hook — fires for every event. Only honoured by convenience methods that consume the stream for you (chat.create(), chat.createObject()); use chat.stream() when you want to process events yourself. | ### `MatildaChatResponse` | Field | Type | Description | | - | - | - | | `outputText` | `string` | The full assistant response text. | | `events` | `MatildaChatStreamEvent[]` | Every event emitted during the stream. | | `streamId` | `string \| undefined` | Durable stream ID (from stream\_init event). | | `lastEventId` | `string \| undefined` | Last Redis stream entry ID (for resume). | | `usage` | `UsageEvent \| undefined` | Token usage data. | | `errors` | `Array<{ code: ChatErrorCode; message: string }>` | Any errors emitted during the stream. | | `truncatedReason` | `string \| undefined` | Why the response was truncated (e.g. 'max\_tokens'). | ### `ChatResponseMode` ```ts type ChatResponseMode = 'auto' | 'instant' | 'deep'; ``` - `'auto'` — Server decides the optimal response depth. - `'instant'` — Optimised for low latency. - `'deep'` — Optimised for thoroughness. --- # Streaming > Full event streaming from the chat API. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-streaming ## `chat.stream(params, options?)` Returns an async generator that yields `MatildaChatStreamEvent` objects as they arrive over SSE. This is the full event stream — tool calls, usage, status changes, safety replacements, and more. ```ts title="stream.ts" for await (const event of client.chat.stream({ input: 'Explain quantum computing.' })) { switch (event.type) { case 'response.created': console.log(`Stream started: ${event.streamId}`); break; case 'response.output_text.delta': process.stdout.write(event.delta); break; case 'response.tool_call.started': console.log(`\nTool: ${event.tool}`); break; case 'response.usage': console.log(`\nTokens: ${event.usage.output_tokens}`); break; case 'response.completed': console.log('\n--- Done ---'); break; case 'response.error': console.error(`Error: ${event.code} — ${event.message}`); break; } } ``` ## `MatildaChatStreamEvent` A discriminated union of 14 event types: ### `response.created` Emitted once at stream start with the durable stream ID. ```ts { type: 'response.created'; streamId: string } ``` ### `response.output_text.delta` A text chunk from the assistant. ```ts { type: 'response.output_text.delta'; delta: string } ``` ### `response.output_text.replace` The server replaced the output (e.g. safety filter). The `content` field holds the replacement text; `categories` lists the safety categories that triggered the replacement. ```ts { type: 'response.output_text.replace'; content?: string; categories?: string[] } ``` ### `response.status` Stream lifecycle status change. ```ts { type: 'response.status'; status: 'thinking' | 'streaming' | 'queued' | 'idle' | 'done' | 'error' | string } ``` ### `response.queued` Queue position update while waiting for a free slot. ```ts { type: 'response.queued'; state: string; position: number; estimatedWaitSeconds: number } ``` ### `response.tool_call.started` A server-side tool invocation began. ```ts { type: 'response.tool_call.started'; tool: string; inputOrArgs?: string | Record; output?: string } ``` ### `response.tool_call.progress` Progress update from a running tool. ```ts { type: 'response.tool_call.progress'; tool: string; message: string } ``` ### `response.tool_call.completed` A tool invocation finished. ```ts { type: 'response.tool_call.completed'; tool: string; status: 'success' | 'error'; input?: string; output?: string } ``` ### `response.generation_status` Generation phase update. ```ts { type: 'response.generation_status'; phase: string } ``` ### `response.usage` Token usage data for the turn. ```ts { type: 'response.usage'; usage: UsageEvent } ``` Where `UsageEvent` is: ```ts interface UsageEvent { output_tokens: number; context_pct?: number; // context window usage (0-100) context_messages_trimmed?: number; // messages trimmed to fit context budget context_budget_tokens?: number; // total context budget in tokens } ``` ### `response.cursor` Durable stream cursor (Redis stream entry ID). Persist this to resume from this point. ```ts { type: 'response.cursor'; lastEventId: string } ``` ### `response.truncated` The response was cut short. ```ts { type: 'response.truncated'; reason: string } ``` ### `response.completed` The stream finished successfully. ```ts { type: 'response.completed' } ``` ### `response.error` An error occurred during the stream. ```ts { type: 'response.error'; code: ChatErrorCode; message: string } ``` --- # Text helpers > Filter a chat stream down to assistant text, with deduped deltas. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-text-helpers These helpers filter the event stream to just text — useful when you only need the response text and don't care about tool calls, usage, or status events. ## `chat.streamText(params, options?)` Returns an async generator that yields raw string deltas. Throws `SafetyReplaceError` when the server replaces the output (safety filter). Throws `Error` on stream errors. ```ts title="text.ts" try { for await (const chunk of client.chat.streamText({ input: 'Write a haiku.' })) { process.stdout.write(chunk); } } catch (err) { if (err instanceof SafetyReplaceError) { console.error(`\nSafety replace: ${err.categories.join(', ')}`); } else { console.error(err); } } ``` > **Note** — **Why throw on safety replace?** The original text has already been yielded to the consumer by the time the replace event arrives. Throwing forces the consumer to handle the replacement explicitly — silently dropping it would lose the replacement message. ## `chat.createText(params, options?)` Non-streaming convenience that returns just the final output text. Safety replace is handled naturally — the replacement text is returned. Throws if the stream produced any error events. ```ts const text = await client.chat.createText({ input: 'What is 2 + 2?' }); console.log(text); // "4" ``` ## `SafetyReplaceError` ```ts class SafetyReplaceError extends Error { readonly categories: string[]; // message = replacement content (or empty string) } ``` --- # Structured output > Constrain a chat response to a zod schema and get a typed object back. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-structured-output Structured output constrains the model's response to a JSON Schema, server-side (grammar-constrained decoding), and then validates it client-side against your zod schema. Pass a zod schema, receive a fully-typed object — no prompt engineering, no brittle JSON extraction. ## `chat.streamObject(params, schema, options?)` Streams exactly like `chat.stream()` — you receive every `MatildaChatStreamEvent` — plus one final event with the parsed, schema-validated object. The `schema` argument is any zod schema (`z` is bundled with the SDK); the SDK converts it to JSON Schema and constrains generation server-side. ```ts title="structured.ts" import { z } from 'zod'; const recipe = z.object({ name: z.string(), prepTimeMinutes: z.number(), ingredients: z.array(z.string()), }); for await (const event of client.chat.streamObject( { input: 'Give me a recipe for pavlova.' }, recipe, )) { if (event.type === 'response.output_text.delta') { process.stdout.write(event.delta); // raw JSON streaming in } if (event.type === 'object') { console.log('\nValidated:', event.object); // typed as z.infer } } ``` The final event: ```ts { type: 'object'; object: T } // T = z.infer ``` ## `chat.createObject(params, schema, options?)` Non-streaming convenience. Returns a `MatildaObjectResponse` — everything `chat.create()` returns, plus the validated `object`. ```ts title="invoice.ts" const response = await client.chat.createObject( { input: 'Extract the invoice total: $1,250.00 AUD due 30 Sep.', conversationId }, z.object({ total: z.number(), currency: z.string() }), ); console.log(response.object.total); // 1250 (number) console.log(response.object.currency); // "AUD" (string) console.log(response.outputText); // raw JSON text as returned ``` ### `MatildaObjectResponse` Extends `MatildaChatResponse` with one additional field: | Field | Type | Description | | - | - | - | | `object` | `T` | The response text parsed as JSON and validated against your schema. | ## Raw JSON Schema via `responseSchema` If you don't want zod validation, pass a stringified JSON Schema directly as `responseSchema` on any chat call: ```ts title="raw-schema.ts" const response = await client.chat.create({ input: 'List three Australian birds.', responseSchema: JSON.stringify({ type: 'object', properties: { birds: { type: 'array', items: { type: 'string' } } }, required: ['birds'], additionalProperties: false, }), }); JSON.parse(response.outputText); // guaranteed valid, schema-conforming JSON ``` With `responseSchema` set, the response text is guaranteed to be valid JSON conforming to the schema — but parsing and validation are up to you. ## OpenAI-compatible endpoint The OpenAI-compatible endpoint (`POST /api/v1/chat/completions`) also honours structured output via the standard `response_format` parameter, so the OpenAI JS SDK's structured-output option works against Matilda as-is: - `{ "type": "json_schema", "json_schema": { "name": "...", "schema": {...} } }` — grammar-constrained to your schema (the schema is applied with `strict: true` server-side; the `strict` and `name` fields you supply are re-wrapped downstream). - `{ "type": "json_object" }` — guarantees valid JSON output without a schema (OpenAI JSON mode). > **Note** — **Safety replace and structured output.** If the server replaces the output mid-stream (safety filter), `streamObject` throws `SafetyReplaceError` — the replacement text is in `.message` and the triggering categories in `.categories`. Deltas already yielded to your consumer are not rolled back; if you render streamed JSON, handle `response.output_text.replace` events (or choose non-streaming `createObject`) to avoid showing half-rendered output that is later discarded. > **Caution** — **Truncation throws.** If the stream is truncated before the JSON completes, both helpers throw `MatildaObjectParseError` with the partial text in `.raw`. See [Error handling](https://maincode.com/docs/client-sdk-error-handling). > **Caution** — **Stream errors throw.** If the server emits an error event mid-stream, both helpers throw an `Error` with the server's error code and message (`${code}: ${message}`). --- # Durable streaming > Resume an interrupted stream from the last received event. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-durable-streaming Durable streaming lets a client disconnect mid-stream and resume from where it left off. The server buffers events in a Redis stream, keyed by a `streamId` advertised at stream start. ## Durable streaming lifecycle 1. Start a stream — `chat.stream()` emits a `response.created` event with a `streamId`. 2. Persist the `streamId` and `conversationId` immediately. 3. If disconnected, call `chat.activeStream(conversationId)` to check if the stream is still live. 4. Call `chat.resume({ streamId, lastEventId })` to replay buffered events from `lastEventId` onwards. ## `chat.resume(params, options?)` Resumes a previously detached stream by replaying buffered events from `lastEventId`. Returns an async generator of `MatildaChatStreamEvent`. ```ts for await (const event of client.chat.resume({ streamId: savedStreamId, lastEventId: savedLastEventId, })) { if (event.type === 'response.output_text.delta') { process.stdout.write(event.delta); } } ``` ### `ChatResumeParams` | Field | Type | Description | | - | - | - | | `streamId` | `string` | The stream ID from response.created. | | `lastEventId` | `string` | The last Redis stream entry ID received. Omit to replay from the start. | ## `chat.activeStream(conversationId, options?)` Checks whether a conversation has an active stream. ```ts const result = await client.chat.activeStream('conv-123'); // { stream_id: 'abc-123' | null, status: 'active' | 'done' | 'error' | null } ``` Returns `Promise`: ```ts interface ActiveStreamLookup { stream_id: string | null; status: 'active' | 'done' | 'error' | null; } ``` ## `chat.notifyOnCompletion(streamId, enabled?, options?)` Request a push notification when a backgrounded stream completes. ```ts await client.chat.notifyOnCompletion(streamId, true); ``` | Field | Type | Description | | - | - | - | | `streamId` | `string` | The stream to watch. | | `enabled` | `boolean` | Enable or disable the notification. Defaults to true. | Returns `Promise<{ status: string }>`. ## Full resume example ```ts title="resume.ts" import Matilda from '@maincode-ai/matilda-client-sdk'; const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api', accessToken: process.env.MATILDA_ACCESS_TOKEN!, }); let streamId: string | null = null; let lastEventId: string | undefined; // Start streaming for await (const event of client.chat.stream({ input: 'Write a long essay about Australia.', conversationId: 'conv-123', })) { if (event.type === 'response.created') { streamId = event.streamId; } if (event.type === 'response.cursor') { lastEventId = event.lastEventId; } if (event.type === 'response.output_text.delta') { process.stdout.write(event.delta); } } // Later — check if the stream is still active, then resume const active = await client.chat.activeStream('conv-123'); if (active.status === 'active' && streamId) { console.log('\n--- Resuming ---'); for await (const event of client.chat.resume({ streamId, lastEventId })) { if (event.type === 'response.output_text.delta') { process.stdout.write(event.delta); } } } ``` --- # Conversations > List, retrieve, rename, and rate the threads behind a conversationId. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-conversations ## `conversations.list(options?)` Lists conversations with pagination. ```ts title="list.ts" const result = await client.conversations.list({ limit: 20, offset: 0 }); for (const conv of result.conversations) { console.log(`${conv.id}: ${conv.title} (updated ${conv.updatedAt})`); } ``` | Field | Type | Description | | - | - | - | | `limit` | `number` | Maximum number of conversations to return. | | `offset` | `number` | Pagination offset. | | `fingerprint` | `string \| null` | Device fingerprint. | | `accessToken` | `string \| null` | Override access token. | Returns `Promise`: ```ts interface ConversationListResponse { conversations: ConversationSummary[]; total: number; limit: number; offset: number; } interface ConversationSummary { id: string; userId: string; title: string; createdAt: string; updatedAt: string; } ``` ## `conversations.retrieve(conversationId, options?)` Retrieves a full conversation thread with all messages. ```ts title="retrieve.ts" const conv = await client.conversations.retrieve('conv-123'); for (const msg of conv.messages) { console.log(`[${msg.role}] ${msg.content}`); } ``` Returns `Promise`: ```ts interface ConversationRecord extends ConversationSummary { messages: ConversationMessage[]; } interface ConversationMessage { id: string; role: 'user' | 'assistant'; content: string; feedback?: 'positive' | 'negative' | null; attachments?: FileAttachment[]; tokensUsed?: number | null; parentId?: string | null; generationOrdinal?: number; status?: 'completed' | 'failed' | 'interrupted'; errorCode?: string | null; createdAt: string; } ``` ## `conversations.update(conversationId, patch, options?)` Updates a conversation's metadata (currently only title). ```ts await client.conversations.update('conv-123', { title: 'My Chat About AI' }); ``` | Field | Type | Description | | - | - | - | | `conversationId` | `string` | The conversation to update. | | `patch` | `{ title?: string }` | Fields to update. | Returns `Promise`. ## `conversations.setMessageFeedback(conversationId, messageId, feedback, options?)` Sets thumbs-up or thumbs-down feedback on a specific message. ```ts await client.conversations.setMessageFeedback('conv-123', 'msg-456', 'positive'); ``` | Field | Type | Description | | - | - | - | | `conversationId` | `string` | The conversation containing the message. | | `messageId` | `string` | The message to rate. | | `feedback` | `'positive' \| 'negative'` | The feedback value. | Returns `Promise<{ ok: boolean }>`. --- # Multi-turn conversations > Carry context across turns with conversationId. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-multi-turn The client SDK does not have a `Session` class. Multi-turn conversations are managed by passing a `conversationId` to each chat call. The server reconstructs the full conversation history server-side from the session store. ## Pattern 1. Generate a conversation ID (any unique string, e.g. a UUID). 2. Pass it to every `chat.create()` or `chat.stream()` call. 3. The server maintains the conversation history — you only send the latest message. ```ts title="conversation.ts" import { randomUUID } from 'node:crypto'; import Matilda from '@maincode-ai/matilda-client-sdk'; const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); // Authenticate with device flow — token refresh is handled automatically if (!(await client.auth.getTokens())) { await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); } const conversationId = randomUUID(); // Turn 1 const r1 = await client.chat.create({ input: 'What is the capital of France?', conversationId }); console.log(r1.outputText); // "Paris" // Turn 2 — server remembers the previous turn const r2 = await client.chat.create({ input: 'What about Germany?', conversationId }); console.log(r2.outputText); // "Berlin" // Turn 3 const r3 = await client.chat.create({ input: 'And Italy?', conversationId }); console.log(r3.outputText); // "Rome" ``` ## Contrasting with the agent SDK The Matilda [agent SDK](https://maincode.com/docs/agent-sdk-overview) provides a `Session` class that wraps an `Agent` with auto-managed `conversationId`, a `turns[]` array, and session-level defaults. If you need client-side tool execution, approval loops, or session state management, consider the agent SDK. For simple chatbot integrations, the client SDK's `conversationId` pattern is sufficient. ## Retrieving conversation history ```ts // List all conversations const list = await client.conversations.list({ limit: 50 }); // Retrieve a specific conversation with full message history const conv = await client.conversations.retrieve(conversationId); for (const msg of conv.messages) { console.log(`[${msg.role}] ${msg.content}`); } ``` --- # Files > Upload one file or many, then attach the IDs to a chat request. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-files ## `files.upload(file, options?)` Uploads a single file. Files at or above the server's chunked threshold use the parallel multipart protocol; smaller files use single-shot upload. ```ts title="upload.ts" const file = new File(['Hello, world!'], 'hello.txt', { type: 'text/plain' }); const result = await client.files.upload(file, { onProgress: (pct) => console.log(`Upload: ${pct}%`), }); console.log(`File ID: ${result.fileId}, Status: ${result.status}`); ``` ### `FileUploadOptions` Extends `MatildaRequestOptions`: | Field | Type | Description | | - | - | - | | `onProgress` | `(pct: number) => void` | Progress callback (0–100). | | `fingerprint` | `string \| null` | Device fingerprint. | | `accessToken` | `string \| null` | Override access token. | | `signal` | `AbortSignal` | Abort the upload. | Returns `Promise`: ```ts interface FileCompleteResponse { fileId: string; status: FileAttachmentStatus; failureReason?: FileFailureReason; } type FileAttachmentStatus = 'pending' | 'scanning' | 'processing' | 'ready' | 'failed' | 'rejected'; ``` ## `files.uploadMany(files, options?)` Uploads multiple files in parallel. One file's failure does not abort the others. Inspect each `PromiseSettledResult` for per-file outcomes. ```ts title="upload-many.ts" const files = [ new File(['doc 1'], 'doc1.txt', { type: 'text/plain' }), new File(['doc 2'], 'doc2.txt', { type: 'text/plain' }), ]; const results = await client.files.uploadMany(files); for (let i = 0; i < results.length; i++) { const result = results[i]; if (result.status === 'fulfilled') { console.log(`File ${i}: ${result.value.fileId} (${result.value.status})`); } else { console.error(`File ${i} failed:`, result.reason); } } ``` Returns `Promise[]>`. ## `files.retrieve(fileId, options?)` Retrieves metadata for a previously uploaded file. ```ts title="retrieve.ts" const file = await client.files.retrieve('file-abc123'); console.log(`${file.filename} — ${file.status} (${file.sizeBytes} bytes)`); if (file.extractedText) { console.log(`Extracted: ${file.extractedText.slice(0, 100)}...`); } ``` Returns `Promise`: ```ts interface FileAttachment { id: string; filename: string; contentType: string; sizeBytes: number; status: FileAttachmentStatus; extractedText?: string; thumbnailUrl?: string; localUri?: string; failureReason?: FileFailureReason; createdAt: string; } interface FileFailureReason { code: string; message: string; retryable: boolean; } ``` ## Using files in chat Upload a file, then reference its `fileId` in a chat message: ```ts title="attach.ts" const fileResult = await client.files.upload( new File(['Quarterly report content...'], 'report.txt', { type: 'text/plain' }), ); const response = await client.chat.create({ input: 'Summarise this report.', fileIds: [fileResult.fileId], }); console.log(response.outputText); ``` --- # Feedback and devices > Report harmful content and manage push notification devices. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-feedback-devices ## Feedback ### `feedback.report(params, options?)` Reports a message for harmful, inaccurate, off-topic, or privacy-violating content. ```ts title="report.ts" await client.feedback.report({ messageId: 'msg-456', conversationId: 'conv-123', reason: 'inaccurate', comment: 'The capital of Australia is Canberra, not Sydney.', }); ``` #### Parameters | Field | Type | Description | | - | - | - | | `messageId` | `string` | The message being reported. | | `conversationId` | `string` | The conversation containing the message. | | `reason` | `'harmful' \| 'inaccurate' \| 'off_topic' \| 'privacy' \| 'other'` | Report reason. | | `comment` | `string` | Optional additional context. | Returns `Promise<{ reportId: string; acknowledgedAt: string }>`. ### `feedback.response(params, options?)` Submits general response feedback (positive/negative sentiment with platform context). ```ts title="response.ts" await client.feedback.response({ messageId: 'msg-456', conversationId: 'conv-123', comment: 'Great answer!', platform: 'web', appVersion: '1.0.0', }); ``` #### Parameters | Field | Type | Description | | - | - | - | | `messageId` | `string` | The message being rated. | | `conversationId` | `string` | The conversation containing the message. | | `comment` | `string` | Optional feedback text. | | `platform` | `'web' \| 'ios' \| 'android' \| 'unknown'` | Client platform. | | `appVersion` | `string` | App version string. | | `buildNumber` | `string` | Build number. | Returns `Promise<{ feedbackId: string; acknowledgedAt: string }>`. ### `feedback.reportBug(params, options?)` Files a bug report from your application. Reports land as GitHub issues in the internal feedback repository — titled `[SDK] ` and labelled `sdk-feedback` — so engineering can triage them directly. The SDK automatically stamps the report with its package name (`@maincode-ai/matilda-client-sdk`), package version, and — in Node — the Node.js version, so you usually only need `title` and `description`. Pass the optional fields to override or supply anything else. ```ts title="report-bug.ts" await client.feedback.reportBug({ title: 'stream() silently closes on 429', description: 'The stream closes without error when the API returns 429.', reproduction: 'Call chat.stream() in a loop until rate-limited; observe the close event.', }); ``` #### Parameters | Field | Type | Description | | - | - | - | | `title` | `string` | One-line summary of the bug (required, max 200 chars). | | `description` | `string` | What went wrong (required, max 4000 chars). | | `reproduction` | `string` | Optional steps to reproduce (max 4000 chars). | | `package` | `string` | Reporting package name — defaults to this SDK's package name. | | `packageVersion` | `string` | Reporting package version — defaults to this SDK's version. | | `nodeVersion` | `string` | Node.js version — auto-detected in Node, omitted in browsers. | | `platform` | `'web' \| 'ios' \| 'android' \| 'unknown'` | Client platform. | | `appVersion` | `string` | Your application's version string. | | `idempotencyKey` | `string` | Optional key (max 128 chars) so retries don't create duplicate issues. | Returns `Promise<{ feedbackId: string; acknowledgedAt: string }>`. ## Devices ### `devices.register(device, options?)` Registers a push notification device. ```ts title="register.ts" await client.devices.register({ expo_push_token: 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]', device_id: 'device-uuid-123', platform: 'ios', app_version: '1.0.0', }); ``` #### `PushDevice` | Field | Type | Description | | - | - | - | | `expo_push_token` | `string` | Expo push notification token. | | `device_id` | `string` | Unique device identifier. | | `platform` | `'ios' \| 'android'` | Device platform. | | `app_version` | `string` | App version. | Returns `Promise<{ status: string }>`. ### `devices.list(options?)` Lists all registered push devices for the current user. ```ts const devices = await client.devices.list(); for (const device of devices) { console.log(`${device.platform}: ${device.expo_push_token}`); } ``` Returns `Promise<PushDevice[]>`. ### `devices.unregister(expoPushToken, options?)` Unregisters a push device by its Expo push token. ```ts await client.devices.unregister('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'); ``` Returns `Promise<{ deleted: boolean }>`. --- # Error handling > Typed error classes, chat error codes, and the SSE stall watchdog. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-error-handling ## `MatildaAPIError` Thrown on non-2xx HTTP responses from the API. ```ts class MatildaAPIError extends Error { readonly status: number; // HTTP status code readonly responseText: string; // Raw response body } ``` ## `SafetyReplaceError` Thrown by `chat.streamText()`, `chat.streamObject()`, and `chat.createObject()` when the server replaces the output via a safety filter. The `message` property contains the replacement text (or empty string), and `categories` lists the safety categories that triggered the replacement. ```ts class SafetyReplaceError extends Error { readonly categories: string[]; } ``` ## `MatildaObjectParseError` Thrown by `chat.streamObject()` and `chat.createObject()` when the response cannot be parsed as JSON or fails zod validation — e.g. when a stream is truncated. `raw` contains the full response text; `cause` is the underlying `JSON.parse` or zod error. See [Structured output](https://maincode.com/docs/client-sdk-structured-output). ```ts class MatildaObjectParseError extends Error { readonly raw: string; readonly cause: unknown; } ``` ## `AuthError` Thrown by auth flows. The `code` field is an OAuth error code (e.g. `'invalid_grant'`, `'authorization_pending'`, `'expired_token'`, `'access_denied'`). The `retryable` field distinguishes transient failures (5xx, network) from permanent ones (revoked refresh token). ```ts class AuthError extends Error { readonly code: string; readonly retryable: boolean; } ``` ## Chat error codes (`ChatErrorCode`) These codes are emitted via the `response.error` stream event and appear in `MatildaChatResponse.errors`: | Code | Description | | - | - | | `internal_error` | Server-side failure. | | `upstream_unavailable` | The AI model is not responding. | | `rate_limited` | Too many requests. | | `content_blocked` | Safety filter blocked the content. | | `stream_aborted` | The stream was interrupted before completion. | | `deadline_exceeded` | The response did not finish before the deadline. | | `context_too_large` | The conversation is too long for the model. | | `stalled` | No SSE events for the configured stall window. | | `stream_expired` | The durable stream buffer expired (resume path only). | | `unknown` | Unclassified error (old server without typed codes). | ## Stall watchdog The streaming parser arms an idle-event watchdog. If no SSE event arrives for `stallTimeoutMs` milliseconds, the stream is considered dead and aborted with a `'stalled'` error. - **Default:** `45_000` ms (3× the server's 15-second keep-alive ping cadence) - **Disable:** Pass `stallTimeoutMs: 0` in `MatildaRequestOptions` (not recommended — mobile loses background-to-foreground hung-stream recovery) ## Error handling example ```ts title="errors.ts" import Matilda, { MatildaAPIError, SafetyReplaceError } from '@maincode-ai/matilda-client-sdk'; try { const text = await client.chat.createText({ input: 'Hello!' }); console.log(text); } catch (err) { if (err instanceof MatildaAPIError) { if (err.status === 401) { console.error('Session expired — re-authenticate.'); } else if (err.status === 429) { console.error('Rate limited — slow down.'); } else { console.error(`API error ${err.status}: ${err.responseText}`); } } else if (err instanceof SafetyReplaceError) { console.error(`Safety filter: ${err.categories.join(', ')}`); } else { console.error('Unexpected error:', err); } } ``` --- # Recipes > Six runnable examples, from a CLI chatbot to durable stream recovery. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-recipes ## Recipe 1: CLI chatbot A complete interactive CLI chatbot with device-flow auth and streaming. ```ts title="cli-chatbot.ts" import * as readline from 'node:readline/promises'; import { stdin, stdout } from 'node:process'; import Matilda from '@maincode-ai/matilda-client-sdk'; import { createFileTokenStore } from '@maincode-ai/matilda-client-sdk/auth/node'; import { homedir } from 'node:os'; import { join } from 'node:path'; const { store, lock } = createFileTokenStore(join(homedir(), '.matilda', 'tokens.json')); const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); // Authenticate if needed if (!(await client.auth.getTokens())) { console.log('Starting device flow authentication...'); await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code', tokenStore: store, tokenLock: lock, }); console.log('Authenticated!'); } // Start chatting const rl = readline.createInterface({ input: stdin, output: stdout }); const conversationId = crypto.randomUUID(); while (true) { const input = await rl.question('\nYou: '); if (!input.trim() || input.toLowerCase() === 'exit') break; process.stdout.write('Matilda: '); for await (const chunk of client.chat.streamText({ input, conversationId })) { process.stdout.write(chunk); } process.stdout.write('\n'); } rl.close(); ``` ## Recipe 2: File Q\&A Upload a document and ask questions about it. ```ts title="file-qa.ts" import { readFileSync } from 'node:fs'; import { randomUUID } from 'node:crypto'; import Matilda from '@maincode-ai/matilda-client-sdk'; const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); // Authenticate with device flow — token refresh is handled automatically if (!(await client.auth.getTokens())) { await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); } // Upload a file const buffer = readFileSync('./report.pdf'); const file = new File([buffer], 'report.pdf', { type: 'application/pdf' }); const upload = await client.files.upload(file, { onProgress: (pct) => process.stdout.write(`\rUploading: ${pct}%`), }); console.log(`\nUploaded: ${upload.fileId} (${upload.status})`); // A conversationId is required for follow-ups to share history — omitting it // auto-creates a new conversation per call. const conversationId = randomUUID(); // Ask a question about it const response = await client.chat.create({ input: 'Summarise the key findings in this report.', fileIds: [upload.fileId], conversationId, }); console.log(response.outputText); // Follow-up question in the same conversation const followUp = await client.chat.create({ input: 'What are the recommendations?', fileIds: [upload.fileId], conversationId, }); console.log(followUp.outputText); ``` ## Recipe 3: Conversation history browser List, paginate, and inspect conversation history. ```ts title="history-browser.ts" import Matilda from '@maincode-ai/matilda-client-sdk'; const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); // Authenticate with device flow — token refresh is handled automatically if (!(await client.auth.getTokens())) { await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); } // List first page let offset = 0; const limit = 10; let page = await client.conversations.list({ limit, offset }); console.log(`Total conversations: ${page.total}\n`); for (const conv of page.conversations) { console.log(`[${conv.id}] ${conv.title}`); console.log(` Updated: ${conv.updatedAt}`); console.log(); } // Load next page offset += limit; if (offset < page.total) { page = await client.conversations.list({ limit, offset }); for (const conv of page.conversations) { console.log(`[${conv.id}] ${conv.title}`); } } // Retrieve a full conversation if (page.conversations.length > 0) { const full = await client.conversations.retrieve(page.conversations[0].id); console.log(`\n--- ${full.title} ---`); for (const msg of full.messages) { console.log(`\n[${msg.role.toUpperCase()}]`); console.log(msg.content); if (msg.feedback) { console.log(` Feedback: ${msg.feedback}`); } } } ``` ## Recipe 4: Durable stream recovery Start a stream, simulate a disconnect, and resume from the last cursor. ```ts title="stream-recovery.ts" import Matilda from '@maincode-ai/matilda-client-sdk'; const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); // Authenticate with device flow — token refresh is handled automatically if (!(await client.auth.getTokens())) { await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); } const conversationId = crypto.randomUUID(); let streamId: string | null = null; let lastEventId: string | undefined; let receivedText = ''; // Start streaming — simulate disconnect after a few events console.log('Starting stream...'); try { for await (const event of client.chat.stream({ input: 'Write a very long, detailed essay about the history of computing.', conversationId, })) { if (event.type === 'response.created') streamId = event.streamId; if (event.type === 'response.cursor') lastEventId = event.lastEventId; if (event.type === 'response.output_text.delta') { receivedText += event.delta; // Simulate disconnect after 500 chars if (receivedText.length > 500) { console.log('\n--- Simulated disconnect ---'); break; } } if (event.type === 'response.completed') { console.log('Stream completed naturally.'); } } } catch (err) { console.log('Disconnected:', err); } console.log(`Received ${receivedText.length} chars before disconnect.`); // Check if the stream is still active if (streamId) { const active = await client.chat.activeStream(conversationId); console.log(`Stream status: ${active.status}`); if (active.status === 'active') { console.log('\n--- Resuming ---'); for await (const event of client.chat.resume({ streamId, lastEventId })) { if (event.type === 'response.output_text.delta') { receivedText += event.delta; process.stdout.write(event.delta); } if (event.type === 'response.completed') { console.log('\n\n--- Resume complete ---'); console.log(`Total received: ${receivedText.length} chars`); } } } } ``` ## Recipe 5: Multi-environment setup Run staging and production clients in the same process. Each instance manages its own auth independently. ```ts title="multi-env.ts" import Matilda from '@maincode-ai/matilda-client-sdk'; const staging = new Matilda({ baseUrl: 'https://staging.matilda.maincode.com/api' }); const production = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); // Authenticate each instance independently — device flow or browser login if (!(await staging.auth.getTokens())) { await staging.auth.loginWithDeviceFlow({ clientId: 'matilda-code', }); } if (!(await production.auth.getTokens())) { await production.auth.loginWithBrowser({ clientId: 'matilda-code' }); } // Run the same prompt against both environments const [stagingResponse, prodResponse] = await Promise.all([ staging.chat.createText({ input: 'Explain quantum entanglement.' }), production.chat.createText({ input: 'Explain quantum entanglement.' }), ]); console.log('Staging:', stagingResponse); console.log('Production:', prodResponse); // Instances are fully isolated — each manages its own token lifecycle // Reconfiguring one never affects the other: staging.configure({ baseUrl: 'https://override.example/api' }); // production.config.baseUrl is unchanged ``` ## Recipe 6: Custom token store Implement `StorageAdapter` to store tokens in a database or other custom backend. ```ts title="token-store.ts" import Matilda from '@maincode-ai/matilda-client-sdk'; import type { StorageAdapter } from '@maincode-ai/matilda-client-sdk'; // Example: a database-backed token store class DatabaseTokenStore implements StorageAdapter { constructor(private db: Database) {} async get(key: string): Promise<string | null> { const row = await this.db.query('SELECT value FROM tokens WHERE key = $1', [key]); return row?.value ?? null; } async set(key: string, value: string): Promise<void> { await this.db.query( 'INSERT INTO tokens (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2', [key, value], ); } async remove(key: string): Promise<void> { await this.db.query('DELETE FROM tokens WHERE key = $1', [key]); } } const tokenStore = new DatabaseTokenStore(myDatabase); const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' }); await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code', tokenStore, // No cross-process lock needed — the database handles concurrency }); // Tokens are now persisted in the database and survive process restarts const response = await client.chat.create({ input: 'Hello!' }); console.log(response.outputText); ``` --- # Exports > Every class, interface, and type the client SDK package exports. Section: Client SDK · Source: https://maincode.com/docs/client-sdk-exports ## Default export ```ts import Matilda from '@maincode-ai/matilda-client-sdk'; ``` The `Matilda` class — the main SDK entry point. ## Named exports | Export | Type | Description | | - | - | - | | `Matilda` | `class` | Main client class. | | `MatildaAPIError` | `class` | HTTP error (status, responseText). | | `SafetyReplaceError` | `class` | Safety filter replacement error (categories). | | `MatildaObjectParseError` | `class` | Structured output parse/validation failure (raw, cause). | | `MatildaClientOptions` | `interface` | Constructor options (extends `ClientConfig`). | | `MatildaRequestOptions` | `interface` | Per-request options (signal, stallTimeoutMs, etc.). | | `ChatCreateParams` | `interface` | Chat request params (input, messages, conversationId, fileIds, responseMode, responseSchema). | | `MatildaChatStreamEvent` | `type` | Discriminated union of 14 stream event types. | | `MatildaChatResponse` | `interface` | Collected response from `chat.create()`. | | `MatildaObjectResponse<T>` | `interface` | `chat.createObject()` response (extends `MatildaChatResponse`, adds `object`). | | `MatildaObjectEvent<T>` | `type` | `chat.streamObject()` events — all stream events, plus a final `{ type: 'object' }`. | | `ChatResumeParams` | `interface` | Durable stream resume params (streamId, lastEventId). | | `FileUploadOptions` | `interface` | File upload options (onProgress + request options). | | `BrowserLoginOptions` | `interface` | Browser login (PKCE) options. | | `DeviceLoginOptions` | `interface` | Device flow options. | | `CreateApiKeyOptions` | `interface` | API key creation params (name, scopes, expiresAt). | | `ApiKey` | `interface` | API key metadata (no secret). | | `ApiKeyWithSecret` | `interface` | API key with one-time secret. | | `TokenSet` | `interface` | OAuth token set (accessToken, refreshToken, idToken, expiresAt). | | `StorageAdapter` | `interface` | Pluggable token storage. | | `TokenManager` | `interface` | Token manager interface. | | `LoginFlowEvent` | `type` | Login flow state events. | | `DEFAULT_SUCCESS_REDIRECT` | `const` | Default browser-login success redirect URL. | | `client` | `const` | Pre-created singleton `Matilda` instance (using default config). | ## Re-exported types From `@matilda/api-client`: | Export | Type | Description | | - | - | - | | `MatildaCore` | `class` | The underlying HTTP client that powers all resource groups. | | `configureClient` | `function` | Configure a shared client instance. | | `fetchPublicRuntimeConfig` | `function` | Fetch runtime config from the server. | | `getClientConfig` | `function` | Get the current shared client config. | | `trustApiBaseUrl` | `function` | Validate and brand a URL as trusted. | | `ClientConfig` | — | Base client configuration. | | `RequestOptions` | — | Base request options (fingerprint, accessToken). | | `ChatResponseMode` | — | `'auto' \| 'instant' \| 'deep'`. | | `ChatErrorCode` | — | Chat error code union. | | `GetToken` | — | Token provider function type. | | `TrustedApiBaseUrl` | — | Branded trusted URL type. | | `TrustedApiBaseUrlPolicy` | — | URL validation policy. | | `PublicConfigEndpoint` | — | Public config endpoint union. | | `PublicRuntimeConfig` | — | Runtime config from server. | From `@matilda/shared-types`: | Type | Description | | - | - | | `ApiMessage` | Wire-format message (`{ role, content }`). | | `ChatMessage` | Domain message with full metadata. | | `ConversationSummary` | Conversation list item. | | `ConversationListResponse` | Paginated conversation list. | | `ConversationRecord` | Full conversation with messages. | | `FileAttachment` | File metadata. | | `FileCompleteResponse` | Upload completion response. | | `PushDevice` | Push notification device. | | `UsageEvent` | Token usage event. | | `ActiveStreamLookup` | Active stream check result. | ## Deprecated exports | Export | Description | | - | - | | `PkceLoginOptions` | Use `BrowserLoginOptions` instead. | | `DeviceFlowOptions` | Use `DeviceLoginOptions` instead. | --- # Agent SDK quickstart > Install the agent SDK and run your first agent. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-overview A TypeScript SDK for building agentic applications on Matilda. Provides agent abstractions, client-side tool execution, multi-turn sessions, automatic retry with exponential backoff, DSML tool-call interception, and durable stream resume — all on top of the Matilda-native chat contract. This guide covers SDK version 0.1.0. The agent SDK wraps the [client SDK](https://maincode.com/docs/client-sdk-overview) and adds: - **Agent** — a named, configurable persona with dynamic instructions and purpose-based routing - **Runner** — runs agent turns with streaming, retry, and a client-side tool execution loop - **Session** — multi-turn conversations with auto-managed `conversationId` and turn accumulation - **Client tools** — register handlers the agent can invoke mid-turn; the SDK handles the roundtrip loop - **DSML interception** — tool calls emitted as text tokens (`<|DSML|tool_call>`) are automatically captured and surfaced as native tool events - **Durable stream resume** — reconnect to a detached stream from the last cursor Agent runs send `persist: false` by default — conversations do not appear in the Matilda web app's chat history. ## What's included - **Agent** — named persona with static or dynamic instructions, purpose-based routing - **Runner** — `run()`, `stream()`, `streamText()`, `runText()`, `runObject()`, `streamObject()` with retry and tool execution - **Session** — multi-turn conversations with automatic `conversationId` reuse - **Client tools** — `ToolHandlers` with automatic roundtrip loop and advertised-tool guard - **Files** — upload (single and parallel), retrieve metadata - **Conversations** — list, retrieve, rename, and set message feedback - **Auth** — managed PKCE browser login, RFC 8628 device flow, token restore, persistent token storage ## What's NOT included - **Server-side tool execution** — server-side tools (web search, code execution, etc.) are handled by Matilda core. The agent SDK's tool loop is for client-side tools only. - **Model/provider selection** — Matilda core owns routing, safety, and policy. ## Installation **npm** ```bash npm install @maincode-ai/matilda-agent-sdk ``` **pnpm** ```bash pnpm add @maincode-ai/matilda-agent-sdk ``` **yarn** ```bash yarn add @maincode-ai/matilda-agent-sdk ``` Requires Node.js ≥ 20. `zod` (v3.25+) is a required peer dependency — install it alongside the SDK. It is used by the [structured-output helpers](https://maincode.com/docs/agent-sdk-structured-output): ```bash npm install zod ``` ### ESM import ```ts title="index.ts" import { Agent, Runner, run, stream } from '@maincode-ai/matilda-agent-sdk'; ``` ### CommonJS require ```js title="index.js" const { Agent, Runner, run, stream } = require('@maincode-ai/matilda-agent-sdk'); ``` ## Quick start ### Minimal: run a single agent turn ```ts title="run.ts" import { run } from '@maincode-ai/matilda-agent-sdk'; const result = await run( { name: 'greeter', instructions: 'Be friendly and concise.' }, 'Say hello in three languages.', ); console.log(result.finalOutput); console.log(result.usage); ``` ### Minimal: streaming ```ts title="stream.ts" import { stream } from '@maincode-ai/matilda-agent-sdk'; for await (const event of stream( { name: 'storyteller', instructions: 'Write a short sci-fi haiku.' }, 'Write about a Dyson sphere.', )) { if (event.type === 'message.delta') { process.stdout.write(event.delta); } if (event.type === 'done') { console.log('\n[done]'); } } ``` ### Authenticated: device flow + run ```ts title="device-flow.ts" import { Runner } from '@maincode-ai/matilda-agent-sdk'; import { MatildaCore } from '@maincode-ai/matilda-agent-sdk'; const runner = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); // Authenticate via RFC 8628 device flow — prints a code to stderr if (!(await runner.auth.getTokens())) { await runner.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); } // Token is now managed automatically — refresh on 401 comes for free const result = await runner.run( { name: 'helper', instructions: 'Be concise.' }, 'What is the capital of Australia?', ); console.log(result.finalOutput); ``` --- # Configuration > Configure the shared core, or isolate a Runner with its own client and auth. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-configuration ## `configureClient(options)` Configures the default `MatildaCore` singleton used by `defaultRunner` and the convenience functions (`run`, `stream`, `streamText`, `runText`). Returns nothing. ```ts import { configureClient } from '@maincode-ai/matilda-agent-sdk'; configureClient({ baseUrl: 'https://matilda.maincode.com/api', getToken: async () => myAccessToken, }); ``` ### Options Extends `ClientConfig`. All fields are optional except `baseUrl`. | Field | Type | Description | | - | - | - | | `baseUrl` | `string` | The Matilda API base URL. Must be absolute for auth flows. Defaults to '/api'. | | `accessToken` | `string` | A static access token. Use for quick testing only — prefer managed auth. | | `getToken` | `GetToken` | Dynamic token provider. Called on every request. The SDK's TokenManager implements this. | | `apiVersion` | `string \| null` | API version sent via the X-Matilda-API-Version header. Omit to use the current version. | | `urlPolicy` | `TrustedApiBaseUrlPolicy` | URL validation policy for trustApiBaseUrl(). | | `getCsrfToken` | `() => string \| null` | CSRF token provider for web BFF cookie auth. | ## `MatildaCore` The underlying API client. The agent SDK re-exports it from `@matilda/api-client`. Each `Runner` can hold its own `MatildaCore` instance for isolation, or share the process-wide default. ```ts import { MatildaCore, Runner } from '@maincode-ai/matilda-agent-sdk'; const core = new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api', accessToken: process.env.MATILDA_ACCESS_TOKEN!, }); const runner = new Runner({ core }); ``` ## `Runner` The main agent execution class. Optionally accepts an explicit `MatildaCore` for isolation. ```ts import { Runner, MatildaCore } from '@maincode-ai/matilda-agent-sdk'; // Uses the default core singleton (configured via configureClient) const defaultRunner = new Runner(); // Uses an isolated core — independent config, auth, and token lifecycle const isolatedRunner = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); ``` | Field | Type | Description | | - | - | - | | `core` | `MatildaCore` | Optional explicit core. If omitted, the default singleton is used. | ## Environment URLs | Environment | Base URL | | - | - | | Production | `https://matilda.maincode.com/api` | ## Instance isolation Each `Runner` holds its own `MatildaCore` (either explicit or the default singleton). Resources (`auth`, `files`, `conversations`, `feedback`) resolve their core lazily, so `configureClient()` replacing the default singleton is picked up correctly. ```ts import { Runner, MatildaCore, configureClient } from '@maincode-ai/matilda-agent-sdk'; const runnerA = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); const runnerB = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); // Each runner is fully isolated — independent auth, config, and token lifecycle await runnerA.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); await runnerB.auth.loginWithBrowser({ clientId: 'matilda-code' }); ``` ## Other re-exported configuration utilities | Export | Description | | - | - | | `configureClient(options)` | Configure the default `MatildaCore` singleton. | | `getClientConfig()` | Get the current default core's config. | | `getDefaultCore()` | Get the default `MatildaCore` singleton. | | `trustApiBaseUrl(rawUrl, policy?)` | Validate and brand a URL as a trusted API base URL. | | `MATILDA_API_VERSION_HEADER` | The API version header name. | | `MATILDA_CURRENT_API_VERSION` | The current API version string. | --- # Authentication > AgentAuth on the runner — login, restore, and automatic token refresh. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-authentication The agent SDK provides `AgentAuth` — a managed auth tier that wraps the client SDK's `TokenManager`. On successful login, a `TokenManager` is auto-configured on the runner's `MatildaCore`. Every subsequent request automatically carries a managed access token with single-flight, skew-aware auto-refresh. ## OAuth client ID The agent SDK uses the `matilda-code` OAuth client ID, which supports both PKCE browser login and RFC 8628 device flow. It's the only publicly accessible client ID at the moment — additional client IDs will be documented as access opens up. All examples in this documentation use `matilda-code`. ## API keys Agents can also authenticate with an `mc_live_` API key instead of OAuth — useful for CI and server-side deployments where there's no browser. Keys are minted with the client SDK's `matilda-key` CLI (social-login friendly — no code required): ```bash npx matilda-key create-api-key --name "my-agent" # mints an mc_live_… key after device-flow sign-in; see the client SDK docs for flags ``` Then supply it as a static access token: ```ts configureClient({ baseUrl: 'https://matilda.maincode.com/api', accessToken: process.env.MATILDA_API_KEY, }); ``` ## `runner.auth.loginWithBrowser(opts)` Managed PKCE browser login (RFC 8252 loopback). Starts a temporary local server, opens the browser, receives the auth code, exchanges it for tokens, and auto-wires a `TokenManager` on the runner's core. ```ts const tokens = await runner.auth.loginWithBrowser({ clientId: 'matilda-code', openBrowser: (url) => console.log(`Open: ${url}`), }); console.log(tokens.accessToken); ``` ### `BrowserLoginOptions` | Field | Type | Description | | - | - | - | | `clientId` | `string` | OAuth client alias (e.g. 'matilda-code'). Required. | | `scope` | `string` | Space-separated OAuth scopes. Defaults to 'openid offline\_access'. | | `identityProviderId` | `string` | Route straight to a federated IdP (e.g. Google SSO). | | `callbackPort` | `number` | Fixed loopback port. Defaults to a random ephemeral port. | | `timeoutMs` | `number` | How long to wait for the browser callback. Defaults to 300\_000 (5 min). | | `openBrowser` | `(url: string) => void \| Promise<void>` | Called with the authorize URL. | | `successRedirect` | `string` | URL the browser is 302-redirected to on success. Defaults to 'https\://matilda.maincode.com/cli/signed-in'. | | `errorRedirect` | `string` | URL for the error case. | | `fetchImpl` | `typeof fetch` | Override fetch. Defaults to the global fetch. | | `tokenStore` | `StorageAdapter` | Custom token persistence. Defaults to memoryStorage(). | | `tokenLock` | `<T>(fn: () => Promise<T>) => Promise<T>` | Cross-process critical-section lock for token refresh. | | `onEvent` | `(e: LoginFlowEvent) => void` | Subscribe to login flow state events. | Returns `Promise<TokenSet>`. ## `runner.auth.loginWithDeviceFlow(opts)` Managed RFC 8628 device flow. Requests a device code, prints the user code and verification URL to stderr (by default), and polls until the user authorises. ```ts const tokens = await runner.auth.loginWithDeviceFlow({ clientId: 'matilda-code', onEvent: (e) => { if (e.type === 'user_code') { console.log(`Visit ${e.verificationUri} and enter code: ${e.userCode}`); } }, }); ``` If no `onEvent` handler is provided, the SDK prints the user code and verification URL to stderr automatically. ### `DeviceLoginOptions` | Field | Type | Description | | - | - | - | | `clientId` | `string` | OAuth client alias. Required. | | `scope` | `string` | Space-separated OAuth scopes. Defaults to 'openid offline\_access'. | | `timeoutMs` | `number` | Polling timeout. Defaults to 300\_000 (5 min). | | `signal` | `AbortSignal` | Abort the polling loop. | | `fetchImpl` | `typeof fetch` | Override fetch. Defaults to the global fetch. | | `tokenStore` | `StorageAdapter` | Custom token persistence. Defaults to memoryStorage(). | | `tokenLock` | `<T>(fn: () => Promise<T>) => Promise<T>` | Cross-process lock for token refresh. | | `onEvent` | `(e: LoginFlowEvent) => void` | Subscribe to login flow events. Defaults to defaultDeviceOnEvent. | Returns `Promise<TokenSet>`. ## `runner.auth.restore(opts)` Adopts tokens that were already persisted (e.g. by `createFileTokenStore`) without repeating the interactive login. Returns `null` when the store holds nothing usable, so a caller can fall back to `loginWith*`. ```ts import { createFileTokenStore } from '@maincode-ai/matilda-agent-sdk'; import { homedir } from 'node:os'; import { join } from 'node:path'; const { store, lock } = createFileTokenStore(join(homedir(), '.matilda', 'tokens.json')); const tokens = await runner.auth.restore({ clientId: 'matilda-code', tokenStore: store, tokenLock: lock, }); if (!tokens) { // No persisted tokens — fall back to interactive login await runner.auth.loginWithDeviceFlow({ clientId: 'matilda-code', tokenStore: store, tokenLock: lock, }); } ``` ### Parameters | Field | Type | Description | | - | - | - | | `clientId` | `string` | OAuth client alias. | | `tokenStore` | `StorageAdapter` | Token persistence adapter. | | `tokenLock` | `<T>(fn: () => Promise<T>) => Promise<T>` | Cross-process lock. | | `fetchImpl` | `typeof fetch` | Override fetch. | | `metadata` | `AuthServerMetadata` | Pre-fetched server metadata (skips discovery). | Returns `Promise<TokenSet | null>`. ## `runner.auth.getTokens()` Returns the current token set from the managed `TokenManager`, or `null` if not authenticated. ```ts const tokens = await runner.auth.getTokens(); if (tokens) { console.log(`Token expires at: ${new Date(tokens.expiresAt).toISOString()}`); } ``` Returns `Promise<TokenSet | null>`. ## `runner.auth.logout()` Clears the token store, destroys the `TokenManager`, and restores the core's previous `getToken` provider (important when the core is the shared default singleton). ```ts await runner.auth.logout(); ``` Returns `Promise<void>`. ## Token persistence By default, tokens are stored in memory (`memoryStorage()`). For cross-process persistence (e.g. CLI sessions), use `createFileTokenStore`: ```ts import { Runner, MatildaCore, createFileTokenStore } from '@maincode-ai/matilda-agent-sdk'; import { homedir } from 'node:os'; import { join } from 'node:path'; const tokenPath = join(homedir(), '.matilda', 'tokens.json'); const { store, lock } = createFileTokenStore(tokenPath); const runner = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); await runner.auth.loginWithBrowser({ clientId: 'matilda-code', tokenStore: store, tokenLock: lock, }); ``` The file store uses a `0600` JSON file with a lockfile-based single-writer lock to prevent cross-process refresh races. ## `StorageAdapter` interface ```ts interface StorageAdapter { get(key: string): string | null | Promise<string | null>; set(key: string, value: string): void | Promise<void>; remove(key: string): void | Promise<void>; } ``` ## `TokenManager` interface ```ts interface TokenManager { getAccessToken(opts?: { forceRefresh?: boolean }): Promise<string>; getTokens(): Promise<TokenSet | null>; setTokens(tokens: TokenSet): Promise<void>; clear(): Promise<void>; } ``` ## `TokenSet` interface ```ts interface TokenSet { accessToken: string; refreshToken?: string; idToken?: string; expiresAt: number; // epoch milliseconds } ``` ## `AuthError` class ```ts class AuthError extends Error { readonly code: string; // OAuth error code (e.g. 'invalid_grant', 'authorization_pending') readonly retryable: boolean; // true for transient 5xx/network; false for revoked tokens } ``` ## `LoginFlowEvent` type ```ts type LoginFlowEvent = | { type: 'state'; status: 'idle' | 'awaiting_user' | 'exchanging' | 'authenticated' | 'error' } | { type: 'authorize_url'; url: string } | { type: 'user_code'; userCode: string; verificationUri: string; verificationUriComplete?: string }; ``` --- # Agent > Configure agents with instructions, tools, compaction, and resumable state. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-agent An `Agent` is a named, immutable persona with optional instructions, context, purpose, and metadata. You can pass a plain `AgentOptions` object anywhere an `Agent` is accepted — the SDK normalises it. ## `Agent` class ```ts import { Agent } from '@maincode-ai/matilda-agent-sdk'; const reviewer = new Agent({ name: 'code-reviewer', purpose: 'code', instructions: 'Review code for correctness, security, and readability.', context: 'Project: matilda-core\nLanguage: TypeScript', metadata: { team: 'platform' }, }); ``` The `Agent` class is immutable and reusable — construct once, run many times. ### `AgentOptions` | Field | Type | Description | | - | - | - | | `name` | `string` | Agent name. Must be non-empty. Required. | | `instructions` | `AgentInstructions` | Static string or dynamic function (see below). | | `context` | `string` | Additional context appended to instructions under a Context: header. | | `purpose` | `AgentPurpose` | Controls the default responseMode and how the server routes the request. Defaults to 'code'. | | `responseMode` | `ChatResponseMode` | Override the response mode. If omitted, derived from purpose. | | `metadata` | `Record<string, unknown>` | Custom data available to dynamic instructions. Frozen on construction. Defaults to {}. | ## `AgentPurpose` ```ts type AgentPurpose = 'code' | 'analysis' | 'general'; ``` | Purpose | `responseMode` default | | - | - | | `'code'` | `'auto'` | | `'analysis'` | `'deep'` | | `'general'` | `'auto'` | ## Dynamic instructions Instructions can be a function that receives runtime context, letting you customise behaviour per-call: ```ts const agent = new Agent({ name: 'code-reviewer', purpose: 'code', instructions: ({ input, metadata }) => { const lang = (metadata.language as string) ?? 'auto-detect'; const strictness = (metadata.strictness as string) ?? 'normal'; return [ 'Review the following code.', `Language: ${lang}`, `Strictness: ${strictness}`, 'Focus on: correctness, security, and readability.', ].join('\n'); }, }); const result = await run(agent, 'function add(a, b) { return a + b }', { metadata: { language: 'JavaScript', strictness: 'strict' }, }); ``` ### `AgentInstructions` ```ts type AgentInstructions = | string | ((context: AgentRunContext) => string | Promise<string>); ``` ### `AgentRunContext` ```ts interface AgentRunContext { agentName: string; input: string; purpose: AgentPurpose; metadata: Readonly<Record<string, unknown>>; } ``` ## How agent messages are constructed The SDK packs the agent's identity, instructions, context, and the user's prompt into a single user message with labelled sections: ```text Agent: code-reviewer Instructions: Review the following code. Language: JavaScript ... Context: Project: matilda-core Code task: function add(a, b) { return a + b } ``` ## `buildAgentChatRequest(opts)` Low-level builder that constructs the `ChatRequest` object without executing it. Useful for testing, logging, or custom execution paths. ```ts import { buildAgentChatRequest } from '@maincode-ai/matilda-agent-sdk'; const request = await buildAgentChatRequest({ prompt: 'Fix the failing test', agent: { name: 'helper', purpose: 'code' }, instructions: 'Be specific.', context: 'Project root: /repo', conversationId: 'conv-1', fileIds: ['file-1'], clientTools: [{ name: 'read_file', description: 'Read a file', parameters: { type: 'object' } }], }); // request.messages, request.responseMode, request.conversation_id, etc. ``` ### `BuildAgentChatRequestOptions` | Field | Type | Description | | - | - | - | | `prompt` | `string` | The user's message. Required. | | `purpose` | `AgentPurpose` | Fallback purpose if agent doesn't specify one. | | `agent` | `Agent \| AgentOptions` | Optional agent to use. Defaults to a generic agent. | | `instructions` | `AgentInstructions` | Override the agent's instructions. | | `context` | `string` | Override the agent's context. | | `metadata` | `Record<string, unknown>` | Merge with agent's metadata. | | `conversationId` | `string` | Associate with a conversation thread. | | `fileIds` | `string[]` | File IDs to attach. | | `clientTools` | `ClientTool[]` | Client tools to advertise. | | `responseMode` | `ChatResponseMode` | Override response mode. | Returns `Promise<ChatRequest>`. --- # Run > Run a prompt to completion and get the final output. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-run ## `runner.run(agent, input, options?)` Runs an agent turn and returns the complete result. Internally streams and collects all events. ```ts import { Runner } from '@maincode-ai/matilda-agent-sdk'; const runner = new Runner(); const result = await runner.run( { name: 'helper', instructions: 'Be concise.' }, 'What is the capital of Australia?', { conversationId: 'conv-123', responseMode: 'instant', callbacks: { onToken: (delta) => process.stdout.write(delta), onDone: () => console.log('\n[done]'), }, }, ); console.log(result.finalOutput); console.log(result.usage); ``` ## `AgentRunOptions` Extends `RequestOptions`. All fields optional. | Field | Type | Description | | - | - | - | | `signal` | `AbortSignal` | Abort the run. | | `conversationId` | `string` | Associates this turn with a conversation thread. Auto-generated when omitted. | | `fileIds` | `string[]` | File IDs to attach (from files.upload()). | | `clientTools` | `ClientTool[]` | Client tools to advertise for this turn. | | `context` | `string` | Override the agent's context. | | `responseMode` | `ChatResponseMode` | Override the response mode. | | `responseSchema` | `string` | Raw JSON Schema (as a string) to grammar-constrain the response to. Prefer runner.runObject / runner.streamObject, which convert a zod schema for you. | | `stallTimeoutMs` | `number` | SSE stall watchdog timeout in ms. Defaults to 45\_000. Pass 0 to disable. | | `metadata` | `Record<string, unknown>` | Custom data available to dynamic instructions. | | `toolHandlers` | `ToolHandlers` | Handlers for client tools. | | `maxToolRoundtrips` | `number` | Maximum tool roundtrip cycles before stopping. Defaults to 25. | | `maxRetries` | `number` | Maximum retries on retryable errors (5xx, 429, 408, network). Defaults to 0. | | `throwOnStreamError` | `boolean` | Throw MatildaAgentStreamError if the stream emits an error event. Defaults to true. | | `callbacks` | `AgentCallbacks` | Callback hooks for events (see below). | | `core` | `MatildaCore` | Override the runner's core for this run. | | `fingerprint` | `string` | Device fingerprint for rate limiting. | | `accessToken` | `string` | Override the core-level access token for this request. | > **Note** — Prefer `runner.runObject` / `runner.streamObject` over `responseSchema` — they convert a zod schema for you; see [Structured output](https://maincode.com/docs/agent-sdk-structured-output). Client tool handlers are wired up in [Client tools](https://maincode.com/docs/agent-sdk-client-tools). ## `AgentCallbacks` Simple callback hooks that fire as events arrive. An alternative to manually iterating `stream()`. ```ts const result = await runner.run(agent, input, { callbacks: { onToken: (delta) => process.stdout.write(delta), onToolCall: (name, args) => console.log(`Tool: ${name}`), onToolResult: (name, result, isError) => console.log(`Result: ${result}`), onUsage: (usage) => console.log(`Tokens: ${usage.output_tokens}`), onError: (code, message) => console.error(`Error: ${code}`), onRetry: (attempt, error, delayMs) => console.log(`Retry ${attempt} in ${delayMs}ms`), onDone: () => console.log('Done'), }, }); ``` | Field | Type | Description | | - | - | - | | `onEvent` | `(event: AgentRunEvent) => void` | Every event — catch-all, fires before the typed hooks below. Useful for telemetry, UI plumbing, or event logging. | | `onToken` | `(delta: string) => void` | A text chunk arrives. | | `onToolCall` | `(name: string, args: Record<string, unknown>) => void` | The agent calls a client tool. | | `onToolResult` | `(name: string, result: string, isError: boolean) => void` | A client tool handler returns. | | `onUsage` | `(usage: UsageEvent) => void` | Token usage data arrives. | | `onError` | `(code: ChatErrorCode, message: string) => void` | A stream error occurs. | | `onRetry` | `(attempt: number, error: { code: string; message: string }, delayMs: number) => void` | A retryable error triggers a retry. | | `onDone` | `() => void` | The stream finishes. Fires per-turn in multi-turn tool loops. | ## `AgentRunResult` | Field | Type | Description | | - | - | - | | `agentName` | `string` | The agent's name. | | `finalOutput` | `string` | The full assistant response text. Accumulated from message.delta events. | | `events` | `AgentRunEvent[]` | Every event emitted during the run. | | `streamId` | `string \| undefined` | Durable stream ID (from stream.started event). | | `lastEventId` | `string \| undefined` | Last stream event ID (for resume). | | `usage` | `UsageEvent \| undefined` | Token usage. Accumulated across multi-roundtrip runs. | | `errors` | `Array<{ code: ChatErrorCode; message: string }>` | Any errors emitted during the run. | | `truncatedReason` | `string \| undefined` | Why the response was truncated (e.g. 'max\_tokens', 'max\_tool\_roundtrips'). | | `safetyReplace` | `{ message: string; categories: string[] } \| undefined` | Set when the backend replaced the answer for safety. finalOutput holds the replacement text. | ## `throwOnStreamError: false` By default, `run()` throws `MatildaAgentStreamError` if the stream emits an error event. Pass `throwOnStreamError: false` to suppress the throw and inspect errors on the returned result instead: ```ts const result = await runner.run(agent, input, { throwOnStreamError: false }); if (result.errors.length > 0) { for (const err of result.errors) { console.log(`${err.code}: ${err.message}`); } } console.log('Partial output:', result.finalOutput || '(none)'); ``` --- # Stream > Full event streaming from an agent run. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-stream ## `runner.stream(agent, input, options?)` Returns an async generator that yields `AgentRunEvent` objects as they arrive. This is the full event stream — tool calls, usage, status changes, safety replacements, and more. ```ts for await (const event of runner.stream( { name: 'explainer', instructions: 'Explain quantum computing.' }, 'What is quantum entanglement?', )) { switch (event.type) { case 'run.started': console.log(`Agent "${event.agentName}" started.`); break; case 'stream.started': console.log(`Stream ${event.streamId} connected.`); break; case 'message.delta': process.stdout.write(event.delta); break; case 'client.tool.requested': console.log(`\nTool requested: ${event.name}`); break; case 'client.tool.result': console.log(`Tool result: ${event.result}`); break; case 'usage': console.log(`\nTokens: ${event.usage.output_tokens}`); break; case 'done': console.log('\n[done]'); break; case 'error': console.error(`Error: ${event.code} — ${event.message}`); break; } } ``` ## `AgentRunEvent` A discriminated union of 22 event types: ### `run.started` Emitted once at the start of a run with the agent's name. ```ts { type: 'run.started'; agentName: string } ``` ### `stream.started` Emitted once when the SSE stream connects, with the durable stream ID. ```ts { type: 'stream.started'; streamId: string } ``` ### `message.delta` A text chunk from the assistant. ```ts { type: 'message.delta'; delta: string } ``` ### `status.changed` Stream lifecycle status change. ```ts { type: 'status.changed'; status: 'thinking' | 'streaming' | 'queued' | 'idle' | 'done' | 'error' | string } ``` ### `queue.status` Queue position update while waiting for a free slot. ```ts { type: 'queue.status'; state: string; position: number; estimatedWaitSeconds: number } ``` ### `tool.started` A server-side tool invocation began. ```ts { type: 'tool.started'; tool: string; inputOrArgs?: string | Record<string, unknown>; output?: string } ``` ### `tool.progress` Progress update from a running server-side tool. ```ts { type: 'tool.progress'; tool: string; message: string } ``` ### `tool.completed` A server-side tool invocation finished. ```ts { type: 'tool.completed'; tool: string; status: 'success' | 'error'; input?: string; output?: string } ``` ### `client.tool.requested` The agent called a client tool. The SDK will execute the matching handler from `toolHandlers`. ```ts { type: 'client.tool.requested'; id?: string; name: string; args: Record<string, unknown> } ``` ### `client.tool.executing` The SDK is about to execute the handler for a requested client tool. ```ts { type: 'client.tool.executing'; id?: string; name: string; args: Record<string, unknown> } ``` ### `client.tool.result` A client tool handler returned a result. ```ts { type: 'client.tool.result'; id?: string; name: string; result: string; isError: boolean } ``` ### `client.tool.roundtrip` Emitted after each tool roundtrip cycle, showing progress against the maximum. ```ts { type: 'client.tool.roundtrip'; turn: number; maxTurns: number } ``` ### `turn.retrying` A retryable error occurred and the turn is being retried. ```ts { type: 'turn.retrying'; attempt: number; maxRetries: number; error: { code: string; message: string }; delayMs: number } ``` ### `generation.status` Generation phase update. ```ts { type: 'generation.status'; phase: string } ``` ### `safety.replace` The server replaced the output via a safety filter. `message` holds the replacement text; `categories` lists the safety categories. ```ts { type: 'safety.replace'; message?: string; categories: string[] } ``` ### `usage` Token usage data for the turn. ```ts { type: 'usage'; usage: UsageEvent } ``` Where `UsageEvent` is: ```ts interface UsageEvent { output_tokens: number; context_pct?: number; context_messages_trimmed?: number; context_budget_tokens?: number; } ``` ### `cursor` Durable stream cursor (event ID). Persist this to resume from this point. ```ts { type: 'cursor'; lastEventId: string } ``` ### `truncated` The response was cut short. ```ts { type: 'truncated'; reason: string } ``` ### `replace` A generic replace event from the server. ```ts { type: 'replace' } ``` ### `done` The stream finished successfully. ```ts { type: 'done' } ``` ### `error` An error occurred during the stream. ```ts { type: 'error'; code: ChatErrorCode; message: string } ``` --- # Text helpers > Filter a run's stream to assistant text, plus the module-level run and stream helpers. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-text-helpers These helpers filter the event stream to just text — useful when you only need the response text and don't care about tool calls, usage, or status events. ## `runner.streamText(agent, input, options?)` Returns an async generator that yields raw string deltas. Throws `SafetyReplaceError` when the server replaces the output (safety filter). Throws `Error` on stream errors. ```ts try { for await (const chunk of runner.streamText( { name: 'poet', instructions: 'Write a haiku.' }, 'Write about the ocean.', )) { process.stdout.write(chunk); } } catch (err) { if (err instanceof SafetyReplaceError) { console.error(`\nSafety replace: ${err.categories.join(', ')}`); } else { console.error(err); } } ``` > **Note** — **Why throw on safety replace?** The original text has already been yielded to the consumer by the time the replace event arrives. Throwing forces the consumer to handle the replacement explicitly — silently dropping it would lose the replacement message. ## `runner.runText(agent, input, options?)` Non-streaming convenience that returns just the final output text. Safety replace is handled by throwing `SafetyReplaceError`. Throws if the stream produced any error events. ```ts const text = await runner.runText( { name: 'helper' }, 'What is 2 + 2?', ); console.log(text); // "4" ``` ## `SafetyReplaceError` ```ts class SafetyReplaceError extends Error { readonly categories: string[]; // message = replacement content (or empty string) } ``` ## Convenience functions The SDK exports default-runner-backed convenience functions so you don't need to instantiate a `Runner` for simple use cases: ```ts import { run, stream, streamText, runText } from '@maincode-ai/matilda-agent-sdk'; // These are equivalent to defaultRunner.run(), defaultRunner.stream(), etc. const result = await run(agent, input, options); const text = await runText(agent, input, options); for await (const event of stream(agent, input, options)) { /* ... */ } for await (const chunk of streamText(agent, input, options)) { /* ... */ } ``` These use the default `MatildaCore` singleton (configured via `configureClient()`). For isolated config or auth, instantiate your own `Runner`. --- # Structured output > Constrain an agent turn to a zod schema with runObject and streamObject. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-structured-output Structured output constrains the agent's response to a JSON Schema, server-side (grammar-constrained decoding), and validates it client-side against your zod schema. Pass a zod schema, receive a fully-typed object — no prompt engineering, no brittle JSON extraction. ## `runner.streamObject(agent, input, schema, options?)` Streams exactly like `runner.stream()` — you receive every `AgentRunEvent` (including tool-loop and usage events) — plus one final event with the parsed, schema-validated object. Options are `AgentRunOptions`. ```ts import { z } from 'zod'; const review = z.object({ summary: z.string(), issues: z.array(z.object({ severity: z.enum(['low', 'medium', 'high']), description: z.string(), })), }); const reviewer = new Agent({ name: 'reviewer', instructions: 'Review the code the user provides.', }); for await (const event of runner.streamObject(reviewer, 'Review this function: ...', review)) { if (event.type === 'message.delta') process.stdout.write(event.delta); if (event.type === 'object') { console.log('\nValidated:', event.object); // typed as z.infer<typeof review> } } ``` The final event: ```ts { type: 'object'; object: T } // T = z.infer<typeof schema> ``` ## `runner.runObject(agent, input, schema, options?)` Non-streaming convenience. Like `runner.run()`, it honours `callbacks`, `throwOnStreamError`, retries, and the tool loop — and returns an `AgentObjectResult<T>`: the full `AgentRunResult` plus the validated `object`. ```ts const result = await runner.runObject( extractor, 'Invoice total $1,250.00 AUD due 30 Sep.', z.object({ total: z.number(), currency: z.string() }), ); console.log(result.object.total); // 1250 (number) console.log(result.object.currency); // "AUD" (string) console.log(result.finalOutput); // raw JSON text as returned console.log(result.usage); // token usage, as usual ``` ### `AgentObjectResult<T>` Extends `AgentRunResult` with one additional field: | Field | Type | Description | | - | - | - | | `object` | `T` | The response text parsed as JSON and validated against your schema. | ## Convenience functions Default-runner-backed, like the other top-level helpers (`streamObject` / `runObject` use the default `MatildaCore` singleton): ```ts import { streamObject, runObject } from '@maincode-ai/matilda-agent-sdk'; const result = await runObject(agent, input, schema, options); for await (const event of streamObject(agent, input, schema, options)) { /* ... */ } ``` ## Raw JSON Schema via `responseSchema` `AgentRunOptions` (and therefore `SessionOptions`) accepts a stringified JSON Schema directly on any run or stream: ```ts const result = await runner.run(agent, 'List three Australian birds.', { responseSchema: JSON.stringify({ type: 'object', properties: { birds: { type: 'array', items: { type: 'string' } } }, required: ['birds'], additionalProperties: false, }), }); JSON.parse(result.finalOutput); // guaranteed valid, schema-conforming JSON ``` With `responseSchema` set, `finalOutput` is guaranteed to be valid JSON conforming to the schema — but parsing and validation are up to you. > **Caution** — **Safety replace and structured output.** Like `runText()` / `streamText()`, the object helpers throw `SafetyReplaceError` when the server replaces the output mid-stream — the replacement text is in `.message` and the triggering categories in `.categories`. Token deltas already yielded to your consumer are not rolled back; `runObject()` is unaffected at the value level, since it throws before returning a result. > **Note** — **Truncation throws.** If the stream is truncated before the JSON completes, both helpers throw `MatildaObjectParseError` with the partial text in `.raw`. See [Error handling](https://maincode.com/docs/agent-sdk-error-handling). > **Note** — **Stream errors throw.** If the server emits an `error` event mid-stream, `streamObject()` throws an `Error` with the server's error code and message, and `runObject()` throws `MatildaAgentStreamError` with the partial `result` attached — matching the behaviour of the text helpers. --- # Client tools > Tools that execute locally, wired into the agent loop. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-client-tools Client tools are handlers you register that the agent can invoke mid-turn. The SDK handles the entire roundtrip loop: detecting tool calls, executing your handler, feeding the result back to the agent, and repeating until the agent stops calling tools or the roundtrip limit is reached. ## Tool execution loop ```text ┌─────────────────────────────────────────────────────────┐ │ Turn 0 │ │ 1. Send messages + clientTools to server │ │ 2. Stream events — agent responds, may call tools │ │ 3. If tool calls detected: │ │ a. client.tool.requested → client.tool.executing │ │ b. Execute handler from toolHandlers │ │ c. client.tool.result │ │ d. Append result to messages as a user message │ │ e. client.tool.roundtrip (turn + 1 / maxTurns) │ │ f. Go to Turn 1 │ │ 4. If no tool calls: run is done │ │ 5. If turn >= maxToolRoundtrips: truncated │ └─────────────────────────────────────────────────────────┘ ``` ## Declaring and handling tools ```ts import { stream, type ToolHandlers } from '@maincode-ai/matilda-agent-sdk'; // 1. Declare the tools so the server knows they exist const clientTools = [ { name: 'get_weather', description: 'Get current weather for a city', parameters: { type: 'object' } }, { name: 'calculate', description: 'Evaluate a math expression', parameters: { type: 'object' } }, ]; // 2. Register handlers — the SDK calls these when the agent invokes a tool const toolHandlers: ToolHandlers = { get_weather: async (args) => { const city = (args.city as string) ?? 'unknown'; return { content: JSON.stringify({ city, temp: 22, condition: 'sunny' }) }; }, calculate: async (args) => { const expr = args.expression as string; try { const result = Function(`return (${expr})`)(); return { content: String(result) }; } catch { return { content: 'Invalid expression', isError: true }; } }, }; // 3. Pass both to stream() or run() for await (const event of stream( { name: 'assistant', instructions: 'Use the available tools to answer questions.' }, 'What is the weather in Sydney, and what is 15 * 23?', { toolHandlers, clientTools }, )) { if (event.type === 'client.tool.requested') { console.log(`→ Agent requested: ${event.name}(${JSON.stringify(event.args)})`); } if (event.type === 'client.tool.executing') { console.log(`⚙ Executing: ${event.name}`); } if (event.type === 'client.tool.result') { console.log(`← Result: ${event.result}${event.isError ? ' (error)' : ''}`); } if (event.type === 'client.tool.roundtrip') { console.log(` Roundtrip ${event.turn}/${event.maxTurns}`); } if (event.type === 'message.delta') { process.stdout.write(event.delta); } } ``` ## `ToolHandler` ```ts type ToolHandler = ( args: Record<string, unknown>, ctx: ToolExecutionContext, ) => Promise<ToolResult>; ``` ## `ToolExecutionContext` ```ts interface ToolExecutionContext { toolCallId?: string; signal?: AbortSignal; // The run's AbortSignal, if provided } ``` ## `ToolResult` ```ts interface ToolResult { content: string; isError?: boolean; } ``` ## `ToolHandlers` ```ts type ToolHandlers = Record<string, ToolHandler>; ``` ## `maxToolRoundtrips` Controls how many back-and-forth tool cycles the SDK allows before stopping. Default is `25` (`DEFAULT_MAX_TOOL_ROUNDTRIPS`). Lower it to prevent infinite loops or control cost. ```ts const result = await runner.run( { name: 'tool-heavy-agent', instructions: 'Use tools to gather information.' }, 'Do a task that needs tools', { maxToolRoundtrips: 5, toolHandlers: { search: async () => ({ content: 'search results...' }) }, clientTools: [{ name: 'search', description: 'Search', parameters: { type: 'object' } }], }, ); const roundtrips = result.events.filter((e) => e.type === 'client.tool.roundtrip'); console.log(`Roundtrips used: ${roundtrips.length} (max was 5)`); ``` ## Advertised-tool guard The SDK enforces that the agent can only call tools that were advertised for the current turn. If the model calls a tool that wasn't in `clientTools`, the SDK returns an error result instead of executing a handler: ```text Tool not offered this turn: <name> ``` This prevents a model from talking the runner into invoking a handler that was never offered — tool output is untrusted input. ## DSML tool-call interception Some models emit tool calls as text tokens wrapped in DSML markup (`<|DSML|tool_call>{...}<|DSML|/tool_call>`) instead of using native function calling. The SDK automatically intercepts these text tokens, parses the JSON payload, and surfaces them as native `client.tool.requested` events — the consumer never sees the raw markup. This interception is fully automatic and applies to all streaming paths. ## Human-in-the-loop The tool execution loop makes human-in-the-loop trivial: a tool handler is just an async function, so it can block on stdin, a UI prompt, or any other input source. ```ts import * as readline from 'node:readline/promises'; const toolHandlers: ToolHandlers = { ask_user: async (args) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); try { const answer = await rl.question(`\n Agent asks: ${args.question}\n > `); return { content: answer.trim() }; } finally { rl.close; } }, }; const clientTools = [ { name: 'ask_user', description: 'Ask the user a question', parameters: { type: 'object' } }, ]; const result = await runner.run( { name: 'clarifier', instructions: 'Ask the user for clarification when needed.' }, 'Help me plan a trip', { toolHandlers, clientTools }, ); ``` --- # Session > Stateful multi-turn conversations with a session object. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-session A `Session` wraps an `Agent` with auto-managed `conversationId` and accumulates turn results. The server maintains conversation history server-side using the `conversationId`, so each turn has full context. ## `createSession(agent, options?)` ```ts import { createSession } from '@maincode-ai/matilda-agent-sdk'; const session = createSession({ name: 'tutor', instructions: 'You are a patient programming tutor. Explain concepts simply.', }); console.log('Conversation ID:', session.conversationId); // Turn 1 const r1 = await session.run('What is a closure in JavaScript?'); console.log('Turn 1:', r1.finalOutput.slice(0, 100), '...'); // Turn 2 — the server remembers the previous exchange via conversationId const r2 = await session.run('Can you show me a simple example?'); console.log('Turn 2:', r2.finalOutput.slice(0, 100), '...'); // The session accumulates all turn results console.log('Total turns:', session.turns.length); console.log('Last turn stream ID:', session.lastTurn?.streamId); ``` ## `Session` class ### `session.run(input, options?)` Runs a turn and accumulates the result in `session.turns`. | Field | Type | Description | | - | - | - | | `input` | `string` | The user's message. | | `options` | `Omit<AgentRunOptions, 'conversationId'>` | Per-turn options. Merged with session defaults. | Returns `Promise<AgentRunResult>`. ### `session.stream(input, options?)` Streams a turn, yielding `AgentRunEvent` as they arrive. The result is accumulated in `session.turns` when the stream completes. ```ts for await (const event of session.stream('Explain async/await in one paragraph.')) { if (event.type === 'message.delta') { process.stdout.write(event.delta); } } console.log('\n[Turns accumulated]:', session.turns.length); console.log('[Final output cached]:', session.lastTurn?.finalOutput.slice(0, 60), '...'); ``` ### `session.conversationId` The auto-generated (or provided) conversation ID. Reused across all turns. ### `session.turns` A readonly array of `AgentRunResult` — one per completed turn. ### `session.lastTurn` Getter for the most recent `AgentRunResult`, or `undefined` if no turns have run. ## `SessionOptions` Extends `Omit<AgentRunOptions, 'conversationId'>`. | Field | Type | Description | | - | - | - | | `runner` | `Runner` | Custom runner instance. Defaults to defaultRunner. | | `conversationId` | `string` | Explicit conversation ID. Auto-generated when omitted. | | `(all AgentRunOptions fields)` | `—` | Session-level defaults applied to every turn. | ## Metadata passthrough Metadata can be set at multiple levels: `Agent` construction, `Session` construction, or per-call. Per-call metadata merges with (and overrides) session defaults. ```ts const session = createSession( { name: 'helper', instructions: ({ metadata }) => `Environment: ${metadata.env ?? 'unknown'}. User: ${metadata.user ?? 'anonymous'}.`, }, { metadata: { env: 'staging', user: 'demo-user' } }, ); // Session-level metadata is used by default await session.run('Who am I?'); // Per-call metadata overrides session defaults await session.run('Who am I now?', { metadata: { user: 'admin' } }); // → env=staging (from session), user=admin (overridden per-call) ``` ## Custom runner A `Session` can use a custom `Runner` for dependency injection in tests or isolated configuration: ```ts import { Runner, Session } from '@maincode-ai/matilda-agent-sdk'; const myRunner = new Runner(); const session = new Session( { name: 'custom-runner-agent', instructions: 'Be brief.' }, { runner: myRunner }, ); const result = await session.run('What is 2 + 2?'); ``` --- # Stream resume > Durable handles and resuming interrupted agent runs. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-stream-resume Durable streaming lets a client disconnect mid-stream and resume from where it left off. The server buffers events, keyed by a `streamId` advertised at stream start. ## `resumeAgentStream(streamId, lastEventId, handlers, options?)` Resumes a previously detached stream by replaying buffered events from `lastEventId`. Returns the accumulated `AgentRunResult`. ```ts import { resumeAgentStream, type AgentRunEvent } from '@maincode-ai/matilda-agent-sdk'; const result = await resumeAgentStream( savedStreamId, savedLastEventId, { onEvent: (event: AgentRunEvent) => { if (event.type === 'message.delta') process.stdout.write(event.delta); }, }, ); console.log('Resumed output:', result.finalOutput); ``` ## Parameters | Field | Type | Description | | - | - | - | | `streamId` | `string` | The stream ID from stream.started event (or result.streamId). | | `lastEventId` | `string \| undefined` | The last cursor received (from cursor event or result.lastEventId). Omit to replay from the start. | | `handlers` | `{ onEvent?: (event: AgentRunEvent) => void }` | Event handler callback. | | `options` | `RequestOptions & { signal?: AbortSignal; core?: MatildaCore }` | Request options. | Returns `Promise<AgentRunResult>`. ## 401 auto-refresh If the resume request returns 401 and the core has a `getToken` provider, the SDK automatically refreshes the token and retries once. ## Full resume example ```ts import { stream, resumeAgentStream } from '@maincode-ai/matilda-agent-sdk'; let streamId: string | undefined; let lastEventId: string | undefined; let receivedText = ''; // Start streaming — capture IDs for potential resume for await (const event of stream({ name: 'resumable-agent' }, 'Tell me a fact.')) { if (event.type === 'stream.started') streamId = event.streamId; if (event.type === 'cursor') lastEventId = event.lastEventId; if (event.type === 'message.delta') { receivedText += event.delta; process.stdout.write(event.delta); } } console.log('\n[Stream completed — streamId:', streamId, 'cursor:', lastEventId, ']'); // Later — resume from the last cursor if the stream was interrupted if (streamId) { const result = await resumeAgentStream(streamId, lastEventId, { onEvent: (event) => { if (event.type === 'message.delta') process.stdout.write(event.delta); }, }); console.log('\n[Resumed — output:', result.finalOutput.slice(0, 60), '...]'); } ``` --- # Files > The runner files resource — upload, retrieve, and attach file IDs to a run. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-files The `Runner` exposes a `files` resource for uploading and retrieving files. Uploaded files can be attached to agent runs via `fileIds`. ## `runner.files.upload(file, options?)` Uploads a single file. Files at or above the server's chunked threshold use the parallel multipart protocol; smaller files use single-shot upload. ```ts const file = new File(['Hello, world!'], 'hello.txt', { type: 'text/plain' }); const result = await runner.files.upload(file, { onProgress: (pct) => console.log(`Upload: ${pct}%`), }); console.log(`File ID: ${result.fileId}, Status: ${result.status}`); ``` ### `FileUploadOptions` Extends `RequestOptions`. All fields optional. | Field | Type | Description | | - | - | - | | `onProgress` | `(pct: number) => void` | Progress callback (0–100). | | `signal` | `AbortSignal` | Abort the upload. | | `fingerprint` | `string \| null` | Device fingerprint. | | `accessToken` | `string \| null` | Override access token. | Returns `Promise<FileCompleteResponse>`: ```ts interface FileCompleteResponse { fileId: string; status: FileAttachmentStatus; failureReason?: FileFailureReason; } type FileAttachmentStatus = 'pending' | 'scanning' | 'processing' | 'ready' | 'failed' | 'rejected'; ``` ## `runner.files.uploadMany(files, options?)` Uploads multiple files in parallel. One file's failure does not abort the others. ```ts const files = [ new File(['doc 1'], 'doc1.txt', { type: 'text/plain' }), new File(['doc 2'], 'doc2.txt', { type: 'text/plain' }), ]; const results = await runner.files.uploadMany(files); for (let i = 0; i < results.length; i++) { const result = results[i]; if (result.status === 'fulfilled') { console.log(`File ${i}: ${result.value.fileId} (${result.value.status})`); } else { console.error(`File ${i} failed:`, result.reason); } } ``` Returns `Promise<PromiseSettledResult<FileCompleteResponse>[]>`. ## `runner.files.retrieve(fileId, options?)` Retrieves metadata for a previously uploaded file. ```ts const file = await runner.files.retrieve('file-abc123'); console.log(`${file.filename} — ${file.status} (${file.sizeBytes} bytes)`); ``` Returns `Promise<FileAttachment>`: ```ts interface FileAttachment { id: string; filename: string; contentType: string; sizeBytes: number; status: FileAttachmentStatus; extractedText?: string; thumbnailUrl?: string; localUri?: string; failureReason?: FileFailureReason; createdAt: string; } ``` ## Using files in agent runs Upload a file, then reference its `fileId` in an agent run: ```ts const fileResult = await runner.files.upload( new File(['Quarterly report content...'], 'report.txt', { type: 'text/plain' }), ); const result = await runner.run( { name: 'analyst', purpose: 'analysis', instructions: 'Summarise the report.' }, 'What are the key findings?', { fileIds: [fileResult.fileId] }, ); console.log(result.finalOutput); ``` --- # Conversations > Read, rename, and rate conversation threads from the runner. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-conversations The `Runner` exposes a `conversations` resource for listing, retrieving, renaming, and providing feedback on conversations. > **Note** — **Note:** Agent runs send `persist: false` by default, so they do not appear in the Matilda web app's chat history. The conversations resource accesses conversations created by other clients (e.g. the web app). If you need agent runs to appear in chat history, you would need to override the `persist` flag — but this is not exposed as a public option in the agent SDK. ## `runner.conversations.list(options?)` Lists conversations with pagination. ```ts const result = await runner.conversations.list({ limit: 20, offset: 0 }); for (const conv of result.conversations) { console.log(`${conv.id}: ${conv.title} (updated ${conv.updatedAt})`); } ``` | Field | Type | Description | | - | - | - | | `limit` | `number` | Maximum number of conversations to return. | | `offset` | `number` | Pagination offset. | Returns `Promise<ConversationListResponse>`: ```ts interface ConversationListResponse { conversations: ConversationSummary[]; total: number; limit: number; offset: number; } interface ConversationSummary { id: string; userId: string; title: string; createdAt: string; updatedAt: string; } ``` ## `runner.conversations.retrieve(conversationId, options?)` Retrieves a full conversation thread with all messages. ```ts const conv = await runner.conversations.retrieve('conv-123'); for (const msg of conv.messages) { console.log(`[${msg.role}] ${msg.content}`); } ``` Returns `Promise<ConversationRecord>`: ```ts interface ConversationRecord extends ConversationSummary { messages: ConversationMessage[]; } ``` ## `runner.conversations.update(conversationId, patch, options?)` Updates a conversation's metadata (currently only title). ```ts await runner.conversations.update('conv-123', { title: 'My Chat About AI' }); ``` Returns `Promise<void>`. ## `runner.conversations.setMessageFeedback(conversationId, messageId, feedback, options?)` Sets thumbs-up or thumbs-down feedback on a specific message. ```ts await runner.conversations.setMessageFeedback('conv-123', 'msg-456', 'positive'); ``` | Field | Type | Description | | - | - | - | | `conversationId` | `string` | The conversation containing the message. | | `messageId` | `string` | The message to rate. | | `feedback` | `'positive' \| 'negative'` | The feedback value. | Returns `Promise<{ ok: boolean }>`. --- # Feedback > File bug reports from your application. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-feedback ## `runner.feedback.reportBug(params, options?)` Files a bug report from your application. Reports land as GitHub issues in the internal feedback repository — titled `[SDK] <title>` and labelled `sdk-feedback` — so engineering can triage them directly. The SDK automatically stamps the report with its package name (`@maincode-ai/matilda-agent-sdk`), package version, and — in Node — the Node.js version, so you usually only need `title` and `description`. Pass the optional fields to override or supply anything else. ```ts title="report-bug.ts" await runner.feedback.reportBug({ title: 'runner.stream() hangs after client.tool.execute throws', description: 'The run promise never settles when a tool handler throws asynchronously.', reproduction: 'Register a tool whose handler throws after an await, then call runner.stream()', }); ``` ### Parameters | Field | Type | Description | | - | - | - | | `title` | `string` | One-line summary of the bug (required, max 200 chars). | | `description` | `string` | What went wrong (required, max 4000 chars). | | `reproduction` | `string` | Optional steps to reproduce (max 4000 chars). | | `package` | `string` | Reporting package name — defaults to this SDK's package name. | | `packageVersion` | `string` | Reporting package version — defaults to this SDK's version. | | `nodeVersion` | `string` | Node.js version — auto-detected in Node, omitted in browsers. | | `platform` | `'web' \| 'ios' \| 'android' \| 'unknown'` | Client platform. | | `appVersion` | `string` | Your application's version string. | | `idempotencyKey` | `string` | Optional key (max 128 chars) so retries don't create duplicate issues. | Returns `Promise<{ feedbackId: string; acknowledgedAt: string }>`. --- # Error handling > Run and stream errors, exponential-backoff retries, and partial results after a failure. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-error-handling ## `MatildaAgentRunError` Thrown on HTTP-level failures (non-2xx response from the server). ```ts class MatildaAgentRunError extends Error { readonly status: number; // HTTP status code readonly responseText: string; // Raw response body } ``` ## `MatildaAgentStreamError` Thrown by `run()` when the stream emits an error event (unless `throwOnStreamError: false`). Carries the full `AgentRunResult` with whatever was collected before the error. ```ts class MatildaAgentStreamError extends Error { readonly code: ChatErrorCode; readonly errors: ReadonlyArray<{ code: ChatErrorCode; message: string }>; readonly result: AgentRunResult; } ``` ```ts import { MatildaAgentStreamError, MatildaAgentRunError } from '@maincode-ai/matilda-agent-sdk'; try { await runner.run(agent, input); } catch (err) { if (err instanceof MatildaAgentStreamError) { console.log('Stream error code:', err.code); console.log('Partial output before error:', err.result.finalOutput); console.log('All errors:', err.errors); } else if (err instanceof MatildaAgentRunError) { console.log('HTTP error status:', err.status); console.log('Response body:', err.responseText); } else { throw err; } } ``` ## `SafetyReplaceError` Thrown by `streamText()`, `runText()`, `streamObject()`, and `runObject()` when the server replaces the output via a safety filter. The `message` property contains the replacement text (or empty string), and `categories` lists the safety categories. ```ts class SafetyReplaceError extends Error { readonly categories: string[]; } ``` ## `MatildaObjectParseError` Thrown by `streamObject()` / `runObject()` when the response cannot be parsed as JSON or fails zod validation — e.g. a truncated stream (see [Structured output](https://maincode.com/docs/agent-sdk-structured-output)). `raw` holds the full response text; `cause` is the underlying `JSON.parse` or zod error. Safety replacement does not surface here — it throws `SafetyReplaceError` first. ```ts class MatildaObjectParseError extends Error { readonly raw: string; readonly cause: unknown; } ``` ## `AuthError` Thrown by auth flows. The `code` field is an OAuth error code. The `retryable` field distinguishes transient failures from permanent ones. ```ts class AuthError extends Error { readonly code: string; readonly retryable: boolean; } ``` ## Chat error codes (`ChatErrorCode`) These codes are emitted via the `error` stream event and appear in `AgentRunResult.errors`: | Code | Description | | - | - | | `internal_error` | Server-side failure. | | `upstream_unavailable` | The AI model is not responding. | | `rate_limited` | Too many requests. | | `content_blocked` | Safety filter blocked the content. | | `stream_aborted` | The stream was interrupted before completion. | | `deadline_exceeded` | The response did not finish before the deadline. | | `context_too_large` | The conversation is too long for the model. | | `stalled` | No SSE events for the configured stall window. | | `stream_expired` | The durable stream buffer expired (resume path only). | | `unknown` | Unclassified error (old server without typed codes). | ## Stall watchdog The streaming parser arms an idle-event watchdog. If no SSE event arrives for `stallTimeoutMs` milliseconds, the stream is considered dead and aborted with a `'stalled'` error. - **Default:** `45_000` ms (`DEFAULT_AGENT_STALL_TIMEOUT_MS`) - **Disable:** Pass `stallTimeoutMs: 0` in `AgentRunOptions` (not recommended) ## Retry behaviour The SDK retries retryable errors within a single turn. Retries are controlled by `maxRetries` (default: `0` — no retries). **Retryable errors:** - HTTP 429 (rate limited), 408 (request timeout), 5xx (server errors) - Network errors: `UND_ERR_SOCKET`, `UND_ERR_FETCH_ERROR`, `ETIMEDOUT`, `ECONNRESET` - SSE stall watchdog (`stalled` error code) - `TimeoutError` (but not `AbortError`) **Retry delay:** Exponential backoff with jitter, capped at 30 seconds. **Important:** Retries only happen when no text has been yielded to the consumer yet. Retrying past that point would replay text the caller already has. When a retry occurs, a `turn.retrying` event is emitted: ```ts for await (const event of runner.stream(agent, input, { maxRetries: 3 })) { if (event.type === 'turn.retrying') { console.log(`Retry ${event.attempt}/${event.maxRetries} in ${event.delayMs}ms: ${event.error.message}`); } } ``` ## Error handling example ```ts import { Runner, MatildaAgentStreamError, MatildaAgentRunError, SafetyReplaceError, } from '@maincode-ai/matilda-agent-sdk'; const runner = new Runner(); try { const text = await runner.runText(agent, 'Hello!'); console.log(text); } catch (err) { if (err instanceof MatildaAgentStreamError) { console.error(`Stream error: ${err.code} — ${err.message}`); console.log('Partial output:', err.result.finalOutput); } else if (err instanceof MatildaAgentRunError) { if (err.status === 401) { console.error('Session expired — re-authenticate.'); } else if (err.status === 429) { console.error('Rate limited — slow down.'); } else { console.error(`API error ${err.status}: ${err.responseText}`); } } else if (err instanceof SafetyReplaceError) { console.error(`Safety filter: ${err.categories.join(', ')}`); } else { console.error('Unexpected error:', err); } } ``` --- # Multi-agent patterns > Compose, parallelise, and route between agents. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-multi-agent The SDK has no built-in orchestrator — multi-agent emerges from composition. The `Runner` is your execution primitive, and standard JavaScript patterns (chaining, `Promise.all`, tool-based delegation) build the architecture. ## Pattern 1: Sequential pipeline Chain `run()` calls, feeding each agent's output to the next. Each stage has a single responsibility. ```ts import { Agent, Runner } from '@maincode-ai/matilda-agent-sdk'; const runner = new Runner(); const researcher = new Agent({ name: 'researcher', purpose: 'analysis', instructions: 'Produce a structured list of key facts for a blog post. Bullet points only.', }); const writer = new Agent({ name: 'writer', purpose: 'general', instructions: 'Given research notes, write an engaging blog post draft under 400 words.', }); const editor = new Agent({ name: 'editor', purpose: 'general', instructions: 'Polish the draft for clarity, grammar, and flow. Return the full revised post.', }); const topic = 'Why developers are adopting AI coding assistants'; // Stage 1 → 2 → 3 const research = await runner.run(researcher, `Research this topic: ${topic}`); const draft = await runner.run(writer, research.finalOutput); const edited = await runner.run(editor, draft.finalOutput); console.log(edited.finalOutput); // Total token usage across the pipeline const totalTokens = (research.usage?.output_tokens ?? 0) + (draft.usage?.output_tokens ?? 0) + (edited.usage?.output_tokens ?? 0); console.log(`Total output tokens: ${totalTokens}`); ``` ## Pattern 2: Parallel fan-out / fan-in Run multiple specialist agents concurrently with `Promise.all()`, then feed their outputs to a synthesiser. ```ts const securityReviewer = new Agent({ name: 'security-reviewer', purpose: 'analysis', instructions: 'Review code for vulnerabilities. Report only security issues.', }); const performanceReviewer = new Agent({ name: 'performance-reviewer', purpose: 'analysis', instructions: 'Review code for efficiency. Report only performance issues.', }); const synthesiser = new Agent({ name: 'synthesiser', purpose: 'analysis', instructions: 'Given reviews from multiple reviewers, produce a prioritised action list.', }); const code = 'function getUserData(userId, db) { /* ... */ }'; // Fan-out: three reviewers analyse concurrently const [security, performance] = await Promise.all([ runner.run(securityReviewer, `Review this code:\n\`\`\`javascript\n${code}\n\`\`\``), runner.run(performanceReviewer, `Review this code:\n\`\`\`javascript\n${code}\n\`\`\``), ]); // Fan-in: synthesiser merges the reviews const combinedInput = [ '## Security Review', security.finalOutput, '## Performance Review', performance.finalOutput, ].join('\n'); const synthesis = await runner.run(synthesiser, combinedInput); console.log(synthesis.finalOutput); ``` ## Pattern 3: Router / delegator A triage agent receives queries and decides which specialist to invoke. Each specialist is exposed as a client tool — when the agent calls a tool, the SDK handler runs the specialist agent via `run()` and returns its output. ```ts import { Agent, Runner, type ToolHandlers, type AgentRunOptions } from '@maincode-ai/matilda-agent-sdk'; const runner = new Runner(); const billingSpecialist = new Agent({ name: 'billing-specialist', instructions: 'You are a billing support specialist.', }); const technicalSpecialist = new Agent({ name: 'technical-specialist', instructions: 'You are a technical support specialist. Include code examples when relevant.', }); const triageAgent = new Agent({ name: 'triage', purpose: 'general', instructions: 'You are a customer support triage specialist.', }); const triageTools = [ { name: 'ask_billing_specialist', description: 'Route billing questions to the billing specialist.', parameters: { type: 'object' as const, properties: { question: { type: 'string' } }, required: ['question'] }, }, { name: 'ask_technical_specialist', description: 'Route technical questions to the technical specialist.', parameters: { type: 'object' as const, properties: { question: { type: 'string' } }, required: ['question'] }, }, ]; const toolHandlers: ToolHandlers = { ask_billing_specialist: async (args) => { const result = await runner.run(billingSpecialist, `Answer: ${args.question}`); return { content: result.finalOutput }; }, ask_technical_specialist: async (args) => { const result = await runner.run(technicalSpecialist, `Answer: ${args.question}`); return { content: result.finalOutput }; }, }; const runOptions: AgentRunOptions = { toolHandlers, clientTools: triageTools, maxToolRoundtrips: 6, callbacks: { onToolCall: (name) => console.log(`Triage chose: ${name}`), onToolResult: (_name, result) => console.log(`Specialist responded.`), onToken: (delta) => process.stdout.write(delta), }, }; const triagePrompt = [ 'A customer asked:', '"I\'m getting a 401 Unauthorized error when calling the /api/chat endpoint."', 'You MUST forward this to a specialist by calling a tool.', 'After the specialist responds, relay their answer.', ].join('\n'); await runner.run(triageAgent, triagePrompt, runOptions); ``` > **Tip** — **Tip:** Keep the triage agent's instructions short — put the routing rules in the task prompt. If routing rules are in the `instructions` field, the server's Auto-mode system prompt may interpret them as a prompt-injection attempt rather than operating instructions. --- # Recipes > Six runnable examples, from a device-flow CLI agent to a multi-agent review pipeline. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-recipes ## Recipe 1: CLI agent with device-flow auth and streaming A complete interactive CLI agent with device-flow auth, streaming, and multi-turn sessions. ```ts import * as readline from 'node:readline/promises'; import { stdin, stdout } from 'node:process'; import { Runner, MatildaCore, createFileTokenStore, createSession } from '@maincode-ai/matilda-agent-sdk'; import { homedir } from 'node:os'; import { join } from 'node:path'; const { store, lock } = createFileTokenStore(join(homedir(), '.matilda', 'tokens.json')); const runner = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); // Try to restore persisted tokens, fall back to interactive login const restored = await runner.auth.restore({ clientId: 'matilda-code', tokenStore: store, tokenLock: lock }); if (!restored) { console.log('Starting device flow authentication...'); await runner.auth.loginWithDeviceFlow({ clientId: 'matilda-code', tokenStore: store, tokenLock: lock }); console.log('Authenticated!'); } const session = createSession({ name: 'cli-assistant', purpose: 'general', instructions: 'Be helpful, concise, and friendly.', }); const rl = readline.createInterface({ input: stdin, output: stdout }); while (true) { const input = await rl.question('\nYou: '); if (!input.trim() || input.toLowerCase() === 'exit') break; process.stdout.write('Agent: '); for await (const event of session.stream(input)) { if (event.type === 'message.delta') process.stdout.write(event.delta); } process.stdout.write('\n'); } rl.close(); ``` ## Recipe 2: Client tools (weather + calculator) An agent that uses client tools to answer questions requiring external data. ```ts import { Runner, MatildaCore, stream, type ToolHandlers } from '@maincode-ai/matilda-agent-sdk'; const runner = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); if (!(await runner.auth.getTokens())) { await runner.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); } const clientTools = [ { name: 'get_weather', description: 'Get current weather for a city', parameters: { type: 'object' } }, { name: 'calculate', description: 'Evaluate a math expression', parameters: { type: 'object' } }, ]; const toolHandlers: ToolHandlers = { get_weather: async (args) => { const city = (args.city as string) ?? 'unknown'; // In reality, call a weather API return { content: JSON.stringify({ city, temp: 22, condition: 'sunny' }) }; }, calculate: async (args) => { try { const result = Function(`return (${args.expression})`)(); return { content: String(result) }; } catch { return { content: 'Invalid expression', isError: true }; } }, }; for await (const event of stream( { name: 'assistant', instructions: 'Use the available tools to answer.' }, 'What is the weather in Sydney, and what is 15 * 23?', { toolHandlers, clientTools }, )) { if (event.type === 'client.tool.requested') { console.log(`→ ${event.name}(${JSON.stringify(event.args)})`); } if (event.type === 'client.tool.result') { console.log(`← ${event.result}`); } if (event.type === 'message.delta') process.stdout.write(event.delta); } ``` ## Recipe 3: Multi-agent code review pipeline Sequential pipeline: security review → performance review → synthesis. ```ts import { Agent, Runner, MatildaCore } from '@maincode-ai/matilda-agent-sdk'; const runner = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); if (!(await runner.auth.getTokens())) { await runner.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); } const security = new Agent({ name: 'security', purpose: 'analysis', instructions: 'Review for vulnerabilities. Be specific.', }); const performance = new Agent({ name: 'performance', purpose: 'analysis', instructions: 'Review for efficiency. Be specific.', }); const synthesiser = new Agent({ name: 'synthesiser', purpose: 'analysis', instructions: 'Merge reviews into a prioritised action list. Use 🔴 🟡 🟢 priority.', }); const code = 'function getUserData(userId, db) { var query = "SELECT * FROM users WHERE id = " + userId; }'; const [sec, perf] = await Promise.all([ runner.run(security, `Review:\n\`\`\`javascript\n${code}\n\`\`\``), runner.run(performance, `Review:\n\`\`\`javascript\n${code}\n\`\`\``), ]); const combined = `## Security\n${sec.finalOutput}\n\n## Performance\n${perf.finalOutput}`; const result = await runner.run(synthesiser, combined); console.log(result.finalOutput); ``` ## Recipe 4: Dynamic instructions with metadata An agent whose instructions adapt based on runtime metadata. ```ts import { Agent, Runner, MatildaCore, run } from '@maincode-ai/matilda-agent-sdk'; const runner = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); if (!(await runner.auth.getTokens())) { await runner.auth.loginWithDeviceFlow({ clientId: 'matilda-code' }); } const agent = new Agent({ name: 'code-reviewer', purpose: 'code', instructions: ({ input, metadata }) => { const lang = (metadata.language as string) ?? 'auto-detect'; const strictness = (metadata.strictness as string) ?? 'normal'; return [ 'Review the following code.', `Language: ${lang}`, `Strictness: ${strictness}`, 'Focus on: correctness, security, and readability.', 'Cite line numbers when possible.', ].join('\n'); }, }); const result = await run(agent, 'function add(a, b) { return a + b }', { metadata: { language: 'JavaScript', strictness: 'strict' }, }); console.log(result.finalOutput); ``` ## Recipe 5: Stream resume with disconnect recovery Start a stream, simulate a disconnect, and resume from the last cursor. ```ts import { stream, resumeAgentStream, configureClient } from '@maincode-ai/matilda-agent-sdk'; configureClient({ baseUrl: 'https://matilda.maincode.com/api' }); let streamId: string | null = null; let lastEventId: string | undefined; let receivedText = ''; console.log('Starting stream...'); try { for await (const event of stream({ name: 'writer' }, 'Write a very long essay about Australia.')) { if (event.type === 'stream.started') streamId = event.streamId; if (event.type === 'cursor') lastEventId = event.lastEventId; if (event.type === 'message.delta') { receivedText += event.delta; // Simulate disconnect after 500 chars if (receivedText.length > 500) { console.log('\n--- Simulated disconnect ---'); break; } } } } catch (err) { console.log('Disconnected:', err); } console.log(`Received ${receivedText.length} chars before disconnect.`); // Resume from the last cursor if (streamId) { console.log('\n--- Resuming ---'); const result = await resumeAgentStream(streamId, lastEventId, { onEvent: (event) => { if (event.type === 'message.delta') process.stdout.write(event.delta); }, }); console.log(`\nTotal output: ${result.finalOutput.length} chars`); } ``` ## Recipe 6: Custom Runner with file token store A standalone Runner with persistent auth for CLI or long-running service use. ```ts import { Runner, MatildaCore, createFileTokenStore, configureClient, } from '@maincode-ai/matilda-agent-sdk'; import { homedir } from 'node:os'; import { join } from 'node:path'; const tokenPath = join(homedir(), '.matilda', 'tokens.json'); const { store, lock } = createFileTokenStore(tokenPath); const runner = new Runner({ core: new MatildaCore({ baseUrl: 'https://matilda.maincode.com/api' }), }); // Restore persisted tokens or login interactively const restored = await runner.auth.restore({ clientId: 'matilda-code', tokenStore: store, tokenLock: lock }); if (!restored) { await runner.auth.loginWithDeviceFlow({ clientId: 'matilda-code', tokenStore: store, tokenLock: lock, }); } // Runner is ready — tokens auto-refresh on 401 const result = await runner.run( { name: 'helper', instructions: 'Be concise.' }, 'What is the capital of Australia?', ); console.log(result.finalOutput); // Later: logout clears the token store // await runner.auth.logout(); ``` --- # Exports > Classes, functions, constants, and types exported by the agent SDK package. Section: Agent SDK · Source: https://maincode.com/docs/agent-sdk-exports ## Classes | Export | Description | | - | - | | `Agent` | Named persona with static or dynamic instructions. | | `Runner` | Main execution class. Holds `auth`, `files`, `conversations`, `feedback` resources. | | `Session` | Multi-turn conversation wrapper with auto-managed `conversationId`. | | `AgentAuth` | Managed auth: login, restore, token refresh, logout. | | `MatildaAgentRunError` | HTTP-level failure (status, responseText). | | `MatildaAgentStreamError` | SSE stream error (code, errors, partial result). | | `SafetyReplaceError` | Safety filter replacement error (categories). | | `MatildaObjectParseError` | Structured output parse/validation failure (raw, cause). | | `MatildaCore` | Underlying API client (re-exported from `@matilda/api-client`). | | `FilesResource` | File upload/retrieve resource. | | `ConversationsResource` | Conversation list/retrieve/update/feedback resource. | | `FeedbackResource` | Bug-report resource (`reportBug`). | ## Functions | Export | Description | | - | - | | `run(agent, input, options?)` | Run an agent turn via `defaultRunner`. Returns `Promise<AgentRunResult>`. | | `stream(agent, input, options?)` | Stream agent events via `defaultRunner`. Returns `AsyncGenerator<AgentRunEvent>`. | | `streamText(agent, input, options?)` | Stream text deltas via `defaultRunner`. Returns `AsyncGenerator<string>`. | | `runText(agent, input, options?)` | Run and return just the text via `defaultRunner`. Returns `Promise<string>`. | | `streamObject(agent, input, schema, options?)` | Stream with structured output via `defaultRunner`. Returns `AsyncGenerator<AgentObjectEvent<T>>`. | | `runObject(agent, input, schema, options?)` | Run with structured output via `defaultRunner`. Returns `Promise<AgentObjectResult<T>>`. | | `createSession(agent, options?)` | Create a `Session`. | | `buildAgentChatRequest(opts)` | Build a `ChatRequest` without executing. | | `resumeAgentStream(streamId, lastEventId, handlers, options?)` | Resume a detached durable stream. | | `configureClient(options)` | Configure the default `MatildaCore` singleton. | | `getClientConfig()` | Get the default core's config. | | `getDefaultCore()` | Get the default `MatildaCore` singleton. | | `trustApiBaseUrl(rawUrl, policy?)` | Validate and brand a URL as trusted. | | `createFileTokenStore(path)` | `0600` JSON file token store with cross-process lock. | | `memoryStorage()` | In-memory `StorageAdapter`. | ## Constants | Export | Value | Description | | - | - | - | | `DEFAULT_AGENT_PURPOSE` | `'code'` | Default agent purpose. | | `DEFAULT_AGENT_STALL_TIMEOUT_MS` | `45_000` | Default SSE stall watchdog timeout. | | `DEFAULT_MAX_TOOL_ROUNDTRIPS` | `25` | Default maximum tool roundtrips. | | `DEFAULT_MAX_RETRIES` | `0` | Default maximum retries. | | `DEFAULT_SUCCESS_REDIRECT` | `'https://matilda.maincode.com/cli/signed-in'` | Default browser login success redirect. | | `MATILDA_API_VERSION_HEADER` | — | API version header name. | | `MATILDA_CURRENT_API_VERSION` | — | Current API version string. | ## Instances | Export | Description | | - | - | | `defaultRunner` | A `Runner` using the default `MatildaCore` singleton. | ## Types | Export | Description | | - | - | | `AgentPurpose` | `'code' \| 'analysis' \| 'general'` | | `AgentInstructions` | `string \| ((ctx: AgentRunContext) => string \| Promise<string>)` | | `AgentRunContext` | Context passed to dynamic instructions. | | `AgentOptions` | Constructor options for `Agent`. | | `AgentRunOptions` | Options for `runner.run()` / `runner.stream()`. | | `AgentRunEvent` | 22-variant discriminated union of stream events. | | `AgentRunResult` | Result of an agent run. | | `AgentObjectEvent<T>` | `AgentRunEvent` plus a final `{ type: 'object'; object: T }`. | | `AgentObjectResult<T>` | `AgentRunResult` plus the validated `object`. | | `AgentObjectRunOptions<T>` | `AgentRunOptions` with an embedded zod `schema`. | | `AgentCallbacks` | Callback hooks for `run()`. | | `BuildAgentChatRequestOptions` | Options for `buildAgentChatRequest()`. | | `ToolHandler` | `(args, ctx) => Promise<ToolResult>` | | `ToolHandlers` | `Record<string, ToolHandler>` | | `ToolExecutionContext` | Context passed to tool handlers. | | `ToolResult` | `{ content: string; isError?: boolean }` | | `SessionOptions` | Options for `Session` / `createSession()`. | | `FileUploadOptions` | Options for `files.upload()`. | | `CoreSource` | `MatildaCore \| (() => MatildaCore)` | | `BrowserLoginOptions` | Options for `auth.loginWithBrowser()`. | | `DeviceLoginOptions` | Options for `auth.loginWithDeviceFlow()`. | | `TokenSet` | `{ accessToken, refreshToken?, idToken?, expiresAt }` | | `TokenManager` | Token manager interface. | | `StorageAdapter` | Token persistence interface. | | `LoginFlowEvent` | Login flow state event type. | | `FileTokenStore` | File token store return type. | | `ClientConfig` | Core config type (re-exported). | | `GetToken` | Token provider function type (re-exported). | | `TrustedApiBaseUrl` | Branded string type (re-exported). | | `TrustedApiBaseUrlPolicy` | URL validation policy (re-exported). | | `ApiMessage` | `{ role, content }` (re-exported). | | `ChatRequest` | Chat request type (re-exported). | | `ChatResponseMode` | `'auto' \| 'instant' \| 'deep'` (re-exported). | | `ChatSseEvent` | SSE event type (re-exported). | | `ChatSseEventName` | SSE event name type (re-exported). | | `SafetyReplaceEvent` | Safety replace event type (re-exported). | | `UsageEvent` | Token usage type (re-exported). | | `ConversationListResponse` | Conversation list response (re-exported). | | `ConversationRecord` | Full conversation record (re-exported). | | `ConversationSummary` | Conversation summary (re-exported). | | `FileAttachment` | File metadata (re-exported). | | `FileCompleteResponse` | File upload result (re-exported). | | `UploadFilesOptions` | Multi-file upload options (re-exported). | ## Error classes re-exported | Export | Description | | - | - | | `AuthError` | OAuth error (code, retryable). Re-exported from client SDK. |