@agentproto/adapter-mastra-agent 0.4.2 → 0.5.0

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.
package/dist/index.d.ts CHANGED
@@ -1,77 +1,20 @@
1
1
  import { AgentCliHandle, AgentCliRuntime } from '@agentproto/driver-agent-cli';
2
2
  export { AgentCliHandle, AgentCliRuntime } from '@agentproto/driver-agent-cli';
3
3
  import { MastraToolLike } from '@agentproto/mastra';
4
- import { Agent, AgentSideConnection, InitializeRequest, InitializeResponse, AuthenticateRequest, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, CancelNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SessionUpdate, ToolKind } from '@agentclientprotocol/sdk';
4
+ import { BuiltinToolId, AgentController, PermissionRules, AgentControllerEvent } from '@mastra/core/agent-controller';
5
+ import { Agent, AgentSideConnection, InitializeRequest, InitializeResponse, AuthenticateRequest, NewSessionRequest, NewSessionResponse, LoadSessionRequest, LoadSessionResponse, PromptRequest, PromptResponse, CancelNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SessionUpdate, ToolKind } from '@agentclientprotocol/sdk';
6
+ import { LanguageModelV3 } from '@ai-sdk/provider';
5
7
  import { ModelRef, MemoryConfig } from '@agentproto/agent';
6
8
  import { createTool } from '@mastra/core/tools';
9
+ import { LibSQLStore } from '@mastra/libsql';
7
10
  import { Memory } from '@mastra/memory';
8
11
 
9
12
  /**
10
- * WP2 the agent side of AIP-44 ACP, backed by a live Mastra agent.
11
- *
12
- * Implements the `@agentclientprotocol/sdk` `Agent` interface: handles the
13
- * session lifecycle and, on `session/prompt`, drives a Mastra agent's
14
- * `stream()` relaying each text delta as an `agent_message_chunk`
15
- * `session/update`, exactly as an IDE/host expects from codex or claude-code.
16
- *
17
- * No Mastra-specific protocol knowledge leaks past this file; everything above
18
- * is the standard ACP wire, so the daemon spawns this like any other arm.
19
- */
20
-
21
- /** A built Mastra agent — only the surface we need (its `stream`). Typed
22
- * structurally so we don't couple to a specific @mastra/core version.
23
- *
24
- * We read `fullStream` (the typed chunk union: text deltas AND tool-call /
25
- * tool-result events) so tool activity surfaces as ACP `tool_call` updates,
26
- * not only the final prose. `textStream` is kept as an optional fallback for
27
- * a stripped agent that exposes only text. */
28
- interface MastraLike {
29
- stream(input: string, options?: {
30
- abortSignal?: AbortSignal;
31
- /** Memory threading — `thread` scopes recall to one ACP session. */
32
- memory?: {
33
- thread?: string;
34
- resource?: string;
35
- };
36
- /** Max agentic loop steps (tool-call → execute → continue). Default 1 = no loop. */
37
- maxSteps?: number;
38
- }): Promise<{
39
- fullStream?: ReadableStream<unknown>;
40
- textStream?: ReadableStream<string>;
41
- text?: Promise<string>;
42
- }>;
43
- }
44
- /** Lazily builds the Mastra agent (so model/key errors surface on the first
45
- * prompt with a clear message, not at process spawn). */
46
- type AgentFactory = () => Promise<MastraLike>;
47
- /** Pull the user's text out of an ACP prompt (its `text` content blocks). */
48
- declare function promptText(params: PromptRequest): string;
49
- declare class MastraAcpAgent implements Agent {
50
- #private;
51
- constructor(conn: AgentSideConnection, buildAgent: AgentFactory, resource?: string);
52
- initialize(_params: InitializeRequest): Promise<InitializeResponse>;
53
- authenticate(_params: AuthenticateRequest): Promise<Record<string, never>>;
54
- newSession(_params: NewSessionRequest): Promise<NewSessionResponse>;
55
- prompt(params: PromptRequest): Promise<PromptResponse>;
56
- cancel(params: CancelNotification): Promise<void>;
57
- /**
58
- * The host applies the `model` (and other operator options) as a `--model`
59
- * spawn arg via the manifest `bin_args_template`, then ALSO calls this ACP
60
- * config hook (the daemon's default "config" apply path). The model is
61
- * already in effect, so this is a no-op that just reports our (empty) set of
62
- * runtime-configurable options. Without it the spawn fails with
63
- * "Method not found: session/set_config_option".
64
- */
65
- setSessionConfigOption(_params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>;
66
- /** No agent-specific modes; accept and ignore so a host that sets one
67
- * doesn't error. */
68
- setSessionMode(_params: SetSessionModeRequest): Promise<Record<string, never>>;
69
- }
70
-
71
- /**
72
- * Builds a runnable Mastra agent from an AIP-42 AGENT.md — either a caller's
73
- * file or a zero-config built-in default — wiring the model, the markdown body
74
- * as instructions, the SQLite memory, and the workspace toolset.
13
+ * Builds a runnable Mastra `AgentController` from an AIP-42 AGENT.md either
14
+ * a caller's file or a zero-config built-in default — wiring the model, the
15
+ * markdown body as instructions, the SQLite memory/storage, and the workspace
16
+ * toolset, then wrapping the built `Agent` as the controller's shared backing
17
+ * agent. The ACP host drives controller sessions, not the raw agent stream.
75
18
  */
76
19
 
77
20
  /** Cheap OpenRouter coder by default — this is the budget first-party arm,
@@ -94,16 +37,203 @@ interface AgentSourceOptions {
94
37
  * wants to grant tools the built-in toolset doesn't cover. No CLI flag.
95
38
  */
96
39
  extraTools?: Record<string, MastraToolLike>;
40
+ /**
41
+ * `false` runs WP-1 parity: a single mode, no tool approvals, yolo
42
+ * execution — exactly the old raw-stream behavior. Anything else (or
43
+ * omitted) runs the plan/build/review modes with real tool approvals
44
+ * (WP-3 default). The `AGENTPROTO_MASTRA_NO_MODES` env var is the
45
+ * spawn-time equivalent of passing `false` here, for hosts that can't pass
46
+ * this option directly (e.g. the CLI, which owns no `--no-modes` flag yet
47
+ * — see this WP's report).
48
+ */
49
+ modes?: false | "default";
97
50
  }
