@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.
@@ -0,0 +1,2094 @@
1
+ import { promises, mkdirSync } from 'fs';
2
+ import { homedir, tmpdir } from 'os';
3
+ import { resolve, join, isAbsolute, relative, sep, dirname, basename } from 'path';
4
+ import { exec, execFile } from 'child_process';
5
+ import { randomUUID } from 'crypto';
6
+ import { promisify } from 'util';
7
+ import { createTool } from '@mastra/core/tools';
8
+ import { loadAllowlistEntries, isCommandAllowed, isInterpreterBasename, interpreterExecWarning } from '@agentproto/runtime/command-allowlist';
9
+ import { z } from 'zod';
10
+ import { LibSQLStore } from '@mastra/libsql';
11
+ import { Memory } from '@mastra/memory';
12
+ import { createAnthropic } from '@ai-sdk/anthropic';
13
+ import { readFile } from 'fs/promises';
14
+ import { parseAgentManifest, agentFromManifest } from '@agentproto/agent';
15
+ import { buildMastraAgent } from '@agentproto/mastra';
16
+ import { ProviderHistoryCompat } from '@mastra/core/processors';
17
+ import { AgentController } from '@mastra/core/agent-controller';
18
+ import { Workspace } from '@mastra/core/workspace';
19
+ import { createNotificationInboxTool } from '@mastra/core/notifications';
20
+ import { SignalProvider } from '@mastra/core/signals';
21
+ import { PROTOCOL_VERSION, ndJsonStream, AgentSideConnection } from '@agentclientprotocol/sdk';
22
+ import { Writable, Readable } from 'stream';
23
+
24
+ /**
25
+ * @agentproto/adapter-mastra-agent v0.1.0-alpha
26
+ * First-party agentproto agent: AGENT.md -> Mastra agent -> ACP server.
27
+ */
28
+
29
+ var DaemonNotFoundError = class extends Error {
30
+ constructor(message = "no agentproto daemon found \u2014 set AGENTPROTO_DAEMON_URL or run inside a daemon-spawned session") {
31
+ super(message);
32
+ this.name = "DaemonNotFoundError";
33
+ }
34
+ };
35
+ var DaemonHttpError = class extends Error {
36
+ constructor(message, status, body) {
37
+ super(message);
38
+ this.status = status;
39
+ this.body = body;
40
+ this.name = "DaemonHttpError";
41
+ }
42
+ status;
43
+ body;
44
+ };
45
+ function defaultIsPidAlive(pid) {
46
+ try {
47
+ process.kill(pid, 0);
48
+ return true;
49
+ } catch (err) {
50
+ const code = err.code;
51
+ return code === "EPERM";
52
+ }
53
+ }
54
+ async function readJsonFile(path) {
55
+ try {
56
+ const raw = await promises.readFile(path, "utf8");
57
+ return JSON.parse(raw);
58
+ } catch {
59
+ return void 0;
60
+ }
61
+ }
62
+ async function endpointFromRuntimeJson(path, isPidAlive) {
63
+ const parsed = await readJsonFile(path);
64
+ if (!parsed || typeof parsed.port !== "number") return void 0;
65
+ if (typeof parsed.pid === "number" && !isPidAlive(parsed.pid)) return void 0;
66
+ return {
67
+ url: `http://${typeof parsed.bind === "string" ? parsed.bind : "127.0.0.1"}:${parsed.port}`,
68
+ sourcePath: path,
69
+ ...typeof parsed.token === "string" ? { token: parsed.token } : {}
70
+ };
71
+ }
72
+ async function discoverDaemonEndpoint(opts = {}) {
73
+ const env = opts.env ?? process.env;
74
+ const cwd = opts.cwd ?? process.cwd();
75
+ const homeDir = opts.homeDir ?? homedir();
76
+ const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
77
+ if (env.AGENTPROTO_DAEMON_URL) {
78
+ const url = env.AGENTPROTO_DAEMON_URL.replace(/\/+$/, "");
79
+ return { url, ...env.AGENTPROTO_DAEMON_TOKEN ? { token: env.AGENTPROTO_DAEMON_TOKEN } : {} };
80
+ }
81
+ const cwdRuntime = await endpointFromRuntimeJson(resolve(cwd, ".agentproto", "runtime.json"), isPidAlive);
82
+ if (cwdRuntime) return cwdRuntime;
83
+ const homeRuntime = await endpointFromRuntimeJson(resolve(homeDir, ".agentproto", "runtime.json"), isPidAlive);
84
+ if (homeRuntime) return homeRuntime;
85
+ const registryDir = opts.registryDir ?? resolve(homeDir, ".agentproto", "daemons");
86
+ let names;
87
+ try {
88
+ names = await promises.readdir(registryDir);
89
+ } catch {
90
+ return void 0;
91
+ }
92
+ const entries = [];
93
+ for (const name of names) {
94
+ if (!name.endsWith(".json")) continue;
95
+ const path = join(registryDir, name);
96
+ try {
97
+ const st = await promises.stat(path);
98
+ entries.push({ path, mtimeMs: st.mtimeMs });
99
+ } catch {
100
+ }
101
+ }
102
+ entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
103
+ for (const entry of entries) {
104
+ const ep = await endpointFromRuntimeJson(entry.path, isPidAlive);
105
+ if (ep) return ep;
106
+ }
107
+ return void 0;
108
+ }
109
+ var DaemonClient = class {
110
+ cwd;
111
+ env;
112
+ fetchImpl;
113
+ homeDir;
114
+ registryDir;
115
+ isPidAlive;
116
+ cachedEndpoint;
117
+ constructor(opts = {}) {
118
+ this.cwd = opts.cwd ?? process.cwd();
119
+ this.env = opts.env ?? process.env;
120
+ if (!opts.fetchImpl && typeof fetch !== "function") {
121
+ throw new Error("DaemonClient requires a global `fetch` (Node >=18) or an injected `fetchImpl`.");
122
+ }
123
+ this.fetchImpl = opts.fetchImpl ?? fetch;
124
+ this.homeDir = opts.homeDir;
125
+ this.registryDir = opts.registryDir;
126
+ this.isPidAlive = opts.isPidAlive;
127
+ this.cachedEndpoint = opts.endpoint;
128
+ }
129
+ async resolveEndpoint(forceRediscover = false) {
130
+ if (this.cachedEndpoint && !forceRediscover) return this.cachedEndpoint;
131
+ const found = await discoverDaemonEndpoint({
132
+ cwd: this.cwd,
133
+ env: this.env,
134
+ ...this.homeDir ? { homeDir: this.homeDir } : {},
135
+ ...this.registryDir ? { registryDir: this.registryDir } : {},
136
+ ...this.isPidAlive ? { isPidAlive: this.isPidAlive } : {}
137
+ });
138
+ if (!found) throw new DaemonNotFoundError();
139
+ this.cachedEndpoint = found;
140
+ return found;
141
+ }
142
+ async request(method, path, opts = {}) {
143
+ const endpoint = await this.resolveEndpoint();
144
+ const doFetch = async (ep) => {
145
+ const url = new URL(path, ep.url);
146
+ for (const [k, v] of Object.entries(opts.query ?? {})) {
147
+ if (v !== void 0) url.searchParams.set(k, v);
148
+ }
149
+ const headers = {};
150
+ if (ep.token) headers.authorization = `Bearer ${ep.token}`;
151
+ if (opts.body !== void 0) headers["content-type"] = "application/json";
152
+ return this.fetchImpl(url.toString(), {
153
+ method,
154
+ headers,
155
+ ...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
156
+ });
157
+ };
158
+ let res;
159
+ try {
160
+ res = await doFetch(endpoint);
161
+ } catch (err) {
162
+ const rediscovered = await this.resolveEndpoint(true);
163
+ try {
164
+ res = await doFetch(rediscovered);
165
+ } catch (retryErr) {
166
+ throw retryErr instanceof Error ? retryErr : new Error(String(retryErr));
167
+ }
168
+ }
169
+ const text = await res.text();
170
+ if (!res.ok) {
171
+ throw new DaemonHttpError(
172
+ `daemon HTTP ${res.status} ${method} ${path}: ${text.slice(0, 200)}`,
173
+ res.status,
174
+ text
175
+ );
176
+ }
177
+ return text.length > 0 ? JSON.parse(text) : void 0;
178
+ }
179
+ /**
180
+ * Spawn a child session — `POST /sessions/agent`. `parentSessionId` is
181
+ * derived from `AGENTPROTO_SESSION_ID` (set by the daemon on every
182
+ * ACP-spawned adapter) when present, so session lineage is recorded
183
+ * without the caller having to thread it through.
184
+ */
185
+ async startAgent(input) {
186
+ const parentSessionId = this.env.AGENTPROTO_SESSION_ID;
187
+ return this.request("POST", "/sessions/agent", {
188
+ body: {
189
+ ...input,
190
+ ...parentSessionId ? { parentSessionId } : {}
191
+ }
192
+ });
193
+ }
194
+ /** Send a follow-up turn to a live session — `POST /sessions/:id/prompt`. */
195
+ async promptAgent(sessionId, text, opts = {}) {
196
+ return this.request("POST", `/sessions/${encodeURIComponent(sessionId)}/prompt`, {
197
+ body: { prompt: text, ...opts.interrupt ? { interrupt: true } : {} },
198
+ query: opts.wait === false ? { wait: "false" } : {}
199
+ });
200
+ }
201
+ /** List sessions — `GET /sessions`. */
202
+ async listSessions(opts = {}) {
203
+ return this.request("GET", "/sessions", {
204
+ query: {
205
+ includeArchived: opts.includeArchived ? "true" : void 0,
206
+ kind: opts.kind
207
+ }
208
+ });
209
+ }
210
+ /**
211
+ * Read a session's transcript — `GET /sessions/:id/export`. Chosen over
212
+ * `/sessions/:id/conversation` (needs a provider-native store registered
213
+ * for the adapter; not every adapter has one) and `/sessions/:id/preview`
214
+ * (a raw ring-buffer snapshot, not a rendered transcript): `/export`
215
+ * works for ANY agent-cli session, falling back to the daemon's own
216
+ * `events.jsonl` capture when no provider-native reader is registered
217
+ * (`source: "auto"`, see `transcript-export.ts`) — the one built for
218
+ * programmatic reads of "what did this session say".
219
+ */
220
+ async readOutput(sessionId, opts = {}) {
221
+ return this.request("GET", `/sessions/${encodeURIComponent(sessionId)}/export`, {
222
+ query: { format: opts.format ?? "json" }
223
+ });
224
+ }
225
+ /**
226
+ * Cursor-based poll of a session's structured event log —
227
+ * `GET /sessions/:id/events`. NOT the same event source as the MCP
228
+ * `session_events_poll` tool: that tool reads the daemon's in-memory,
229
+ * cross-session `EventRing` (turn-end/awaiting-input/exited/... lifecycle
230
+ * events, `packages/runtime/src/orchestration-tools.ts:528`), which has no
231
+ * plain-HTTP equivalent — it's only reachable over the MCP JSON-RPC
232
+ * transport, which this zero-dependency client doesn't speak. This route
233
+ * is the per-SESSION structured `events.jsonl` transcript instead
234
+ * (`packages/runtime/src/transcript-writer.ts`): cursor-based via
235
+ * `since`/`nextSeq` same as the MCP tool, and its records DO carry many of
236
+ * the same `kind`s (`turn-end`, `error`, `permission-resolved`, ...) — but
237
+ * it has no `exited` / `session:spawned` records (those are registry
238
+ * state changes, not transcript writes) and no cross-session fan-in
239
+ * (`sessionIds` filter). `types` is therefore filtered client-side here
240
+ * against each record's `kind`, not sent as a query param.
241
+ *
242
+ * WP-6 (`AgentprotoSignalProvider`, polling every 5s): this is a
243
+ * non-blocking snapshot read, so it's a direct fit for a poll loop. If
244
+ * WP-6 needs the actual cross-session lifecycle events (`exited`,
245
+ * `session:spawned`, multi-session fan-in), it either needs its own
246
+ * light MCP JSON-RPC client to call `session_events_poll` directly, or
247
+ * per-session `GET /sessions/:id/wait?since=&event=` (a blocking
248
+ * long-poll over the SAME EventRing `session_events_poll` reads, but
249
+ * scoped to one session and one event name at a time — no `types` array).
250
+ */
251
+ async pollEvents(sessionId, opts = {}) {
252
+ const result = await this.request(
253
+ "GET",
254
+ `/sessions/${encodeURIComponent(sessionId)}/events`,
255
+ {
256
+ query: {
257
+ since: opts.since !== void 0 ? String(opts.since) : void 0,
258
+ limit: opts.limit !== void 0 ? String(opts.limit) : void 0
259
+ }
260
+ }
261
+ );
262
+ if (!opts.types || opts.types.length === 0) return result;
263
+ const wanted = new Set(opts.types);
264
+ return {
265
+ ...result,
266
+ events: result.events.filter((e) => typeof e.kind === "string" && wanted.has(e.kind))
267
+ };
268
+ }
269
+ };
270
+ var execAsync = promisify(exec);
271
+ var execFileAsync = promisify(execFile);
272
+ var ALLOWED_TEST_ARGV0 = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "node", "npx"]);
273
+ function tail(s, maxChars = 4e3) {
274
+ return s.length > maxChars ? s.slice(-maxChars) : s;
275
+ }
276
+ function extractPatchPaths(patch) {
277
+ const paths = /* @__PURE__ */ new Set();
278
+ for (const line of patch.split("\n")) {
279
+ const m = /^(?:\+\+\+|---) (?:a\/|b\/)?(.+?)(?:\t.*)?$/.exec(line);
280
+ if (!m) continue;
281
+ const p = m[1].trim();
282
+ if (p === "/dev/null") continue;
283
+ paths.add(p);
284
+ }
285
+ return [...paths];
286
+ }
287
+ function withTimeoutGuard(id, timeoutMs, execute) {
288
+ return async (input) => {
289
+ let timer;
290
+ try {
291
+ return await Promise.race([
292
+ execute(input),
293
+ new Promise((_, reject) => {
294
+ timer = setTimeout(() => {
295
+ reject(
296
+ new Error(
297
+ `tool '${id}' timed out after ${timeoutMs}ms (adapter execution guard) \u2014 aborted so the turn can't hang indefinitely.`
298
+ )
299
+ );
300
+ }, timeoutMs);
301
+ })
302
+ ]);
303
+ } finally {
304
+ clearTimeout(timer);
305
+ }
306
+ };
307
+ }
308
+ function resolveInCwd(cwd, p) {
309
+ const base = resolve(cwd);
310
+ const target = isAbsolute(p) ? resolve(p) : resolve(base, p);
311
+ const rel = relative(base, target);
312
+ if (rel === "") return target;
313
+ if (rel.startsWith("..") || isAbsolute(rel) && !target.startsWith(base + sep)) {
314
+ throw new Error(
315
+ `path '${p}' escapes the workspace (resolved to '${target}', outside '${base}').`
316
+ );
317
+ }
318
+ return target;
319
+ }
320
+ function makeUnwiredToolStub(id) {
321
+ return createTool({
322
+ id,
323
+ description: `NOT WIRED: '${id}' is declared in this agent's AGENT.md but this adapter has no executor for it. Calling it always fails immediately with an error \u2014 do not retry it.`,
324
+ inputSchema: z.record(z.string(), z.unknown()),
325
+ outputSchema: z.object({ error: z.string() }),
326
+ execute: async () => {
327
+ throw new Error(
328
+ `tool '${id}' is declared in AGENT.md but not wired to any executor in this adapter (adapters/mastra-agent/src/workspace-tools.ts). This call cannot succeed \u2014 stop retrying it.`
329
+ );
330
+ }
331
+ });
332
+ }
333
+ function makeWorkspaceTools(opts) {
334
+ const cwd = resolve(opts.cwd);
335
+ const allowExec = opts.allowExec ?? true;
336
+ const execTimeoutMs = opts.execTimeoutMs ?? 12e4;
337
+ const execEnv = { ...process.env, GIT_CEILING_DIRECTORIES: resolve(cwd, "..") };
338
+ const guard = (id, execute) => withTimeoutGuard(id, execTimeoutMs, execute);
339
+ const doListDir = async (input) => {
340
+ const dir = resolveInCwd(cwd, input.path ?? ".");
341
+ const dirents = await promises.readdir(dir, { withFileTypes: true });
342
+ return {
343
+ entries: dirents.map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort()
344
+ };
345
+ };
346
+ const doReadFile = async (input) => {
347
+ const file = resolveInCwd(cwd, input.path);
348
+ return { content: await promises.readFile(file, "utf8") };
349
+ };
350
+ const doWriteFile = async (input) => {
351
+ const file = resolveInCwd(cwd, input.path);
352
+ await promises.mkdir(resolve(file, ".."), { recursive: true });
353
+ await promises.writeFile(file, input.content, "utf8");
354
+ return { path: input.path, bytes: Buffer.byteLength(input.content, "utf8") };
355
+ };
356
+ const list_dir = createTool({
357
+ id: "list_dir",
358
+ description: "List the entries of a directory in the workspace. Returns names with a trailing '/' for directories. Path is relative to the workspace root (default '.').",
359
+ inputSchema: z.object({
360
+ path: z.string().default(".").describe("Directory path, relative to the workspace root.")
361
+ }),
362
+ outputSchema: z.object({ entries: z.array(z.string()) }),
363
+ execute: guard("list_dir", doListDir)
364
+ });
365
+ const directory_list = createTool({
366
+ id: "directory_list",
367
+ description: "Alias of `list_dir` (daemon-vocabulary id) \u2014 list a workspace directory's entries.",
368
+ inputSchema: z.object({
369
+ path: z.string().default(".").describe("Directory path, relative to the workspace root.")
370
+ }),
371
+ outputSchema: z.object({ entries: z.array(z.string()) }),
372
+ execute: guard("directory_list", doListDir)
373
+ });
374
+ const read_file = createTool({
375
+ id: "read_file",
376
+ description: "Read a UTF-8 text file from the workspace. Path is relative to the workspace root.",
377
+ inputSchema: z.object({
378
+ path: z.string().describe("File path, relative to the workspace root.")
379
+ }),
380
+ outputSchema: z.object({ content: z.string() }),
381
+ execute: guard("read_file", doReadFile)
382
+ });
383
+ const file_read = createTool({
384
+ id: "file_read",
385
+ description: "Alias of `read_file` (daemon-vocabulary id) \u2014 read a UTF-8 text file from the workspace.",
386
+ inputSchema: z.object({
387
+ path: z.string().describe("File path, relative to the workspace root.")
388
+ }),
389
+ outputSchema: z.object({ content: z.string() }),
390
+ execute: guard("file_read", doReadFile)
391
+ });
392
+ const write_file = createTool({
393
+ id: "write_file",
394
+ description: "Write (creating or overwriting) a UTF-8 text file in the workspace. Creates parent directories as needed. Path is relative to the workspace root.",
395
+ inputSchema: z.object({
396
+ path: z.string().describe("File path, relative to the workspace root."),
397
+ content: z.string().describe("Full file contents to write.")
398
+ }),
399
+ outputSchema: z.object({ path: z.string(), bytes: z.number() }),
400
+ execute: guard("write_file", doWriteFile)
401
+ });
402
+ const file_write = createTool({
403
+ id: "file_write",
404
+ description: "Alias of `write_file` (daemon-vocabulary id) \u2014 write a UTF-8 text file to the workspace.",
405
+ inputSchema: z.object({
406
+ path: z.string().describe("File path, relative to the workspace root."),
407
+ content: z.string().describe("Full file contents to write.")
408
+ }),
409
+ outputSchema: z.object({ path: z.string(), bytes: z.number() }),
410
+ execute: guard("file_write", doWriteFile)
411
+ });
412
+ const edit_file = createTool({
413
+ id: "edit_file",
414
+ description: "Replace an exact substring in a workspace file. `old_string` must occur exactly once. Use for targeted edits instead of rewriting the whole file.",
415
+ inputSchema: z.object({
416
+ path: z.string().describe("File path, relative to the workspace root."),
417
+ old_string: z.string().describe("Exact text to replace (must be unique in the file)."),
418
+ new_string: z.string().describe("Replacement text.")
419
+ }),
420
+ outputSchema: z.object({ path: z.string(), replaced: z.boolean() }),
421
+ execute: guard("edit_file", async (input) => {
422
+ const file = resolveInCwd(cwd, input.path);
423
+ const current = await promises.readFile(file, "utf8");
424
+ const count = current.split(input.old_string).length - 1;
425
+ if (count === 0) throw new Error(`old_string not found in '${input.path}'.`);
426
+ if (count > 1) {
427
+ throw new Error(`old_string occurs ${count}\xD7 in '${input.path}' \u2014 make it unique.`);
428
+ }
429
+ await promises.writeFile(file, current.replace(input.old_string, input.new_string), "utf8");
430
+ return { path: input.path, replaced: true };
431
+ })
432
+ });
433
+ const file_info = createTool({
434
+ id: "file_info",
435
+ description: "Stat a file or directory in the workspace \u2014 returns name, type, size (bytes), and modified/created timestamps.",
436
+ inputSchema: z.object({
437
+ path: z.string().describe("File or directory path, relative to the workspace root.")
438
+ }),
439
+ outputSchema: z.object({
440
+ name: z.string(),
441
+ path: z.string(),
442
+ type: z.enum(["file", "directory"]),
443
+ size: z.number(),
444
+ modified: z.string(),
445
+ created: z.string()
446
+ }),
447
+ execute: guard("file_info", async (input) => {
448
+ const abs = resolveInCwd(cwd, input.path);
449
+ const info = await promises.stat(abs);
450
+ return {
451
+ name: abs.split(sep).pop() ?? input.path,
452
+ path: input.path,
453
+ type: info.isDirectory() ? "directory" : "file",
454
+ size: info.size,
455
+ modified: info.mtime.toISOString(),
456
+ created: info.birthtime.toISOString()
457
+ };
458
+ })
459
+ });
460
+ const tools = {
461
+ list_dir,
462
+ directory_list,
463
+ read_file,
464
+ file_read,
465
+ write_file,
466
+ file_write,
467
+ edit_file,
468
+ file_info
469
+ };
470
+ if (allowExec) {
471
+ tools.run_command = createTool({
472
+ id: "run_command",
473
+ description: "Run a shell command in the workspace directory and return its stdout/stderr/exit code. Runs with a timeout; use for builds, tests, git, etc.",
474
+ inputSchema: z.object({
475
+ command: z.string().describe("The shell command to run (executed in the workspace root).")
476
+ }),
477
+ outputSchema: z.object({
478
+ stdout: z.string(),
479
+ stderr: z.string(),
480
+ exitCode: z.number()
481
+ }),
482
+ execute: guard("run_command", async (input) => {
483
+ try {
484
+ const { stdout, stderr } = await execAsync(input.command, {
485
+ cwd,
486
+ timeout: execTimeoutMs,
487
+ maxBuffer: 10 * 1024 * 1024,
488
+ env: execEnv
489
+ });
490
+ return { stdout, stderr, exitCode: 0 };
491
+ } catch (err) {
492
+ const e = err;
493
+ return {
494
+ stdout: e.stdout ?? "",
495
+ stderr: e.stderr ?? e.message ?? String(err),
496
+ exitCode: typeof e.code === "number" ? e.code : 1
497
+ };
498
+ }
499
+ })
500
+ });
501
+ tools.command_execute = createTool({
502
+ id: "command_execute",
503
+ description: "Run an allowlisted command in the workspace directory. The command's basename must appear in <workspace>/.agentproto/allowed-commands.json (default-deny \u2014 a missing or empty file means nothing runs). Unlike `run_command`, args are passed as an argv array with no shell interpolation (spawned with shell:false). Returns stdout/stderr/exitCode.",
504
+ inputSchema: z.object({
505
+ command: z.string().min(1).describe("Executable name or path \u2014 checked against the allowlist by basename."),
506
+ args: z.array(z.string()).optional().describe("Argv array, passed verbatim (no shell expansion).")
507
+ }),
508
+ outputSchema: z.object({
509
+ stdout: z.string(),
510
+ stderr: z.string(),
511
+ exitCode: z.number(),
512
+ warning: z.string().optional()
513
+ }),
514
+ execute: guard("command_execute", async (input) => {
515
+ const allowlistEntries = await loadAllowlistEntries(cwd);
516
+ const baseName = basename(input.command);
517
+ if (!isCommandAllowed(allowlistEntries, baseName, input.args ?? [])) {
518
+ const allowedBasenames = [...new Set(allowlistEntries.map((e) => e.command))].sort().join(", ") || "(empty)";
519
+ const basenameKnown = allowlistEntries.some((e) => e.command === baseName);
520
+ throw new Error(
521
+ basenameKnown ? `command_execute: '${baseName}' is allowlisted but its argv doesn't match any allowed pattern for it. Check the "args" constraints in .agentproto/allowed-commands.json.` : `command_execute: '${baseName}' is not in the workspace allowlist. Add it to .agentproto/allowed-commands.json under "commands": [...]. Currently allowed: ${allowedBasenames}.`
522
+ );
523
+ }
524
+ const warning = isInterpreterBasename(baseName) ? interpreterExecWarning(baseName) : void 0;
525
+ if (warning) console.error(`[command_execute] \u26A0 ${warning}`);
526
+ try {
527
+ const { stdout, stderr } = await execFileAsync(input.command, input.args ?? [], {
528
+ cwd,
529
+ timeout: execTimeoutMs,
530
+ maxBuffer: 10 * 1024 * 1024,
531
+ env: execEnv
532
+ });
533
+ return { stdout, stderr, exitCode: 0, ...warning ? { warning } : {} };
534
+ } catch (err) {
535
+ const e = err;
536
+ return {
537
+ stdout: e.stdout ?? "",
538
+ stderr: e.stderr ?? e.message ?? String(err),
539
+ exitCode: typeof e.code === "number" ? e.code : 1,
540
+ ...warning ? { warning } : {}
541
+ };
542
+ }
543
+ })
544
+ });
545
+ tools.read_diff = createTool({
546
+ id: "read_diff",
547
+ description: "Show `git diff` for the workspace \u2014 staged and unstaged changes against HEAD (or against `base` if given), as unified diff text. Optionally scoped to `paths`.",
548
+ inputSchema: z.object({
549
+ paths: z.array(z.string()).optional().describe("Restrict the diff to these paths, relative to the workspace root."),
550
+ base: z.string().optional().describe("Git ref to diff against. Defaults to HEAD.")
551
+ }),
552
+ outputSchema: z.object({ diff: z.string() }),
553
+ execute: guard("read_diff", async (input) => {
554
+ const relPaths = (input.paths ?? []).map((p) => {
555
+ const abs = resolveInCwd(cwd, p);
556
+ return relative(cwd, abs) || ".";
557
+ });
558
+ const args = [
559
+ "diff",
560
+ input.base ?? "HEAD",
561
+ ...relPaths.length ? ["--", ...relPaths] : []
562
+ ];
563
+ try {
564
+ const { stdout } = await execFileAsync("git", args, {
565
+ cwd,
566
+ timeout: execTimeoutMs,
567
+ maxBuffer: 10 * 1024 * 1024,
568
+ env: execEnv
569
+ });
570
+ return { diff: stdout };
571
+ } catch (err) {
572
+ const e = err;
573
+ throw new Error(`git diff failed: ${e.stderr ?? e.message ?? String(err)}`);
574
+ }
575
+ })
576
+ });
577
+ tools.apply_patch = createTool({
578
+ id: "apply_patch",
579
+ description: "Apply a unified diff to files in the workspace (`git apply --whitespace=nowarn`). Paths in the patch that escape the workspace are rejected.",
580
+ inputSchema: z.object({
581
+ patch: z.string().describe("Unified diff text to apply.")
582
+ }),
583
+ outputSchema: z.object({ applied: z.boolean(), output: z.string() }),
584
+ execute: guard("apply_patch", async (input) => {
585
+ for (const p of extractPatchPaths(input.patch)) {
586
+ resolveInCwd(cwd, p);
587
+ }
588
+ const patchFile = join(tmpdir(), `mastra-agent-patch-${randomUUID()}.diff`);
589
+ await promises.writeFile(patchFile, input.patch, "utf8");
590
+ try {
591
+ const { stdout, stderr } = await execFileAsync(
592
+ "git",
593
+ ["apply", "--whitespace=nowarn", patchFile],
594
+ { cwd, timeout: execTimeoutMs, maxBuffer: 10 * 1024 * 1024, env: execEnv }
595
+ );
596
+ return { applied: true, output: stdout || stderr || "" };
597
+ } catch (err) {
598
+ const e = err;
599
+ throw new Error(`git apply failed: ${e.stderr ?? e.stdout ?? e.message ?? String(err)}`);
600
+ } finally {
601
+ await promises.unlink(patchFile).catch(() => {
602
+ });
603
+ }
604
+ })
605
+ });
606
+ tools.run_tests = createTool({
607
+ id: "run_tests",
608
+ description: "Run the workspace's test command (default `npm test`, overridable via `command` or the MASTRA_AGENT_TEST_CMD env) and return its exit code + output tail.",
609
+ inputSchema: z.object({
610
+ command: z.string().optional().describe("Override the test command. Its argv0 must be one of npm, pnpm, yarn, node, npx.")
611
+ }),
612
+ outputSchema: z.object({ exitCode: z.number(), output: z.string() }),
613
+ execute: guard("run_tests", async (input) => {
614
+ const commandStr = input.command ?? process.env.MASTRA_AGENT_TEST_CMD ?? "npm test";
615
+ const argv0 = commandStr.trim().split(/\s+/)[0];
616
+ if (!argv0 || !ALLOWED_TEST_ARGV0.has(argv0)) {
617
+ throw new Error(
618
+ `run_tests: command '${commandStr}' is not allowed \u2014 argv0 must be one of ${[...ALLOWED_TEST_ARGV0].join(", ")}.`
619
+ );
620
+ }
621
+ try {
622
+ const { stdout, stderr } = await execAsync(commandStr, {
623
+ cwd,
624
+ timeout: execTimeoutMs,
625
+ maxBuffer: 10 * 1024 * 1024,
626
+ env: execEnv
627
+ });
628
+ return { exitCode: 0, output: tail(stdout + stderr) };
629
+ } catch (err) {
630
+ const e = err;
631
+ return {
632
+ exitCode: typeof e.code === "number" ? e.code : 1,
633
+ output: tail((e.stdout ?? "") + (e.stderr ?? e.message ?? String(err)))
634
+ };
635
+ }
636
+ })
637
+ });
638
+ }
639
+ return {
640
+ ...tools,
641
+ ...opts.extraTools
642
+ };
643
+ }
644
+ function tail2(s, maxChars = 4e3) {
645
+ return s.length > maxChars ? s.slice(-maxChars) : s;
646
+ }
647
+ function makeDaemonTools(opts = {}) {
648
+ const client = opts.client ?? new DaemonClient(opts.clientOptions);
649
+ const execTimeoutMs = opts.execTimeoutMs ?? 3e4;
650
+ const guard = (id, execute) => withTimeoutGuard(id, execTimeoutMs, execute);
651
+ const agent_start = createTool({
652
+ id: "agent_start",
653
+ description: "Spawn a new agent session via the agentproto daemon (any adapter: claude-code, hermes, mastra-agent, ...). The spawned session's lineage is recorded as a child of this session when running under a daemon-spawned session. Returns the daemon's session descriptor (includes the new session's id \u2014 pass it to `agent_prompt` / `agent_output`).",
654
+ inputSchema: z.object({
655
+ adapter: z.string().describe("Adapter id to spawn, e.g. 'claude-code', 'hermes', 'mastra-agent'."),
656
+ cwd: z.string().optional().describe("Working directory for the spawned session. Defaults to the daemon's workspace resolution."),
657
+ model: z.string().optional().describe("Model id to spawn the session with, if the adapter supports selecting one."),
658
+ prompt: z.string().optional().describe("Initial prompt to send once the session is up."),
659
+ label: z.string().optional().describe("Human-readable label for the spawned session.")
660
+ }),
661
+ outputSchema: z.record(z.string(), z.unknown()),
662
+ execute: guard(
663
+ "agent_start",
664
+ async (input) => client.startAgent(input)
665
+ )
666
+ });
667
+ const agent_prompt = createTool({
668
+ id: "agent_prompt",
669
+ description: "Send a follow-up message to a live session spawned via `agent_start` (or any other daemon session id). Blocks until the turn drains by default; set `wait: false` to return as soon as the prompt is queued.",
670
+ inputSchema: z.object({
671
+ sessionId: z.string().describe("Target session id (from `agent_start`'s result or `session_list`)."),
672
+ text: z.string().describe("The message to send."),
673
+ interrupt: z.boolean().optional().describe("Cancel the target session's in-flight turn and deliver this prompt instead. Default false."),
674
+ wait: z.boolean().optional().describe("Wait for the turn to drain before returning. Default true.")
675
+ }),
676
+ outputSchema: z.record(z.string(), z.unknown()),
677
+ execute: guard(
678
+ "agent_prompt",
679
+ async (input) => client.promptAgent(input.sessionId, input.text, {
680
+ ...input.interrupt !== void 0 ? { interrupt: input.interrupt } : {},
681
+ ...input.wait !== void 0 ? { wait: input.wait } : {}
682
+ })
683
+ )
684
+ });
685
+ const agent_output = createTool({
686
+ id: "agent_output",
687
+ description: "Read a session's transcript (works for any agent-cli session, not just ones this agent spawned). Output is tailed to the most recent text when it's large.",
688
+ inputSchema: z.object({
689
+ sessionId: z.string().describe("Session id to read."),
690
+ format: z.enum(["markdown", "json"]).optional().describe("Transcript rendering. Default 'json'.")
691
+ }),
692
+ outputSchema: z.object({
693
+ adapter: z.string().optional(),
694
+ content: z.string().optional()
695
+ }).catchall(z.unknown()),
696
+ execute: guard("agent_output", async (input) => {
697
+ const result = await client.readOutput(input.sessionId, {
698
+ ...input.format ? { format: input.format } : {}
699
+ });
700
+ return typeof result.content === "string" ? { ...result, content: tail2(result.content) } : result;
701
+ })
702
+ });
703
+ const session_list = createTool({
704
+ id: "session_list",
705
+ description: "List sessions known to the daemon \u2014 spawned by this agent or any other client.",
706
+ inputSchema: z.object({
707
+ includeArchived: z.boolean().optional().describe("Include archived sessions. Default false."),
708
+ kind: z.string().optional().describe("Filter by session kind (e.g. 'agent-cli', 'command', 'all').")
709
+ }),
710
+ outputSchema: z.object({ sessions: z.array(z.record(z.string(), z.unknown())) }),
711
+ execute: guard(
712
+ "session_list",
713
+ async (input) => client.listSessions(input)
714
+ )
715
+ });
716
+ return { agent_start, agent_prompt, agent_output, session_list };
717
+ }
718
+ function resolveMemoryDbPath(env = process.env) {
719
+ const override = env.AGENTPROTO_MASTRA_MEMORY_DB;
720
+ if (override) return override;
721
+ const dir = join(homedir(), ".agentproto", "mastra-agent");
722
+ mkdirSync(dir, { recursive: true });
723
+ return join(dir, "memory.db");
724
+ }
725
+ function buildSqliteStore(env = process.env) {
726
+ const dbPath = resolveMemoryDbPath(env);
727
+ return new LibSQLStore({ id: "mastra-agent-memory", url: `file:${dbPath}` });
728
+ }
729
+ function buildSqliteMemory(config, env = process.env, store) {
730
+ if (config?.scope === "none") return void 0;
731
+ const lastMessages = typeof config?.retention_turns === "number" && config.retention_turns > 0 ? config.retention_turns : 20;
732
+ return new Memory({
733
+ storage: store ?? buildSqliteStore(env),
734
+ options: {
735
+ lastMessages,
736
+ semanticRecall: false,
737
+ workingMemory: { enabled: false }
738
+ }
739
+ });
740
+ }
741
+ var PROVIDER_ENV = {
742
+ openai: "OPENAI_API_KEY",
743
+ anthropic: "ANTHROPIC_API_KEY",
744
+ openrouter: "OPENROUTER_API_KEY",
745
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
746
+ groq: "GROQ_API_KEY",
747
+ xai: "XAI_API_KEY",
748
+ mistral: "MISTRAL_API_KEY",
749
+ deepseek: "DEEPSEEK_API_KEY"
750
+ };
751
+ function modelRefToString(ref) {
752
+ if (typeof ref === "string") return ref.trim();
753
+ if (ref && typeof ref === "object" && typeof ref.ref === "string") {
754
+ return ref.ref.trim();
755
+ }
756
+ throw new Error(
757
+ "mastra-agent: AGENT.md `model` must be a `provider/model` string (or { ref }); inline model objects are not supported by this adapter."
758
+ );
759
+ }
760
+ function providerOf(modelId) {
761
+ const slash = modelId.indexOf("/");
762
+ return slash > 0 ? modelId.slice(0, slash) : modelId;
763
+ }
764
+ function normalizeModelId(modelId) {
765
+ if (modelId.includes("/")) return modelId;
766
+ if (/^claude[-.]/i.test(modelId)) return `anthropic/${modelId}`;
767
+ return modelId;
768
+ }
769
+ function resolveMastraModel(ref, env = process.env) {
770
+ const modelId = normalizeModelId(modelRefToString(ref));
771
+ if (!modelId) {
772
+ throw new Error("mastra-agent: empty `model` ref.");
773
+ }
774
+ const provider = providerOf(modelId);
775
+ const envKey = PROVIDER_ENV[provider];
776
+ if (envKey && !env[envKey]) {
777
+ throw new Error(
778
+ `mastra-agent: model '${modelId}' needs ${envKey} in the environment (provider '${provider}'). Set it on the spawn env or export it.`
779
+ );
780
+ }
781
+ if (provider === "anthropic") {
782
+ const key = env["ANTHROPIC_API_KEY"];
783
+ if (key?.startsWith("sk-ant-oat")) {
784
+ const bareId = modelId.slice(modelId.indexOf("/") + 1);
785
+ return createAnthropic({ authToken: key })(bareId);
786
+ }
787
+ }
788
+ return modelId;
789
+ }
790
+ var WATCHED_EVENT_KINDS = [
791
+ "turn-end",
792
+ "error",
793
+ "agent-prompt",
794
+ "permission-resolved"
795
+ ];
796
+ var EXIT_STATUSES = /* @__PURE__ */ new Set(["exited", "killed", "error"]);
797
+ var MAX_WARNED_MESSAGES = 200;
798
+ var AgentprotoSignalProvider = class extends SignalProvider {
799
+ id = "agentproto-daemon";
800
+ pollInterval;
801
+ #client;
802
+ #stateEmitter;
803
+ #warned = /* @__PURE__ */ new Set();
804
+ constructor(opts = {}) {
805
+ super();
806
+ this.#client = opts.client ?? new DaemonClient(opts.clientOptions);
807
+ this.#stateEmitter = opts.stateEmitter;
808
+ this.pollInterval = opts.pollInterval ?? 5e3;
809
+ }
810
+ /**
811
+ * Subscribe a thread to a daemon session's lifecycle. The public wrapper
812
+ * around the base class's protected `subscribe` — `sessionId` is the daemon
813
+ * session id (subscription `externalResourceId`). Optional `metadata.label`
814
+ * feeds the notification summary.
815
+ */
816
+ watch(target, sessionId, metadata) {
817
+ return this.subscribe(target, sessionId, metadata);
818
+ }
819
+ /** Undo a {@link watch}. Returns false when no such subscription existed. */
820
+ unwatch(target, sessionId) {
821
+ return this.unsubscribe(target, sessionId);
822
+ }
823
+ /**
824
+ * One poll cycle over all subscriptions. The base class only calls this
825
+ * when at least one subscription exists, so an idle provider (nothing
826
+ * watched) never touches the daemon — that's the "dormant when no daemon
827
+ * is discoverable" behavior: discovery isn't even attempted until a watch
828
+ * exists, and a failing first poll warns once instead of throwing.
829
+ */
830
+ async poll(subscriptions) {
831
+ if (subscriptions.length === 0) return;
832
+ let sessionsById;
833
+ try {
834
+ const { sessions } = await this.#client.listSessions({ includeArchived: true });
835
+ sessionsById = new Map(
836
+ sessions.filter((s) => typeof s.id === "string").map((s) => [s.id, s])
837
+ );
838
+ } catch (err) {
839
+ this.#warnOnce(`listSessions failed (exit detection skipped): ${errorMessage(err)}`);
840
+ }
841
+ for (const sub of subscriptions) {
842
+ try {
843
+ await this.#pollSubscription(sub, sessionsById);
844
+ } catch (err) {
845
+ this.#warnOnce(`session ${sub.externalResourceId}: ${errorMessage(err)}`);
846
+ }
847
+ }
848
+ await this.#emitStateSignals(subscriptions);
849
+ }
850
+ /** WP-7: one daemon-state signal per watching thread per cycle (see
851
+ * {@link AgentprotoSignalProviderOptions.stateEmitter}). Never throws. */
852
+ async #emitStateSignals(subscriptions) {
853
+ const emitter = this.#stateEmitter;
854
+ const agent = this.agent;
855
+ if (!emitter || !agent) return;
856
+ const byTarget = /* @__PURE__ */ new Map();
857
+ for (const sub of subscriptions) {
858
+ const key = `${sub.resourceId}:${sub.threadId}`;
859
+ const entry = byTarget.get(key) ?? {
860
+ threadId: sub.threadId,
861
+ resourceId: sub.resourceId,
862
+ watched: []
863
+ };
864
+ entry.watched.push(sub.externalResourceId);
865
+ byTarget.set(key, entry);
866
+ }
867
+ for (const { threadId, resourceId, watched } of byTarget.values()) {
868
+ try {
869
+ await emitter.emit(agent, { threadId, resourceId }, watched);
870
+ } catch (err) {
871
+ this.#warnOnce(`state signal for thread ${threadId}: ${errorMessage(err)}`);
872
+ }
873
+ }
874
+ }
875
+ async #pollSubscription(sub, sessionsById) {
876
+ const sessionId = sub.externalResourceId;
877
+ const target = {
878
+ threadId: sub.threadId,
879
+ resourceId: sub.resourceId,
880
+ ...sub.metadata.ifIdle ? { ifIdle: sub.metadata.ifIdle } : {}
881
+ };
882
+ const cursor = typeof sub.metadata.cursor === "number" ? sub.metadata.cursor : void 0;
883
+ const result = await this.#client.pollEvents(sessionId, {
884
+ ...cursor !== void 0 ? { since: cursor } : {},
885
+ types: [...WATCHED_EVENT_KINDS]
886
+ });
887
+ for (const [i, event] of result.events.entries()) {
888
+ const kind = typeof event.kind === "string" ? event.kind : "unknown";
889
+ const seq = typeof event.seq === "number" ? String(event.seq) : `i${i}`;
890
+ await this.notify(
891
+ {
892
+ source: this.id,
893
+ kind,
894
+ priority: kind === "error" ? "high" : "medium",
895
+ summary: this.#summary(sessionId, sub, sessionsById, kind),
896
+ payload: event,
897
+ dedupeKey: `${this.id}:${sessionId}:${seq}`
898
+ },
899
+ target
900
+ );
901
+ }
902
+ sub.metadata.cursor = result.nextSeq;
903
+ const record = sessionsById?.get(sessionId);
904
+ if (!record) return;
905
+ const status = typeof record.status === "string" ? record.status : void 0;
906
+ if (!status) return;
907
+ const lastStatus = typeof sub.metadata.lastStatus === "string" ? sub.metadata.lastStatus : void 0;
908
+ sub.metadata.lastStatus = status;
909
+ const alreadyNotified = lastStatus !== void 0 && EXIT_STATUSES.has(lastStatus);
910
+ if (!EXIT_STATUSES.has(status) || alreadyNotified) return;
911
+ const exitedWithError = status === "error" || typeof record.exitCode === "number" && record.exitCode !== 0;
912
+ await this.notify(
913
+ {
914
+ source: this.id,
915
+ kind: "exited",
916
+ priority: exitedWithError ? "high" : "medium",
917
+ summary: `${this.#summary(sessionId, sub, sessionsById, "exited")} (status ${status})`,
918
+ payload: record,
919
+ dedupeKey: `${this.id}:${sessionId}:exited`
920
+ },
921
+ target
922
+ );
923
+ }
924
+ /** `Session <id> (<label>): <kind>` — label from watch metadata, falling
925
+ * back to the daemon's session record. */
926
+ #summary(sessionId, sub, sessionsById, kind) {
927
+ const recordLabel = sessionsById?.get(sessionId)?.label;
928
+ const label = typeof sub.metadata.label === "string" ? sub.metadata.label : typeof recordLabel === "string" ? recordLabel : void 0;
929
+ return `Session ${sessionId}${label ? ` (${label})` : ""}: ${kind}`;
930
+ }
931
+ #warnOnce(message) {
932
+ if (this.#warned.has(message)) return;
933
+ if (this.#warned.size >= MAX_WARNED_MESSAGES) this.#warned.clear();
934
+ this.#warned.add(message);
935
+ console.warn(`[@agentproto/adapter-mastra-agent] ${this.id}: ${message}`);
936
+ }
937
+ /**
938
+ * Agent-callable subscription management. The target thread resolves from
939
+ * the tool's own execution context (`context.agent.threadId`/`resourceId`,
940
+ * populated by the agent tool-call step) — explicit `threadId`/`resourceId`
941
+ * inputs override it for the rare host driving these outside a run.
942
+ *
943
+ * Subscriptions made here set `ifIdle: { behavior: "persist" }`: this
944
+ * adapter fronts an ACP client that only streams during a prompt turn, so
945
+ * WAKING an idle thread would run a hidden background turn nobody sees.
946
+ * Persisting instead lands the notification in the thread history + inbox,
947
+ * where the next real turn picks it up.
948
+ */
949
+ getTools() {
950
+ const watch_session = createTool({
951
+ id: "watch_session",
952
+ description: "Watch a daemon session: subscribe this conversation to its lifecycle (turn ends, errors, permission asks, exit). Events arrive as notifications \u2014 check the notification inbox or the conversation for them. Use session ids from `agent_start` / `session_list`.",
953
+ inputSchema: z.object({
954
+ sessionId: z.string().describe("Daemon session id to watch."),
955
+ label: z.string().optional().describe("Human-readable label used in notification summaries."),
956
+ threadId: z.string().optional().describe("Explicit target thread id. Defaults to the current thread."),
957
+ resourceId: z.string().optional().describe("Explicit target resource id. Defaults to the current resource.")
958
+ }),
959
+ outputSchema: z.object({ subscribed: z.boolean(), sessionId: z.string(), threadId: z.string() }),
960
+ execute: async (input, context) => {
961
+ const threadId = input.threadId ?? context.agent?.threadId;
962
+ const resourceId = input.resourceId ?? context.agent?.resourceId;
963
+ if (!threadId || !resourceId) {
964
+ throw new Error(
965
+ "watch_session: no target thread \u2014 this call ran outside an agent thread, so pass threadId and resourceId explicitly."
966
+ );
967
+ }
968
+ this.watch({ threadId, resourceId, ifIdle: { behavior: "persist" } }, input.sessionId, {
969
+ ...input.label ? { label: input.label } : {},
970
+ ifIdle: { behavior: "persist" }
971
+ });
972
+ return { subscribed: true, sessionId: input.sessionId, threadId };
973
+ }
974
+ });
975
+ const unwatch_session = createTool({
976
+ id: "unwatch_session",
977
+ description: "Stop watching a daemon session previously subscribed with `watch_session`.",
978
+ inputSchema: z.object({
979
+ sessionId: z.string().describe("Daemon session id to stop watching."),
980
+ threadId: z.string().optional().describe("Explicit target thread id. Defaults to the current thread."),
981
+ resourceId: z.string().optional().describe("Explicit target resource id. Defaults to the current resource.")
982
+ }),
983
+ outputSchema: z.object({ unsubscribed: z.boolean(), sessionId: z.string() }),
984
+ execute: async (input, context) => {
985
+ const threadId = input.threadId ?? context.agent?.threadId;
986
+ const resourceId = input.resourceId ?? context.agent?.resourceId;
987
+ if (!threadId || !resourceId) {
988
+ throw new Error(
989
+ "unwatch_session: no target thread \u2014 this call ran outside an agent thread, so pass threadId and resourceId explicitly."
990
+ );
991
+ }
992
+ const unsubscribed = this.unwatch({ threadId, resourceId }, input.sessionId);
993
+ return { unsubscribed, sessionId: input.sessionId };
994
+ }
995
+ });
996
+ return { watch_session, unwatch_session };
997
+ }
998
+ };
999
+ function errorMessage(err) {
1000
+ return err instanceof Error ? err.message : String(err);
1001
+ }
1002
+ var execFileAsync2 = promisify(execFile);
1003
+ var DAEMON_STATE_SIGNAL_ID = "agentproto-daemon-state";
1004
+ var DaemonStateEmitter = class {
1005
+ #client;
1006
+ #cwd;
1007
+ #env;
1008
+ #runGit;
1009
+ #lastByTarget = /* @__PURE__ */ new Map();
1010
+ constructor(opts) {
1011
+ this.#client = opts.client;
1012
+ this.#cwd = opts.cwd ?? process.cwd();
1013
+ this.#env = opts.env ?? process.env;
1014
+ this.#runGit = opts.runGit ?? defaultRunGit;
1015
+ }
1016
+ /**
1017
+ * Compute the current daemon-state snapshot: sessions that are either this
1018
+ * adapter's own daemon children or in `watchedSessionIds`, plus git status.
1019
+ * Either half failing degrades (empty sessions / no git) rather than throws
1020
+ * only for git; a daemon listSessions failure DOES throw — the caller (the
1021
+ * signal provider's poll loop) already isolates and warn-onces it.
1022
+ */
1023
+ async computeSnapshot(watchedSessionIds) {
1024
+ const ownSessionId = this.#env.AGENTPROTO_SESSION_ID;
1025
+ const watched = new Set(watchedSessionIds);
1026
+ const { sessions: rows } = await this.#client.listSessions({ includeArchived: true });
1027
+ const sessions = rows.filter((row) => typeof row.id === "string").filter(
1028
+ (row) => watched.has(row.id) || ownSessionId !== void 0 && row.parentSessionId === ownSessionId
1029
+ ).map(
1030
+ (row) => ({
1031
+ id: row.id,
1032
+ ...typeof row.label === "string" ? { label: row.label } : {},
1033
+ ...typeof row.adapter === "string" ? { adapter: row.adapter } : {},
1034
+ ...typeof row.status === "string" ? { status: row.status } : {},
1035
+ ...typeof row.parentSessionId === "string" ? { parentSessionId: row.parentSessionId } : {}
1036
+ })
1037
+ ).sort((a, b) => a.id.localeCompare(b.id));
1038
+ const git = await this.#gitState();
1039
+ return { sessions, ...git ? { git } : {} };
1040
+ }
1041
+ /**
1042
+ * Compute + send the state signal to one target thread. Returns what was
1043
+ * sent: a full `snapshot` (first emission for this target), a `delta`
1044
+ * (changed since last), or `unchanged` (nothing sent).
1045
+ */
1046
+ async emit(agent, target, watchedSessionIds) {
1047
+ const snapshot = await this.computeSnapshot(watchedSessionIds);
1048
+ const cacheKey = snapshotCacheKey(snapshot);
1049
+ const targetKey = `${target.resourceId}:${target.threadId}`;
1050
+ const prev = this.#lastByTarget.get(targetKey);
1051
+ if (prev?.cacheKey === cacheKey) return "unchanged";
1052
+ const mode = prev ? "delta" : "snapshot";
1053
+ const contents = mode === "snapshot" ? renderSnapshot(snapshot) : renderDelta(prev.snapshot, snapshot);
1054
+ await agent.sendStateSignal(
1055
+ {
1056
+ id: DAEMON_STATE_SIGNAL_ID,
1057
+ cacheKey,
1058
+ mode,
1059
+ contents,
1060
+ value: snapshot,
1061
+ ...mode === "delta" ? { delta: diffSnapshots(prev.snapshot, snapshot) } : {}
1062
+ },
1063
+ { ...target, ifIdle: { behavior: "persist" } }
1064
+ );
1065
+ this.#lastByTarget.set(targetKey, { cacheKey, snapshot });
1066
+ return mode;
1067
+ }
1068
+ async #gitState() {
1069
+ try {
1070
+ const out = await this.#runGit(["status", "--porcelain=v1", "--branch"], this.#cwd);
1071
+ const lines = out.split("\n").filter((line) => line.length > 0);
1072
+ const header = lines[0]?.startsWith("## ") ? lines[0].slice(3) : void 0;
1073
+ const branch = header?.split("...")[0];
1074
+ const dirtyFiles = lines.filter((line) => !line.startsWith("## ")).length;
1075
+ return { ...branch ? { branch } : {}, dirtyFiles };
1076
+ } catch {
1077
+ return void 0;
1078
+ }
1079
+ }
1080
+ };
1081
+ async function defaultRunGit(args, cwd) {
1082
+ const { stdout } = await execFileAsync2("git", args, {
1083
+ cwd,
1084
+ env: {
1085
+ ...process.env,
1086
+ // Stop repo discovery from walking above the workspace (see module doc).
1087
+ GIT_CEILING_DIRECTORIES: dirname(resolve(cwd))
1088
+ },
1089
+ timeout: 1e4
1090
+ });
1091
+ return stdout;
1092
+ }
1093
+ function snapshotCacheKey(snapshot) {
1094
+ return JSON.stringify(snapshot);
1095
+ }
1096
+ function describeSession(s) {
1097
+ const label = s.label ? ` (${s.label})` : "";
1098
+ const adapter = s.adapter ? ` [${s.adapter}]` : "";
1099
+ return `${s.id}${label}${adapter}: ${s.status ?? "unknown"}`;
1100
+ }
1101
+ function renderSnapshot(snapshot) {
1102
+ const lines = ["Daemon state snapshot:"];
1103
+ if (snapshot.sessions.length === 0) {
1104
+ lines.push("- sessions: none (no children or watched sessions)");
1105
+ } else {
1106
+ lines.push("- sessions:");
1107
+ for (const s of snapshot.sessions) lines.push(` - ${describeSession(s)}`);
1108
+ }
1109
+ if (snapshot.git) {
1110
+ lines.push(
1111
+ `- workspace git: ${snapshot.git.branch ?? "?"}, ${snapshot.git.dirtyFiles} dirty file(s)`
1112
+ );
1113
+ }
1114
+ return lines.join("\n");
1115
+ }
1116
+ function diffSnapshots(prev, next) {
1117
+ const prevById = new Map(prev.sessions.map((s) => [s.id, s]));
1118
+ const nextById = new Map(next.sessions.map((s) => [s.id, s]));
1119
+ const addedSessions = next.sessions.filter((s) => !prevById.has(s.id));
1120
+ const removedSessions = prev.sessions.filter((s) => !nextById.has(s.id));
1121
+ const statusChanges = next.sessions.filter((s) => prevById.has(s.id) && prevById.get(s.id).status !== s.status).map((s) => {
1122
+ const from = prevById.get(s.id).status;
1123
+ return { id: s.id, ...from !== void 0 ? { from } : {}, ...s.status !== void 0 ? { to: s.status } : {} };
1124
+ });
1125
+ const gitChanged = JSON.stringify(prev.git) !== JSON.stringify(next.git);
1126
+ return {
1127
+ addedSessions,
1128
+ removedSessions,
1129
+ statusChanges,
1130
+ ...gitChanged ? {
1131
+ git: {
1132
+ ...prev.git ? { from: prev.git } : {},
1133
+ ...next.git ? { to: next.git } : {}
1134
+ }
1135
+ } : {}
1136
+ };
1137
+ }
1138
+ function renderDelta(prev, next) {
1139
+ const delta = diffSnapshots(prev, next);
1140
+ const lines = ["Daemon state changed:"];
1141
+ for (const s of delta.addedSessions) lines.push(`- new session ${describeSession(s)}`);
1142
+ for (const s of delta.removedSessions) lines.push(`- session ${s.id} no longer listed`);
1143
+ for (const c of delta.statusChanges) {
1144
+ lines.push(`- session ${c.id}: ${c.from ?? "unknown"} \u2192 ${c.to ?? "unknown"}`);
1145
+ }
1146
+ if (delta.git) {
1147
+ const to = delta.git.to;
1148
+ lines.push(
1149
+ to ? `- workspace git: ${to.branch ?? "?"}, ${to.dirtyFiles} dirty file(s)` : "- workspace git state no longer readable"
1150
+ );
1151
+ }
1152
+ return lines.join("\n");
1153
+ }
1154
+
1155
+ // src/tool-categories.ts
1156
+ var CATEGORY_BY_TOOL = {
1157
+ list_dir: "read",
1158
+ read_file: "read",
1159
+ read_diff: "read",
1160
+ write_file: "edit",
1161
+ edit_file: "edit",
1162
+ apply_patch: "edit",
1163
+ run_command: "execute",
1164
+ run_tests: "execute",
1165
+ // Daemon verbs (WP-5) — spawning/prompting/reading a sibling session is the
1166
+ // same "reach outside this run" shape as an MCP tool call, so it gets
1167
+ // Mastra's `mcp` category (default policy `ask`, see below).
1168
+ agent_start: "mcp",
1169
+ agent_prompt: "mcp",
1170
+ agent_output: "mcp",
1171
+ session_list: "mcp",
1172
+ // AgentController's built-in `subagent` spawner (WP-5) — spawning an
1173
+ // in-process reviewer is the same "reach outside this run" shape as the
1174
+ // daemon spawn verbs above, so it shares their `mcp` category and default
1175
+ // `ask` policy rather than `other`'s (also `ask` today, but a distinct
1176
+ // category keeps the two spawn surfaces — daemon vs. in-process — tunable
1177
+ // independently later without a behavior change now).
1178
+ subagent: "mcp",
1179
+ // Signal-provider subscription tools (WP-6) — deliberately NOT `mcp` like
1180
+ // the daemon verbs above: watching a session only registers an in-process
1181
+ // subscription whose polling reads session metadata and transcript events,
1182
+ // the same risk class as reading files. It can't spawn, prompt, or mutate
1183
+ // anything outside this process, so it auto-allows as `read` instead of
1184
+ // prompting on every watch.
1185
+ watch_session: "read",
1186
+ unwatch_session: "read",
1187
+ // Notification inbox (WP-7, Mastra's `createNotificationInboxTool`) —
1188
+ // `read`: its mutating actions (markSeen/dismiss/archive) only flip inbox
1189
+ // bookkeeping on the agent's OWN notifications, never workspace or daemon
1190
+ // state. Prompting for approval on every inbox check would defeat the
1191
+ // inbox (the agent is supposed to consult it freely each turn).
1192
+ "notification-inbox": "read",
1193
+ // `submit_plan` (WP-3) gates itself: calling it always suspends the run
1194
+ // for the user's approve/reject decision (see modes.ts's plan mode). It
1195
+ // has no "other"-category behavior worth asking about a second time before
1196
+ // that suspend even runs — see the per-tool "allow" override below.
1197
+ submit_plan: "other"
1198
+ };
1199
+ function toolCategoryResolver(toolName) {
1200
+ return CATEGORY_BY_TOOL[toolName] ?? null;
1201
+ }
1202
+ var DEFAULT_PERMISSION_RULES = {
1203
+ categories: {
1204
+ read: "allow",
1205
+ edit: "ask",
1206
+ execute: "ask",
1207
+ mcp: "ask",
1208
+ other: "ask"
1209
+ },
1210
+ tools: {
1211
+ submit_plan: "allow"
1212
+ }
1213
+ };
1214
+
1215
+ // src/modes.ts
1216
+ var PLAN_MODE = {
1217
+ id: "plan",
1218
+ name: "Plan",
1219
+ description: "Investigate the task and get a plan approved before touching code.",
1220
+ instructions: "Investigate the task: read the relevant code and history, run read-only commands to understand current behavior. Do not write or edit files in this mode. Once you have a plan, call `submit_plan` with the path to the plan you've written for the user to review \u2014 do not paste the plan into chat. Wait for approval before implementing.",
1221
+ availableTools: ["read_file", "list_dir", "read_diff", "run_command", "submit_plan"],
1222
+ transitionsTo: "build",
1223
+ metadata: { default: true }
1224
+ };
1225
+ var BUILD_MODE = {
1226
+ id: "build",
1227
+ name: "Build",
1228
+ description: "Implement the approved plan.",
1229
+ instructions: "Implement the approved plan. Write and edit files and run commands as needed, keeping changes scoped to what the plan described. When the implementation is done, move on to review.",
1230
+ transitionsTo: "review"
1231
+ };
1232
+ var REVIEW_MODE = {
1233
+ id: "review",
1234
+ name: "Review",
1235
+ description: "Review the changes, run checks, and report.",
1236
+ instructions: "Review the changes made in build mode: read the diff, run type-checks and tests, and report the result. If something is broken, describe what's wrong so planning can resume.",
1237
+ availableTools: ["read_file", "read_diff", "run_command", "run_tests", "list_dir"],
1238
+ transitionsTo: "plan"
1239
+ };
1240
+ var DEFAULT_MODES = [PLAN_MODE, BUILD_MODE, REVIEW_MODE];
1241
+ var DEFAULT_MODE_ID = "plan";
1242
+ var MODES_HEADING_RE = /^##\s+Modes\s*$/i;
1243
+ var TOP_HEADING_RE = /^##\s+/;
1244
+ var MODE_SUBHEADING_RE = /^###\s+(.*)$/;
1245
+ function titleCase(id) {
1246
+ return id.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1247
+ }
1248
+ function parseModesFromAgentMd(body) {
1249
+ const lines = body.split(/\r?\n/);
1250
+ let start = -1;
1251
+ for (let i = 0; i < lines.length; i++) {
1252
+ if (MODES_HEADING_RE.test(lines[i])) {
1253
+ start = i + 1;
1254
+ break;
1255
+ }
1256
+ }
1257
+ if (start === -1) return void 0;
1258
+ let end = lines.length;
1259
+ for (let i = start; i < lines.length; i++) {
1260
+ if (TOP_HEADING_RE.test(lines[i])) {
1261
+ end = i;
1262
+ break;
1263
+ }
1264
+ }
1265
+ const sectionLines = lines.slice(start, end);
1266
+ const subsections = [];
1267
+ let current;
1268
+ for (const line of sectionLines) {
1269
+ const heading = MODE_SUBHEADING_RE.exec(line);
1270
+ if (heading) {
1271
+ const id = heading[1].trim();
1272
+ if (!id) {
1273
+ throw new Error("AGENT.md '## Modes': a '### ' subsection has no mode id.");
1274
+ }
1275
+ current = { id, lines: [] };
1276
+ subsections.push(current);
1277
+ } else if (current) {
1278
+ current.lines.push(line);
1279
+ }
1280
+ }
1281
+ if (subsections.length === 0) {
1282
+ throw new Error(
1283
+ "AGENT.md declares a '## Modes' section but no '### <mode-id>' subsections were found \u2014 each mode needs its own '### <id>' heading."
1284
+ );
1285
+ }
1286
+ const seenIds = /* @__PURE__ */ new Set();
1287
+ const modes = [];
1288
+ let explicitDefaultId;
1289
+ for (const { id, lines: bodyLines } of subsections) {
1290
+ if (seenIds.has(id)) {
1291
+ throw new Error(`AGENT.md '## Modes': duplicate mode id '${id}'.`);
1292
+ }
1293
+ seenIds.add(id);
1294
+ let tools;
1295
+ let transitionsTo;
1296
+ let isDefault = false;
1297
+ let bodyStart = 0;
1298
+ for (; bodyStart < bodyLines.length; bodyStart++) {
1299
+ const line = bodyLines[bodyStart].trim();
1300
+ if (line === "") continue;
1301
+ const toolsMatch = /^tools:\s*(.*)$/i.exec(line);
1302
+ const transitionsMatch = /^transitions_to:\s*(.*)$/i.exec(line);
1303
+ const defaultMatch = /^default:\s*(true|false)\s*$/i.exec(line);
1304
+ if (toolsMatch) {
1305
+ tools = toolsMatch[1].split(",").map((t) => t.trim()).filter(Boolean);
1306
+ } else if (transitionsMatch) {
1307
+ transitionsTo = transitionsMatch[1].trim() || void 0;
1308
+ } else if (defaultMatch) {
1309
+ isDefault = defaultMatch[1].toLowerCase() === "true";
1310
+ } else {
1311
+ break;
1312
+ }
1313
+ }
1314
+ const instructions = bodyLines.slice(bodyStart).join("\n").trim();
1315
+ if (isDefault) {
1316
+ if (explicitDefaultId) {
1317
+ throw new Error(
1318
+ `AGENT.md '## Modes': more than one mode flagged 'default: true' ('${explicitDefaultId}', '${id}').`
1319
+ );
1320
+ }
1321
+ explicitDefaultId = id;
1322
+ }
1323
+ modes.push({
1324
+ id,
1325
+ name: titleCase(id),
1326
+ ...instructions ? { instructions } : {},
1327
+ ...tools ? { availableTools: tools } : {},
1328
+ ...transitionsTo ? { transitionsTo } : {}
1329
+ });
1330
+ }
1331
+ const defaultModeId = explicitDefaultId ?? modes[0].id;
1332
+ const defaultMode = modes.find((m) => m.id === defaultModeId);
1333
+ defaultMode.metadata = { ...defaultMode.metadata, default: true };
1334
+ return { modes, defaultModeId };
1335
+ }
1336
+ function resolveModes(body) {
1337
+ return parseModesFromAgentMd(body) ?? {
1338
+ modes: [...DEFAULT_MODES],
1339
+ defaultModeId: DEFAULT_MODE_ID
1340
+ };
1341
+ }
1342
+
1343
+ // src/default-agent.ts
1344
+ var DEFAULT_MODEL = "openrouter/z-ai/glm-5.2";
1345
+ var DEFAULT_TOOL_IDS = [
1346
+ "list_dir",
1347
+ "read_file",
1348
+ "write_file",
1349
+ "edit_file",
1350
+ "run_command"
1351
+ ];
1352
+ function envFlag(value) {
1353
+ return Boolean(value);
1354
+ }
1355
+ function defaultAgentManifest(model) {
1356
+ return [
1357
+ "---",
1358
+ "schema: agent/v1",
1359
+ "id: mastra-agent",
1360
+ "description: A first-party agentproto agent powered by Mastra.",
1361
+ `model: ${model}`,
1362
+ "version: 0.1.0",
1363
+ "tools:",
1364
+ ...DEFAULT_TOOL_IDS.map((id) => ` - ${id}`),
1365
+ "memory:",
1366
+ " scope: per-conversation",
1367
+ " retention_turns: 20",
1368
+ "---",
1369
+ "",
1370
+ "You are a capable, concise coding agent operating inside a workspace ",
1371
+ "directory. You can list, read, write, and edit files and run shell ",
1372
+ "commands there using your tools. Do exactly what the user asks \u2014 when ",
1373
+ "asked to reply with an exact string, reply with only that string.",
1374
+ ""
1375
+ ].join("\n");
1376
+ }
1377
+ function toolRefId(ref) {
1378
+ if (typeof ref === "string") return ref;
1379
+ if (ref && typeof ref === "object" && typeof ref.ref === "string") {
1380
+ return ref.ref;
1381
+ }
1382
+ return void 0;
1383
+ }
1384
+ async function resolveAgentSource(opts = {}) {
1385
+ if (opts.agentFile) return readFile(opts.agentFile, "utf8");
1386
+ return defaultAgentManifest(opts.model ?? DEFAULT_MODEL);
1387
+ }
1388
+ var DISABLED_BUILTIN_TOOL_IDS = [
1389
+ "ask_user",
1390
+ "submit_plan",
1391
+ "task_write",
1392
+ "task_update",
1393
+ "task_complete",
1394
+ "task_check",
1395
+ "subagent"
1396
+ ];
1397
+ var REENABLED_BUILTIN_TOOL_IDS = ["submit_plan", "subagent"];
1398
+ var REVIEWER_ALLOWED_TOOL_IDS = ["read_file", "read_diff", "run_tests", "list_dir"];
1399
+ var REVIEWER_SUBAGENT = {
1400
+ id: "reviewer",
1401
+ name: "Code reviewer",
1402
+ description: "Reviews changes for correctness and reports findings.",
1403
+ instructions: "You are reviewing a change for correctness. Read the diff (`read_diff`) and the files it touches, run the test suite (`run_tests`) if one exists, and report concrete findings: bugs, missed edge cases, and test gaps. You cannot edit files \u2014 report findings back to the caller instead of trying to fix them yourself.",
1404
+ allowedControllerTools: [...REVIEWER_ALLOWED_TOOL_IDS],
1405
+ forked: true
1406
+ };
1407
+ function makeAgentFactory(opts = {}) {
1408
+ return async () => {
1409
+ const source = await resolveAgentSource(opts);
1410
+ const { frontmatter, body } = parseAgentManifest(source);
1411
+ const handle = agentFromManifest({ frontmatter, body });
1412
+ const cwd = opts.cwd ?? process.cwd();
1413
+ const workspaceTools = makeWorkspaceTools({
1414
+ cwd,
1415
+ allowExec: opts.allowExec,
1416
+ extraTools: opts.extraTools
1417
+ });
1418
+ const store = buildSqliteStore();
1419
+ let memory;
1420
+ const historyCompat = new ProviderHistoryCompat({
1421
+ additionalRules: [{
1422
+ name: "strip-trailing-reasoning-from-assistant",
1423
+ applyToPrompt({ prompt }) {
1424
+ let mutated = false;
1425
+ const next = prompt.map((message) => {
1426
+ if (message.role !== "assistant" || !Array.isArray(message.content)) return message;
1427
+ const content = message.content;
1428
+ const last = content[content.length - 1];
1429
+ if (!last || last.type !== "reasoning") return message;
1430
+ const filtered = content.filter((p) => p.type !== "reasoning");
1431
+ mutated = true;
1432
+ return { ...message, content: filtered.length > 0 ? filtered : [{ type: "text", text: "" }] };
1433
+ });
1434
+ return mutated ? next : void 0;
1435
+ }
1436
+ }]
1437
+ });
1438
+ const { agent } = await buildMastraAgent(handle, {
1439
+ inputProcessors: [historyCompat],
1440
+ resolveModel: (ref) => resolveMastraModel(ref),
1441
+ // Match each declared tool ref against the workspace toolset by id. A
1442
+ // ref with no matching executor still resolves — to a stub that fails
1443
+ // fast and clearly on call — rather than being dropped: a dropped ref
1444
+ // leaves the model unable to see it at all, and (if the model still
1445
+ // tries the name from AGENT.md prose) surfaces as an opaque provider
1446
+ // NoSuchToolError this adapter's ACP layer silently swallows (see
1447
+ // tool-call-map.ts), which is how a declared-but-unwired tool used to
1448
+ // hang a turn with zero recorded tool calls instead of failing fast.
1449
+ resolveTool: (ref) => {
1450
+ const id = toolRefId(ref);
1451
+ if (!id) return void 0;
1452
+ const tool = workspaceTools[id];
1453
+ if (tool) return { name: id, tool };
1454
+ console.warn(
1455
+ `[@agentproto/adapter-mastra-agent] agent '${handle.id}' declares tool '${id}' but no executor is wired for it in this adapter's workspace toolset \u2014 calls to it will fail immediately instead of hanging. Wire it in workspace-tools.ts or pass it via extraTools.`
1456
+ );
1457
+ return { name: id, tool: makeUnwiredToolStub(id) };
1458
+ },
1459
+ buildMemory: (config2) => memory = buildSqliteMemory(config2, process.env, store),
1460
+ // The markdown body is the agent's primary system prompt (AIP-42).
1461
+ body
1462
+ });
1463
+ const modesEnabled = opts.modes !== false && !envFlag(process.env.AGENTPROTO_MASTRA_NO_MODES);
1464
+ const { modes, defaultModeId } = modesEnabled ? resolveModes(body) : { modes: [{ id: "main", metadata: { default: true } }], defaultModeId: "main" };
1465
+ let tools = workspaceTools;
1466
+ if (modesEnabled) {
1467
+ const daemonClient = new DaemonClient({ cwd });
1468
+ tools = { ...workspaceTools, ...makeDaemonTools({ client: daemonClient }) };
1469
+ const signalProvider = new AgentprotoSignalProvider({
1470
+ client: daemonClient,
1471
+ stateEmitter: new DaemonStateEmitter({ client: daemonClient, cwd })
1472
+ });
1473
+ signalProvider.connect(agent);
1474
+ signalProvider.startPolling();
1475
+ tools = { ...tools, ...signalProvider.getTools() };
1476
+ const notificationsStorage = await store.getStore("notifications");
1477
+ if (notificationsStorage) {
1478
+ tools = {
1479
+ ...tools,
1480
+ "notification-inbox": createNotificationInboxTool({ storage: notificationsStorage })
1481
+ };
1482
+ }
1483
+ }
1484
+ const config = {
1485
+ id: "agentproto-native",
1486
+ // The AGENT.md-built agent backs every mode; parity mode has exactly
1487
+ // one mode and layers no mode instructions, so runs behave as the raw
1488
+ // agent did.
1489
+ agent,
1490
+ modes,
1491
+ defaultModeId,
1492
+ // Thread rows + per-thread settings persist to the same SQLite file the
1493
+ // agent's Memory writes messages to.
1494
+ storage: store,
1495
+ // Our hardened workspace toolset (+ daemon tools, modes-on) stays the
1496
+ // tool surface. The backing agent already carries the AGENT.md-declared
1497
+ // subset; controller-level tools are an additive toolset, so ids
1498
+ // overlap onto the same executors.
1499
+ tools,
1500
+ // Parity mode disables every built-in controller tool and skips tool
1501
+ // approvals entirely (`yolo: true` keeps `requireToolApproval` off so
1502
+ // tools execute pass-through exactly as the raw stream did). Modes-on
1503
+ // re-enables `submit_plan` (the plan mode's approval gate) and
1504
+ // `subagent` (WP-5's in-process reviewer, see `REVIEWER_SUBAGENT`), and
1505
+ // seeds real per-category tool-approval policy instead.
1506
+ disableBuiltinTools: modesEnabled ? DISABLED_BUILTIN_TOOL_IDS.filter((id) => !REENABLED_BUILTIN_TOOL_IDS.includes(id)) : [...DISABLED_BUILTIN_TOOL_IDS],
1507
+ toolCategoryResolver,
1508
+ initialState: modesEnabled ? { permissionRules: DEFAULT_PERMISSION_RULES } : { yolo: true },
1509
+ // `Session` construction hard-requires a `Workspace` instance
1510
+ // (`createSession` throws "A session requires a valid workspace
1511
+ // instance" without one) — but a *filesystem/sandbox*-backed Workspace
1512
+ // makes Mastra auto-inject its own `mastra_workspace_*` file/exec tools
1513
+ // into every run, duplicating our own hardened workspace toolset. A
1514
+ // skills-only Workspace (no filesystem, no sandbox) satisfies the
1515
+ // former without triggering the latter: `createWorkspaceTools` only
1516
+ // adds filesystem/sandbox tools, so this config contributes none.
1517
+ workspace: new Workspace({ skills: () => [] })
1518
+ };
1519
+ if (memory) config.memory = memory;
1520
+ if (modesEnabled) config.subagents = [REVIEWER_SUBAGENT];
1521
+ return { controller: new AgentController(config) };
1522
+ };
1523
+ }
1524
+
1525
+ // src/tool-call-map.ts
1526
+ function toolKindFor(toolName) {
1527
+ switch (toolName) {
1528
+ case "read_file":
1529
+ case "list_dir":
1530
+ return "read";
1531
+ case "write_file":
1532
+ case "edit_file":
1533
+ return "edit";
1534
+ case "run_command":
1535
+ return "execute";
1536
+ default:
1537
+ return "other";
1538
+ }
1539
+ }
1540
+ function toolCallTitle(toolName, args) {
1541
+ if (args && typeof args === "object") {
1542
+ const { command, path, file } = args;
1543
+ const hint = typeof command === "string" && command || typeof path === "string" && path || typeof file === "string" && file || "";
1544
+ if (hint) return `${toolName}: ${hint}`;
1545
+ }
1546
+ return toolName;
1547
+ }
1548
+ function errorMessage2(error) {
1549
+ if (error instanceof Error) return error.message;
1550
+ if (typeof error === "string") return error;
1551
+ try {
1552
+ return JSON.stringify(error);
1553
+ } catch {
1554
+ return String(error);
1555
+ }
1556
+ }
1557
+ function messageText(message) {
1558
+ const parts = message.content?.parts;
1559
+ if (!Array.isArray(parts)) return "";
1560
+ let text = "";
1561
+ for (const part of parts) {
1562
+ if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
1563
+ text += part.text;
1564
+ }
1565
+ }
1566
+ return text;
1567
+ }
1568
+ function createEventMapper() {
1569
+ const relayed = /* @__PURE__ */ new Map();
1570
+ return (event) => {
1571
+ switch (event.type) {
1572
+ case "message_update": {
1573
+ if (event.message.role !== "assistant") return null;
1574
+ const text = messageText(event.message);
1575
+ const seen = relayed.get(event.message.id) ?? 0;
1576
+ if (text.length <= seen) return null;
1577
+ relayed.set(event.message.id, text.length);
1578
+ return {
1579
+ sessionUpdate: "agent_message_chunk",
1580
+ content: { type: "text", text: text.slice(seen) }
1581
+ };
1582
+ }
1583
+ case "tool_start": {
1584
+ if (!event.toolCallId) return null;
1585
+ const toolName = event.toolName || "tool";
1586
+ return {
1587
+ sessionUpdate: "tool_call",
1588
+ toolCallId: event.toolCallId,
1589
+ title: toolCallTitle(toolName, event.args),
1590
+ kind: toolKindFor(toolName),
1591
+ status: "in_progress",
1592
+ rawInput: event.args
1593
+ };
1594
+ }
1595
+ case "tool_end": {
1596
+ if (!event.toolCallId) return null;
1597
+ return {
1598
+ sessionUpdate: "tool_call_update",
1599
+ toolCallId: event.toolCallId,
1600
+ status: event.isError ? "failed" : "completed",
1601
+ rawOutput: event.isError ? { error: errorMessage2(event.result) } : event.result
1602
+ };
1603
+ }
1604
+ default:
1605
+ return null;
1606
+ }
1607
+ };
1608
+ }
1609
+ var DEFAULT_MODEL_CATALOG = [
1610
+ { id: "openrouter/z-ai/glm-5.2" },
1611
+ { id: "openrouter/deepseek/deepseek-v4-pro" },
1612
+ { id: "openai/gpt-5" }
1613
+ ];
1614
+ var APPROVAL_OPTIONS = [
1615
+ { optionId: "approve", name: "Approve", kind: "allow_once" },
1616
+ {
1617
+ optionId: "always_allow_category",
1618
+ name: "Always allow this category",
1619
+ kind: "allow_always"
1620
+ },
1621
+ { optionId: "decline", name: "Decline", kind: "reject_once" }
1622
+ ];
1623
+ var SUSPENSION_OPTIONS = [
1624
+ { optionId: "approve", name: "Continue", kind: "allow_once" },
1625
+ { optionId: "decline", name: "Cancel", kind: "reject_once" }
1626
+ ];
1627
+ var META_SUSPEND_PAYLOAD = "mastra-agent/suspendPayload";
1628
+ var META_RESUME_SCHEMA = "mastra-agent/resumeSchema";
1629
+ var META_RESUME_DATA = "mastra-agent/resumeData";
1630
+ function emptySessionState() {
1631
+ return {
1632
+ session: null,
1633
+ turn: null,
1634
+ pendingModelId: null,
1635
+ pendingModeId: null,
1636
+ suspensions: /* @__PURE__ */ new Map()
1637
+ };
1638
+ }
1639
+ function promptText(params) {
1640
+ return promptContent(params).text;
1641
+ }
1642
+ function filenameOf(uri) {
1643
+ if (typeof uri !== "string" || !uri) return void 0;
1644
+ return uri.split("/").pop() || void 0;
1645
+ }
1646
+ function textMediaType(mimeType) {
1647
+ if (mimeType && (mimeType.startsWith("text/") || mimeType === "application/json")) {
1648
+ return mimeType;
1649
+ }
1650
+ return "text/plain";
1651
+ }
1652
+ function promptContent(params) {
1653
+ const blocks = Array.isArray(params.prompt) ? params.prompt : [];
1654
+ let text = "";
1655
+ const files = [];
1656
+ for (const block of blocks) {
1657
+ if (!block || typeof block !== "object") continue;
1658
+ switch (block.type) {
1659
+ case "text":
1660
+ if (typeof block.text === "string") text += block.text;
1661
+ break;
1662
+ case "image": {
1663
+ const filename = filenameOf(block.uri);
1664
+ files.push({
1665
+ data: block.data,
1666
+ mediaType: block.mimeType,
1667
+ ...filename ? { filename } : {}
1668
+ });
1669
+ break;
1670
+ }
1671
+ case "audio":
1672
+ files.push({ data: block.data, mediaType: block.mimeType });
1673
+ break;
1674
+ case "resource": {
1675
+ const resource = block.resource;
1676
+ if (!resource || typeof resource !== "object") break;
1677
+ const filename = filenameOf(resource.uri);
1678
+ if ("text" in resource && typeof resource.text === "string") {
1679
+ files.push({
1680
+ data: resource.text,
1681
+ mediaType: textMediaType(resource.mimeType),
1682
+ ...filename ? { filename } : {}
1683
+ });
1684
+ } else if ("blob" in resource && typeof resource.blob === "string") {
1685
+ files.push({
1686
+ data: resource.blob,
1687
+ mediaType: resource.mimeType || "application/octet-stream",
1688
+ ...filename ? { filename } : {}
1689
+ });
1690
+ }
1691
+ break;
1692
+ }
1693
+ }
1694
+ }
1695
+ return { text: text.trim(), files };
1696
+ }
1697
+ function describeError(err) {
1698
+ if (!(err instanceof Error)) return String(err);
1699
+ const msg = err.message || void 0;
1700
+ const causeMsg = err.cause instanceof Error ? err.cause.message : void 0;
1701
+ const detail = msg && msg !== "Error" ? msg : causeMsg || msg || "unknown error";
1702
+ const frames = err.stack?.split("\n").slice(1, 3).map((l) => l.trim()).filter(Boolean);
1703
+ return frames?.length ? `${detail} [${frames.join(" \u2190 ")}]` : detail;
1704
+ }
1705
+ var MastraAcpAgent = class {
1706
+ #conn;
1707
+ #buildController;
1708
+ #resource;
1709
+ #models;
1710
+ #sessions = /* @__PURE__ */ new Map();
1711
+ #controller = null;
1712
+ constructor(conn, buildController, resource = "mastra-agent", models = DEFAULT_MODEL_CATALOG) {
1713
+ this.#conn = conn;
1714
+ this.#buildController = buildController;
1715
+ this.#resource = resource;
1716
+ this.#models = models;
1717
+ }
1718
+ async initialize(_params) {
1719
+ return {
1720
+ protocolVersion: PROTOCOL_VERSION,
1721
+ agentCapabilities: {
1722
+ // session/load reconnects the Mastra thread and replays its history.
1723
+ loadSession: true,
1724
+ promptCapabilities: {
1725
+ image: true,
1726
+ audio: true,
1727
+ embeddedContext: true
1728
+ }
1729
+ }
1730
+ };
1731
+ }
1732
+ async authenticate(_params) {
1733
+ return {};
1734
+ }
1735
+ async newSession(_params) {
1736
+ const sessionId = randomId();
1737
+ this.#sessions.set(sessionId, emptySessionState());
1738
+ return { sessionId };
1739
+ }
1740
+ /**
1741
+ * Resume an existing session: reconnect the controller session (scope +
1742
+ * thread keyed by the ACP session id — `createSession` resumes an existing
1743
+ * Mastra thread with full history) and replay the conversation to the
1744
+ * client, as the `session/load` contract requires, via `user_message_chunk`
1745
+ * / `agent_message_chunk` updates.
1746
+ *
1747
+ * Unlike `prompt`, this NEEDS the controller now (replay reads the thread),
1748
+ * so build errors surface as this request's JSON-RPC error rather than a
1749
+ * first-prompt error chunk.
1750
+ */
1751
+ async loadSession(params) {
1752
+ const sessionId = params.sessionId;
1753
+ let state = this.#sessions.get(sessionId);
1754
+ if (!state) {
1755
+ state = emptySessionState();
1756
+ this.#sessions.set(sessionId, state);
1757
+ }
1758
+ const session = await this.#ensureSession(sessionId, state);
1759
+ const messages = await session.thread.listMessages({ threadId: sessionId });
1760
+ for (const message of messages) {
1761
+ if (message.role !== "user" && message.role !== "assistant") continue;
1762
+ const text = messageText(message);
1763
+ if (!text) continue;
1764
+ await this.#conn.sessionUpdate({
1765
+ sessionId,
1766
+ update: {
1767
+ sessionUpdate: message.role === "user" ? "user_message_chunk" : "agent_message_chunk",
1768
+ content: { type: "text", text }
1769
+ }
1770
+ });
1771
+ }
1772
+ return { configOptions: this.#modelConfigOptions(this.#currentModelId(state)) };
1773
+ }
1774
+ async prompt(params) {
1775
+ const state = this.#sessions.get(params.sessionId);
1776
+ if (!state) throw new Error(`unknown session ${params.sessionId}`);
1777
+ const interrupted = state.turn;
1778
+ if (interrupted) interrupted.cancelled = true;
1779
+ const turn = { cancelled: false };
1780
+ state.turn = turn;
1781
+ const { text, files } = promptContent(params);
1782
+ try {
1783
+ const session = await this.#ensureSession(params.sessionId, state);
1784
+ const map = createEventMapper();
1785
+ let endReason;
1786
+ let lastError = null;
1787
+ let relay = Promise.resolve();
1788
+ let resolveAgentEnd;
1789
+ const agentEndPromise = new Promise((r) => {
1790
+ resolveAgentEnd = r;
1791
+ });
1792
+ const unsubscribe = session.subscribe((event) => {
1793
+ if (event.type === "error") {
1794
+ lastError = event.error;
1795
+ return;
1796
+ }
1797
+ if (event.type === "agent_end") {
1798
+ endReason = event.reason ?? "complete";
1799
+ resolveAgentEnd?.();
1800
+ return;
1801
+ }
1802
+ if (event.type === "tool_approval_required") {
1803
+ const tail3 = relay;
1804
+ void tail3.catch(() => {
1805
+ }).then(() => this.#bridgeApproval(params.sessionId, session, event)).catch(() => {
1806
+ });
1807
+ return;
1808
+ }
1809
+ if (event.type === "tool_suspended") {
1810
+ const pending = { cancelled: false };
1811
+ state.suspensions.set(event.toolCallId, pending);
1812
+ const tail3 = relay;
1813
+ void tail3.catch(() => {
1814
+ }).then(
1815
+ () => this.#bridgeSuspension(params.sessionId, session, state, event, pending)
1816
+ ).catch(() => {
1817
+ });
1818
+ return;
1819
+ }
1820
+ if (event.type === "tool_suspension_cancelled") {
1821
+ const pending = state.suspensions.get(event.toolCallId);
1822
+ if (pending) pending.cancelled = true;
1823
+ state.suspensions.delete(event.toolCallId);
1824
+ return;
1825
+ }
1826
+ const update = map(event);
1827
+ if (!update) return;
1828
+ relay = relay.then(
1829
+ () => this.#conn.sessionUpdate({ sessionId: params.sessionId, update })
1830
+ );
1831
+ });
1832
+ try {
1833
+ if (interrupted) {
1834
+ if (files.length) {
1835
+ session.abort();
1836
+ await session.sendMessage({ content: text, files });
1837
+ } else {
1838
+ await session.steer({ content: text });
1839
+ }
1840
+ } else if (files.length) {
1841
+ await session.sendMessage({ content: text, files });
1842
+ } else {
1843
+ await session.sendMessage({ content: text });
1844
+ }
1845
+ if (!endReason && !lastError && !turn.cancelled) {
1846
+ await agentEndPromise;
1847
+ }
1848
+ } finally {
1849
+ unsubscribe();
1850
+ await relay.catch(() => {
1851
+ });
1852
+ }
1853
+ if (turn.cancelled || endReason === "aborted") {
1854
+ return { stopReason: "cancelled" };
1855
+ }
1856
+ if (endReason === "error") {
1857
+ throw lastError ?? new Error("the agent run ended with an error");
1858
+ }
1859
+ return { stopReason: "end_turn" };
1860
+ } catch (err) {
1861
+ if (turn.cancelled) return { stopReason: "cancelled" };
1862
+ await this.#conn.sessionUpdate({
1863
+ sessionId: params.sessionId,
1864
+ update: {
1865
+ sessionUpdate: "agent_message_chunk",
1866
+ content: {
1867
+ type: "text",
1868
+ text: `
1869
+ [mastra-agent error] ${describeError(err)}
1870
+ `
1871
+ }
1872
+ }
1873
+ });
1874
+ return { stopReason: "refusal" };
1875
+ } finally {
1876
+ if (state.turn === turn) state.turn = null;
1877
+ }
1878
+ }
1879
+ async cancel(params) {
1880
+ const state = this.#sessions.get(params.sessionId);
1881
+ if (!state) return;
1882
+ if (state.turn) state.turn.cancelled = true;
1883
+ state.session?.abort();
1884
+ }
1885
+ /**
1886
+ * The host applies the operator's `model` as a `--model` spawn arg via the
1887
+ * manifest `bin_args_template`, then ALSO calls this ACP config hook (the
1888
+ * daemon's default "config" apply path). Runtime switches go through
1889
+ * `session.model.switch`; a choice made before the controller session
1890
+ * exists is remembered and applied on creation, preserving the invariant
1891
+ * that controller build errors surface on the first prompt.
1892
+ */
1893
+ async setSessionConfigOption(params) {
1894
+ const state = this.#sessions.get(params.sessionId);
1895
+ if (!state) throw new Error(`unknown session ${params.sessionId}`);
1896
+ if (params.configId === "model") {
1897
+ const modelId = String(params.value ?? "");
1898
+ if (modelId) {
1899
+ if (state.session) await state.session.model.switch({ modelId });
1900
+ else state.pendingModelId = modelId;
1901
+ }
1902
+ }
1903
+ return {
1904
+ configOptions: this.#modelConfigOptions(this.#currentModelId(state))
1905
+ };
1906
+ }
1907
+ /** Switch the controller session's mode (the catalog itself is controller
1908
+ * config — WP-3). Mode-change confirmations reach the client via the
1909
+ * session-lifetime `mode_changed` → `current_mode_update` relay. */
1910
+ async setSessionMode(params) {
1911
+ const state = this.#sessions.get(params.sessionId);
1912
+ if (!state) throw new Error(`unknown session ${params.sessionId}`);
1913
+ if (state.session) await state.session.mode.switch({ modeId: params.modeId });
1914
+ else state.pendingModeId = params.modeId;
1915
+ return {};
1916
+ }
1917
+ /** The ACP `model` config option (a select), reflecting `currentValue`.
1918
+ * A free-form current id not in the catalog is appended so the reported
1919
+ * state stays coherent. */
1920
+ #modelConfigOptions(currentValue) {
1921
+ const options = this.#models.map((m) => ({ value: m.id, name: m.name ?? m.id }));
1922
+ if (currentValue && !options.some((o) => o.value === currentValue)) {
1923
+ options.push({ value: currentValue, name: currentValue });
1924
+ }
1925
+ return [
1926
+ {
1927
+ type: "select",
1928
+ id: "model",
1929
+ name: "Model",
1930
+ category: "model",
1931
+ description: "Model id routed via Mastra's model gateway",
1932
+ currentValue,
1933
+ options
1934
+ }
1935
+ ];
1936
+ }
1937
+ #currentModelId(state) {
1938
+ const selected = state.session?.model.get() ?? "";
1939
+ return selected || state.pendingModelId || this.#models[0]?.id || "";
1940
+ }
1941
+ /** Relay a parked `tool_approval_required` gate as an ACP permission
1942
+ * request and feed the user's decision back to the controller session. */
1943
+ async #bridgeApproval(sessionId, session, event) {
1944
+ const toolName = event.toolName || "tool";
1945
+ let decision = "decline";
1946
+ try {
1947
+ const { outcome } = await this.#conn.requestPermission({
1948
+ sessionId,
1949
+ toolCall: {
1950
+ toolCallId: event.toolCallId,
1951
+ title: toolCallTitle(toolName, event.args),
1952
+ kind: toolKindFor(toolName),
1953
+ status: "pending",
1954
+ rawInput: event.args
1955
+ },
1956
+ options: APPROVAL_OPTIONS
1957
+ });
1958
+ if (outcome.outcome === "selected") {
1959
+ if (outcome.optionId === "approve" || outcome.optionId === "always_allow_category") {
1960
+ decision = outcome.optionId;
1961
+ }
1962
+ }
1963
+ } catch {
1964
+ }
1965
+ session.respondToToolApproval({ decision, toolCallId: event.toolCallId });
1966
+ }
1967
+ /**
1968
+ * Relay a `tool_suspended` run as an ACP permission request. ACP's answer is
1969
+ * an option pick, not free-form data, so the suspend payload and resume
1970
+ * schema ride out on the request's `_meta` and the resume data rides back on
1971
+ * the outcome's `_meta` (`mastra-agent/resumeData`) when the client supplies
1972
+ * it — else Continue/Cancel resume with `{ approved: true | false }` (the
1973
+ * shape the built-in `submit_plan` approval path consumes).
1974
+ */
1975
+ async #bridgeSuspension(sessionId, session, state, event, pending) {
1976
+ const toolName = event.toolName || "tool";
1977
+ try {
1978
+ const { outcome } = await this.#conn.requestPermission({
1979
+ sessionId,
1980
+ toolCall: {
1981
+ toolCallId: event.toolCallId,
1982
+ title: toolCallTitle(toolName, event.args),
1983
+ kind: toolKindFor(toolName),
1984
+ status: "in_progress",
1985
+ rawInput: event.args,
1986
+ _meta: {
1987
+ [META_SUSPEND_PAYLOAD]: event.suspendPayload,
1988
+ ...event.resumeSchema !== void 0 ? { [META_RESUME_SCHEMA]: event.resumeSchema } : {}
1989
+ }
1990
+ },
1991
+ options: SUSPENSION_OPTIONS
1992
+ });
1993
+ if (pending.cancelled) return;
1994
+ if (outcome.outcome !== "selected") return;
1995
+ const meta = outcome._meta;
1996
+ const resumeData = meta && META_RESUME_DATA in meta ? meta[META_RESUME_DATA] : { approved: outcome.optionId === "approve" };
1997
+ await session.respondToToolSuspension({
1998
+ resumeData,
1999
+ toolCallId: event.toolCallId
2000
+ });
2001
+ } catch {
2002
+ if (!pending.cancelled) {
2003
+ await session.respondToToolSuspension({
2004
+ resumeData: { approved: false },
2005
+ toolCallId: event.toolCallId
2006
+ }).catch(() => {
2007
+ });
2008
+ }
2009
+ } finally {
2010
+ if (state.suspensions.get(event.toolCallId) === pending) {
2011
+ state.suspensions.delete(event.toolCallId);
2012
+ }
2013
+ }
2014
+ }
2015
+ /** Resolve this ACP session's controller session, creating it on first use.
2016
+ * Deferred to the first prompt (not session/new) on purpose: controller
2017
+ * construction parses the AGENT.md and resolves the model, and those
2018
+ * errors must keep surfacing as a first-prompt error chunk + "refusal",
2019
+ * never as a session/new JSON-RPC failure. (`session/load` opts into the
2020
+ * eager path — replay can't happen without the controller.) */
2021
+ async #ensureSession(acpSessionId, state) {
2022
+ if (state.session) return state.session;
2023
+ const controller = await this.#ensureController();
2024
+ const session = await controller.createSession({
2025
+ resourceId: this.#resource,
2026
+ scope: acpSessionId,
2027
+ threadId: acpSessionId
2028
+ });
2029
+ session.subscribe((event) => {
2030
+ if (event.type === "mode_changed") {
2031
+ void this.#conn.sessionUpdate({
2032
+ sessionId: acpSessionId,
2033
+ update: {
2034
+ sessionUpdate: "current_mode_update",
2035
+ currentModeId: event.modeId
2036
+ }
2037
+ }).catch(() => {
2038
+ });
2039
+ } else if (event.type === "model_changed") {
2040
+ void this.#conn.sessionUpdate({
2041
+ sessionId: acpSessionId,
2042
+ update: {
2043
+ sessionUpdate: "config_option_update",
2044
+ // Read the session's own selection: a `model_changed` scoped to
2045
+ // a non-active mode doesn't move the effective model.
2046
+ configOptions: this.#modelConfigOptions(
2047
+ session.model.get() || event.modelId
2048
+ )
2049
+ }
2050
+ }).catch(() => {
2051
+ });
2052
+ }
2053
+ });
2054
+ state.session = session;
2055
+ if (state.pendingModeId) {
2056
+ const modeId = state.pendingModeId;
2057
+ state.pendingModeId = null;
2058
+ await session.mode.switch({ modeId });
2059
+ }
2060
+ if (state.pendingModelId) {
2061
+ const modelId = state.pendingModelId;
2062
+ state.pendingModelId = null;
2063
+ await session.model.switch({ modelId });
2064
+ }
2065
+ return session;
2066
+ }
2067
+ async #ensureController() {
2068
+ if (this.#controller) return this.#controller;
2069
+ const { controller } = await this.#buildController();
2070
+ await controller.init();
2071
+ this.#controller = controller;
2072
+ return controller;
2073
+ }
2074
+ };
2075
+ function randomId() {
2076
+ const bytes = new Uint8Array(16);
2077
+ crypto.getRandomValues(bytes);
2078
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
2079
+ }
2080
+ function runAcpOverStdio(buildController) {
2081
+ const toClient = Writable.toWeb(process.stdout);
2082
+ const fromClient = Readable.toWeb(
2083
+ process.stdin
2084
+ );
2085
+ const stream = ndJsonStream(toClient, fromClient);
2086
+ return new AgentSideConnection(
2087
+ (conn) => new MastraAcpAgent(conn, buildController),
2088
+ stream
2089
+ );
2090
+ }
2091
+
2092
+ export { DEFAULT_MODEL, DEFAULT_MODEL_CATALOG, DEFAULT_TOOL_IDS, DISABLED_BUILTIN_TOOL_IDS, DaemonClient, DaemonHttpError, DaemonNotFoundError, MastraAcpAgent, buildSqliteMemory, buildSqliteStore, createEventMapper, defaultAgentManifest, discoverDaemonEndpoint, makeAgentFactory, makeDaemonTools, makeWorkspaceTools, messageText, modelRefToString, promptContent, promptText, providerOf, resolveInCwd, resolveMastraModel, resolveMemoryDbPath, runAcpOverStdio, toolCallTitle, toolKindFor };
2093
+ //# sourceMappingURL=chunk-5C2H4JSD.mjs.map
2094
+ //# sourceMappingURL=chunk-5C2H4JSD.mjs.map