@agentproto/runtime 2.2.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/README.md CHANGED
@@ -42,6 +42,7 @@ A per-boot bearer token is generated automatically and written into `<workspace>
42
42
  | MCP | `POST /mcp` (Streamable HTTP) | Stateless mode; per-request transport |
43
43
  | Conversations | `GET /conversations` / `GET /conversations/<id>` | Markdown bodies |
44
44
  | Adapter discovery | `GET /adapters` / `POST /adapters/:slug/install` | When `listAgentAdapters` / `installAgentAdapter` is wired |
45
+ | App scope mounts | `POST /apps/:appId/apply` / `DELETE /apps/:appId/apply` / `GET /scopes/:scopeId/apps` | Mirrors MCP `app_apply` / `app_unapply` / `app_list_applied`; needs `appRegistry` |
45
46
  | Sessions list | `GET /sessions` / `GET /sessions/:id` / `GET /sessions/summaries` | id-or-name in `:id`; summaries are lightweight + paginated |
46
47
  | Agent spawn | `POST /sessions/agent` | Long-lived ACP agent (needs `resolveAgentAdapter`) |
47
48
  | Interrupt turn | `POST /sessions/:id/interrupt` | Cancel the in-flight turn; session stays alive |
package/dist/index.d.ts CHANGED
@@ -3753,8 +3753,14 @@ interface AuthOptions {
3753
3753
  */
3754
3754
 
3755
3755
  interface SessionObserver {
3756
- /** Record the outgoing message that opens a new turn. */
3757
- recordPrompt(sessionId: string, message: unknown): void;
3756
+ /** Record the outgoing message that opens a new turn. `opts.source`, when
3757
+ * set, is the prompt's provenance — `agent:<sessionId>` for a prompt
3758
+ * injected by another session (`agent_prompt` from a supervisor), absent
3759
+ * for a human operator — so a transcript view can attribute the turn to
3760
+ * its real author instead of "you". */
3761
+ recordPrompt(sessionId: string, message: unknown, opts?: {
3762
+ source?: string;
3763
+ }): void;
3758
3764
  /** Record one structured stream event (text-delta, tool-call, usage_update, …). */
3759
3765
  recordEvent(sessionId: string, evt: AgentStreamEvent): void;
3760
3766
  /** Record the durable turn-boundary / exit usage snapshot. */
@@ -3995,6 +4001,16 @@ interface AgentStreamEvent {
3995
4001
  * but no `cost`). */
3996
4002
  tokensIn?: number;
3997
4003
  tokensOut?: number;
4004
+ /** "permission-resolved" outcome for the "agent-prompt" it answers (same
4005
+ * `toolCallId`) — mirrors `session:permission-resolved`'s `decision` so
4006
+ * the durable transcript can tell an answered ask from a still-pending
4007
+ * one. Not an ACP `StreamEvent` kind (synthesized by the registry, same
4008
+ * as "notice"/"turn-end" synthetics) — see registerPendingPermission's
4009
+ * docblock for the "agent-prompt" ask this resolves. */
4010
+ decision?: "approve" | "deny" | "cancelled";
4011
+ /** "permission-resolved" chosen option id, when the driver's offered
4012
+ * options included one (e.g. ACP's `allow_always`). */
4013
+ optionId?: string;
3998
4014
  }
3999
4015
  /**
4000
4016
  * Env vars the registry injects into every process it spawns on a session's
@@ -4019,6 +4035,14 @@ interface AgentStreamEvent {
4019
4035
  * assign-last rule is what makes that true by construction rather than by
4020
4036
  * accident, and is what guarantees a child spawned from inside a session
4021
4037
  * gets its OWN id rather than inheriting its parent's.
4038
+ *
4039
+ * `PARENT_SESSION_ID_ENV` is the third var, injected by `session-spawn.ts`
4040
+ * only (agent spawns, and only when the spawn resolved a parent): the
4041
+ * recorded `parentSessionId` lineage, so a child agent can know WHO spawned
4042
+ * it without a registry round-trip — the discovery half of the child→parent
4043
+ * report-back channel (`message_parent` is the delivery half). Same
4044
+ * assign-last/no-forgery rule as the other two: it mirrors the descriptor's
4045
+ * own `parentSessionId` field, never anything caller- or env-inherited.
4022
4046
  */
4023
4047
  declare const SESSION_ID_ENV = "AGENTPROTO_SESSION_ID";
4024
4048
  declare const WORKSPACE_SLUG_ENV = "AGENTPROTO_WORKSPACE_SLUG";
@@ -4943,6 +4967,7 @@ interface SessionsRegistry {
4943
4967
  * the previous mid-turn rejection byte-for-byte. */
4944
4968
  sendPrompt(id: string, message: unknown, opts?: {
4945
4969
  interrupt?: boolean;
4970
+ source?: string;
4946
4971
  }): Promise<void>;
4947
4972
  /** Fire-and-forget variant of `sendPrompt` for the TURN ITSELF only.
4948
4973
  * Admission (resume attempt + the missing/wrong-kind/dead/busy
@@ -4962,8 +4987,12 @@ interface SessionsRegistry {
4962
4987
  * prompt is admitted + fired on the SAME live session. Ignored
4963
4988
  * (identical to the default) when the session is idle. Omitted or
4964
4989
  * `false` reproduces today's mid-turn rejection byte-for-byte. */
4990
+ /** `opts.source` on either prompt verb is the turn's provenance
4991
+ * (`agent:<sessionId>` when another session injected it — see
4992
+ * `SessionObserver.recordPrompt`); recording-only, never behavioral. */
4965
4993
  enqueuePrompt(id: string, message: unknown, opts?: {
4966
4994
  interrupt?: boolean;
4995
+ source?: string;
4967
4996
  }): Promise<void>;
4968
4997
  /** Eagerly resume ONE dead-but-resumable agent-cli session IN PLACE,
4969
4998
  * WITHOUT a prompt — the boot-time counterpart to the lazy resume that
package/dist/index.mjs CHANGED
@@ -310,11 +310,16 @@ function createTranscriptWriter(opts) {
310
310
  }, DEBOUNCE_MS);
311
311
  };
312
312
  return {
313
- recordPrompt(sessionId, message) {
313
+ recordPrompt(sessionId, message, opts2) {
314
314
  const state = getState(sessionId);
315
315
  flushBuffers(sessionId, state);
316
316
  const text9 = typeof message === "string" ? message : JSON.stringify(message);
317
- 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
+ });
318
323
  },
319
324
  recordEvent(sessionId, evt) {
320
325
  const state = getState(sessionId);
@@ -404,6 +409,22 @@ function createTranscriptWriter(opts) {
404
409
  options: evt.options
405
410
  });
406
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;
407
428
  case "turn-end":
408
429
  flushBuffers(sessionId, state);
409
430
  writeRecord(sessionId, state, { kind: "turn-end", sessionId, reason: evt.reason });
@@ -5116,6 +5137,14 @@ async function spawnAgentSession(deps2, input) {
5116
5137
  };
5117
5138
  });
5118
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
+ }
5119
5148
  const spawnDefaults = resolveSpawnDefaults(configDefaults, input.adapter, {
5120
5149
  skills: input.skills,
5121
5150
  options: input.options,
@@ -5277,9 +5306,8 @@ async function spawnAgentSession(deps2, input) {
5277
5306
  details: { adapter: input.adapter, gateway }
5278
5307
  };
5279
5308
  }
5280
- let effectivePrompt = input.prompt ? `${composeRoleContext(role, input.promptAppend, roleRegistry)}
5281
-
5282
- ${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;
5283
5311
  const explicitTitle = input.title?.trim() ? input.title.trim() : void 0;
5284
5312
  const spawnLabel = input.label?.trim() ? input.label.trim() : void 0;
5285
5313
  const initialTitle = explicitTitle ?? spawnLabel ?? (input.prompt ? deriveSessionTitle(input.prompt) : void 0);
@@ -5536,7 +5564,12 @@ ${asyncPrompt}`;
5536
5564
  // collide with, so this is the entire env for this spawn.
5537
5565
  env: {
5538
5566
  [SESSION_ID_ENV]: mintedSessionId,
5539
- [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 } : {}
5540
5573
  },
5541
5574
  onActivity: () => {
5542
5575
  if (liveSessionId) registry.pulseActivity(liveSessionId);
@@ -6721,8 +6754,8 @@ function composeSessionObservers(observers) {
6721
6754
  );
6722
6755
  };
6723
6756
  return {
6724
- recordPrompt(sessionId, message) {
6725
- forEachSafe((o) => o.recordPrompt(sessionId, message));
6757
+ recordPrompt(sessionId, message, opts) {
6758
+ forEachSafe((o) => o.recordPrompt(sessionId, message, opts));
6726
6759
  },
6727
6760
  recordEvent(sessionId, evt) {
6728
6761
  forEachSafe((o) => o.recordEvent(sessionId, evt));
@@ -6740,10 +6773,10 @@ function composeSessionObservers(observers) {
6740
6773
  }
6741
6774
  function filterSessionObserver(inner, shouldObserve) {
6742
6775
  return {
6743
- recordPrompt(sessionId, message) {
6776
+ recordPrompt(sessionId, message, opts) {
6744
6777
  if (!shouldObserve(sessionId)) return;
6745
6778
  try {
6746
- inner.recordPrompt(sessionId, message);
6779
+ inner.recordPrompt(sessionId, message, opts);
6747
6780
  } catch {
6748
6781
  }
6749
6782
  },
@@ -7446,6 +7479,7 @@ function mintSessionId() {
7446
7479
  }
7447
7480
  var SESSION_ID_ENV = "AGENTPROTO_SESSION_ID";
7448
7481
  var WORKSPACE_SLUG_ENV = "AGENTPROTO_WORKSPACE_SLUG";
7482
+ var PARENT_SESSION_ID_ENV = "AGENTPROTO_PARENT_SESSION_ID";
7449
7483
  var SessionNotAliveError = class extends Error {
7450
7484
  sessionId;
7451
7485
  status;
@@ -7748,6 +7782,11 @@ function createSessionsRegistry(opts) {
7748
7782
  ts: (/* @__PURE__ */ new Date()).toISOString()
7749
7783
  });
7750
7784
  }
7785
+ transcriptWriter.recordEvent(rt.desc.id, {
7786
+ kind: "permission-resolved",
7787
+ toolCallId: id,
7788
+ decision: "cancelled"
7789
+ });
7751
7790
  }
7752
7791
  delete rt.desc.awaitingPermission;
7753
7792
  };
@@ -7803,6 +7842,12 @@ function createSessionsRegistry(opts) {
7803
7842
  ts: (/* @__PURE__ */ new Date()).toISOString()
7804
7843
  });
7805
7844
  }
7845
+ transcriptWriter.recordEvent(pending.sessionId, {
7846
+ kind: "permission-resolved",
7847
+ toolCallId: id,
7848
+ decision: input.decision,
7849
+ ...chosenOptionId ? { optionId: chosenOptionId } : {}
7850
+ });
7806
7851
  schedulePersist();
7807
7852
  if (!okResolved) {
7808
7853
  return { ok: true, permission: pending, decision: input.decision, ...chosenOptionId ? { optionId: chosenOptionId } : {} };
@@ -8435,7 +8480,7 @@ function createSessionsRegistry(opts) {
8435
8480
  return;
8436
8481
  }
8437
8482
  }
8438
- const runAgentTurn = async (rt, message) => {
8483
+ const runAgentTurn = async (rt, message, turnOpts) => {
8439
8484
  if (!rt.agentSession) {
8440
8485
  throw new Error("runAgentTurn: session has no agentSession");
8441
8486
  }
@@ -8471,7 +8516,11 @@ ${message}`;
8471
8516
  `\x1B[2m\u2500\u2500 \u25B6 ${typeof message === "string" ? message : JSON.stringify(message)} \u2500\u2500\x1B[0m`,
8472
8517
  "stdout"
8473
8518
  );
