@agentproto/runtime 2.1.0 → 2.3.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.mjs CHANGED
@@ -29,12 +29,13 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
29
29
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
30
30
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
31
31
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
32
- import { runWorkflow, compileWorkflow } from '@agentproto/workflow-runtime';
32
+ import { loadAppHandle } from '@agentproto/app-kit';
33
+ import { normalizeToolId } from '@agentproto/driver';
34
+ import { runWorkflow, buildAgentStep, compileWorkflow } from '@agentproto/workflow-runtime';
33
35
  import { loadWorkflowHandle } from '@agentproto/workflow-loader';
34
36
  import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
35
37
  import { Cron } from 'croner';
36
38
  import { parseRoutineManifest } from '@agentproto/routine';
37
- import { normalizeToolId } from '@agentproto/driver';
38
39
  import { createServer as createServer$1 } from 'net';
39
40
  import { promisify } from 'util';
40
41
  import { createRequire } from 'module';
@@ -309,11 +310,16 @@ function createTranscriptWriter(opts) {
309
310
  }, DEBOUNCE_MS);
310
311
  };
311
312
  return {
312
- recordPrompt(sessionId, message) {
313
+ recordPrompt(sessionId, message, opts2) {
313
314
  const state = getState(sessionId);
314
315
  flushBuffers(sessionId, state);
315
316
  const text9 = typeof message === "string" ? message : JSON.stringify(message);
316
- writeRecord(sessionId, state, { kind: "user-prompt", sessionId, text: text9 });
317
+ writeRecord(sessionId, state, {
318
+ kind: "user-prompt",
319
+ sessionId,
320
+ text: text9,
321
+ ...opts2?.source ? { source: opts2.source } : {}
322
+ });
317
323
  },
318
324
  recordEvent(sessionId, evt) {
319
325
  const state = getState(sessionId);
@@ -403,6 +409,22 @@ function createTranscriptWriter(opts) {
403
409
  options: evt.options
404
410
  });
405
411
  break;
412
+ // Durable counterpart to "agent-prompt" — recorded when
413
+ // `respondPermission` (or a dying session's cancel-in-flight sweep)
414
+ // resolves the ask, keyed by the same `toolCallId` so a reader can
415
+ // tell an answered request from a still-pending one without relying
416
+ // on the in-memory pending-permissions map (see sessions.ts's
417
+ // `resolvePendingPermission` / `cancelPendingPermissionsForSession`).
418
+ case "permission-resolved":
419
+ flushBuffers(sessionId, state);
420
+ writeRecord(sessionId, state, {
421
+ kind: "permission-resolved",
422
+ sessionId,
423
+ toolCallId: evt.toolCallId,
424
+ decision: evt.decision,
425
+ ...evt.optionId ? { optionId: evt.optionId } : {}
426
+ });
427
+ break;
406
428
  case "turn-end":
407
429
  flushBuffers(sessionId, state);
408
430
  writeRecord(sessionId, state, { kind: "turn-end", sessionId, reason: evt.reason });
@@ -5115,6 +5137,14 @@ async function spawnAgentSession(deps2, input) {
5115
5137
  };
5116
5138
  });
5117
5139
  }
5140
+ if (mcpServers === void 0 && parentSessionId && buildOrchestratorMcp && input.orchestrator !== false && input.sandbox === void 0) {
5141
+ const injection = buildOrchestratorMcp({
5142
+ tools: ["message_parent"],
5143
+ role: role.name
5144
+ });
5145
+ mcpServers = [injection.entry];
5146
+ bindOrchestratorLifecycle = injection.bindLifecycle;
5147
+ }
5118
5148
  const spawnDefaults = resolveSpawnDefaults(configDefaults, input.adapter, {
5119
5149
  skills: input.skills,
5120
5150
  options: input.options,
@@ -5276,9 +5306,8 @@ async function spawnAgentSession(deps2, input) {
5276
5306
  details: { adapter: input.adapter, gateway }
5277
5307
  };
5278
5308
  }
5279
- let effectivePrompt = input.prompt ? `${composeRoleContext(role, input.promptAppend, roleRegistry)}
5280
-
5281
- ${input.prompt}` : input.prompt;
5309
+ const parentContextLine = parentSessionId ? `You were spawned by session ${parentSessionId} (also available in the ${PARENT_SESSION_ID_ENV} env var). When you finish \u2014 or hit a blocker you cannot resolve \u2014 report back to it via the message_parent tool if one is available (no session id needed; the daemon resolves your parent).` : void 0;
5310
+ let effectivePrompt = input.prompt ? [composeRoleContext(role, input.promptAppend, roleRegistry), parentContextLine, input.prompt].filter((p) => !!p).join("\n\n") : input.prompt;
5282
5311
  const explicitTitle = input.title?.trim() ? input.title.trim() : void 0;
5283
5312
  const spawnLabel = input.label?.trim() ? input.label.trim() : void 0;
5284
5313
  const initialTitle = explicitTitle ?? spawnLabel ?? (input.prompt ? deriveSessionTitle(input.prompt) : void 0);
@@ -5535,7 +5564,12 @@ ${asyncPrompt}`;
5535
5564
  // collide with, so this is the entire env for this spawn.
5536
5565
  env: {
5537
5566
  [SESSION_ID_ENV]: mintedSessionId,
5538
- [WORKSPACE_SLUG_ENV]: resolvedSlug
5567
+ [WORKSPACE_SLUG_ENV]: resolvedSlug,
5568
+ // Lineage (PARENT_SESSION_ID_ENV's doc, sessions.ts) — mirrors
5569
+ // the descriptor's `parentSessionId` so the child can discover
5570
+ // who spawned it without a registry round-trip. Absent on a
5571
+ // parentless root spawn.
5572
+ ...parentSessionId ? { [PARENT_SESSION_ID_ENV]: parentSessionId } : {}
5539
5573
  },
5540
5574
  onActivity: () => {
5541
5575
  if (liveSessionId) registry.pulseActivity(liveSessionId);
@@ -6720,8 +6754,8 @@ function composeSessionObservers(observers) {
6720
6754
  );
6721
6755
  };
6722
6756
  return {
6723
- recordPrompt(sessionId, message) {
6724
- forEachSafe((o) => o.recordPrompt(sessionId, message));
6757
+ recordPrompt(sessionId, message, opts) {
6758
+ forEachSafe((o) => o.recordPrompt(sessionId, message, opts));
6725
6759
  },
6726
6760
  recordEvent(sessionId, evt) {
6727
6761
  forEachSafe((o) => o.recordEvent(sessionId, evt));
@@ -6739,10 +6773,10 @@ function composeSessionObservers(observers) {
6739
6773
  }
6740
6774
  function filterSessionObserver(inner, shouldObserve) {
6741
6775
  return {
6742
- recordPrompt(sessionId, message) {
6776
+ recordPrompt(sessionId, message, opts) {
6743
6777
  if (!shouldObserve(sessionId)) return;
6744
6778
  try {
6745
- inner.recordPrompt(sessionId, message);
6779
+ inner.recordPrompt(sessionId, message, opts);
6746
6780
  } catch {
6747
6781
  }
6748
6782
  },
@@ -7445,6 +7479,7 @@ function mintSessionId() {
7445
7479
  }
7446
7480
  var SESSION_ID_ENV = "AGENTPROTO_SESSION_ID";
7447
7481
  var WORKSPACE_SLUG_ENV = "AGENTPROTO_WORKSPACE_SLUG";
7482
+ var PARENT_SESSION_ID_ENV = "AGENTPROTO_PARENT_SESSION_ID";
7448
7483
  var SessionNotAliveError = class extends Error {
7449
7484
  sessionId;
7450
7485
  status;
@@ -7747,6 +7782,11 @@ function createSessionsRegistry(opts) {
7747
7782
  ts: (/* @__PURE__ */ new Date()).toISOString()
7748
7783
  });
7749
7784
  }
7785
+ transcriptWriter.recordEvent(rt.desc.id, {
7786
+ kind: "permission-resolved",
7787
+ toolCallId: id,
7788
+ decision: "cancelled"
7789
+ });
7750
7790
  }
7751
7791
  delete rt.desc.awaitingPermission;
7752
7792
  };
@@ -7802,6 +7842,12 @@ function createSessionsRegistry(opts) {
7802
7842
  ts: (/* @__PURE__ */ new Date()).toISOString()
7803
7843
  });
7804
7844
  }
7845
+ transcriptWriter.recordEvent(pending.sessionId, {
7846
+ kind: "permission-resolved",
7847
+ toolCallId: id,
7848
+ decision: input.decision,
7849
+ ...chosenOptionId ? { optionId: chosenOptionId } : {}
7850
+ });
7805
7851
  schedulePersist();
7806
7852
  if (!okResolved) {
7807
7853
  return { ok: true, permission: pending, decision: input.decision, ...chosenOptionId ? { optionId: chosenOptionId } : {} };
@@ -8434,7 +8480,7 @@ function createSessionsRegistry(opts) {
8434
8480
  return;
8435
8481
  }
8436
8482
  }
8437
- const runAgentTurn = async (rt, message) => {
8483
+ const runAgentTurn = async (rt, message, turnOpts) => {
8438
8484
  if (!rt.agentSession) {
8439
8485
  throw new Error("runAgentTurn: session has no agentSession");
8440
8486
  }
@@ -8470,7 +8516,11 @@ ${message}`;
8470
8516
  `\x1B[2m\u2500\u2500 \u25B6 ${typeof message === "string" ? message : JSON.stringify(message)} \u2500\u2500\x1B[0m`,
8471
8517
  "stdout"
8472
8518
  );
