@mindstudio-ai/remy 0.1.256 → 0.1.258

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -104,6 +104,22 @@ This is deterministic — same scenario always produces the same state.
104
104
 
105
105
  Scenarios are useful for seeding initial app state after build for testing, as well as to give the user a first impression of an app that is already filled with data and looks and feels usable. The user can choose to run further scenarios after initial build by clicking the Scenarios tab and selecting a scenario to run.
106
106
 
107
+ ## What scenarios don't touch
108
+
109
+ **Scenarios seed database tables and nothing else.** They do not touch file stores or data sources —
110
+ deliberately: both are durable and shared across dev and prod, with no per-release copy to reset, so
111
+ there is nothing to truncate.
112
+
113
+ Don't try to seed documents into a data source from a scenario, and don't write `clear()`-style reset
114
+ helpers for one. Load a test corpus once from the CLI instead:
115
+
116
+ ```bash
117
+ mindstudio-prod datasources add --source policies --wait fixtures/*.pdf
118
+ ```
119
+
120
+ Re-running it is free (documents are content-addressed), so it's safe to keep in a setup script
121
+ beside your scenarios.
122
+
107
123
  ## Scenario Data
108
124
 
109
125
  Align scenario data to the vibe of the app - construct data that feels like it fits.
@@ -83,7 +83,6 @@ result.$billingCost; // cost in credits (if applicable)
83
83
 
84
84
  | Action | What it does |
85
85
  |--------|-------------|
86
- | `uploadFile` | Upload a file to CDN |
87
86
  | `downloadVideo` | Download a video URL |
88
87
  | `getMediaMetadata` | Get dimensions, duration, etc. |
89
88
  | `convertPdfToImages` | PDF pages to PNG images |
@@ -154,4 +153,4 @@ Consider the ways in which AI can be incorporated into backend methods to solve
154
153
 
155
154
  ### Task Agents
156
155
 
157
- For multi-step tasks where the model needs to autonomously compose actions (research + scrape + generate, enrichment pipelines, content creation), use `runTask()` instead of chaining actions manually. It runs an agent loop and returns structured JSON. Its tools can include SDK actions as well as your app's own methods, so the agent can read your data to decide what to do next and write results back itself. See the task agents reference for full details.
156
+ For multi-step tasks where the model needs to autonomously compose actions (research + scrape + generate, enrichment pipelines, content creation), use `runTask()` instead of chaining actions manually. It runs an agent loop and returns structured JSON. Its tools can include SDK actions as well as your app's own methods, so the agent can read your data to decide what to do next and write results back itself. Load the `taskAgents` skill before writing one — it is the full reference.
@@ -1,3 +1,9 @@
1
+ ---
2
+ name: Agent Interfaces
3
+ what: Conversational AI as a first-class interface to the app — an LLM with authenticated, per-user access to the app's methods as tools, paired with a streaming chat UI. The platform handles auth, tool dispatch, threads, and streaming, so the work is authorship: who the agent is, which methods it can reach, and how each one is described to it. Any app whose methods do something interesting can be projected into a conversation this way, often as its most compelling surface. This reference covers the whole feature — writing the agent spec, compiling it, and building the chat frontend.
4
+ when: Before authoring `src/interfaces/agent.md`, compiling `dist/interfaces/agent/`, or building an agent's chat UI.
5
+ ---
6
+
1
7
  # Building Agent Interfaces
2
8
 
3
9
  Guidance for designing conversational AI agents and their frontends. An agent interface pairs an LLM (with per-user-scoped/authenticated access to app methods as tools, handled by platform automatically) with a chat UI. The developer authors the agent's character in MSFM (`src/interfaces/agent.md`); you compile it into a system prompt and tool descriptions (`dist/interfaces/agent/`).
@@ -10,21 +16,32 @@ A good system prompt establishes who the agent is — personality, tone, judgmen
10
16
 