98
51
  /** The built-in AGENT.md used when no file is supplied. */
99
52
  declare function defaultAgentManifest(model: string): string;
53
+ /** Every AgentController built-in tool id — WP-1 parity mode disables them ALL
54
+ * so the controller adds nothing the raw-stream agent didn't have. Modes-on
55
+ * (the default, WP-3/WP-5) re-enables `submit_plan` and `subagent` — see
56
+ * `makeAgentFactory`. */
57
+ declare const DISABLED_BUILTIN_TOOL_IDS: readonly BuiltinToolId[];
58
+ /** Session state the controller carries for us (see `initialState` below). */
59
+ interface AdapterControllerState {
60
+ /** WP-1 parity mode only: skips tool approvals entirely. Unset when modes are on. */
61
+ yolo?: boolean;
62
+ /** Modes-on default: seeded from {@link DEFAULT_PERMISSION_RULES}, read by `session.permissions`. */
63
+ permissionRules?: PermissionRules;
64
+ }
100
65
  /**
101
66
  * A lazy factory: parses the AGENT.md, builds the Mastra agent with the model
102
67
  * resolver, SQLite memory, the markdown body as instructions, and the
103
- * workspace toolset (matched by tool id), then returns it as the structural
104
- * `MastraLike` the ACP host needs.
68
+ * workspace toolset (matched by tool id), then wraps it in an
69
+ * `AgentController` the ACP host creates sessions on.
105
70
  */