8473
- transcriptWriter.recordPrompt(rt.desc.id, message);
8519
+ transcriptWriter.recordPrompt(
8520
+ rt.desc.id,
8521
+ message,
8522
+ turnOpts?.promptSource ? { source: turnOpts.promptSource } : void 0
8523
+ );
8474
8524
  const wrapped = typeof message === "string" ? { type: "text", text: message } : message;
8475
8525
  for await (const evt of rt.agentSession.send(wrapped)) {
8476
8526
  transcriptWriter.recordEvent(rt.desc.id, evt);
@@ -8881,7 +8931,11 @@ ${message}`;
8881
8931
  schedulePersist();
8882
8932
  recordConversationLink(rt);
8883
8933
  if (input.initialPrompt) {
8884
- void runAgentTurn(rt, input.initialPrompt).catch((err) => {
8934
+ void runAgentTurn(
8935
+ rt,
8936
+ input.initialPrompt,
8937
+ desc.parentSessionId ? { promptSource: `agent:${desc.parentSessionId}` } : void 0
8938
+ ).catch((err) => {
8885
8939
  appendLine(
8886
8940
  rt,
8887
8941
  `[turn error] ${err instanceof Error ? err.message : String(err)}`,
@@ -9009,7 +9063,11 @@ ${message}`;
9009
9063
  schedulePersist();
9010
9064
  recordConversationLink(rt);
9011
9065
  if (outcome.initialPrompt) {
9012
- void runAgentTurn(rt, outcome.initialPrompt).catch((err) => {
9066
+ void runAgentTurn(
9067
+ rt,
9068
+ outcome.initialPrompt,
9069
+ rt.desc.parentSessionId ? { promptSource: `agent:${rt.desc.parentSessionId}` } : void 0
9070
+ ).catch((err) => {
9013
9071
  appendLine(
9014
9072
  rt,
9015
9073
  `[turn error] ${err instanceof Error ? err.message : String(err)}`,
@@ -9267,7 +9325,7 @@ ${message}`;
9267
9325
  }
9268
9326
  if (rtPre) await maybeResumeAgent(rtPre);
9269
9327
  const rt = validateAgentTurn(id, "sendPrompt");
9270
- await runAgentTurn(rt, message);
9328
+ await runAgentTurn(rt, message, opts2?.source ? { promptSource: opts2.source } : void 0);
9271
9329
  },
9272
9330
  async enqueuePrompt(id, message, opts2) {
9273
9331
  const rtPre = sessions.get(id);
@@ -9279,7 +9337,7 @@ ${message}`;
9279
9337
  }
9280
9338
  await maybeResumeAgent(rtPre);
9281
9339
  const rt = validateAgentTurn(id, "enqueuePrompt");
9282
- void runAgentTurn(rt, message).catch((err) => {
9340
+ void runAgentTurn(rt, message, opts2?.source ? { promptSource: opts2.source } : void 0).catch((err) => {
9283
9341
  appendLine(
9284
9342
  rtPre,
9285
9343
  `[error] ${err instanceof Error ? err.message : String(err)}`,
@@ -10706,12 +10764,17 @@ function registerFsTools(server, opts) {
10706
10764
  const anchor = makeAnchor(opts.workspace);
10707
10765
  server.tool(
10708
10766
  "file_read",
10709
- "Read a UTF-8 file from the workspace.",
10710
- { path: z.string().describe("Workspace-relative path to the file.") },
10711
- async ({ path }) => {
10767
+ `Read a file from the workspace. Defaults to UTF-8 text; pass encoding: "base64" for binary files (images, audio, video, \u2026) \u2014 UTF-8 decoding a non-text file replaces invalid byte sequences (e.g. a PNG's 0x89 header byte) with U+FFFD, corrupting the content.`,
10768
+ {
10769
+ path: z.string().describe("Workspace-relative path to the file."),
10770
+ encoding: z.enum(["utf8", "base64"]).optional().describe(
10771
+ '"utf8" (default) for text files, "base64" for binary files.'
10772
+ )
10773
+ },
10774
+ async ({ path, encoding }) => {
10712
10775
  const abs = anchor(path);
10713
10776
  const buf = await readFile(abs);
10714
- return text(buf.toString("utf8"));
10777
+ return text(buf.toString(encoding === "base64" ? "base64" : "utf8"));
10715
10778
  }
10716
10779
  );
10717
10780
  server.tool(
@@ -11287,8 +11350,10 @@ function registerAgentTools(server, opts) {
11287
11350
  const sessionId = resolveSessionIdArg(input);
11288
11351
  if (!sessionId) return missingSessionIdError("agent_prompt");
11289
11352
  try {
11353
+ const promptSource = callerScope?.ownerSessionId ?? callerSessionId;
11290
11354
  await registry.enqueuePrompt(sessionId, input.prompt, {
11291
- interrupt: input.interrupt
11355
+ interrupt: input.interrupt,
11356
+ ...promptSource ? { source: `agent:${promptSource}` } : {}
11292
11357
  });
11293
11358
  return {
11294
11359
  content: [
@@ -11315,6 +11380,67 @@ function registerAgentTools(server, opts) {
11315
11380
  }
11316
11381
  }
11317
11382
  );
11383
+ server.tool(
11384
+ "message_parent",
11385
+ "Report a message UP to the session that spawned you (your parent/supervisor) \u2014 a result, a progress update, or a blocker. No session id needed: the daemon resolves your recorded parent from your own session identity (also visible as the AGENTPROTO_PARENT_SESSION_ID env var). Delivered as a prompt when the parent is idle, or queued onto its next turn when it's mid-turn (never interrupts). Errors if this session has no recorded parent or the parent is gone.",
11386
+ {
11387
+ message: z.string().min(1).describe("The message to deliver to your parent session (plain text).")
11388
+ },
11389
+ async (input) => {
11390
+ const fail = (text9) => ({
11391
+ content: [{ type: "text", text: text9 }],
11392
+ isError: true
11393
+ });
11394
+ const selfId = callerScope?.ownerSessionId ?? callerSessionId;
11395
+ if (!selfId) {
11396
+ return fail(
11397
+ "message_parent: cannot identify the calling session \u2014 this tool needs gateway access attributed to a session (a scoped orchestrator gateway, or a daemon `/mcp` URL carrying `?callerSessionId=`). A human/root caller has no parent to message."
11398
+ );
11399
+ }
11400
+ const self = registry.get(selfId);
11401
+ if (!self) {
11402
+ return fail(`message_parent: calling session "${selfId}" is not in the registry.`);
11403
+ }
11404
+ const parentId = self.parentSessionId;
11405
+ if (!parentId || parentId === selfId) {
11406
+ return fail(
11407
+ "message_parent: this session has no recorded parent \u2014 it was spawned at the root, so there is no one to report up to."
11408
+ );
11409
+ }
11410
+ const parent = registry.get(parentId);
11411
+ if (!parent) {
11412
+ return fail(`message_parent: parent session "${parentId}" no longer exists.`);
11413
+ }
11414
+ if (parent.status !== "running" && parent.status !== "starting") {
11415
+ return fail(
11416
+ `message_parent: parent session "${parentId}" is not running (status: ${parent.status}) \u2014 the message cannot be delivered.`
11417
+ );
11418
+ }
11419
+ const who = self.label ?? selfId;
11420
+ const notice = `[child-message] ${who} (${selfId}): ${input.message}`;
11421
+ const done = (delivery) => ({
11422
+ content: [
11423
+ {
11424
+ type: "text",
11425
+ text: JSON.stringify({ ok: true, parentSessionId: parentId, delivery }, null, 2)
11426
+ }
11427
+ ]
11428
+ });
11429
+ if (!parent.busy) {
11430
+ try {
11431
+ await registry.enqueuePrompt(parentId, notice, {});
11432
+ return done("enqueued");
11433
+ } catch {
11434
+ }
11435
+ }
11436
+ if (!registry.stampPendingChildCrashNotice(parentId, notice)) {
11437
+ return fail(
11438
+ `message_parent: parent session "${parentId}" vanished mid-delivery.`
11439
+ );
11440
+ }
11441
+ return done("queued-next-turn");
11442
+ }
11443
+ );
11318
11444
  server.tool(
11319
11445
  "agent_output",
11320
11446
  "Tail the recent output of a session. Returns the last N lines of the ring buffer (stdout + stderr inter-leaved, newest last). Use this to read an agent's reply after `agent_prompt`.",
@@ -21944,6 +22070,18 @@ async function startHttpServer(opts) {
21944
22070
  const handled = await handleRoutineDefs(req, res, path, opts.routineRegistrar);
21945
22071
  if (handled) return;
21946
22072
  }
22073
+ if (opts.appRegistry && opts.performAppInstall && opts.listRegisteredToolIds && (path.startsWith("/apps/") || path.startsWith("/scopes/"))) {
22074
+ const handled = await handleApps(
22075
+ req,
22076
+ res,
22077
+ path,
22078
+ opts.appRegistry,
22079
+ opts.performAppInstall,
22080
+ opts.listRegisteredToolIds,
22081
+ opts.resolveAgentAdapter
22082
+ );
22083
+ if (handled) return;
22084
+ }
21947
22085
  res.writeHead(404, { "content-type": "application/json" });
21948
22086
  res.end(JSON.stringify({ error: "not_found", path }));
21949
22087
  } catch (err) {
@@ -24152,6 +24290,116 @@ async function handleProviderInbound(req, res, slug, deps2) {
24152
24290
  res.writeHead(200, { "content-type": "application/json" });
24153
24291
  res.end(JSON.stringify(result));
24154
24292
  }
24293
+ async function handleApps(req, res, path, appRegistry, performInstall2, listRegisteredToolIds, resolveAgentAdapter) {
24294
+ const method = req.method ?? "GET";
24295
+ const applyMatch = path.match(/^\/apps\/([^/]+)\/apply$/);
24296
+ if (applyMatch && method === "POST") {
24297
+ const appId = decodeURIComponent(applyMatch[1]);
24298
+ const body = await readJsonBody(req);
24299
+ const scopeId = body?.scopeId ?? "root";
24300
+ let installed = appRegistry.getApp(appId);
24301
+ if (!installed && body?.dir) {
24302
+ const installResult = await performInstall2(body.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter);
24303
+ if (!installResult.ok) {
24304
+ res.writeHead(400, { "content-type": "application/json" });
24305
+ res.end(JSON.stringify({ error: installResult.error }));
24306
+ return true;
24307
+ }
24308
+ installed = installResult.record;
24309
+ }
24310
+ if (!installed) {
24311
+ res.writeHead(400, { "content-type": "application/json" });
24312
+ res.end(
24313
+ JSON.stringify({
24314
+ error: `app "${appId}" is not installed. Either call app_install first or provide a 'dir' parameter.`
24315
+ })
24316
+ );
24317
+ return true;
24318
+ }
24319
+ if (installed.requires && installed.requires.length > 0) {
24320
+ const applied = appRegistry.listApplied(scopeId);
24321
+ const appliedIds = new Set(applied.map((m) => m.appId));
24322
+ const missing = installed.requires.filter((reqId) => !appliedIds.has(reqId));
24323
+ if (missing.length > 0) {
24324
+ res.writeHead(400, { "content-type": "application/json" });
24325
+ res.end(
24326
+ JSON.stringify({
24327
+ error: `app "${appId}" requires the following apps to be applied to scope "${scopeId}" first: ${missing.join(", ")}`
24328
+ })
24329
+ );
24330
+ return true;
24331
+ }
24332
+ }
24333
+ const mount = appRegistry.applyApp({ scopeId, appId });
24334
+ res.writeHead(200, { "content-type": "application/json" });
24335
+ res.end(
24336
+ JSON.stringify({
24337
+ scopeId: mount.scopeId,
24338
+ appId: mount.appId,
24339
+ appliedAt: mount.appliedAt,
24340
+ agents: installed.agents,
24341
+ workflows: installed.workflows,
24342
+ unvalidatedAgentTools: installed.unvalidatedAgentTools
24343
+ })
24344
+ );
24345
+ return true;
24346
+ }
24347
+ if (applyMatch && method === "DELETE") {
24348
+ const appId = decodeURIComponent(applyMatch[1]);
24349
+ const body = await readJsonBody(req);
24350
+ const url = new URL(req.url ?? "", `http://${req.headers.host}`);
24351
+ const scopeId = body?.scopeId ?? url.searchParams.get("scopeId") ?? "root";
24352
+ const applied = appRegistry.listApplied(scopeId);
24353
+ const dependents = [];
24354
+ for (const mount of applied) {
24355
+ if (mount.appId === appId) continue;
24356
+ const app = appRegistry.getApp(mount.appId);
24357
+ if (app?.requires?.includes(appId)) {
24358
+ dependents.push(mount.appId);
24359
+ }
24360
+ }
24361
+ if (dependents.length > 0) {
24362
+ res.writeHead(400, { "content-type": "application/json" });
24363
+ res.end(
24364
+ JSON.stringify({
24365
+ error: `cannot unapply app "${appId}" from scope "${scopeId}" \u2014 the following apps in this scope require it: ${dependents.join(", ")}`
24366
+ })
24367
+ );
24368
+ return true;
24369
+ }
24370
+ const removed = appRegistry.unapplyApp({ scopeId, appId });
24371
+ if (!removed) {
24372
+ res.writeHead(404, { "content-type": "application/json" });
24373
+ res.end(JSON.stringify({ error: `app "${appId}" is not applied to scope "${scopeId}".` }));
24374
+ return true;
24375
+ }
24376
+ res.writeHead(200, { "content-type": "application/json" });
24377
+ res.end(JSON.stringify({ scopeId: removed.scopeId, appId: removed.appId, appliedAt: removed.appliedAt }));
24378
+ return true;
24379
+ }
24380
+ const scopesMatch = path.match(/^\/scopes\/([^/]+)\/apps$/);
24381
+ if (scopesMatch && method === "GET") {
24382
+ const scopeId = decodeURIComponent(scopesMatch[1]);
24383
+ const mounts = appRegistry.listApplied(scopeId);
24384
+ const result = mounts.map((mount) => {
24385
+ const app = appRegistry.getApp(mount.appId);
24386
+ return {
24387
+ scopeId: mount.scopeId,
24388
+ appId: mount.appId,
24389
+ appliedAt: mount.appliedAt,
24390
+ ...app ? {
24391
+ agents: app.agents,
24392
+ workflows: app.workflows,
24393
+ unvalidatedAgentTools: app.unvalidatedAgentTools
24394
+ } : {}
24395
+ };
24396
+ });
24397
+ res.writeHead(200, { "content-type": "application/json" });
24398
+ res.end(JSON.stringify(result));
24399
+ return true;
24400
+ }
24401
+ return false;
24402
+ }
24155
24403
  async function readJsonBody(req) {
24156
24404
  const chunks = [];
24157
24405
  for await (const chunk of req) {
@@ -25260,6 +25508,525 @@ stderr: ${stderrText.slice(-600)}` : msg);
25260
25508
  `import "${entry.alias}" has unsupported transport type "${snap.type}"`
25261
25509
  );
25262
25510
  }
25511
+ function collectToolIds(steps, out) {
25512
+ for (const step of steps) {
25513
+ switch (step.kind) {
25514
+ case "tool": {
25515
+ const ref = typeof step.tool === "string" ? step.tool : step.tool.entry;
25516
+ out.add(normalizeToolId(ref));
25517
+ break;
25518
+ }
25519
+ case "map":
25520
+ case "loop":
25521
+ collectToolIds(step.steps, out);
25522
+ break;
25523
+ case "parallel":
25524
+ for (const branch of step.branches) collectToolIds(branch.steps, out);
25525
+ break;
25526
+ }
25527
+ }
25528
+ }
25529
+ function passthroughToolHandle(id) {
25530
+ return {
25531
+ id,
25532
+ name: id,
25533
+ description: `Daemon tool '${id}', dispatched via the routine/cron dispatchTool bridge.`,
25534
+ mutates: [],
25535
+ requires: { network: [], secrets: [], tools: [] },
25536
+ approval: "auto",
25537
+ riskLevel: 0,
25538
+ costClass: "trivial",
25539
+ timeoutMs: 3e4,
25540
+ tags: [],
25541
+ metadata: {},
25542
+ idempotent: false,
25543
+ driverConstraints: { forbid: [], requireKind: [] }
25544
+ };
25545
+ }
25546
+ function unwrapDispatchResult(result) {
25547
+ if (!result || typeof result !== "object" || !("content" in result)) return result;
25548
+ const r = result;
25549
+ const text9 = (r.content ?? []).map((c) => c.text ?? "").join("\n").trim();
25550
+ if (r.isError) throw new Error(text9 || "tool call failed");
25551
+ if (!text9) return void 0;
25552
+ try {
25553
+ return JSON.parse(text9);
25554
+ } catch {
25555
+ return text9;
25556
+ }
25557
+ }
25558
+ function createDaemonToolRegistry(handle, dispatchTool) {
25559
+ const toolIds = /* @__PURE__ */ new Set();
25560
+ collectToolIds(handle.steps, toolIds);
25561
+ const tools = {};
25562
+ const execute = {};
25563
+ for (const id of toolIds) {
25564
+ tools[id] = passthroughToolHandle(id);
25565
+ execute[id] = async ({ input }) => unwrapDispatchResult(await dispatchTool(id, input ?? {}));
25566
+ }
25567
+ if (toolIds.size === 0) return { tools, candidates: [] };
25568
+ const daemonDriver = {
25569
+ id: "daemon-tool-dispatch",
25570
+ name: "Daemon tool dispatch",
25571
+ description: "Catch-all AIP-30 provider routing every referenced WORKFLOW.md tool step through the daemon's dispatchTool.",
25572
+ kind: "builtin",
25573
+ implements: [...toolIds].map((id) => ({ tool: id, version: "*" })),
25574
+ execute,
25575
+ install: [],
25576
+ network: { egress: [], ingress: [] },
25577
+ region: ["global"],
25578
+ policyTags: [],
25579
+ tags: [],
25580
+ metadata: {}
25581
+ };
25582
+ return { tools, candidates: [daemonDriver] };
25583
+ }
25584
+ var DEFAULT_PERSIST_PATH2 = () => join(homedir(), ".agentproto", "apps.json");
25585
+ function loadState(persistPath) {
25586
+ const empty = { apps: [], runs: [], applied: [] };
25587
+ if (!existsSync(persistPath)) return empty;
25588
+ let raw;
25589
+ try {
25590
+ raw = readFileSync(persistPath, "utf8");
25591
+ } catch {
25592
+ return empty;
25593
+ }
25594
+ try {
25595
+ const parsed = JSON.parse(raw);
25596
+ return {
25597
+ apps: Array.isArray(parsed.apps) ? parsed.apps : [],
25598
+ runs: Array.isArray(parsed.runs) ? parsed.runs : [],
25599
+ applied: Array.isArray(parsed.applied) ? parsed.applied : []
25600
+ };
25601
+ } catch {
25602
+ return empty;
25603
+ }
25604
+ }
25605
+ function saveState(state, persistPath) {
25606
+ try {
25607
+ mkdirSync(dirname(persistPath), { recursive: true });
25608
+ const tmp = `${persistPath}.tmp.${process.pid}`;
25609
+ writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
25610
+ renameSync(tmp, persistPath);
25611
+ } catch {
25612
+ }
25613
+ }
25614
+ function createAppRegistry(opts) {
25615
+ const persistPath = opts?.persistPath ?? DEFAULT_PERSIST_PATH2();
25616
+ const shouldPersist = opts?.persist ?? opts?.persistPath !== void 0;
25617
+ const state = shouldPersist ? loadState(persistPath) : { apps: [], runs: [], applied: [] };
25618
+ const persist = () => {
25619
+ if (shouldPersist) saveState(state, persistPath);
25620
+ };
25621
+ return {
25622
+ upsertApp(input) {
25623
+ const now = (/* @__PURE__ */ new Date()).toISOString();
25624
+ const idx = state.apps.findIndex((a) => a.appId === input.appId);
25625
+ const record2 = {
25626
+ ...input,
25627
+ installedAt: idx === -1 ? now : state.apps[idx].installedAt,
25628
+ updatedAt: now
25629
+ };
25630
+ if (idx === -1) state.apps.push(record2);
25631
+ else state.apps[idx] = record2;
25632
+ persist();
25633
+ return record2;
25634
+ },
25635
+ getApp(appId) {
25636
+ return state.apps.find((a) => a.appId === appId);
25637
+ },
25638
+ listApps() {
25639
+ return [...state.apps];
25640
+ },
25641
+ createRun(input) {
25642
+ const run = {
25643
+ appRunId: `apprun_${randomUUID()}`,
25644
+ appId: input.appId,
25645
+ sessions: input.sessions,
25646
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
25647
+ status: "running"
25648
+ };
25649
+ state.runs.push(run);
25650
+ persist();
25651
+ return run;
25652
+ },
25653
+ getRun(appRunId) {
25654
+ return state.runs.find((r) => r.appRunId === appRunId);
25655
+ },
25656
+ listRuns() {
25657
+ return [...state.runs];
25658
+ },
25659
+ endRun(appRunId) {
25660
+ const run = state.runs.find((r) => r.appRunId === appRunId);
25661
+ if (!run) return void 0;
25662
+ run.status = "stopped";
25663
+ run.endedAt = (/* @__PURE__ */ new Date()).toISOString();
25664
+ persist();
25665
+ return run;
25666
+ },
25667
+ applyApp(input) {
25668
+ const idx = state.applied.findIndex(
25669
+ (m) => m.scopeId === input.scopeId && m.appId === input.appId
25670
+ );
25671
+ const mount = {
25672
+ scopeId: input.scopeId,
25673
+ appId: input.appId,
25674
+ appliedAt: idx === -1 ? (/* @__PURE__ */ new Date()).toISOString() : state.applied[idx].appliedAt
25675
+ };
25676
+ if (idx === -1) state.applied.push(mount);
25677
+ else state.applied[idx] = mount;
25678
+ persist();
25679
+ return mount;
25680
+ },
25681
+ unapplyApp(input) {
25682
+ const idx = state.applied.findIndex(
25683
+ (m) => m.scopeId === input.scopeId && m.appId === input.appId
25684
+ );
25685
+ if (idx === -1) return void 0;
25686
+ const removed = state.applied[idx];
25687
+ state.applied.splice(idx, 1);
25688
+ persist();
25689
+ return removed;
25690
+ },
25691
+ listApplied(scopeId) {
25692
+ if (scopeId === void 0) return [...state.applied];
25693
+ return state.applied.filter((m) => m.scopeId === scopeId);
25694
+ }
25695
+ };
25696
+ }
25697
+
25698
+ // src/app-tools.ts
25699
+ var DEFAULT_AGENT_ADAPTER = "mastra-agent";
25700
+ function resolveAgentRefsForWorkflow(appRegistry, workflowId) {
25701
+ const app = appRegistry.listApps().find((a) => a.workflows.some((w) => w.id === workflowId));
25702
+ if (!app) return void 0;
25703
+ const refs = {};
25704
+ for (const agent of app.agents) {
25705
+ refs[agent.id] = { adapter: DEFAULT_AGENT_ADAPTER, options: { agent: agent.path } };
25706
+ }
25707
+ return refs;
25708
+ }
25709
+ function textResult(body) {
25710
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
25711
+ }
25712
+ function errorResult(text9) {
25713
+ return { content: [{ type: "text", text: JSON.stringify({ error: text9 }) }], isError: true };
25714
+ }
25715
+ function refIdOf(ref) {
25716
+ if (typeof ref === "string") return ref;
25717
+ return ref.ref ?? ref.file ?? "inline";
25718
+ }
25719
+ function resolveRef(dir, path) {
25720
+ return isAbsolute(path) ? path : join(dir, path);
25721
+ }
25722
+ async function readAppRefs(dir) {
25723
+ const appPath = join(dir, ".agentproto", "APP.md");
25724
+ const source = await readFile(appPath, "utf8");
25725
+ const { data } = matter(source);
25726
+ const toRefs = (v) => Array.isArray(v) ? v.filter(
25727
+ (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.path === "string"
25728
+ ).map((e) => ({ id: e.id, path: resolveRef(dir, e.path) })) : [];
25729
+ return { agents: toRefs(data.agents), workflows: toRefs(data.workflows) };
25730
+ }
25731
+ async function performInstall(dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter) {
25732
+ let handle;
25733
+ try {
25734
+ handle = await loadAppHandle(dir);
25735
+ } catch (err) {
25736
+ return { ok: false, error: `${err instanceof Error ? err.message : String(err)}` };
25737
+ }
25738
+ if (!handle.id) {
25739
+ return { ok: false, error: "the app has no `id` \u2014 set one in defineApp()/APP.md frontmatter to install it." };
25740
+ }
25741
+ const missingByWorkflow = {};
25742
+ const registeredIds = new Set(await listRegisteredToolIds());
25743
+ for (const workflow of handle.workflows) {
25744
+ const { tools } = createDaemonToolRegistry(workflow, async () => void 0);
25745
+ const missing = Object.keys(tools).filter((id) => !registeredIds.has(id));
25746
+ if (missing.length > 0) missingByWorkflow[workflow.id] = missing;
25747
+ }
25748
+ if (Object.keys(missingByWorkflow).length > 0) {
25749
+ return {
25750
+ ok: false,
25751
+ error: `unknown daemon tool id(s) referenced by workflow step(s) \u2014 would otherwise fail at STEP-DISPATCH time: ${JSON.stringify(missingByWorkflow)}`
25752
+ };
25753
+ }
25754
+ if (handle.agents.length > 0) {
25755
+ const resolved = resolveAgentAdapter ? await resolveAgentAdapter(DEFAULT_AGENT_ADAPTER) : null;
25756
+ if (!resolved) {
25757
+ return {
25758
+ ok: false,
25759
+ error: `agent adapter "${DEFAULT_AGENT_ADAPTER}" could not be resolved \u2014 run \`agentproto install ${DEFAULT_AGENT_ADAPTER}\` first.`
25760
+ };
25761
+ }
25762
+ }
25763
+ const refs = await readAppRefs(dir);
25764
+ const unvalidatedAgentTools = [
25765
+ ...new Set(handle.agents.flatMap((e) => (e.agent.tools ?? []).map(refIdOf)))
25766
+ ];
25767
+ const record2 = appRegistry.upsertApp({
25768
+ appId: handle.id,
25769
+ dir,
25770
+ ...handle.version ? { version: handle.version } : {},
25771
+ ...handle.name ? { name: handle.name } : {},
25772
+ agents: refs.agents,
25773
+ workflows: refs.workflows,
25774
+ unvalidatedAgentTools,
25775
+ ...handle.requires ? { requires: handle.requires } : {}
25776
+ });
25777
+ return { ok: true, record: record2 };
25778
+ }
25779
+ function registerAppTools(server, opts) {
25780
+ const { registry, resolveAgentAdapter, listRegisteredToolIds, workflowRunner } = opts;
25781
+ const appRegistry = opts.appRegistry ?? createAppRegistry({
25782
+ ...opts.persistPath !== void 0 ? { persistPath: opts.persistPath } : {},
25783
+ ...opts.persist !== void 0 ? { persist: opts.persist } : {}
25784
+ });
25785
+ const notEnabled = (tool) => errorResult(
25786
+ `${tool} is not enabled \u2014 the daemon was started without an adapter resolver. Re-run the daemon with the \`@agentproto/cli\` shim wired (see playground/scripts/gateway.ts).`
25787
+ );
25788
+ server.tool(
25789
+ "app_install",
25790
+ "Install an @agentproto/app-kit app from its emitted directory (`<dir>/.agentproto/APP.md` \u2014 see `defineApp().emit(dir)`). Validates every WORKFLOW.md `tool` step's id against the daemon's dispatchable tools (missing ids are reported ALL at once, instead of failing one at a time at STEP-DISPATCH time) and checks the `mastra-agent` adapter resolves. Agent-declared tool refs (workspace tools like `read_file`) are the adapter's own business and are never validated here \u2014 see `unvalidatedAgentTools` on the result. Re-installing the same appId upserts.",
25791
+ { dir: z.string().describe("Absolute path to the app's directory.") },
25792
+ async (input) => {
25793
+ const result = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter);
25794
+ if (!result.ok) return errorResult(`app_install: ${result.error}`);
25795
+ return textResult(result.record);
25796
+ }
25797
+ );
25798
+ server.tool(
25799
+ "app_list",
25800
+ "List installed apps, each with a summary of its app_run history.",
25801
+ {},
25802
+ async () => {
25803
+ const runs = appRegistry.listRuns();
25804
+ const apps = appRegistry.listApps().map((app) => ({
25805
+ ...app,
25806
+ runs: runs.filter((r) => r.appId === app.appId).map((r) => ({
25807
+ appRunId: r.appRunId,
25808
+ status: r.status,
25809
+ startedAt: r.startedAt,
25810
+ ...r.endedAt ? { endedAt: r.endedAt } : {},
25811
+ sessions: r.sessions.length
25812
+ }))
25813
+ }));
25814
+ return textResult(apps);
25815
+ }
25816
+ );
25817
+ server.tool(
25818
+ "app_run",
25819
+ "Run an installed app's agents as live sessions \u2014 one `agent_start`-equivalent spawn per selected agent (adapter `mastra-agent`, pointed at that agent's emitted AGENT.md via the adapter's `agent` option), grouped under a fresh appRunId. Re-reads the app's directory first, so a stale install record (paths moved, a workflow renamed) is refreshed before spawning \u2014 the same refreshed paths are what make `workflow_run_file` work against this app's WORKFLOW.md files. Poll with `app_status`, kill with `app_stop`.",
25820
+ {
25821
+ appId: z.string(),
25822
+ agents: z.array(z.string()).optional().describe("Agent ids to run. Omit to run every agent the app bundles."),
25823
+ prompt: z.string().optional().describe("Prompt to send to each spawned agent session."),
25824
+ cwd: z.string().optional().describe("Working directory for spawned sessions. Defaults to the app's installed `dir`."),
25825
+ scopeId: z.string().optional().describe("When passed, refuse to run if the app is not applied to this scope.")
25826
+ // follow-up: no sandbox support in this WP — the e2b image doesn't carry
25827
+ // the mastra-agent adapter yet (see output/phase-a-findings.md A3). Thread
25828
+ // a `sandbox` field through to `spawnAgentSession` here once an image
25829
+ // provisions it (or `app_install`/boot does `agentproto install
25830
+ // mastra-agent` inside the box).
25831
+ },
25832
+ async (input) => {
25833
+ if (!resolveAgentAdapter) return notEnabled("app_run");
25834
+ const installed = appRegistry.getApp(input.appId);
25835
+ if (!installed) {
25836
+ return errorResult(`app_run: no installed app "${input.appId}" \u2014 call app_install first.`);
25837
+ }
25838
+ if (input.scopeId) {
25839
+ const applied = appRegistry.listApplied(input.scopeId);
25840
+ if (!applied.some((m) => m.appId === input.appId)) {
25841
+ return errorResult(
25842
+ `app_run: app "${input.appId}" is not applied to scope "${input.scopeId}". Call app_apply first.`
25843
+ );
25844
+ }
25845
+ }
25846
+ let refs;
25847
+ try {
25848
+ refs = await readAppRefs(installed.dir);
25849
+ } catch (err) {
25850
+ return errorResult(
25851
+ `app_run: could not re-read "${installed.dir}": ${err instanceof Error ? err.message : String(err)}`
25852
+ );
25853
+ }
25854
+ const app = appRegistry.upsertApp({ ...installed, agents: refs.agents, workflows: refs.workflows });
25855
+ const selected = input.agents ?? app.agents.map((a) => a.id);
25856
+ const unknown = selected.filter((id) => !app.agents.some((a) => a.id === id));
25857
+ if (unknown.length > 0) {
25858
+ return errorResult(
25859
+ `app_run: unknown agent id(s) for app "${input.appId}": ${unknown.join(", ")}`
25860
+ );
25861
+ }
25862
+ const sessions = [];
25863
+ const errors = [];
25864
+ for (const agentId of selected) {
25865
+ const agentPath = app.agents.find((a) => a.id === agentId).path;
25866
+ const result = await spawnAgentSession(
25867
+ { registry, resolveAgentAdapter },
25868
+ {
25869
+ adapter: DEFAULT_AGENT_ADAPTER,
25870
+ cwd: input.cwd ?? app.dir,
25871
+ ...input.prompt ? { prompt: input.prompt } : {},
25872
+ options: { agent: agentPath },
25873
+ label: `app:${app.appId}:${agentId}`
25874
+ }
25875
+ );
25876
+ if (result.ok) sessions.push({ agentId, sessionId: result.descriptor.id });
25877
+ else errors.push({ agentId, error: result.message });
25878
+ }
25879
+ const run = appRegistry.createRun({ appId: app.appId, sessions });
25880
+ return textResult({
25881
+ appRunId: run.appRunId,
25882
+ sessions,
25883
+ ...errors.length > 0 ? { errors } : {}
25884
+ });
25885
+ }
25886
+ );
25887
+ server.tool(
25888
+ "app_status",
25889
+ "Status of an app_run: its sessions' live descriptors, plus any workflow runs belonging to the app (any run of one of its bundled WORKFLOW.md files, however it was started).",
25890
+ { appRunId: z.string() },
25891
+ async (input) => {
25892
+ const run = appRegistry.getRun(input.appRunId);
25893
+ if (!run) return errorResult(`app_status: no app run "${input.appRunId}".`);
25894
+ const app = appRegistry.getApp(run.appId);
25895
+ const sessions = run.sessions.map((s) => ({
25896
+ agentId: s.agentId,
25897
+ sessionId: s.sessionId,
25898
+ descriptor: registry.get(s.sessionId)
25899
+ }));
25900
+ const workflowRuns = workflowRunner && app ? workflowRunner.list().filter((r) => app.workflows.some((w) => w.id === r.workflowId)) : [];
25901
+ return textResult({
25902
+ appRunId: run.appRunId,
25903
+ appId: run.appId,
25904
+ status: run.status,
25905
+ startedAt: run.startedAt,
25906
+ ...run.endedAt ? { endedAt: run.endedAt } : {},
25907
+ sessions,
25908
+ workflowRuns
25909
+ });
25910
+ }
25911
+ );
25912
+ server.tool(
25913
+ "app_stop",
25914
+ "Kill every session in an app_run (existing kill path) and mark the run ended.",
25915
+ { appRunId: z.string() },
25916
+ async (input) => {
25917
+ const run = appRegistry.getRun(input.appRunId);
25918
+ if (!run) return errorResult(`app_stop: no app run "${input.appRunId}".`);
25919
+ const killed = [];
25920
+ const notFound = [];
25921
+ for (const s of run.sessions) {
25922
+ if (registry.kill(s.sessionId)) killed.push(s.sessionId);
25923
+ else notFound.push(s.sessionId);
25924
+ }
25925
+ const ended = appRegistry.endRun(input.appRunId);
25926
+ return textResult({
25927
+ appRunId: input.appRunId,
25928
+ killed,
25929
+ ...notFound.length > 0 ? { notFound } : {},
25930
+ status: ended?.status ?? run.status
25931
+ });
25932
+ }
25933
+ );
25934
+ server.tool(
25935
+ "app_apply",
25936
+ "Apply an app to a scope, making its capabilities available in that scope. If the app is not installed and `dir` is provided, installs it first. Validates that all `requires` dependencies are already applied to the same scope. Idempotent \u2014 re-applying the same app to the same scope updates the timestamp.",
25937
+ {
25938
+ appId: z.string(),
25939
+ scopeId: z.string().optional().describe("Scope to apply to. Defaults to 'root'."),
25940
+ dir: z.string().optional().describe("Absolute path to install from if not already installed.")
25941
+ },
25942
+ async (input) => {
25943
+ const scopeId = input.scopeId ?? "root";
25944
+ let installed = appRegistry.getApp(input.appId);
25945
+ if (!installed && input.dir) {
25946
+ const installResult = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter);
25947
+ if (!installResult.ok) return errorResult(`app_apply: ${installResult.error}`);
25948
+ installed = installResult.record;
25949
+ } else if (!installed) {
25950
+ return errorResult(
25951
+ `app_apply: app "${input.appId}" is not installed. Either call app_install first or provide a 'dir' parameter.`
25952
+ );
25953
+ }
25954
+ if (installed.requires && installed.requires.length > 0) {
25955
+ const applied = appRegistry.listApplied(scopeId);
25956
+ const appliedIds = new Set(applied.map((m) => m.appId));
25957
+ const missing = installed.requires.filter((reqId) => !appliedIds.has(reqId));
25958
+ if (missing.length > 0) {
25959
+ return errorResult(
25960
+ `app_apply: app "${input.appId}" requires the following apps to be applied to scope "${scopeId}" first: ${missing.join(", ")}`
25961
+ );
25962
+ }
25963
+ }
25964
+ const mount = appRegistry.applyApp({ scopeId, appId: input.appId });
25965
+ return textResult({
25966
+ scopeId: mount.scopeId,
25967
+ appId: mount.appId,
25968
+ appliedAt: mount.appliedAt,
25969
+ agents: installed.agents,
25970
+ workflows: installed.workflows,
25971
+ unvalidatedAgentTools: installed.unvalidatedAgentTools
25972
+ });
25973
+ }
25974
+ );
25975
+ server.tool(
25976
+ "app_unapply",
25977
+ "Remove an app from a scope. Refuses if another applied app in the same scope requires this one.",
25978
+ {
25979
+ appId: z.string(),
25980
+ scopeId: z.string().optional().describe("Scope to unapply from. Defaults to 'root'.")
25981
+ },
25982
+ async (input) => {
25983
+ const scopeId = input.scopeId ?? "root";
25984
+ const applied = appRegistry.listApplied(scopeId);
25985
+ const dependents = [];
25986
+ for (const mount of applied) {
25987
+ if (mount.appId === input.appId) continue;
25988
+ const app = appRegistry.getApp(mount.appId);
25989
+ if (app?.requires?.includes(input.appId)) {
25990
+ dependents.push(mount.appId);
25991
+ }
25992
+ }
25993
+ if (dependents.length > 0) {
25994
+ return errorResult(
25995
+ `app_unapply: cannot unapply app "${input.appId}" from scope "${scopeId}" \u2014 the following apps in this scope require it: ${dependents.join(", ")}`
25996
+ );
25997
+ }
25998
+ const removed = appRegistry.unapplyApp({ scopeId, appId: input.appId });
25999
+ if (!removed) {
26000
+ return errorResult(`app_unapply: app "${input.appId}" is not applied to scope "${scopeId}".`);
26001
+ }
26002
+ return textResult({ scopeId: removed.scopeId, appId: removed.appId, appliedAt: removed.appliedAt });
26003
+ }
26004
+ );
26005
+ server.tool(
26006
+ "app_list_applied",
26007
+ "List applied mounts, optionally filtered by scope. Each mount is joined with its installed app summary.",
26008
+ {
26009
+ scopeId: z.string().optional().describe("Filter by scope. Omit to list all scopes.")
26010
+ },
26011
+ async (input) => {
26012
+ const mounts = appRegistry.listApplied(input.scopeId);
26013
+ const result = mounts.map((mount) => {
26014
+ const app = appRegistry.getApp(mount.appId);
26015
+ return {
26016
+ scopeId: mount.scopeId,
26017
+ appId: mount.appId,
26018
+ appliedAt: mount.appliedAt,
26019
+ ...app ? {
26020
+ agents: app.agents,
26021
+ workflows: app.workflows,
26022
+ unvalidatedAgentTools: app.unvalidatedAgentTools
26023
+ } : {}
26024
+ };
26025
+ });
26026
+ return textResult(result);
26027
+ }
26028
+ );
26029
+ }
25263
26030
  function createSessionEventBus() {
25264
26031
  const ee = new EventEmitter();
25265
26032
  ee.setMaxListeners(100);
@@ -25433,7 +26200,8 @@ var SessionsRegistryAgentHost = class {
25433
26200
  cwd,
25434
26201
  workspaceSlug,
25435
26202
  sandbox,
25436
- label: `agent-step:${adapter}`
26203
+ label: `agent-step:${adapter}`,
26204
+ ...opts.options !== void 0 ? { options: opts.options } : {}
25437
26205
  }
25438
26206
  );
25439
26207
  if (!result.ok) {
@@ -25452,7 +26220,8 @@ var SessionsRegistryAgentHost = class {
25452
26220
  env: {
25453
26221
  [SESSION_ID_ENV]: stepSessionId,
25454
26222
  [WORKSPACE_SLUG_ENV]: workspaceSlug
25455
- }
26223
+ },
26224
+ ...opts.options !== void 0 ? { options: opts.options } : {}
25456
26225
  });
25457
26226
  const desc = this.registry.spawnAgent({
25458
26227
  id: stepSessionId,
@@ -25648,16 +26417,32 @@ function translateStages(stages, workflowId) {
25648
26417
  const branches = stage.steps.map((step) => ({
25649
26418
  id: step.label,
25650
26419
  steps: [
25651
- {
25652
- kind: "agent",
25653
- id: step.label,
26420
+ buildAgentStep(step.label, {
26421
+ prompt: (b) => {
26422
+ const base = step.prompt ?? "";
26423
+ const prevTexts = [];
26424
+ if (b.steps && typeof b.steps === "object") {
26425
+ for (const [id, val] of Object.entries(b.steps)) {
26426
+ if (val && typeof val === "object" && "text" in val) {
26427
+ const text9 = val.text;
26428
+ if (text9) prevTexts.push(`[Output from step "${id}"]
26429
+ ${text9}`);
26430
+ }
26431
+ }
26432
+ }
26433
+ if (prevTexts.length > 0) return `${prevTexts.join("\n\n")}
26434
+
26435
+ ---
26436
+
26437
+ ${base}`;
26438
+ return base;
26439
+ },
25654
26440
  ...step.adapter !== void 0 ? { adapter: step.adapter } : {},
25655
26441
  ...step.sessionRef !== void 0 ? { sessionRef: step.sessionRef } : {},
25656
26442
  ...step.sandbox !== void 0 ? { sandbox: step.sandbox } : {},
25657
26443
  ...step.cacheable ? { cacheable: true } : {},
25658
- prompt: () => step.prompt ?? "",
25659
- policy: step.policy ?? { awaiting: "fail" }
25660
- }
26444
+ policy: step.policy
26445
+ })
25661
26446
  ]
25662
26447
  }));
25663
26448
  return {
@@ -25710,7 +26495,7 @@ function runtimeWorkflowToStages(workflow) {
25710
26495
  }
25711
26496
  ];
25712
26497
  }
25713
- var DEFAULT_PERSIST_PATH2 = () => join(homedir(), ".agentproto", "workflow-runs.json");
26498
+ var DEFAULT_PERSIST_PATH3 = () => join(homedir(), ".agentproto", "workflow-runs.json");
25714
26499
  function loadRuns(persistPath) {
25715
26500
  const result = /* @__PURE__ */ new Map();
25716
26501
  if (!existsSync(persistPath)) return result;
@@ -25825,7 +26610,7 @@ function fillStepStates(stages, defs, agents) {
25825
26610
  }
25826
26611
  return sessionIds;
25827
26612
  }
25828
- async function executeRunWorkflow(state, runtimeWf, agents, signal, cache, cacheKey, input) {
26613
+ async function executeRunWorkflow(state, runtimeWf, agents, signal, cache, cacheKey, input, persist) {
25829
26614
  try {
25830
26615
  await runWorkflow({
25831
26616
  workflow: runtimeWf,
@@ -25834,13 +26619,49 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, cache, cache
25834
26619
  cwd: state.cwd,
25835
26620
  workspaceSlug: state.workspaceSlug,
25836
26621
  input,
25837
- ...cache ? { cache, cacheKey } : {}
26622
+ ...cache ? { cache, cacheKey } : {},
26623
+ onStepStart: (stepId) => {
26624
+ for (const stage of state.run.stages) {
26625
+ const step = stage.steps.find((s) => s.label === stepId);
26626
+ if (step) {
26627
+ if (step.status === "pending") {
26628
+ step.status = "running";
26629
+ step.startedAt = (/* @__PURE__ */ new Date()).toISOString();
26630
+ }
26631
+ if (stage.status === "pending") {
26632
+ stage.status = "running";
26633
+ }
26634
+ persist?.();
26635
+ break;
26636
+ }
26637
+ }
26638
+ },
26639
+ onStepComplete: (stepId, output) => {
26640
+ for (const stage of state.run.stages) {
26641
+ const step = stage.steps.find((s) => s.label === stepId);
26642
+ if (step) {
26643
+ step.status = "done";
26644
+ step.endedAt = (/* @__PURE__ */ new Date()).toISOString();
26645
+ if (output && typeof output === "object" && "sessionId" in output) {
26646
+ step.sessionId = output.sessionId;
26647
+ }
26648
+ const allDone = stage.steps.every((s) => s.status === "done");
26649
+ if (allDone) {
26650
+ stage.status = "done";
26651
+ }
26652
+ persist?.();
26653
+ break;
26654
+ }
26655
+ }
26656
+ }
25838
26657
  });
25839
26658
  for (const stage of state.run.stages) {
25840
- stage.status = "done";
26659
+ if (stage.status !== "done") stage.status = "done";
25841
26660
  for (const step of stage.steps) {
25842
- step.status = "done";
25843
- step.endedAt = (/* @__PURE__ */ new Date()).toISOString();
26661
+ if (step.status !== "done") {
26662
+ step.status = "done";
26663
+ step.endedAt = (/* @__PURE__ */ new Date()).toISOString();
26664
+ }
25844
26665
  }
25845
26666
  }
25846
26667
  state.run.status = "done";
@@ -25875,7 +26696,7 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, cache, cache
25875
26696
  }
25876
26697
  function createWorkflowRunner(opts) {
25877
26698
  const { registry, sessionEvents, resolveAgentAdapter, compileWorkflow: compileWorkflow2 } = opts;
25878
- const persistPath = opts.persistPath ?? DEFAULT_PERSIST_PATH2();
26699
+ const persistPath = opts.persistPath ?? DEFAULT_PERSIST_PATH3();
25879
26700
  const shouldPersist = opts.persist ?? opts.persistPath !== void 0;
25880
26701
  const runs = shouldPersist ? loadRuns(persistPath) : /* @__PURE__ */ new Map();
25881
26702
  const persist = () => {
@@ -25926,7 +26747,7 @@ function createWorkflowRunner(opts) {
25926
26747
  }
25927
26748
  );
25928
26749
  const cache = input.cacheKey ? createFileStepCache(input.cacheKey) : void 0;
25929
- void executeRunWorkflow(state, workflow, agents, abort.signal, cache, input.cacheKey).then(() => {
26750
+ void executeRunWorkflow(state, workflow, agents, abort.signal, cache, input.cacheKey, void 0, persist).then(() => {
25930
26751
  persist();
25931
26752
  });
25932
26753
  return run;
@@ -25987,7 +26808,8 @@ function createWorkflowRunner(opts) {
25987
26808
  abort.signal,
25988
26809
  cache,
25989
26810
  args.cacheKey,
25990
- args.input
26811
+ args.input,
26812
+ persist
25991
26813
  ).then(() => {
25992
26814
  persist();
25993
26815
  });
@@ -26620,7 +27442,7 @@ function toDescriptor2(state) {
26620
27442
  spawned: state.spawned
26621
27443
  };
26622
27444
  }
26623
- var DEFAULT_PERSIST_PATH3 = () => join(homedir(), ".agentproto", "cron-jobs.json");
27445
+ var DEFAULT_PERSIST_PATH4 = () => join(homedir(), ".agentproto", "cron-jobs.json");
26624
27446
  var TICK_INTERVAL_MS = 2e4;
26625
27447
  function loadJobs(persistPath) {
26626
27448
  const result = /* @__PURE__ */ new Map();
@@ -26678,7 +27500,7 @@ function nextFireDate(cronInstance) {
26678
27500
  }
26679
27501
  function createCronScheduler(opts) {
26680
27502
  const { sessionEvents, registry, resolveAgentAdapter, dispatchTool, workspace } = opts;
26681
- const persistPath = opts.persistPath ?? DEFAULT_PERSIST_PATH3();
27503
+ const persistPath = opts.persistPath ?? DEFAULT_PERSIST_PATH4();
26682
27504
  const shouldPersist = opts.persist ?? opts.persistPath !== void 0;
26683
27505
  const jobs = shouldPersist ? loadJobs(persistPath) : /* @__PURE__ */ new Map();
26684
27506
  for (const state of jobs.values()) {
@@ -27123,84 +27945,16 @@ function createRoutineRegistrar(opts) {
27123
27945
  }
27124
27946
  };
27125
27947
  }
27126
- function collectToolIds(steps, out) {
27127
- for (const step of steps) {
27128
- switch (step.kind) {
27129
- case "tool": {
27130
- const ref = typeof step.tool === "string" ? step.tool : step.tool.entry;
27131
- out.add(normalizeToolId(ref));
27132
- break;
27133
- }
27134
- case "map":
27135
- case "loop":
27136
- collectToolIds(step.steps, out);
27137
- break;
27138
- case "parallel":
27139
- for (const branch of step.branches) collectToolIds(branch.steps, out);
27140
- break;
27141
- }
27142
- }
27143
- }
27144
- function passthroughToolHandle(id) {
27145
- return {
27146
- id,
27147
- name: id,
27148
- description: `Daemon tool '${id}', dispatched via the routine/cron dispatchTool bridge.`,
27149
- mutates: [],
27150
- requires: { network: [], secrets: [], tools: [] },
27151
- approval: "auto",
27152
- riskLevel: 0,
27153
- costClass: "trivial",
27154
- timeoutMs: 3e4,
27155
- tags: [],
27156
- metadata: {},
27157
- idempotent: false,
27158
- driverConstraints: { forbid: [], requireKind: [] }
27159
- };
27160
- }
27161
- function unwrapDispatchResult(result) {
27162
- if (!result || typeof result !== "object" || !("content" in result)) return result;
27163
- const r = result;
27164
- const text9 = (r.content ?? []).map((c) => c.text ?? "").join("\n").trim();
27165
- if (r.isError) throw new Error(text9 || "tool call failed");
27166
- if (!text9) return void 0;
27167
- try {
27168
- return JSON.parse(text9);
27169
- } catch {
27170
- return text9;
27171
- }
27172
- }
27173
- function createDaemonToolRegistry(handle, dispatchTool) {
27174
- const toolIds = /* @__PURE__ */ new Set();
27175
- collectToolIds(handle.steps, toolIds);
27176
- const tools = {};
27177
- const execute = {};
27178
- for (const id of toolIds) {
27179
- tools[id] = passthroughToolHandle(id);
27180
- execute[id] = async ({ input }) => unwrapDispatchResult(await dispatchTool(id, input ?? {}));
27181
- }
27182
- if (toolIds.size === 0) return { tools, candidates: [] };
27183
- const daemonDriver = {
27184
- id: "daemon-tool-dispatch",
27185
- name: "Daemon tool dispatch",
27186
- description: "Catch-all AIP-30 provider routing every referenced WORKFLOW.md tool step through the daemon's dispatchTool.",
27187
- kind: "builtin",
27188
- implements: [...toolIds].map((id) => ({ tool: id, version: "*" })),
27189
- execute,
27190
- install: [],
27191
- network: { egress: [], ingress: [] },
27192
- region: ["global"],
27193
- policyTags: [],
27194
- tags: [],
27195
- metadata: {}
27196
- };
27197
- return { tools, candidates: [daemonDriver] };
27198
- }
27199
27948
  var DEFAULT_ORCHESTRATOR_TOOLS = [
27200
27949
  "agent_start",
27201
27950
  "agent_prompt",
27202
27951
  "agent_output",
27203
27952
  "agent_kill",
27953
+ // Child→parent report-back. NOT a delegation tool (takes no session id;
27954
+ // the daemon resolves the caller's own recorded parent, and it can reach
27955
+ // nothing else) — it's also the sole tool of the minimal report-only
27956
+ // scope `session-spawn.ts` mints for a gateway-less child with a parent.
27957
+ "message_parent",
27204
27958
  "session_monitor",
27205
27959
  "session_events_poll",
27206
27960
  "session_list",
@@ -30776,6 +31530,13 @@ async function createGateway(opts) {
30776
31530
  }
30777
31531
  return dispatchToolBox.fn(name, inputs);
30778
31532
  };
31533
+ const listToolIdsBox = {};
31534
+ const listRegisteredToolIds = async () => {
31535
+ if (!listToolIdsBox.fn) {
31536
+ throw new Error("app_install: tool registry not ready yet (daemon still booting)");
31537
+ }
31538
+ return listToolIdsBox.fn();
31539
+ };
30779
31540
  const cronScheduler = createCronScheduler({
30780
31541
  sessionEvents,
30781
31542
  registry: sessions,
@@ -30789,6 +31550,7 @@ async function createGateway(opts) {
30789
31550
  cronScheduler,
30790
31551
  dispatchTool
30791
31552
  });
31553
+ const appRegistry = createAppRegistry();
30792
31554
  const workflowRunner = opts.resolveAgentAdapter ? createWorkflowRunner({
30793
31555
  registry: sessions,
30794
31556
  sessionEvents,
@@ -30805,7 +31567,13 @@ async function createGateway(opts) {
30805
31567
  // the same `dispatchTool` the routine registrar / cron scheduler use
30806
31568
  // (see `workflow-tool-registry.ts`). Agent-step workflows are
30807
31569
  // unaffected: an empty tool registry compiles them exactly as before.
30808
- compileWorkflow: (handle) => compileWorkflow(handle, createDaemonToolRegistry(handle, dispatchTool))
31570
+ // `agentRefs` resolves a declarative agent-step's `agent.ref` against
31571
+ // whichever installed app bundles this workflow id (undefined when
31572
+ // none does — a plain `workflow_run_file` outside any app).
31573
+ compileWorkflow: (handle) => compileWorkflow(handle, {
31574
+ ...createDaemonToolRegistry(handle, dispatchTool),
31575
+ agentRefs: resolveAgentRefsForWorkflow(appRegistry, handle.id)
31576
+ })
30809
31577
  }) : void 0;
30810
31578
  const operatorWorkspaceSlug = await loadWorkspacesConfig().then((cfg) => getActiveWorkspace(cfg)?.slug).catch(() => void 0);
30811
31579
  const taskLedger = createTaskLedger({
@@ -30981,6 +31749,13 @@ async function createGateway(opts) {
30981
31749
  endpointStore: inboundEndpointStore,
30982
31750
  telegramCreds: telegramBotCreds
30983
31751
  });
31752
+ registerAppTools(server, {
31753
+ registry: sessions,
31754
+ listRegisteredToolIds,
31755
+ appRegistry,
31756
+ ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
31757
+ ...workflowRunner ? { workflowRunner } : {}
31758
+ });
30984
31759
  registerTelegramBotTools(server, { telegramCreds: telegramBotCreds });
30985
31760
  const listSessionsFiltered = (filter) => {
30986
31761
  let rows = sessions.list().filter((s) => s.kind !== "command");
@@ -31078,6 +31853,12 @@ async function createGateway(opts) {
31078
31853
  }
31079
31854
  return tool.handler(inputs, {});
31080
31855
  };
31856
+ listToolIdsBox.fn = async () => {
31857
+ if (!internalToolServerPromise) internalToolServerPromise = mcpServerFactory();
31858
+ const internalServer = await internalToolServerPromise;
31859
+ const internal = internalServer;
31860
+ return Object.keys(internal._registeredTools ?? {});
31861
+ };
31081
31862
  try {
31082
31863
  routineRegistrar.reconcile();
31083
31864
  } catch (err) {
@@ -31136,6 +31917,9 @@ async function createGateway(opts) {
31136
31917
  activityProjector,
31137
31918
  taskLedger,
31138
31919
  ...workflowRunner ? { workflowRunner } : {},
31920
+ appRegistry,
31921
+ performAppInstall: performInstall,
31922
+ listRegisteredToolIds,
31139
31923
  // POST /inbound push ingress — same shared transmitterBindings store
31140
31924
  // and liveness/restart adapters the inbound watcher's "route"/
31141
31925
  // "route-or-spawn" modes use above. No `spawnForContact`: an