11
17
  Short and opinionated beats long and comprehensive. "Sounds like a sharp, organized friend — brief by default" gives the model more to work with than a page of behavioral rules. Define constraints through character, not checklists. Let the model's judgment work.
12
18
 
13
- #### System Prompt Specifics
14
- Always include a note like "## Tool Usage
15
- - When multiple tool calls are independent, make them all in a single turn. Searching for three different products, or fetching two reference sites: batch them instead of doing one per turn." to help the model know it can run tools in parallel
16
- - The user's name and current role(s) at the time of message, if any, will be automatically appended to the end of every system prompt at runtime like:
19
+ Three things every compiled system prompt should carry, on top of the character:
20
+
21
+ **Parallel tool use.** The model won't batch independent calls unless told it can. Include a section like:
17
22
 
23
+ ```markdown
24
+ ## Tool Usage
25
+
26
+ When multiple tool calls are independent, make them all in a single turn.
27
+ Searching for three different products, or fetching two reference sites:
28
+ batch them instead of doing one per turn.
18
29
  ```
30
+
31
+ **Markdown and house style.** Unless the user says otherwise, tell the agent it can use markdown (the chat UI renders it) and to avoid em dashes and emojis.
32
+
33
+ **The current user is appended for you.** At runtime the platform appends the user's name and roles to the end of every system prompt, so don't write your own placeholder for it:
34
+
35
+ ```markdown
19
36
  ## Current User
37
+
20
38
  Name: Jane Smith
21
39
  Roles: editor
22
40
  ```
23
- - Unless the user specifies otherwise, always include a note that the agent can use markdown in responses (since the chat UI renders it) and should avoid using em dashes and emojis in its responses.
24
41
 
25
42
  ### Tool descriptions are the most important artifact
26
43
 
27
- The system prompt says *who* the agent is. The tool descriptions say *what it can do*. A great tool description means the agent uses the tool correctly without explicit instruction. Do not be overly precise or micromanage. Your goal with tool descriptions is to provide context and faming- trust that the model is intelligent enough to fill in the gaps.. Each `tools/*.md` file should cover:
44
+ The system prompt says *who* the agent is. The tool descriptions say *what it can do*. A great tool description means the agent uses the tool correctly without explicit instruction. Do not be overly precise or micromanage. Your goal with tool descriptions is to provide context and framing — trust that the model is intelligent enough to fill in the gaps. Each `tools/*.md` file should cover:
28
45
 
29
46
  - **When to use** this tool (and when NOT to — e.g. "NOT for marking complete, use toggle-todo")
30
47
  - **Parameter guidance** beyond the schema — what makes a good value, when to include optional fields, what to skip
@@ -64,6 +81,8 @@ When building the `dist/interfaces/agent/`, consider the agent spec, as well as
64
81
 
65
82
  **`agent.json`** — ties it together. Model config from frontmatter, paths to system prompt and tool files, optional `webInterfacePath`.
66
83
 
84
+ The exact shape of all three, and of the spec frontmatter they compile from, is in "The wiring" at the end of this document.
85
+
67
86
  ## Chat UI Design
68
87
 
69
88
  When the agent has a web frontend (via `webInterfacePath`), the chat UI is a page within the web interface.
@@ -116,10 +135,12 @@ response.abort();
116
135
 
117
136
  **Attachments:**
118
137
 
119
- Send images or documents alongside a message. Upload via `platform.uploadFile()` first, then pass CDN URLs as the 4th argument:
138
+ Send images or documents alongside a message. Upload to the app's file store first (see Files & Storage), then pass the returned URLs as the 4th argument:
120
139
 
121
140
  ```ts
122
- const url = await platform.uploadFile(file);
141
+ // backend method mints a token; the browser uploads straight to storage
142
+ const token = await api.getUploadSlot({ filename: file.name, contentType: file.type });
143
+ const { url } = await platform.upload(token, file);
123
144
 
124
145
  chat.sendMessage(threadId, "What's in this document?", {
125
146
  onText: (delta) => setText((prev) => prev + delta),
@@ -128,7 +149,7 @@ chat.sendMessage(threadId, "What's in this document?", {
128
149
  });
129
150
  ```