106
- declare function makeAgentFactory(opts?: AgentSourceOptions): () => Promise<MastraLike>;
71
+ declare function makeAgentFactory(opts?: AgentSourceOptions): () => Promise<{
72
+ controller: AgentController<AdapterControllerState>;
73
+ }>;
74
+
75
+ /**
76
+ * WP2 — the agent side of AIP-44 ACP, backed by a Mastra `AgentController`.
77
+ *
78
+ * Implements the `@agentclientprotocol/sdk` `Agent` interface: handles the
79
+ * session lifecycle and, on `session/prompt`, drives a controller `Session`'s
80
+ * `sendMessage()` — relaying its subscription events as ACP `session/update`s
81
+ * (text deltas as `agent_message_chunk`, tool activity as `tool_call` /
82
+ * `tool_call_update`), exactly as an IDE/host expects from codex or
83
+ * claude-code.
84
+ *
85
+ * Beyond the prompt loop, this file bridges the rest of the session surface:
86
+ * - `session/load` — reconnect to the Mastra thread keyed by the ACP session
87
+ * id and replay its text history as user/agent message chunks
88
+ * - `tool_approval_required` / `tool_suspended` — parked controller runs
89
+ * surface as `session/request_permission` round-trips
90
+ * - `session/set_config_option` (model) and `session/set_mode` — applied via
91
+ * `session.model.switch` / `session.mode.switch`, with `model_changed` /
92
+ * `mode_changed` relayed back as config/mode session updates
93
+ * - image / audio / embedded-resource prompt blocks — passed to
94
+ * `sendMessage` as file attachments
95
+ *
96
+ * No Mastra-specific protocol knowledge leaks past this file; everything above
97
+ * is the standard ACP wire, so the daemon spawns this like any other arm.
98
+ */
99
+
100
+ /** A prompt attachment in the shape `Session.sendMessage` accepts: text
101
+ * attachments carry raw text, binary ones base64 (Mastra inlines `text/*` /
102
+ * JSON as fenced code and forwards the rest as model file parts). */
103
+ interface PromptFile {
104
+ data: string;
105
+ mediaType: string;
106
+ filename?: string;
107
+ }
108
+ /** The slice of a thread message that history replay reads (text parts). */
109
+ interface ThreadMessageLike {
110
+ role: string;
111
+ content?: {
112
+ parts?: unknown[];
113
+ };
114
+ }
115
+ /** The slice of a controller `Session` this host drives. Typed structurally so
116
+ * tests can script one and we don't couple to a specific @mastra/core
117
+ * version's `Session` generics. */
118
+ interface ControllerSessionLike {
119
+ subscribe(listener: (event: AgentControllerEvent) => void | Promise<void>): () => void;
120
+ sendMessage(input: {
121
+ content: string;
122
+ files?: PromptFile[];
123
+ }): Promise<void>;
124
+ /** Abort the active run and send in one step — the interrupt semantics a
125
+ * mid-turn prompt maps to. */
126
+ steer(input: {
127
+ content: string;
128
+ }): Promise<void>;
129
+ abort(): void;
130
+ respondToToolApproval(input: {
131
+ decision: "approve" | "decline" | "always_allow_category";
132
+ toolCallId?: string;
133
+ declineContext?: {
134
+ reason?: string;
135
+ message?: string;
136
+ };
137
+ }): void;
138
+ respondToToolSuspension(input: {
139
+ resumeData: unknown;
140
+ toolCallId?: string;
141
+ }): Promise<void>;
142
+ model: {
143
+ /** The currently-selected model id ('' when none selected yet). */
144
+ get(): string;
145
+ switch(input: {
146
+ modelId: string;
147
+ }): Promise<void>;
148
+ };
149
+ mode: {
150
+ switch(input: {
151
+ modeId: string;
152
+ }): Promise<void>;
153
+ };
154
+ thread: {
155
+ listMessages(input: {
156
+ threadId: string;
157
+ limit?: number;
158
+ }): Promise<ThreadMessageLike[]>;
159
+ };
160
+ }
161
+ /** The slice of an `AgentController` this host drives (structural, as above).
162
+ * `createSession` keys on (resourceId, scope) — one ACP session maps to one
163
+ * controller session via a unique scope, with the thread pinned to the ACP
164
+ * session id so memory recall is scoped exactly as before. */
165
+ interface ControllerLike {
166
+ init(): Promise<void>;
167
+ createSession(opts: {
168
+ resourceId?: string;
169
+ scope?: string;
170
+ threadId?: string;
171
+ }): Promise<ControllerSessionLike>;
172
+ }
173
+ /** Lazily builds the controller (so model/key/AGENT.md errors surface on the
174
+ * first prompt with a clear message, not at process spawn or session/new). */
175
+ type ControllerFactory = () => Promise<{
176
+ controller: ControllerLike;
177
+ }>;
178
+ /** The model catalog reported through the `model` session config option.
179
+ * Mirrors `models.allowed` in the manifest (index.ts) — keep in sync. Any
180
+ * Mastra-routable id is still accepted as a value; this is just what the
181
+ * client's selector lists. */
182
+ declare const DEFAULT_MODEL_CATALOG: ReadonlyArray<{
183
+ id: string;
184
+ name?: string;
185
+ }>;
186
+ /** Pull the user's text out of an ACP prompt (its `text` content blocks). */
187
+ declare function promptText(params: PromptRequest): string;
188
+ /**
189
+ * Split an ACP prompt into the user's text (its `text` blocks, exactly as
190
+ * {@link promptText} always did) and file attachments: `image`/`audio` blocks
191
+ * (base64 + mime type) and embedded `resource` blocks (text or blob contents).
192
+ * `resource_link` blocks carry no contents to attach — hosts inline referenced
193
+ * context as `resource` blocks when `embeddedContext` is on — so they are
194
+ * skipped, as before.
195
+ */
196
+ declare function promptContent(params: PromptRequest): {
197
+ text: string;
198
+ files: PromptFile[];
199
+ };
200
+ declare class MastraAcpAgent implements Agent {
201
+ #private;
202
+ constructor(conn: AgentSideConnection, buildController: ControllerFactory, resource?: string, models?: ReadonlyArray<{
203
+ id: string;
204
+ name?: string;
205
+ }>);
206
+ initialize(_params: InitializeRequest): Promise<InitializeResponse>;
207
+ authenticate(_params: AuthenticateRequest): Promise<Record<string, never>>;
208
+ newSession(_params: NewSessionRequest): Promise<NewSessionResponse>;
209
+ /**
210
+ * Resume an existing session: reconnect the controller session (scope +
211
+ * thread keyed by the ACP session id — `createSession` resumes an existing
212
+ * Mastra thread with full history) and replay the conversation to the
213
+ * client, as the `session/load` contract requires, via `user_message_chunk`
214
+ * / `agent_message_chunk` updates.
215
+ *
216
+ * Unlike `prompt`, this NEEDS the controller now (replay reads the thread),
217
+ * so build errors surface as this request's JSON-RPC error rather than a
218
+ * first-prompt error chunk.
219
+ */
220
+ loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse>;
221
+ prompt(params: PromptRequest): Promise<PromptResponse>;
222
+ cancel(params: CancelNotification): Promise<void>;
223
+ /**
224
+ * The host applies the operator's `model` as a `--model` spawn arg via the
225
+ * manifest `bin_args_template`, then ALSO calls this ACP config hook (the
226
+ * daemon's default "config" apply path). Runtime switches go through
227
+ * `session.model.switch`; a choice made before the controller session
228
+ * exists is remembered and applied on creation, preserving the invariant
229
+ * that controller build errors surface on the first prompt.
230
+ */
231
+ setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>;
232
+ /** Switch the controller session's mode (the catalog itself is controller
233
+ * config — WP-3). Mode-change confirmations reach the client via the
234
+ * session-lifetime `mode_changed` → `current_mode_update` relay. */
235
+ setSessionMode(params: SetSessionModeRequest): Promise<Record<string, never>>;
236
+ }
107
237
 