8474
- transcriptWriter.recordPrompt(rt.desc.id, message);
8519
+ transcriptWriter.recordPrompt(
8520
+ rt.desc.id,
8521
+ message,
8522
+ turnOpts?.promptSource ? { source: turnOpts.promptSource } : void 0
8523
+ );
8475
8524
  const wrapped = typeof message === "string" ? { type: "text", text: message } : message;
8476
8525
  for await (const evt of rt.agentSession.send(wrapped)) {
8477
8526
  transcriptWriter.recordEvent(rt.desc.id, evt);
@@ -8882,7 +8931,11 @@ ${message}`;
8882
8931
  schedulePersist();
8883
8932
  recordConversationLink(rt);
8884
8933
  if (input.initialPrompt) {
8885
- 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) => {
8886
8939
  appendLine(
8887
8940
  rt,
8888
8941
  `[turn error] ${err instanceof Error ? err.message : String(err)}`,
@@ -9010,7 +9063,11 @@ ${message}`;
9010
9063
  schedulePersist();
9011
9064
  recordConversationLink(rt);
9012
9065
  if (outcome.initialPrompt) {
9013
- 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) => {
9014
9071
  appendLine(
9015
9072
  rt,
9016
9073
  `[turn error] ${err instanceof Error ? err.message : String(err)}`,
@@ -9268,7 +9325,7 @@ ${message}`;
9268
9325
  }
9269
9326
  if (rtPre) await maybeResumeAgent(rtPre);
9270
9327
  const rt = validateAgentTurn(id, "sendPrompt");
9271
- await runAgentTurn(rt, message);
9328
+ await runAgentTurn(rt, message, opts2?.source ? { promptSource: opts2.source } : void 0);
9272
9329
  },
9273
9330
  async enqueuePrompt(id, message, opts2) {
9274
9331
  const rtPre = sessions.get(id);
@@ -9280,7 +9337,7 @@ ${message}`;
9280
9337
  }
9281
9338
  await maybeResumeAgent(rtPre);
9282
9339
  const rt = validateAgentTurn(id, "enqueuePrompt");
9283
- void runAgentTurn(rt, message).catch((err) => {
9340
+ void runAgentTurn(rt, message, opts2?.source ? { promptSource: opts2.source } : void 0).catch((err) => {
9284
9341
  appendLine(
9285
9342
  rtPre,
9286
9343
  `[error] ${err instanceof Error ? err.message : String(err)}`,
@@ -10707,12 +10764,17 @@ function registerFsTools(server, opts) {
10707
10764
  const anchor = makeAnchor(opts.workspace);
10708
10765
  server.tool(
10709
10766
  "file_read",
10710
- "Read a UTF-8 file from the workspace.",
10711
- { path: z.string().describe("Workspace-relative path to the file.") },
10712
- 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 }) => {
10713
10775
  const abs = anchor(path);
10714
10776
  const buf = await readFile(abs);
10715
- return text(buf.toString("utf8"));
10777
+ return text(buf.toString(encoding === "base64" ? "base64" : "utf8"));
10716
10778
  }
10717
10779
  );
10718
10780
  server.tool(
@@ -11288,8 +11350,10 @@ function registerAgentTools(server, opts) {
11288
11350
  const sessionId = resolveSessionIdArg(input);
11289
11351
  if (!sessionId) return missingSessionIdError("agent_prompt");
11290
11352
  try {
11353
+ const promptSource = callerScope?.ownerSessionId ?? callerSessionId;
11291
11354
  await registry.enqueuePrompt(sessionId, input.prompt, {
11292
- interrupt: input.interrupt
11355
+ interrupt: input.interrupt,
11356
+ ...promptSource ? { source: `agent:${promptSource}` } : {}
11293
11357
  });
11294
11358
  return {
11295
11359
  content: [
@@ -11316,6 +11380,67 @@ function registerAgentTools(server, opts) {
11316
11380
  }
11317
11381
  }
11318
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
+ );
11319
11444
  server.tool(
11320
11445
  "agent_output",
11321
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`.",
@@ -27825,6 +27950,11 @@ var DEFAULT_ORCHESTRATOR_TOOLS = [
27825
27950
  "agent_prompt",
27826
27951
  "agent_output",
27827
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",
27828
27958
  "session_monitor",
27829
27959
  "session_events_poll",
27830
27960
  "session_list",