130
151
 
131
- Images (`i.mscdn.ai`) are sent as vision input. Documents (`f.mscdn.ai`) have text extracted server-side and included in context. Attachments are preserved in thread history.
152
+ Images are sent as vision input; documents have their text extracted server-side and included in context. Attachments are preserved in thread history.
132
153
 
133
154
  **Key points:**
134
155
  - `onText` and `onThinking` receive deltas (append to state, don't replace)
@@ -174,7 +195,7 @@ The first screen should invite conversation. A greeting from the agent, a few su
174
195
 
175
196
  ### Mobile
176
197
 
177
- Chat is inherently mobile-friendly — lean into it. Pay attention to viewport sizing on mobile as the virtual keyboard changes the available height.
198
+ Chat is inherently mobile-friendly — lean into it. Pay attention to viewport sizing on mobile as the virtual keyboard changes the available height.
178
199
 
179
200
  ### Respect the brand
180
201
 
@@ -184,3 +205,90 @@ The chat UI uses the app's design system — colors, typography, voice from `@br
184
205
 
185
206
  - Avoid designs that look like dated messaging apps from 2015
186
207
  - Avoid robotic empty states ("Hello! I'm your AI assistant. How can I help you today?")
208
+
209
+ ---
210
+
211
+ # The wiring
212
+
213
+ ## Spec: `src/interfaces/agent.md`
214
+
215
+ The human-readable spec. Frontmatter contains structured fields; the prose body is the behavioral spec —
216
+ voice, personality, capabilities, rules — written in MSFM.
217
+
218
+ ```yaml
219
+ ---
220
+ name: Todo Assistant
221
+ model: {"model": "claude-4-5-haiku", "temperature": 0.5, "maxResponseTokens": 16000}
222
+ description: Conversational agent that helps users manage their to-do list.
223
+ ---
224
+ ```
225
+
226
+ Frontmatter fields:
227
+
228
+ - `name` — agent display name
229
+ - `model` — JSON string with `model` (MindStudio model ID), `temperature`, `maxResponseTokens`, and
230
+ optional `config` (model-specific settings like `reasoning`, `tools`, etc.). Ask `askMindStudioSdk`
231
+ for available model IDs and their config options — MindStudio's ids don't match vendor ids, so treat
232
+ any id in this document's examples as illustrative rather than current. The user's UI has a visual picker for changing it later, so only validate
233
+ the model when you're setting it; if the value changes afterwards, assume it's correct.
234
+ - `description` — one-liner for agent card/listing
235
+
236
+ The prose body contains sections like Voice & Personality, Capabilities, Behavior — whatever structure
237
+ serves the agent's character. This is compiled into the system prompt and tool descriptions.
238
+
239
+ ## Compiled Output: `dist/interfaces/agent/`
240
+
241
+ ```
242
+ dist/interfaces/agent/
243
+ ├── agent.json ← config the platform reads
244
+ ├── system.md ← compiled system prompt
245
+ └── tools/
246
+ ├── createTodo.md ← rich tool description per method
247
+ ├── listTodos.md
248
+ └── ...
249
+ ```
250
+
251
+ ## Config (`agent.json`)
252
+
253
+ ```json
254
+ {
255
+ "agent": {
256
+ "model": "claude-4-5-haiku",
257
+ "temperature": 0.5,
258
+ "maxTokens": 16000,
259
+ "systemPrompt": "system.md",
260
+ "tools": [
261
+ { "method": "create-todo", "description": "tools/createTodo.md" },
262
+ { "method": "list-todos", "description": "tools/listTodos.md" }
263
+ ],
264
+ "webInterfacePath": "/chat"
265
+ }
266
+ }
267
+ ```
268
+
269
+ **The token-limit field is renamed during compilation.** The spec frontmatter calls it
270
+ `maxResponseTokens`; the compiled `agent.json` calls it `maxTokens`. Same value, two names — carry it
271
+ across rather than copying the key.
272
+
273
+ | Field | Description |
274
+ |-------|-------------|
275
+ | `model` | MindStudio model ID. Comes from the spec frontmatter; look it up with `askMindStudioSdk` rather than guessing |
276
+ | `temperature` | Model temperature |
277
+ | `maxTokens` | Max response tokens (the spec's `maxResponseTokens`) |
278
+ | `systemPrompt` | Relative path to the compiled system prompt markdown file |
279
+ | `tools` | Array of tool entries — `method` references a method `id` from the manifest, `description` is a relative path to a markdown file with rich tool docs (when to use, examples, edge cases, parameter guidance) |
280
+ | `webInterfacePath` | Optional. If the app has a web interface with a chat page, this path tells the IDE where to show the preview. Otherwise the agent is accessed via API. |
281
+
282
+ Declare it in `mindstudio.json`:
283
+
284
+ ```json
285
+ { "type": "agent", "path": "dist/interfaces/agent/agent.json" }
286
+ ```
287
+
288
+ ## Auth
289
+
290
+ Agent chat runs as the **authenticated user**, not as a system role — tool calls carry that user's
291
+ roles, so a method gated with `auth.requireRole` behaves exactly as it would if the user had called it
292
+ from the web frontend. That's what makes exposing real methods safe; it's also why role restrictions
293
+ belong in the tool descriptions, so the agent can decline gracefully instead of surfacing a rejection.
294
+
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: Data Sources
3
+ what: A managed retrieval system for document corpora, not a bolt-on keyword search. Documents are chunked and embedded, candidate hits are re-scored by a reranking model, semantic search runs alongside exact keyword matching so part numbers and error codes still land, images inside documents are described by a vision model and made searchable, and every hit returns a citation that links to its source page. Chunking and embedding settings are versioned — a rebuilt corpus can be compared against the live one and promoted without downtime. All of that applies to unstructured documents queried by meaning, and to nothing else.
4
+ when: Only for unstructured documents queried by meaning — "find the clause about early termination". Structured data belongs in `db`: if the question can be expressed as a filter, it is not a search problem, and a `WHERE` clause is faster, cheaper and exact. Load before defining or querying a data source.
5
+ ---
6
+
7
+ # Data Sources (Search Over Documents)
8
+
9
+ Per-app searchable document corpora: upload documents, ask in plain language, get back the passages
10
+ that answer it with a citation to the source.
11
+
12
+ **Most apps should not use one — check this before reaching for it.** Structured data (rows with
13
+ fields you filter on) belongs in `db`; a `WHERE` clause is faster, cheaper and exact. A data source
14
+ earns its cost only for **unstructured documents queried by meaning**. "Find the clause about early
15
+ termination" is a data source. "Find contracts signed after March" is a `db` query. If the question
16
+ can be expressed as a filter, it is not a search problem.
17
+
18
+ Don't use one when: the data is structured (`db`), you only need to store files (`files` — nobody is
19
+ searching the contents), the requirement is exact lookup by identifier, or the corpus is a handful of
20
+ short docs that fit in a prompt.
21
+
22
+ ## Behaviour (read before the API)
23
+
24
+ - **One corpus shared across dev and prod** — like a file store, not a table. No dev copy, no
25
+ per-release isolation. A document added while building is already live.
26
+ - **Scenarios never reset a data source.** Don't write `clear()`-style reset helpers.
27
+ - **Re-adding the same bytes is free** — content-addressed, so ingest scripts are safe to re-run.
28
+ - **Ingest is async.** `add()` returns once queued; poll `documents()`, or use `--wait` from the CLI.
29
+ - **Reprocessing costs real money**, so changing how a corpus is built is always explicit.
30
+ - **Limits apply**: 25 data sources per app, 5,000 documents per source, 10,000 chunks per
31
+ document, 300 searches/minute. Well clear of normal use — but **source names must be fixed, not
32
+ computed per user or per request**, since referencing one creates it. Partition inside a source
33
+ with a metadata filter instead.
34
+
35
+ ## Defining and searching
36
+
37
+ ```typescript
38
+ import { dataSources } from '@mindstudio-ai/agent';
39
+ export const Policies = dataSources.defineDataSource('policies'); // lowercase [a-z0-9_-], ≤64
40
+
41
+ const { results } = await Policies.search('what are the payment terms?', { topK: 5 });
42
+ const context = results.map((r) => r.text).join('\n\n');
43
+ ```
44
+
45
+ Hits are `{ score, text, citation }` with
46
+ `citation: { documentId, filename, pageNumber, chunkIndex, headingPath, boundingBox?, url }`, plus
47
+ `retrievalRank`/`retrievalScore` — the position before reranking, so you can show what reranking did.
48
+
49
+ **Always render the citation.** `citation.url` is a stable on-domain link — put it in an `<a href>`
50
+ beside the answer. Retrieval is approximate; a user who can click through can judge for themselves.
51
+ An answer with no citation is an assertion.
52
+
53
+ Created on first use, so searching a source the build hasn't populated returns no results rather than
54
+ throwing. `search` options: `topK` (default 5, max 50), `scoreThreshold`, `rerank`, `hybrid`.
55
+
56
+ Search is deterministic for a fixed corpus and configuration, so eval sets and regression checks are
57
+ meaningful — key them on `(documentId, chunkIndex)` rather than on chunk text.
58
+
59
+ **Debugging retrieval.** Two opt-in options, neither of which changes the results or their order:
60
+ `explain: true` adds `explain.{dense, lexical, matchedVia}` (which half of hybrid found each hit;
61
+ costs two extra round trips), and `expand: 1` adds `neighbors.{before, after}` for surrounding
62
+ context. When a document never comes back at all, `Policies.stats()` reports the config actually in
63
+ effect and `Policies.chunks(documentId)` shows exactly how it was split.
64
+
65
+ **Configuration is not declared in code** — chunking and embedding settings live on the corpus and are
66
+ set with the CLI, so code and reality can't drift.
67
+
68
+ ## Loading documents — normally at build time, from the CLI
69
+
70
+ ```bash
71
+ mindstudio-prod datasources add --source policies --wait docs/*.pdf
72
+ mindstudio-prod datasources search --source policies "what are the payment terms?" # sanity-check
73
+ mindstudio-prod datasources delete --source policies # whole source; --source is required, never defaulted
74
+ ```
75
+
76
+ `--wait` blocks until processing finishes and exits non-zero on failure. Also `datasources list`,
77
+ `status` (per-document state + ingest errors), `rm --document <id>`. `--help` for flags.
78
+
79
+ **Seeding a test corpus:** scenarios don't touch data sources, so load fixtures with the same command
80
+ in a setup script — `datasources add --source <slug> --wait fixtures/*.pdf`. Re-running is free, so
81
+ it needs no guard.
82
+
83
+ Use the SDK's `add()` only when *users* upload documents that must become searchable:
84
+
85
+ ```typescript
86
+ await Policies.add(buffer, { filename: 'policy.pdf', contentType: 'application/pdf' });
87
+ const docs = await Policies.documents(); // 'processing' | 'done' | 'error'
88
+ await Policies.remove(documentId);
89
+ ```
90
+
91
+ Formats: pdf, docx, pptx, xlsx, odt, rtf, epub, images, txt, md, json, csv, tsv, log, html.
92
+
93
+ ## Answering from results
94
+
95
+ Retrieve → join passages as context → have a model answer *from that context* → render citations.
96
+ Never paste raw chunks at the user; they're fragments. For agentic flows, give the model `search` as a
97
+ tool so it can query repeatedly and refine, rather than retrieving once up front.
98
+
99
+ ## Tuning — two kinds of setting
100
+
101
+ | Kind | Settings | Cost |
102
+ |---|---|---|
103
+ | **Free** (ranking) | `--rerank`, `--rerank-model`, `--hybrid`, `--top-k` | none, next search |
104
+ | **Rebuild** (how docs become vectors) | `--max-chars`, `--min-chars`, `--drop-blocks`, `--contextual`, `--describe-images`, `--embedding-model`, `--extraction-model` | every document reprocessed |
105
+
106
+ Images inside documents are described by a vision model and the description substituted into the
107
+ searchable text (`--describe-images`, on by default) — without it a chart contributes nothing to
108
+ search at all. Documents with no images cost nothing.
109
+
110
+ `rerank` and `hybrid` default on and are usually right — reranking is the biggest quality lever, and
111
+ hybrid is what finds part numbers, error codes and proper nouns a semantic model never learned. Both
112
+ are also per-query (`search(q, { rerank: false })`) for a latency-sensitive path.
113
+
114
+ ```bash
115
+ mindstudio-prod datasources config --source policies # show
116
+ mindstudio-prod datasources config --source policies --top-k 8 # free, immediate
117
+ ```
118
+
119
+ **A rebuild-class change on a populated corpus is rejected** — you're told what it would invalidate
120
+ and what it costs. To make it, build a new version alongside the live one:
121
+
122
+ ```bash
123
+ mindstudio-prod datasources revectorize --source policies --max-chars 900 --wait
124
+ mindstudio-prod datasources search --source policies --candidate "payment terms" # compare
125
+ mindstudio-prod datasources promote --source policies # go live
126
+ ```
127
+
128
+ Search serves the current version throughout, so nothing degrades while the new one builds.
129
+ `datasources drop` discards an unwanted candidate.
130
+
131
+ For anything deeper on the SDK, ask `askMindStudioSdk` rather than guessing at an API.
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: Inbound Email
3
+ what: The app has its own email address, and mail sent to it runs a method. Every address on the app's subdomain routes to one handler, so `support@`, `receipts@` and `anything@` all arrive without registering anything — branch on the recipient in code. Attachments arrive as CDN URLs the platform has already uploaded, threading headers come through intact so replies land in the same conversation, and a verified custom domain both receives and sends under the app's own brand. "Forward a receipt and the app files it" is a real feature that costs one method.
4
+ when: Before writing an email-handler method, adding an `email` interface, or promising anything about what happens when a user emails the app.
5
+ ---
6
+
7
+ # Inbound Email Interfaces
8
+
9
+ Inbound email triggers. Each app has **one** email-handler method; the platform routes all inbound
10
+ mail destined for the app — across any of its address tiers — to that method.
11
+
12
+ The addresses themselves are configured at the project level by the user through the Remy platform.
13
+ Your job is the `interface.json` and the method that handles the mail, not domain registration or MX
14
+ records.
15
+
16
+ ## Address tiers
17
+
18
+ Three tiers, all delivered to the same handler method. The new tiers are catchall (no localpart
19
+ registration); the legacy tier is specific-localpart and frozen for new apps.
20
+
21
+ | Tier | Address | How it's set up |
22
+ |---|---|---|
23
+ | Platform subdomain (default) | `*@<custom_subdomain>.madewithremy.com` | Automatic the moment the app has a `custom_subdomain` set. Every address on that subdomain delivers to the handler. |
24
+ | Custom domain | `*@<their-domain>` | The user adds a domain in the dashboard's email-domains settings and points one MX record at `mx.msagent.ai`. Not something the agent provisions. |
25
+ | Legacy `mindstudio-hooks.com` | `<name>@mindstudio-hooks.com` | Existing apps only — frozen for new apps. Don't recommend it; treat as read-only history. |
26
+
27
+ Because the new tiers are catchall, `to` carries an arbitrary localpart. Methods that need to branch on
28
+ it should read `input.to` (e.g. `if (input.to.startsWith('support@')) ...`). This is what makes
29
+ per-purpose addresses free: you don't register `support@` anywhere, you just check for it.
30
+
31
+ A verified custom domain (and the app's `madewithremy.com` subdomain) also **sends** outbound mail, not
32
+ just receives — `sendEmail` picks the app's own-brand sender automatically, configured in the
33
+ dashboard's **Email** settings.
34
+
35
+ ## Config (`interface.json`)
36
+
37
+ The top-level key must match the interface type (`email`):
38
+
39
+ ```json
40
+ {
41
+ "email": {
42
+ "method": "handle-inbound-email",
43
+ "approvedSenders": ["billing@vendor.com", "*@trusted-partner.com"]
44
+ }
45
+ }
46
+ ```
47
+
48
+ `approvedSenders` is optional. When set, only senders matching an exact address or `*@domain.com`
49
+ wildcard reach the method; everything else is rejected by the platform with `400 invalid_sender` before
50
+ the method runs (silently — the sender isn't bounced). Matching is case-insensitive. The same list
51
+ applies uniformly across all three address tiers.
52
+
53
+ Declare it in `mindstudio.json`:
54
+
55
+ ```json
56
+ { "type": "email", "path": "dist/interfaces/email/interface.json" }
57
+ ```
58
+
59
+ ## Input shape
60
+
61
+ ```ts
62
+ {
63
+ to: string; // full recipient address; localpart is arbitrary on catchall tiers
64
+ from: string; // bare sender address, extracted from "Name <a@b>" form
65
+ fromName: string | null; // sender display name, or null
66
+ subject: string; // 'No Subject' if missing
67
+ message: string; // plain-text body, falls back to HTML if text is missing; 'No Body' if neither was sent
68
+ html: string; // HTML body, or '' when text-only
69
+ attachments: string[]; // CDN URLs — already uploaded by the platform
70
+ messageId: string | null; // this email's Message-ID, angle-bracketed (<id@host>)
71
+ inReplyTo: string | null; // Message-ID this email is replying to, if any
72
+ references: string[]; // prior Message-IDs in the thread (angle-bracketed); [] if none
73
+ replyTo: string | null; // Reply-To address — reply here, not `from`, when set
74
+ cc: string[]; // Cc recipient addresses
75
+ date: string | null; // original send time, ISO-8601
76
+ }
77
+ ```
78
+
79
+ ## Replying in thread
80
+
81
+ Replies go out through the SDK's `sendEmail` action (its full parameter list is in the SDK actions
82
+ reference in your system prompt). Set `inReplyTo` to the incoming `messageId` and `references` to
83
+ `[...references, messageId]`. Send to `replyTo` when it's set, otherwise `from`.
84
+
85
+ ```typescript
86
+ await mindstudio.sendEmail({
87
+ to: input.replyTo ?? input.from,
88
+ subject: `Re: ${input.subject}`,
89
+ body: reply,
90
+ inReplyTo: input.messageId ?? undefined,
91
+ references: input.messageId
92
+ ? [...input.references, input.messageId]
93
+ : input.references,
94
+ cc: input.cc, // reply-all
95
+ });
96
+ ```
97
+
98
+ `sendEmail` returns `{ recipients, cc, bcc, from }` — who it sent to and the sender used. It does
99
+ **not** return the sent message's own `Message-ID`, so thread off *inbound* mail, never off messages
100
+ you sent.
101
+
102
+ ## Attachments and size limits
103
+
104
+ `attachments[]` is an array of CDN URLs — the platform has already received and uploaded the files.
105
+ Fetch them server-side via the URL when you need the bytes; pass them through as URLs to UI or
106
+ downstream services.
107
+
108
+ Max inbound message size is 25 MB total (including all attachments). Oversized messages are rejected by
109
+ the platform before the method runs.
110
+
111
+ ## Auth
112
+
113
+ Methods invoked through this interface run with `auth.roles: ['system']` — the platform is calling, not
114
+ a user session, so there's no user to impersonate. Use `auth.requireRole('system')` to gate methods that
115
+ should only be reachable via email. The auth reference in your system prompt covers the system role in
116
+ full.