108
238
  /**
109
239
  * WP1 — AIP-42 `model:` ref -> a model Mastra's `Agent` can run.
@@ -114,7 +244,12 @@ declare function makeAgentFactory(opts?: AgentSourceOptions): () => Promise<Mast
114
244
  * the provider's key from the environment. So the resolver is a thin,
115
245
  * *validated* pass-through: extract the ref string, sanity-check the shape,
116
246
  * surface a friendly error when the obvious provider key is missing, and hand
117
- * the string to Mastra. No bespoke provider wiring, no extra ai-sdk deps.
247
+ * the string to Mastra.
248
+ *
249
+ * Exception: Anthropic OAuth Access Tokens (OATs, `sk-ant-oat*`) need
250
+ * `Authorization: Bearer`, not `x-api-key`. Mastra's router always sends
251
+ * `x-api-key`, so OATs bypass the string path — the resolver builds the
252
+ * ai-sdk model directly via `createAnthropic({ authToken })`.
118
253
  */
119
254
 
120
255
  /** Pull the model id string out of an AIP-42 `ModelRef` (string | { ref }). */
@@ -123,10 +258,15 @@ declare function modelRefToString(ref: ModelRef): string;
123
258
  declare function providerOf(modelId: string): string;
124
259
  /**
125
260
  * Resolve an AIP-42 model ref to the value Mastra's `Agent` constructor takes.
126
- * Returns the `provider/model` string — Mastra routes it. `env` defaults to
127
- * `process.env`; injectable for tests.
261
+ *
262
+ * For most providers this returns the `provider/model` string and Mastra's
263
+ * internal router does the rest. For Anthropic OAuth Access Tokens (OATs) it
264
+ * returns a pre-built ai-sdk `LanguageModel` with `authToken` so the token
265
+ * is sent as `Authorization: Bearer` instead of `x-api-key`.
266
+ *
267
+ * `env` defaults to `process.env`; injectable for tests.
128
268
  */
129
- declare function resolveMastraModel(ref: ModelRef, env?: Record<string, string | undefined>): string;
269
+ declare function resolveMastraModel(ref: ModelRef, env?: Record<string, string | undefined>): string | LanguageModelV3;
130
270
 
131
271
  /**
132
272
  * Workspace toolset — gives the Mastra agent the ability to inspect, edit, and
@@ -137,6 +277,38 @@ declare function resolveMastraModel(ref: ModelRef, env?: Record<string, string |
137
277
  * execution runs with `cwd` and a timeout, and is gated by `allowExec` (the CLI
138
278
  * sets it from `AGENTPROTO_MASTRA_NO_EXEC`). The agent only ever touches the
139
279
  * directory the daemon spawned it in.
280
+ *
281
+ * ## Tool id vocabulary
282
+ *
283
+ * Two tool-id vocabularies exist in this codebase for the same operations:
284
+ * this adapter's own coding-agent style (`list_dir`, `read_file`, `write_file`,
285
+ * `run_command`, ...) and the daemon's MCP-filesystem-compatible style
286
+ * (`directory_list`, `file_read`, `file_write`, `file_info`, `command_execute`,
287
+ * matching `packages/runtime/src/fs-tools.ts` / `command-tools.ts`). Apps'
288
+ * AGENT.md `tools:` lists have shipped with either vocabulary (see
289
+ * `packages/apps/src/media-viewer/agents/cataloger.ts` mixing `list_dir` +
290
+ * `file_info`), so both id sets resolve here — the daemon-style ids are
291
+ * aliases over the same implementations, plus `file_info` and `command_execute`
292
+ * which have no adapter-style equivalent.
293
+ *
294
+ * ## Fail-fast for unresolved tool refs
295
+ *
296
+ * `resolveTool` in `default-agent.ts` no longer silently drops an AGENT.md
297
+ * tool ref that doesn't match anything here — see `makeUnwiredToolStub`. A
298
+ * declared-but-unwired tool stays visible to the model and fails fast and
299
+ * clearly instead of the model either never seeing it (silent drop) or
300
+ * hallucinating a call the SDK can't resolve, which used to surface as an
301
+ * opaque `NoSuchToolError` this adapter's ACP layer then dropped on the
302
+ * floor (see `tool-call-map.ts`'s `tool-error` handling), leaving the turn
303
+ * looking hung with zero recorded tool calls.
304
+ *
305
+ * ## Execution guard
306
+ *
307
+ * Every tool's `execute` is wrapped with a hard timeout (`execTimeoutMs`,
308
+ * default 120s) so a stalled fs op or an unresponsive allowlist check can't
309
+ * block a turn indefinitely — past the deadline the call rejects with a
310
+ * clear timeout error (surfaced to the model as a normal tool failure) in
311
+ * place of an unbounded hang.
140
312
  */
141
313
 
142
314
  interface WorkspaceToolsOptions {
@@ -144,7 +316,7 @@ interface WorkspaceToolsOptions {
144
316
  cwd: string;
145
317
  /** When false, `run_command` (and the other exec-gated tools) are omitted. Default true. */
146
318
  allowExec?: boolean;
147
- /** Per-command timeout (ms). Default 120_000. */
319
+ /** Per-tool execution timeout (ms), enforced on every tool by `withTimeoutGuard`. Default 120_000. */
148
320
  execTimeoutMs?: number;
149
321
  /**
150
322
  * Extra tools merged over the built-ins, keyed by id — lets an embedding
@@ -161,6 +333,214 @@ declare function resolveInCwd(cwd: string, p: string): string;
161
333
  */
162
334
  declare function makeWorkspaceTools(opts: WorkspaceToolsOptions): Record<string, ReturnType<typeof createTool>>;
163
335
 
336
+ /**
337
+ * Zero-dependency HTTP client for the agentproto daemon.
338
+ *
339
+ * The adapter is spawned as an ACP child with `AGENTPROTO_SESSION_ID` /
340
+ * `AGENTPROTO_PARENT_SESSION_ID` in its env, but no daemon URL/token — unlike
341
+ * the CLI (`packages/cli/src/commands/_daemon-helpers.ts`, read as reference),
342
+ * this package cannot import `@agentproto/runtime` or `@agentproto/cli`
343
+ * (would pull the whole daemon runtime into an agent process). So this is a
344
+ * deliberately smaller reimplementation of that discovery order:
345
+ *
346
+ * 1. `AGENTPROTO_DAEMON_URL` env (+ optional `AGENTPROTO_DAEMON_TOKEN`)
347
+ * 2. `<cwd>/.agentproto/runtime.json`
348
+ * 3. `~/.agentproto/runtime.json`
349
+ * 4. central registry `~/.agentproto/daemons/*.json`, newest-mtime-first
350
+ *
351
+ * Unlike the CLI helper, this skips the `workspaces.json` walk (needs
352
+ * `@agentproto/runtime`'s config loader) and the declared-port preference
353
+ * in the central registry (needs `loadConfig()`). Good enough for an agent
354
+ * process discovering the daemon that spawned it or one on the same host.
355
+ *
356
+ * A runtime.json / registry entry whose `pid` is dead (`process.kill(pid, 0)`
357
+ * throwing anything but `EPERM`) is never trusted — same footgun the CLI
358
+ * helper guards against (a crashed daemon's stale token 401ing a fresh one
359
+ * on the same port).
360
+ */
361
+ interface DaemonEndpoint {
362
+ url: string;
363
+ token?: string;
364
+ /** Path of the runtime.json / registry file the endpoint came from.
365
+ * Undefined when the endpoint came from env vars. */
366
+ sourcePath?: string;
367
+ }
368
+ /** Thrown when no daemon endpoint can be discovered. */
369
+ declare class DaemonNotFoundError extends Error {
370
+ constructor(message?: string);
371
+ }
372
+ /** Thrown on a non-2xx daemon HTTP response. */
373
+ declare class DaemonHttpError extends Error {
374
+ readonly status: number;
375
+ readonly body: string;
376
+ constructor(message: string, status: number, body: string);
377
+ }
378
+ interface DiscoverDaemonOptions {
379
+ cwd?: string;
380
+ env?: NodeJS.ProcessEnv;
381
+ /** Override `homedir()` for BOTH `~/.agentproto/runtime.json` and the
382
+ * central registry dir (unless `registryDir` also overrides the latter
383
+ * separately) — test hook, so discovery never has to touch a real
384
+ * developer machine's `~/.agentproto`. */
385
+ homeDir?: string;
386
+ /** Override the central registry directory — test hook. Wins over `homeDir`. */
387
+ registryDir?: string;
388
+ /** Override the liveness check for a runtime.json / registry entry's
389
+ * `pid` — test hook. Defaults to a real `process.kill(pid, 0)` probe. */
390
+ isPidAlive?: (pid: number) => boolean;
391
+ }
392
+ /**
393
+ * Resolve the first live daemon endpoint, in the order documented above.
394
+ * Returns undefined when nothing live is found.
395
+ */
396
+ declare function discoverDaemonEndpoint(opts?: DiscoverDaemonOptions): Promise<DaemonEndpoint | undefined>;
397
+ interface StartAgentInput {
398
+ adapter: string;
399
+ cwd?: string;
400
+ model?: string;
401
+ prompt?: string;
402
+ label?: string;
403
+ }
404
+ interface PromptAgentOptions {
405
+ /** Cancel the in-flight turn and deliver this prompt instead. Default false. */
406
+ interrupt?: boolean;
407
+ /** `false` returns as soon as the prompt is queued/validated, without
408
+ * waiting for the turn to drain. Default true (blocking). */
409
+ wait?: boolean;
410
+ }
411
+ interface ListSessionsOptions {
412
+ includeArchived?: boolean;
413
+ kind?: string;
414
+ }
415
+ interface ReadOutputOptions {
416
+ format?: "markdown" | "json";
417
+ }
418
+ interface PollEventsOptions {
419
+ /** Cursor from a prior call's `nextSeq`. Omit to start from the beginning. */
420
+ since?: number;
421
+ /** Client-side filter on each record's `kind` field. Omit → all kinds. */
422
+ types?: string[];
423
+ limit?: number;
424
+ }
425
+ interface PollEventsResult {
426
+ sessionId: string;
427
+ events: Array<Record<string, unknown>>;
428
+ nextSeq: number;
429
+ complete: boolean;
430
+ }
431
+ interface DaemonClientOptions {
432
+ cwd?: string;
433
+ env?: NodeJS.ProcessEnv;
434
+ /** Injectable fetch — test hook. Defaults to the global `fetch`. */
435
+ fetchImpl?: typeof fetch;
436
+ /** Pre-resolved endpoint — skips discovery, mainly a test hook. */
437
+ endpoint?: DaemonEndpoint;
438
+ /** Test hooks forwarded to {@link discoverDaemonEndpoint}. */
439
+ homeDir?: string;
440
+ registryDir?: string;
441
+ isPidAlive?: (pid: number) => boolean;
442
+ }
443
+ /**
444
+ * Thin HTTP client over the daemon's REST surface (`packages/runtime/src/http-server.ts`,
445
+ * read-only reference — this package never imports it). Discovers the daemon
446
+ * endpoint lazily on first call and caches it; a connection failure triggers
447
+ * one re-discovery attempt (covers a daemon restart on the same well-known
448
+ * runtime.json / registry path but a new port/token).
449
+ */
450
+ declare class DaemonClient {
451
+ private readonly cwd;
452
+ private readonly env;
453
+ private readonly fetchImpl;
454
+ private readonly homeDir;
455
+ private readonly registryDir;
456
+ private readonly isPidAlive;
457
+ private cachedEndpoint;
458
+ constructor(opts?: DaemonClientOptions);
459
+ private resolveEndpoint;
460
+ private request;
461
+ /**
462
+ * Spawn a child session — `POST /sessions/agent`. `parentSessionId` is
463
+ * derived from `AGENTPROTO_SESSION_ID` (set by the daemon on every
464
+ * ACP-spawned adapter) when present, so session lineage is recorded
465
+ * without the caller having to thread it through.
466
+ */
467
+ startAgent(input: StartAgentInput): Promise<Record<string, unknown>>;
468
+ /** Send a follow-up turn to a live session — `POST /sessions/:id/prompt`. */
469
+ promptAgent(sessionId: string, text: string, opts?: PromptAgentOptions): Promise<Record<string, unknown>>;
470
+ /** List sessions — `GET /sessions`. */
471
+ listSessions(opts?: ListSessionsOptions): Promise<{
472
+ sessions: Array<Record<string, unknown>>;
473
+ }>;
474
+ /**
475
+ * Read a session's transcript — `GET /sessions/:id/export`. Chosen over
476
+ * `/sessions/:id/conversation` (needs a provider-native store registered
477
+ * for the adapter; not every adapter has one) and `/sessions/:id/preview`
478
+ * (a raw ring-buffer snapshot, not a rendered transcript): `/export`
479
+ * works for ANY agent-cli session, falling back to the daemon's own
480
+ * `events.jsonl` capture when no provider-native reader is registered
481
+ * (`source: "auto"`, see `transcript-export.ts`) — the one built for
482
+ * programmatic reads of "what did this session say".
483
+ */
484
+ readOutput(sessionId: string, opts?: ReadOutputOptions): Promise<Record<string, unknown>>;
485
+ /**
486
+ * Cursor-based poll of a session's structured event log —
487
+ * `GET /sessions/:id/events`. NOT the same event source as the MCP
488
+ * `session_events_poll` tool: that tool reads the daemon's in-memory,
489
+ * cross-session `EventRing` (turn-end/awaiting-input/exited/... lifecycle
490
+ * events, `packages/runtime/src/orchestration-tools.ts:528`), which has no
491
+ * plain-HTTP equivalent — it's only reachable over the MCP JSON-RPC
492
+ * transport, which this zero-dependency client doesn't speak. This route
493
+ * is the per-SESSION structured `events.jsonl` transcript instead
494
+ * (`packages/runtime/src/transcript-writer.ts`): cursor-based via
495
+ * `since`/`nextSeq` same as the MCP tool, and its records DO carry many of
496
+ * the same `kind`s (`turn-end`, `error`, `permission-resolved`, ...) — but
497
+ * it has no `exited` / `session:spawned` records (those are registry
498
+ * state changes, not transcript writes) and no cross-session fan-in
499
+ * (`sessionIds` filter). `types` is therefore filtered client-side here
500
+ * against each record's `kind`, not sent as a query param.
501
+ *
502
+ * WP-6 (`AgentprotoSignalProvider`, polling every 5s): this is a
503
+ * non-blocking snapshot read, so it's a direct fit for a poll loop. If
504
+ * WP-6 needs the actual cross-session lifecycle events (`exited`,
505
+ * `session:spawned`, multi-session fan-in), it either needs its own
506
+ * light MCP JSON-RPC client to call `session_events_poll` directly, or
507
+ * per-session `GET /sessions/:id/wait?since=&event=` (a blocking
508
+ * long-poll over the SAME EventRing `session_events_poll` reads, but
509
+ * scoped to one session and one event name at a time — no `types` array).
510
+ */
511
+ pollEvents(sessionId: string, opts?: PollEventsOptions): Promise<PollEventsResult>;
512
+ }
513
+
514
+ /**
515
+ * Daemon sub-agent-spawning tools — the Mastra-tool front for
516
+ * {@link DaemonClient}. Lets the agent spawn/prompt/read/list sibling
517
+ * sessions through the agentproto daemon (any adapter: claude-code, hermes,
518
+ * another mastra-agent, ...), mirroring `workspace-tools.ts`'s style
519
+ * (`createTool`, zod schemas, a bounded `tail` on large text output, a
520
+ * per-call timeout guard).
521
+ *
522
+ * Every tool fails fast with a clear message when no daemon is discoverable
523
+ * ({@link DaemonNotFoundError}'s message) instead of hanging — `DaemonClient`
524
+ * throws that before any network call is attempted.
525
+ */
526
+
527
+ interface DaemonToolsOptions {
528
+ /** Reuse a pre-built client (mainly a test hook) — one is built otherwise. */
529
+ client?: DaemonClient;
530
+ /** Forwarded to `new DaemonClient(...)` when no `client` is supplied. */
531
+ clientOptions?: DaemonClientOptions;
532
+ /** Per-tool execution timeout (ms). Default 30_000 — network calls, not
533
+ * local fs/exec, so a shorter default than workspace-tools' 120_000. */
534
+ execTimeoutMs?: number;
535
+ }
536
+ /**
537
+ * Build the daemon sub-agent-spawning toolset: `agent_start`, `agent_prompt`,
538
+ * `agent_output`, `session_list`. Tool ids match `tool-categories.ts`'s
539
+ * `CATEGORY_BY_TOOL` entries exactly (all category `mcp`, default policy
540
+ * `ask`).
541
+ */
542
+ declare function makeDaemonTools(opts?: DaemonToolsOptions): Record<string, ReturnType<typeof createTool>>;
543
+
164
544
  /**
165
545
  * SQLite-backed memory for the agent, via Mastra's LibSQL store.
166
546
  *
@@ -176,6 +556,13 @@ type MastraMemoryLike = Memory;
176
556
  /** Resolve the SQLite file path, creating the parent dir. `AGENTPROTO_MASTRA_MEMORY_DB`
177
557
  * wins; otherwise `~/.agentproto/mastra-agent/memory.db`. */
178
558
  declare function resolveMemoryDbPath(env?: Record<string, string | undefined>): string;
559
+ /**
560
+ * Build the LibSQL store backing the agent's memory. Exposed separately so the
561
+ * `AgentController` can share the SAME store instance as its `storage` (thread
562
+ * rows, per-thread settings) — two stores on one SQLite file would race each
563
+ * other on writes.
564
+ */
565
+ declare function buildSqliteStore(env?: Record<string, string | undefined>): LibSQLStore;
179
566
  /**
180
567
  * Build a Mastra `Memory` from an AIP-42 `memory:` config. Returns `undefined`
181
568
  * when memory is disabled (`scope: "none"`) so `buildMastraAgent` attaches none.
@@ -184,58 +571,61 @@ declare function resolveMemoryDbPath(env?: Record<string, string | undefined>):
184
571
  * replay into context). Defaults to 20.
185
572
  * - Semantic recall is left off (it needs an embedder + vector index) — this is
186
573
  * conversation-history memory, the SQLite ask.
574
+ * - `store` lets the caller share one LibSQL store between this memory and the
575
+ * AgentController's storage; omitted, a fresh store is built.
187
576
  */
188
- declare function buildSqliteMemory(config?: MemoryConfig, env?: Record<string, string | undefined>): MastraMemoryLike | undefined;
577
+ declare function buildSqliteMemory(config?: MemoryConfig, env?: Record<string, string | undefined>, store?: LibSQLStore): MastraMemoryLike | undefined;
189
578
 
190
579
  /**
191
- * Pure mapping from Mastra `fullStream` chunks to AIP-44 ACP `session/update`
192
- * payloads. Kept dependency-light (no SDK import beyond types) and side-effect
193
- * free so it is straightforward to unit-test; the ACP host (acp-host.ts) owns
194
- * the wire/IO and just forwards whatever this returns.
580
+ * Pure mapping from Mastra `AgentController` session events to AIP-44 ACP
581
+ * `session/update` payloads. Kept dependency-light (type-only imports) and
582
+ * IO-free so it is straightforward to unit-test; the ACP host (acp-host.ts)
583
+ * owns the wire and just forwards whatever this returns.
584
+ *
585
+ * A controller session (`session.subscribe`) emits `AgentControllerEvent`s.
586
+ * We surface three kinds:
587
+ * - `message_update` → ACP `agent_message_chunk` (assistant prose, as a delta)
588
+ * - `tool_start` → ACP `tool_call` (a tool started, status in_progress)
589
+ * - `tool_end` → ACP `tool_call_update` (that tool finished: completed/failed)
590
+ * Everything else (message_start/end, tool_input_*, usage, display state,
591
+ * agent_start/end, error, …) has no ACP surface here — `error` and `agent_end`
592
+ * are read by the host to pick the turn's stop reason, not mapped to updates.
195
593
  *
196
- * Mastra emits a typed chunk union on `agent.stream(...).fullStream`. We care
197
- * about three kinds:
198
- * - `text-delta` ACP `agent_message_chunk` (assistant prose)
199
- * - `tool-call` → ACP `tool_call` (a tool started, status in_progress)
200
- * - `tool-result` ACP `tool_call_update` (that tool finished/failed)
201
- * Everything else (reasoning, step boundaries, finish, …) is not surfaced.
594
+ * `message_update` carries the FULL message accumulated so far (Mastra's run
595
+ * engine re-emits the whole `MastraDBMessage` on every text delta), while ACP
596
+ * `agent_message_chunk` is a delta wire. The mapper is therefore a stateful
597
+ * factory: it remembers how much of each message's text it has already
598
+ * relayed and emits only the new suffix. Create one mapper per prompt turn.
599
+ *
600
+ * `tool_end` covers BOTH a successful tool result and a failed one (a tool's
601
+ * own `execute` throwing, or the SDK failing to resolve the call) — the old
602
+ * raw-stream `tool-result`/`tool-error` split collapses into `isError`.
603
+ * Dropping failures used to leave the matching `tool_call` stuck
604
+ * "in_progress" forever on the ACP wire, so failed calls map to a `failed`
605
+ * update carrying the error text.
202
606
  */
203
607
 
204
- /** The narrow slice of a Mastra fullStream chunk we read. Typed structurally
205
- * so we don't couple to a specific @mastra/core version.
206
- * Mastra 1.45 wraps all chunk data in a `payload` field. */
207
- type MastraStreamChunk = {
208
- type: "text-delta";
209
- payload?: {
210
- text?: string;
211
- };
212
- } | {
213
- type: "tool-call";
214
- payload?: {
215
- toolCallId?: string;
216
- toolName?: string;
217
- args?: unknown;
218
- };
219
- } | {
220
- type: "tool-result";
221
- payload?: {
222
- toolCallId?: string;
223
- result?: unknown;
224
- isError?: boolean;
225
- };
226
- };
227
608
  /** Map a workspace tool id to the ACP {@link ToolKind} that drives client
228
609
  * icon/UI treatment. Unknown ids fall back to "other". */
229
610
  declare function toolKindFor(toolName: string): ToolKind;
230
611
  /** A short, human-readable title for a tool call, e.g. `run_command: ls -la`
231
612
  * or `read_file: src/index.ts`. Falls back to the bare tool name. */
232
613
  declare function toolCallTitle(toolName: string, args: unknown): string;
614
+ /** Concatenate a message's plain-text parts (assistant prose; reasoning and
615
+ * tool-invocation parts are not ACP message text). Also used by the host's
616
+ * `session/load` replay to render prior thread messages. */
617
+ declare function messageText(message: {
618
+ content?: {
619
+ parts?: unknown[];
620
+ };
621
+ }): string;
233
622
  /**
234
- * Translate one Mastra chunk into an ACP `session/update` payload, or `null`
235
- * when the chunk has no ACP surface (or is missing the ids we need). The ACP
236
- * host wraps the result with the `sessionId`.
623
+ * Create a stateful event mapper for one prompt turn: translates each
624
+ * controller event into an ACP `session/update` payload, or `null` when the
625
+ * event has no ACP surface (or carries no new text). The ACP host wraps each
626
+ * result with the `sessionId`.
237
627
  */
238
- declare function chunkToSessionUpdate(chunk: MastraStreamChunk): SessionUpdate | null;
628
+ declare function createEventMapper(): (event: AgentControllerEvent) => SessionUpdate | null;
239
629
 
240
630
  /**
241
631
  * Boots the ACP server over stdio — the standard wiring for a spawned ACP
@@ -243,7 +633,7 @@ declare function chunkToSessionUpdate(chunk: MastraStreamChunk): SessionUpdate |
243
633
  * JSON-RPC to stdout and reads from stdin.
244
634
  */
245
635
 
246
- declare function runAcpOverStdio(buildAgent: AgentFactory): AgentSideConnection;
636
+ declare function runAcpOverStdio(buildController: ControllerFactory): AgentSideConnection;
247
637
 
248
638
  /**
249
639
  * @agentproto/adapter-mastra-agent — the first-party agentproto agent.
@@ -260,4 +650,4 @@ declare function runAcpOverStdio(buildAgent: AgentFactory): AgentSideConnection;
260
650
  declare const mastraAgent: AgentCliHandle;
261
651
  declare function mastraAgentRuntime(): AgentCliRuntime;
262
652
 
263
- export { DEFAULT_MODEL, DEFAULT_TOOL_IDS, MastraAcpAgent, type MastraLike, type MastraStreamChunk, buildSqliteMemory, chunkToSessionUpdate, defaultAgentManifest, makeAgentFactory, makeWorkspaceTools, mastraAgent, mastraAgentRuntime, modelRefToString, promptText, providerOf, resolveInCwd, resolveMastraModel, resolveMemoryDbPath, runAcpOverStdio, toolCallTitle, toolKindFor };
653
+ export { type ControllerFactory, type ControllerLike, type ControllerSessionLike, DEFAULT_MODEL, DEFAULT_MODEL_CATALOG, DEFAULT_TOOL_IDS, DISABLED_BUILTIN_TOOL_IDS, DaemonClient, type DaemonClientOptions, type DaemonEndpoint, DaemonHttpError, DaemonNotFoundError, type DaemonToolsOptions, MastraAcpAgent, type PromptFile, type ThreadMessageLike, buildSqliteMemory, buildSqliteStore, createEventMapper, defaultAgentManifest, discoverDaemonEndpoint, makeAgentFactory, makeDaemonTools, makeWorkspaceTools, mastraAgent, mastraAgentRuntime, messageText, modelRefToString, promptContent, promptText, providerOf, resolveInCwd, resolveMastraModel, resolveMemoryDbPath, runAcpOverStdio, toolCallTitle, toolKindFor };