@demicodes/agent 0.20.0 → 0.22.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
@@ -1553,11 +1553,15 @@ var AgentSession = class AgentSession {
1553
1553
  };
1554
1554
  }
1555
1555
  waitUntilDone() {
1556
- if (!this.workerRunning && this.pendingActions.length === 0) return Promise.resolve();
1556
+ if (this.isSettled()) return Promise.resolve();
1557
1557
  return new Promise((resolve) => {
1558
1558
  this.idleResolvers.push(resolve);
1559
1559
  });
1560
1560
  }
1561
+ /** True when no turn is running and no action (including queued user sends) is pending. */
1562
+ isSettled() {
1563
+ return !this.workerRunning && this.pendingActions.length === 0;
1564
+ }
1561
1565
  /**
1562
1566
  * Tears the session down: aborts any in-flight turn and releases provider-held resources
1563
1567
  * (e.g. a long-lived CLI subprocess). Called when the owning connection closes.
@@ -2597,7 +2601,7 @@ function formatShellToolResult(result, options) {
2597
2601
  lines.push(`metaPath: ${result.artifactDir}/meta.json`);
2598
2602
  }
2599
2603
  if (options.includePreview) appendPreview(lines, result, options.previewBudgetTokens ?? SMALL_CONTEXT_PREVIEW_TOKENS);
2600
- if (result.status === "running") lines.push("next: command is still running; check again with shell_status, or call yield to end this turn and be woken later, or shell_abort to stop it.");
2604
+ if (result.status === "running") lines.push(result.runningHint ?? "next: command is still running; check again with shell_status, or call yield to end this turn and be woken later, or shell_abort to stop it.");
2601
2605
  else if (result.status === "aborted") lines.push("next: command was intentionally stopped.");
2602
2606
  else if (exposeCommandHandle) lines.push("next: command is complete; read the artifact only if the preview is insufficient.");
2603
2607
  return lines.join("\n");
@@ -2652,22 +2656,112 @@ function shellCommandHandleRequired(result, budgetTokens) {
2652
2656
  }
2653
2657
  //#endregion
2654
2658
  //#region src/subagent.ts
2659
+ /** Default per-session live-children ceiling; override with `AgentServerOptions.subagents.maxLiveSubagents`. */
2655
2660
  const MAX_LIVE_SUBAGENTS = 8;
2656
- const MAX_ARCHIVED_SUBAGENTS = 16;
2657
2661
  const SUBAGENT_RESULT_MAX_BYTES = 32768;
2658
2662
  const SHOW_RECENT_TOOLS = 8;
2663
+ const SPAWN_RUNNING_HINT = "next: the child agent is still working; a long-running spawn is normal. shell_write steers it, shell_abort aborts it. Otherwise stop attending and end the turn — the child's completion returns as this command's result and wakes the session when it is idle. Do not poll with shell_status or timed yields; use `demi agent show` only to decide a steer or abort.";
2659
2664
  const SPAWN_PROMPT_DESCRIPTION = "The child's first user message and only task brief. The child starts with an empty transcript and cannot see this conversation: do not refer to prior turns, and do not paste this conversation or the product user's message unchanged. Include the goal for this child, applicable decisions and constraints, whether to edit or only report, how to verify, and every concrete identifier it needs (paths, ids, error text, commands already tried and their key results). State the exact shape of the last assistant text it should return.";
2660
2665
  /**
2661
- * Per-parent subagent supervisor. Owns every child `AgentSession` this parent
2662
- * spawns: lifecycle (spawn / steer / abort / natural end), the `demi agent`
2663
- * command tree, child shell environments, and the `subagent*` protocol frames
2664
- * on the parent connection. Children persist exactly like the parent: each
2665
- * live child keeps a checkpoint and job record under the parent's session
2666
- * directory, and reopening the parent restores and resumes them (`restore`).
2667
- * A closed child moves to the archive: its transcript checkpoint stays on
2668
- * store and `demi agent resume` revives it with a new user message.
2666
+ * Connection-wide flat registry of every live session in the tree: the root
2667
+ * plus every subagent at any depth. The sole basis for cross-tree addressing
2668
+ * `send`, `steer`, `show`, and `list` resolve here, with no routing rules
2669
+ * along the tree.
2670
+ */
2671
+ var AgentDirectory = class {
2672
+ root = null;
2673
+ entries = /* @__PURE__ */ new Map();
2674
+ attachRoot(session, supervisor) {
2675
+ this.root = {
2676
+ session,
2677
+ supervisor
2678
+ };
2679
+ }
2680
+ rootId() {
2681
+ if (!this.root) throw new Error("agent directory has no root session");
2682
+ return this.root.session.id();
2683
+ }
2684
+ rootSession() {
2685
+ if (!this.root) throw new Error("agent directory has no root session");
2686
+ return this.root.session;
2687
+ }
2688
+ register(job, owner) {
2689
+ this.entries.set(job.id, {
2690
+ job,
2691
+ owner
2692
+ });
2693
+ }
2694
+ unregister(id) {
2695
+ this.entries.delete(id);
2696
+ }
2697
+ liveEntry(id) {
2698
+ return this.entries.get(id) ?? null;
2699
+ }
2700
+ /** The parent session id of a live agent; null for the root, undefined for an unknown id. */
2701
+ parentIdOf(id) {
2702
+ if (this.root && this.root.session.id() === id) return null;
2703
+ return this.entries.get(id)?.owner.ownerId();
2704
+ }
2705
+ /**
2706
+ * The whole session tree: the root, every live agent, and each live node's
2707
+ * archived children (their supervisors exist, so their archives are
2708
+ * readable). Live children order by spawn time; archived newest first.
2709
+ */
2710
+ async tree() {
2711
+ if (!this.root) return [];
2712
+ const build = async (id, parentId, job, owner, supervisor) => {
2713
+ const liveChildren = [...this.entries.values()].filter((entry) => entry.owner === supervisor).sort((a, b) => a.job.spawnedAt - b.job.spawnedAt);
2714
+ const children = [];
2715
+ for (const entry of liveChildren) children.push(await build(entry.job.id, id, entry.job, entry.owner, entry.job.ownSupervisor));
2716
+ const now = Date.now();
2717
+ for (const archived of await supervisor.listArchivedJobs()) children.push({
2718
+ id: archived.id,
2719
+ parentId: id,
2720
+ kind: "archived",
2721
+ description: archived.meta.description,
2722
+ profile: archived.meta.profileName,
2723
+ phase: archived.meta.closedPhase ?? "completed",
2724
+ closedAgoMs: archived.meta.closedAt === void 0 ? null : now - archived.meta.closedAt,
2725
+ line: null,
2726
+ children: []
2727
+ });
2728
+ if (job && owner) return {
2729
+ id,
2730
+ parentId,
2731
+ kind: "live",
2732
+ description: job.description,
2733
+ profile: job.profileName,
2734
+ phase: job.phase,
2735
+ closedAgoMs: null,
2736
+ line: owner.renderListLine(job),
2737
+ children
2738
+ };
2739
+ return {
2740
+ id,
2741
+ parentId,
2742
+ kind: "root",
2743
+ description: "",
2744
+ profile: null,
2745
+ phase: "running",
2746
+ closedAgoMs: null,
2747
+ line: null,
2748
+ children
2749
+ };
2750
+ };
2751
+ return [await build(this.rootId(), null, null, null, this.root.supervisor)];
2752
+ }
2753
+ };
2754
+ /**
2755
+ * Per-session subagent supervisor. Every session — root or subagent — owns one
2756
+ * and carries the identical `demi agent` command tree, so spawn nests to any
2757
+ * depth. The supervisor owns its direct children's lifecycle (spawn / abort /
2758
+ * resume / natural end), their shell environments, and the `subagent*`
2759
+ * protocol frames; communication and reads (`send` / `steer` / `show` /
2760
+ * `list`) resolve through the shared AgentDirectory and reach any live agent
2761
+ * in the tree. Children persist under the owner's session directory,
2762
+ * recursively, and reopening a session restores its whole subtree.
2669
2763
  */
2670
- var ChildSupervisor = class {
2764
+ var ChildSupervisor = class ChildSupervisor {
2671
2765
  options;
2672
2766
  jobs = /* @__PURE__ */ new Map();
2673
2767
  parentSession = null;
@@ -2678,18 +2772,35 @@ var ChildSupervisor = class {
2678
2772
  attachParent(session) {
2679
2773
  this.parentSession = session;
2680
2774
  }
2681
- /** The `agent` node AgentServer grafts under the parent registry's `demi` root. */
2775
+ ownerId() {
2776
+ if (!this.parentSession) throw new Error("subagent supervisor has no owner session");
2777
+ return this.parentSession.id();
2778
+ }
2779
+ hasLiveJobs() {
2780
+ return this.jobs.size > 0;
2781
+ }
2782
+ /** The `agent` node AgentServer (and every child assembly) grafts under the registry's `demi` root. */
2682
2783
  rootCommandNode() {
2784
+ const full = this.spawnableCommandNode();
2785
+ if (this.options.canSpawn) return full;
2786
+ return {
2787
+ name: "agent",
2788
+ summary: "Agent tree communication. This session may not spawn subagents: send/steer message any live agent, list renders the tree, show snapshots one agent.",
2789
+ subcommands: (full.subcommands ?? []).filter((command) => command.name !== "abort" && command.name !== "resume")
2790
+ };
2791
+ }
2792
+ spawnableCommandNode() {
2683
2793
  const profileNames = this.configuredProfileNames();
2684
2794
  return {
2685
2795
  name: "agent",
2686
- summary: "Start an isolated child agent session and wait for its result. The command stays running until the child session ends; stdout is the child's last assistant text. While it is the foreground job, shell_write steers the child and shell_abort aborts it. Run several in separate shell_exec calls with short timeoutMs to fan out.",
2796
+ summary: "Start an isolated child agent session and wait for its result. The command stays running until the child session ends; stdout is the child's last assistant text. While it is the foreground job, shell_write steers the child and shell_abort aborts it. Run several in separate shell_exec calls with short timeoutMs to fan out, then end the turn — completion wakes an idle session; do not poll. Children can spawn children of their own.",
2687
2797
  successOutput: "first stderr line is \"subagentId: <id>\" at start; stdout is the child's last assistant text (empty is valid), written only at exit",
2688
2798
  failureOutput: "non-zero exit with the abort or failure reason on stderr",
2689
2799
  input: {
2690
2800
  prompt: z.string().optional().describe(SPAWN_PROMPT_DESCRIPTION),
2691
2801
  profile: z.string().optional().describe(`Named subagent profile configured at harness assembly. Available: ${profileNames.join(", ")}.`),
2692
- description: z.string().optional().describe("Short UI title distinguishing concurrent children.")
2802
+ description: z.string().optional().describe("Short UI title distinguishing concurrent children."),
2803
+ "no-subagents": z.boolean().optional().describe("Forbid this child from spawning subagents of its own; it can still send, steer, list, and show.")
2693
2804
  },
2694
2805
  positionals: ["prompt"],
2695
2806
  stdinField: "prompt",
@@ -2697,6 +2808,7 @@ var ChildSupervisor = class {
2697
2808
  subagentId: z.string(),
2698
2809
  text: z.string()
2699
2810
  }) },
2811
+ runningHint: SPAWN_RUNNING_HINT,
2700
2812
  run: async ({ parsed, io, signal, stdinStream }) => {
2701
2813
  const prompt = String(parsed.values.prompt ?? "").trim();
2702
2814
  if (!prompt) {
@@ -2708,7 +2820,8 @@ var ChildSupervisor = class {
2708
2820
  job = await this.spawn({
2709
2821
  prompt,
2710
2822
  profileName: parsed.values.profile === void 0 ? void 0 : String(parsed.values.profile),
2711
- description: parsed.values.description === void 0 ? "" : String(parsed.values.description)
2823
+ description: parsed.values.description === void 0 ? "" : String(parsed.values.description),
2824
+ isSpawnForbidden: parsed.values["no-subagents"] === true
2712
2825
  });
2713
2826
  } catch (error) {
2714
2827
  await io.stderr(`demi agent: ${errorMessage(error)}\n`);
@@ -2722,11 +2835,43 @@ var ChildSupervisor = class {
2722
2835
  });
2723
2836
  },
2724
2837
  subcommands: [
2838
+ {
2839
+ name: "send",
2840
+ summary: "Leave a message for any live agent in the tree (`demi agent list`), or `parent`. The target sees it as a new user turn at its next turn boundary, never mid-turn; a message to a finishing subagent extends its life by one turn. Fire-and-forget: queues and returns, never waits. An archived target fails — only its parent can revive it with resume.",
2841
+ input: {
2842
+ id: z.string().describe("Target agent id from the tree, or \"parent\" for the session that spawned this one"),
2843
+ message: z.string().optional().describe("Message body; positional, or stdin/heredoc when omitted.")
2844
+ },
2845
+ positionals: ["id", "message"],
2846
+ stdinField: "message",
2847
+ output: { json: z.object({
2848
+ id: z.string(),
2849
+ accepted: z.boolean()
2850
+ }) },
2851
+ run: async ({ parsed, io }) => {
2852
+ const message = String(parsed.values.message ?? "").trim();
2853
+ if (!message) {
2854
+ await io.stderr("demi agent send: message must not be empty\n");
2855
+ return { exitCode: 1 };
2856
+ }
2857
+ try {
2858
+ const targetId = this.deliverSend(String(parsed.values.id), message);
2859
+ await io.stdout(parsed.json ? `${JSON.stringify({
2860
+ id: targetId,
2861
+ accepted: true
2862
+ })}\n` : `sent to ${targetId}\n`);
2863
+ return { exitCode: 0 };
2864
+ } catch (error) {
2865
+ await io.stderr(`demi agent send: ${errorMessage(error)}\n`);
2866
+ return { exitCode: 1 };
2867
+ }
2868
+ }
2869
+ },
2725
2870
  {
2726
2871
  name: "steer",
2727
- summary: "Send a user steer to a running child. Queues until the child can take it; does not wait.",
2872
+ summary: "Chime into a running agent's current turn: the target sees the message at its next sampling/tool boundary and continues its current work with the new information. Nothing is cancelled and the turn does not restart. Fails when the target has no running turn — use send for that. Targets any live agent in the tree, or `parent`.",
2728
2873
  input: {
2729
- id: z.string().describe("subagentId from spawn stderr"),
2874
+ id: z.string().describe("Target agent id from the tree, or \"parent\" for the session that spawned this one"),
2730
2875
  message: z.string().optional().describe("Message body; positional, or stdin/heredoc when omitted.")
2731
2876
  },
2732
2877
  positionals: ["id", "message"],
@@ -2736,29 +2881,28 @@ var ChildSupervisor = class {
2736
2881
  accepted: z.boolean()
2737
2882
  }) },
2738
2883
  run: async ({ parsed, io }) => {
2739
- const id = String(parsed.values.id);
2740
2884
  const message = String(parsed.values.message ?? "").trim();
2741
2885
  if (!message) {
2742
2886
  await io.stderr("demi agent steer: message must not be empty\n");
2743
2887
  return { exitCode: 1 };
2744
2888
  }
2745
- const job = this.jobs.get(id);
2746
- if (!job) {
2747
- await io.stderr(`demi agent steer: no running subagent "${id}"\n`);
2889
+ try {
2890
+ const targetId = await this.deliverSteer(String(parsed.values.id), message);
2891
+ await io.stdout(parsed.json ? `${JSON.stringify({
2892
+ id: targetId,
2893
+ accepted: true
2894
+ })}\n` : `steered ${targetId}\n`);
2895
+ return { exitCode: 0 };
2896
+ } catch (error) {
2897
+ await io.stderr(`demi agent steer: ${errorMessage(error)}\n`);
2748
2898
  return { exitCode: 1 };
2749
2899
  }
2750
- await this.steerChild(job, message);
2751
- await io.stdout(parsed.json ? `${JSON.stringify({
2752
- id,
2753
- accepted: true
2754
- })}\n` : `steered ${id}\n`);
2755
- return { exitCode: 0 };
2756
2900
  }
2757
2901
  },
2758
2902
  {
2759
2903
  name: "abort",
2760
- summary: "Abort a running child and its subtree. Siblings are untouched.",
2761
- input: { id: z.string().describe("subagentId from spawn stderr") },
2904
+ summary: "Abort one of your own running children and its whole subtree. Siblings are untouched; only the spawning session may abort a child.",
2905
+ input: { id: z.string().describe("subagentId of one of your running children") },
2762
2906
  positionals: ["id"],
2763
2907
  output: { json: z.object({
2764
2908
  id: z.string(),
@@ -2767,7 +2911,7 @@ var ChildSupervisor = class {
2767
2911
  run: async ({ parsed, io }) => {
2768
2912
  const id = String(parsed.values.id);
2769
2913
  if (!this.jobs.has(id)) {
2770
- await io.stderr(`demi agent abort: no running subagent "${id}"\n`);
2914
+ await io.stderr(`demi agent abort: "${id}" is not one of your running children\n`);
2771
2915
  return { exitCode: 1 };
2772
2916
  }
2773
2917
  await this.abortSubtree(id);
@@ -2780,9 +2924,9 @@ var ChildSupervisor = class {
2780
2924
  },
2781
2925
  {
2782
2926
  name: "resume",
2783
- summary: "Revive an archived (finished) child with a new user message on top of its preserved transcript. Behaves like the spawn command afterwards: stays running until the child ends again, stdout is its new last assistant text, shell_write steers, shell_abort aborts. Archived ids are in `demi agent list`.",
2927
+ summary: "Revive one of your own archived (finished) children with a new user message on top of its preserved transcript. Behaves like the spawn command afterwards: stays running until the child ends again, stdout is its new last assistant text, shell_write steers, shell_abort aborts. Archived ids are in `demi agent list`; only the spawning session may resume its child.",
2784
2928
  input: {
2785
- id: z.string().describe("subagentId of an archived child"),
2929
+ id: z.string().describe("subagentId of one of your archived children"),
2786
2930
  message: z.string().optional().describe("The reviving user message; positional, or stdin/heredoc when omitted.")
2787
2931
  },
2788
2932
  positionals: ["id", "message"],
@@ -2791,6 +2935,7 @@ var ChildSupervisor = class {
2791
2935
  subagentId: z.string(),
2792
2936
  text: z.string()
2793
2937
  }) },
2938
+ runningHint: SPAWN_RUNNING_HINT,
2794
2939
  run: async ({ parsed, io, signal, stdinStream }) => {
2795
2940
  const id = String(parsed.values.id);
2796
2941
  const message = String(parsed.values.message ?? "").trim();
@@ -2815,54 +2960,35 @@ var ChildSupervisor = class {
2815
2960
  },
2816
2961
  {
2817
2962
  name: "list",
2818
- summary: "Snapshot roster of this session's running children, then its archived (finished, revivable via resume) children. One line per child with ages relative to now. A read, not a wait — not for polling loops.",
2819
- output: { json: z.object({
2820
- agents: z.array(z.unknown()),
2821
- archived: z.array(z.unknown())
2822
- }) },
2963
+ summary: "Render the whole session tree from the root down, marking your own position. Live agents show phase, ages, execution, and activity; each node's archived (finished, revivable by its parent) children render beneath it. Every age is relative to now. A read, not a wait — not for polling loops.",
2964
+ output: { json: z.object({ tree: z.array(z.unknown()) }) },
2823
2965
  run: async ({ parsed, io }) => {
2824
- const jobs = [...this.jobs.values()];
2825
- const archived = await this.listArchivedJobs();
2966
+ const nodes = await this.options.directory.tree();
2826
2967
  if (parsed.json) {
2827
- await io.stdout(`${JSON.stringify({
2828
- agents: jobs.map((job) => this.snapshot(job, false)),
2829
- archived: archived.map(({ id, meta }) => ({
2830
- subagentId: id,
2831
- description: meta.description,
2832
- profile: meta.profileName,
2833
- phase: meta.closedPhase,
2834
- closedAgoMs: meta.closedAt === void 0 ? null : Date.now() - meta.closedAt
2835
- }))
2836
- })}\n`);
2968
+ await io.stdout(`${JSON.stringify({ tree: flattenTree(nodes, this.ownerId()) })}\n`);
2837
2969
  return { exitCode: 0 };
2838
2970
  }
2839
- if (jobs.length === 0) await io.stdout("no running subagents\n");
2840
- for (const job of jobs) await io.stdout(`${this.renderListLine(job)}\n`);
2841
- if (archived.length > 0) {
2842
- await io.stdout("archived (revivable with `demi agent resume <id>`):\n");
2843
- for (const { id, meta } of archived) {
2844
- const closedAgo = meta.closedAt === void 0 ? "" : ` closed ${formatDuration(Date.now() - meta.closedAt)} ago`;
2845
- await io.stdout(` ${id} ${meta.closedPhase}${closedAgo} ${meta.description ? `"${meta.description}"` : "(no description)"}\n`);
2846
- }
2847
- }
2971
+ const lines = [];
2972
+ for (const node of nodes) renderTreeNode(node, "", true, this.ownerId(), lines);
2973
+ await io.stdout(`${lines.join("\n")}\n`);
2848
2974
  return { exitCode: 0 };
2849
2975
  }
2850
2976
  },
2851
2977
  {
2852
2978
  name: "show",
2853
- summary: "Bounded snapshot of one running child: execution state, recent tool titles with durations, last assistant text. Every duration is relative to now — use the ages to tell motion from stall. Omits tool outputs, file contents, and older turns. A read, not a wait — not for polling loops.",
2854
- input: { id: z.string().describe("subagentId from spawn stderr") },
2979
+ summary: "Bounded snapshot of any live agent in the tree (root excluded): execution state, recent tool titles with durations, last assistant text. Every duration is relative to now — use the ages to tell motion from stall. Omits tool outputs, file contents, and older turns. A read, not a wait — not for polling loops.",
2980
+ input: { id: z.string().describe("Agent id from the tree") },
2855
2981
  positionals: ["id"],
2856
2982
  output: { json: z.object({ agent: z.unknown() }) },
2857
2983
  run: async ({ parsed, io }) => {
2858
2984
  const id = String(parsed.values.id);
2859
- const job = this.jobs.get(id);
2860
- if (!job) {
2861
- await io.stderr(`demi agent show: no running subagent "${id}"\n`);
2985
+ const entry = this.options.directory.liveEntry(id);
2986
+ if (!entry) {
2987
+ await io.stderr(`demi agent show: no live agent "${id}"\n`);
2862
2988
  return { exitCode: 1 };
2863
2989
  }
2864
- if (parsed.json) await io.stdout(`${JSON.stringify({ agent: this.snapshot(job, true) })}\n`);
2865
- else await io.stdout(this.renderShow(job));
2990
+ if (parsed.json) await io.stdout(`${JSON.stringify({ agent: entry.owner.snapshot(entry.job, true) })}\n`);
2991
+ else await io.stdout(entry.owner.renderShow(entry.job));
2866
2992
  return { exitCode: 0 };
2867
2993
  }
2868
2994
  }
@@ -2872,16 +2998,20 @@ var ChildSupervisor = class {
2872
2998
  hasShell(shellId) {
2873
2999
  return this.environmentScopeForShell(shellId) !== null;
2874
3000
  }
2875
- /** Resolves the child scope owning a shell, for the command bridge dispatch. */
3001
+ /** Resolves the descendant scope owning a shell (recursively), for the command bridge dispatch. */
2876
3002
  environmentScopeForShell(shellId) {
2877
- for (const job of this.jobs.values()) for (const environment of job.environments.values()) if (environment.getShell(shellId)) return {
2878
- environment,
2879
- commandNames: new Set(job.commandNames),
2880
- agentSessionId: job.id
2881
- };
3003
+ for (const job of this.jobs.values()) {
3004
+ for (const environment of job.environments.values()) if (environment.getShell(shellId)) return {
3005
+ environment,
3006
+ commandNames: new Set(job.commandNames),
3007
+ agentSessionId: job.id
3008
+ };
3009
+ const nested = job.ownSupervisor.environmentScopeForShell(shellId);
3010
+ if (nested) return nested;
3011
+ }
2882
3012
  return null;
2883
3013
  }
2884
- /** Re-emits `subagent started` + transcript reset for every running child (transcript resync). */
3014
+ /** Re-emits `subagent started` + transcript reset for the whole live subtree (transcript resync). */
2885
3015
  replay() {
2886
3016
  for (const job of this.jobs.values()) {
2887
3017
  this.options.emit({
@@ -2896,12 +3026,13 @@ var ChildSupervisor = class {
2896
3026
  blocks: structuredClone(transcript.blocks),
2897
3027
  revision: transcript.revision
2898
3028
  });
3029
+ job.ownSupervisor.replay();
2899
3030
  }
2900
3031
  }
2901
3032
  /**
2902
- * Detaches every live child on connection teardown: aborts in-flight turns,
2903
- * flushes checkpoints, and keeps the persisted job so the next open of this
2904
- * parent restores it — the same dispose semantics as the parent session.
3033
+ * Detaches the live subtree on connection teardown: aborts in-flight turns,
3034
+ * flushes checkpoints, and keeps the persisted jobs so the next open of the
3035
+ * owner restores them — the same dispose semantics as the owner session.
2905
3036
  * No `closed` frame is emitted: the children are not done, just paused.
2906
3037
  */
2907
3038
  async dispose() {
@@ -2910,21 +3041,24 @@ var ChildSupervisor = class {
2910
3041
  job.isClosing = true;
2911
3042
  job.unsubscribe();
2912
3043
  this.jobs.delete(job.id);
3044
+ this.options.directory.unregister(job.id);
3045
+ await job.ownSupervisor.dispose();
2913
3046
  await job.session.dispose().catch(noop);
2914
3047
  await this.disposeJobShells(job);
2915
3048
  job.settleClosed({ phase: "aborted" });
3049
+ job.wake?.();
2916
3050
  }
2917
3051
  }
2918
3052
  /**
2919
- * Rebuilds every persisted live child of this parent and finishes what it was
3053
+ * Rebuilds every persisted live child of this owner and finishes what it was
2920
3054
  * doing: an interrupted turn resumes from its resume point; an already
2921
- * quiescent child closes with its result. Children share the parent's
2922
- * persistence lifecycle a parent restore is a child restore.
3055
+ * quiescent child closes with its result. Recursive: each restored child
3056
+ * restores its own subtree. Children share the owner's persistence
3057
+ * lifecycle — a session restore is a subtree restore.
2923
3058
  */
2924
3059
  async restore() {
2925
- const parent = this.parentSession;
2926
- if (!parent || this.isDisposed) return;
2927
- const prefix = `agent-sessions/${parent.id()}/subagents/`;
3060
+ if (!this.parentSession || this.isDisposed) return;
3061
+ const prefix = `${this.options.storePrefix}/subagents/`;
2928
3062
  const keys = await this.options.store.list(prefix).catch(() => []);
2929
3063
  const ids = [...new Set(keys.map((key) => key.slice(prefix.length).split("/")[0] ?? "").filter(Boolean))];
2930
3064
  for (const id of ids) {
@@ -2943,12 +3077,14 @@ var ChildSupervisor = class {
2943
3077
  const checkpoint = await this.childSessionStore(id).loadCheckpoint();
2944
3078
  if (!meta || !checkpoint) throw new Error("incomplete persisted subagent");
2945
3079
  const job = this.reassembleJob(id, meta, checkpoint);
2946
- this.watchTurn(job, job.session.resume(meta.metadata ? { metadata: meta.metadata } : {}));
3080
+ this.trackTurn(job, job.session.resume(meta.metadata ? { metadata: meta.metadata } : {}));
3081
+ this.settleJob(job);
3082
+ await job.ownSupervisor.restore();
2947
3083
  }
2948
3084
  /** Shared by restore and resume: rebuild a persisted child's job and session from its checkpoint. */
2949
3085
  reassembleJob(id, meta, checkpoint) {
2950
3086
  const parent = this.parentSession;
2951
- if (!parent) throw new Error("subagent supervisor has no parent session");
3087
+ if (!parent) throw new Error("subagent supervisor has no owner session");
2952
3088
  const profile = this.resolveProfile(meta.profileName ?? void 0);
2953
3089
  const { job, runtime } = this.assembleJob({
2954
3090
  id,
@@ -2956,7 +3092,8 @@ var ChildSupervisor = class {
2956
3092
  profileName: meta.profileName,
2957
3093
  profile,
2958
3094
  metadata: meta.metadata,
2959
- spawnedAt: meta.spawnedAt
3095
+ spawnedAt: meta.spawnedAt,
3096
+ canSpawnSubagents: meta.canSpawnSubagents !== false
2960
3097
  });
2961
3098
  const session = AgentSession.fromCheckpoint({
2962
3099
  provider: parent.cloneProviderRuntime(),
@@ -2978,10 +3115,10 @@ var ChildSupervisor = class {
2978
3115
  */
2979
3116
  async resumeArchived(id, message) {
2980
3117
  const parent = this.parentSession;
2981
- if (!parent) throw new Error("subagent supervisor has no parent session");
2982
- if (this.isDisposed) throw new Error("parent session is closing");
2983
- if (this.jobs.has(id)) throw new Error(`subagent "${id}" is still running; steer it instead`);
2984
- if (this.jobs.size >= 8) throw new Error(`at most 8 running subagents per session; abort one or wait for a result`);
3118
+ if (!parent) throw new Error("subagent supervisor has no owner session");
3119
+ if (this.isDisposed) throw new Error("owner session is closing");
3120
+ if (this.jobs.has(id)) throw new Error(`subagent "${id}" is still running; send or steer it instead`);
3121
+ if (this.jobs.size >= this.options.maxLiveSubagents) throw new Error(`at most ${this.options.maxLiveSubagents} running subagents per session; abort one or wait for a result`);
2985
3122
  const meta = await this.options.store.readJson(this.childStoreKey(id, "job.json"));
2986
3123
  if (!meta?.closedPhase) throw new Error(`no archived subagent "${id}" (see \`demi agent list\`)`);
2987
3124
  const checkpoint = await this.childSessionStore(id).loadCheckpoint();
@@ -2990,21 +3127,22 @@ var ChildSupervisor = class {
2990
3127
  description: meta.description,
2991
3128
  profileName: meta.profileName,
2992
3129
  metadata: parent.actionMetadata(),
2993
- spawnedAt: Date.now()
3130
+ spawnedAt: Date.now(),
3131
+ ...meta.canSpawnSubagents === false ? { canSpawnSubagents: false } : {}
2994
3132
  };
2995
3133
  await this.options.store.writeJson(this.childStoreKey(id, "job.json"), liveMeta);
2996
3134
  const job = this.reassembleJob(id, liveMeta, checkpoint);
2997
- this.watchTurn(job, job.session.send([{
3135
+ this.trackTurn(job, job.session.send([{
2998
3136
  type: "text",
2999
3137
  text: message
3000
3138
  }], liveMeta.metadata ? { metadata: liveMeta.metadata } : {}));
3139
+ this.settleJob(job);
3001
3140
  return job;
3002
3141
  }
3003
- /** Every archived (finished, revivable) child of this parent, newest first. */
3142
+ /** Every archived (finished, revivable) child of this owner, newest first. */
3004
3143
  async listArchivedJobs() {
3005
- const parent = this.parentSession;
3006
- if (!parent) return [];
3007
- const prefix = `agent-sessions/${parent.id()}/subagents/`;
3144
+ if (!this.parentSession) return [];
3145
+ const prefix = `${this.options.storePrefix}/subagents/`;
3008
3146
  const keys = await this.options.store.list(prefix).catch(() => []);
3009
3147
  const ids = [...new Set(keys.map((key) => key.slice(prefix.length).split("/")[0] ?? "").filter(Boolean))];
3010
3148
  const archived = [];
@@ -3018,42 +3156,42 @@ var ChildSupervisor = class {
3018
3156
  }
3019
3157
  return archived.sort((a, b) => (b.meta.closedAt ?? 0) - (a.meta.closedAt ?? 0));
3020
3158
  }
3021
- async pruneArchive() {
3022
- const archived = await this.listArchivedJobs();
3023
- for (const stale of archived.slice(16)) await this.deletePersistedJob(stale.id);
3024
- }
3025
3159
  async abortSubtree(id) {
3026
3160
  const job = this.jobs.get(id);
3027
3161
  if (!job) return;
3028
3162
  await this.closeJob(job, "aborted");
3029
3163
  }
3030
- /** Aborts every live child; the archive is untouched. */
3164
+ /** Aborts every live child (each with its subtree); the archive is untouched. */
3031
3165
  async abortAll() {
3032
3166
  for (const id of [...this.jobs.keys()]) await this.abortSubtree(id);
3033
3167
  }
3034
3168
  async spawn(input) {
3035
3169
  const parent = this.parentSession;
3036
- if (!parent) throw new Error("subagent supervisor has no parent session");
3037
- if (this.isDisposed) throw new Error("parent session is closing");
3038
- if (this.jobs.size >= 8) throw new Error(`at most 8 running subagents per session; abort one or wait for a result`);
3170
+ if (!parent) throw new Error("subagent supervisor has no owner session");
3171
+ if (!this.options.canSpawn) throw new Error("this session may not spawn subagents");
3172
+ if (this.isDisposed) throw new Error("owner session is closing");
3173
+ if (this.jobs.size >= this.options.maxLiveSubagents) throw new Error(`at most ${this.options.maxLiveSubagents} running subagents per session; abort one or wait for a result`);
3039
3174
  const profile = this.resolveProfile(input.profileName);
3040
3175
  const id = createId();
3041
3176
  const metadata = parent.actionMetadata();
3042
3177
  const profileName = input.profileName ?? (this.options.profiles ? profile.name : null);
3043
3178
  const spawnedAt = Date.now();
3179
+ const canSpawnSubagents = !input.isSpawnForbidden && profile.canSpawnSubagents !== false;
3044
3180
  const { job, runtime } = this.assembleJob({
3045
3181
  id,
3046
3182
  description: input.description,
3047
3183
  profileName,
3048
3184
  profile,
3049
3185
  metadata,
3050
- spawnedAt
3186
+ spawnedAt,
3187
+ canSpawnSubagents
3051
3188
  });
3052
3189
  await this.options.store.writeJson(this.childStoreKey(id, "job.json"), {
3053
3190
  description: input.description,
3054
3191
  profileName,
3055
3192
  metadata,
3056
- spawnedAt
3193
+ spawnedAt,
3194
+ ...canSpawnSubagents ? {} : { canSpawnSubagents }
3057
3195
  });
3058
3196
  const session = new AgentSession({
3059
3197
  provider: parent.cloneProviderRuntime(),
@@ -3067,18 +3205,22 @@ var ChildSupervisor = class {
3067
3205
  ...this.options.sessionOptions
3068
3206
  });
3069
3207
  this.attachSession(job, session);
3070
- this.watchChild(job, input.prompt);
3208
+ this.trackTurn(job, session.send([{
3209
+ type: "text",
3210
+ text: input.prompt
3211
+ }], metadata ? { metadata } : {}));
3212
+ this.settleJob(job);
3071
3213
  return job;
3072
3214
  }
3073
- /** Everything spawn and restore share: the command tree, the harness runtime, and the job record (session attached separately). */
3215
+ /**
3216
+ * Everything spawn and restore share: the command tree (identical to the
3217
+ * owner's — children spawn, message, and observe exactly like any session),
3218
+ * the child's own supervisor, the harness runtime, and the job record
3219
+ * (session attached separately).
3220
+ */
3074
3221
  assembleJob(input) {
3075
3222
  const { id, profile } = input;
3076
- const agentNode = this.createChildAgentNode(id, input.description);
3077
- const commands = injectSubagentCommand(profile.commands ? profile.commands([...this.options.parentCommands]) : [...this.options.parentCommands], agentNode);
3078
- const commandRegistry = new CommandRegistry();
3079
- for (const command of commands) commandRegistry.register(command);
3080
- const commandNames = commandRegistry.list().map((command) => command.name);
3081
- const commandsPrompt = commandRegistry.renderHelp();
3223
+ const inherited = profile.commands ? profile.commands([...this.options.parentCommands]) : [...this.options.parentCommands];
3082
3224
  let settleClosed;
3083
3225
  const closed = new Promise((resolve) => {
3084
3226
  settleClosed = resolve;
@@ -3090,8 +3232,9 @@ var ChildSupervisor = class {
3090
3232
  profileName: input.profileName,
3091
3233
  metadata: input.metadata,
3092
3234
  session: null,
3093
- commandRegistry,
3094
- commandNames,
3235
+ ownSupervisor: null,
3236
+ commandRegistry: new CommandRegistry(),
3237
+ commandNames: [],
3095
3238
  environments: /* @__PURE__ */ new Map(),
3096
3239
  pendingEnvironments: /* @__PURE__ */ new Map(),
3097
3240
  readonlyHosts: /* @__PURE__ */ new WeakMap(),
@@ -3104,8 +3247,20 @@ var ChildSupervisor = class {
3104
3247
  unsubscribe: noop,
3105
3248
  closed,
3106
3249
  settleClosed,
3107
- isClosing: false
3250
+ isClosing: false,
3251
+ wake: null
3108
3252
  };
3253
+ job.ownSupervisor = new ChildSupervisor({
3254
+ ...this.options,
3255
+ parentCommands: inherited,
3256
+ storePrefix: `${this.options.storePrefix}/subagents/${id}`,
3257
+ canSpawn: input.canSpawnSubagents,
3258
+ onJobsChanged: () => job.wake?.()
3259
+ });
3260
+ const commands = injectSubagentCommand(inherited, job.ownSupervisor.rootCommandNode());
3261
+ for (const command of commands) job.commandRegistry.register(command);
3262
+ job.commandNames = job.commandRegistry.list().map((command) => command.name);
3263
+ const commandsPrompt = job.commandRegistry.renderHelp();
3109
3264
  const agent = this.options.agent;
3110
3265
  const preamble = this.subagentPreamble(id);
3111
3266
  return {
@@ -3130,7 +3285,12 @@ var ChildSupervisor = class {
3130
3285
  }
3131
3286
  attachSession(job, session) {
3132
3287
  job.session = session;
3288
+ job.ownSupervisor.attachParent(session);
3133
3289
  job.unsubscribe = session.subscribe((event) => {
3290
+ if (event.type === "phase_changed") {
3291
+ job.wake?.();
3292
+ return;
3293
+ }
3134
3294
  if (event.type !== "transcript_changed") return;
3135
3295
  this.recordTelemetry(job, event.patches);
3136
3296
  this.options.emit({
@@ -3141,6 +3301,8 @@ var ChildSupervisor = class {
3141
3301
  });
3142
3302
  });
3143
3303
  this.jobs.set(job.id, job);
3304
+ this.options.directory.register(job, this);
3305
+ this.options.onJobsChanged?.();
3144
3306
  this.options.emit({
3145
3307
  type: "subagent",
3146
3308
  event: "started",
@@ -3155,7 +3317,7 @@ var ChildSupervisor = class {
3155
3317
  });
3156
3318
  }
3157
3319
  childStoreKey(childId, file) {
3158
- return `agent-sessions/${this.parentSession?.id() ?? ""}/subagents/${childId}/${file}`;
3320
+ return `${this.options.storePrefix}/subagents/${childId}/${file}`;
3159
3321
  }
3160
3322
  childSessionStore(childId) {
3161
3323
  return {
@@ -3195,7 +3357,77 @@ var ChildSupervisor = class {
3195
3357
  await io.stderr(`demi agent: subagent ${job.id} failed: ${close.failure ?? "unknown error"}\n`);
3196
3358
  return { exitCode: 1 };
3197
3359
  }
3198
- /** Delivers one steer to a child: mid-turn as a steer, idle as a new user turn. */
3360
+ /**
3361
+ * Resolves a send/steer target against the directory. `parent` is the
3362
+ * session that spawned the caller; a caller cannot message itself.
3363
+ */
3364
+ resolveTarget(rawId) {
3365
+ const selfId = this.ownerId();
3366
+ let id = rawId;
3367
+ if (id === "parent") {
3368
+ const parentId = this.options.directory.parentIdOf(selfId);
3369
+ if (parentId === null) throw new Error("the root session has no parent");
3370
+ if (parentId === void 0) throw new Error("this session is not in the agent directory");
3371
+ id = parentId;
3372
+ }
3373
+ if (id === selfId) throw new Error("cannot message your own session");
3374
+ if (id === this.options.directory.rootId()) return {
3375
+ id,
3376
+ session: this.options.directory.rootSession(),
3377
+ job: null,
3378
+ owner: null
3379
+ };
3380
+ const entry = this.options.directory.liveEntry(id);
3381
+ if (!entry || entry.job.isClosing) throw new Error(`no live agent "${id}" (see \`demi agent list\`; an archived child is revived only by its parent via resume)`);
3382
+ return {
3383
+ id,
3384
+ session: entry.job.session,
3385
+ job: entry.job,
3386
+ owner: entry.owner
3387
+ };
3388
+ }
3389
+ /**
3390
+ * Mailbox delivery: an ordinary user send on the target session. The
3391
+ * session's own action queue is the inbox — a busy target sees it as a new
3392
+ * user turn after the current one; an idle root wakes; a finishing subagent
3393
+ * is kept open for one more turn by its settle loop (the enqueue lands
3394
+ * before the loop's synchronous close check, so nothing drops silently).
3395
+ */
3396
+ deliverSend(rawId, message) {
3397
+ const target = this.resolveTarget(rawId);
3398
+ const content = [{
3399
+ type: "text",
3400
+ text: `${this.senderPrefix()} ${message}`
3401
+ }];
3402
+ const metadata = target.job ? target.job.metadata : this.senderMetadata();
3403
+ if (target.job && target.owner) {
3404
+ target.owner.trackTurn(target.job, target.session.send(content, metadata ? { metadata } : {}));
3405
+ target.job.wake?.();
3406
+ } else target.session.send(content, metadata ? { metadata } : {}).catch(noop);
3407
+ return target.id;
3408
+ }
3409
+ /** Chime-in delivery: injects into the target's running turn; no fallback when idle. */
3410
+ async deliverSteer(rawId, message) {
3411
+ const target = this.resolveTarget(rawId);
3412
+ if (target.session.phase() === "idle") throw new Error(`agent "${target.id}" has no running turn to steer; use \`demi agent send\``);
3413
+ const content = [{
3414
+ type: "text",
3415
+ text: `${this.senderPrefix()} ${message}`
3416
+ }];
3417
+ await target.session.steer(content);
3418
+ return target.id;
3419
+ }
3420
+ senderPrefix() {
3421
+ const selfId = this.ownerId();
3422
+ const entry = this.options.directory.liveEntry(selfId);
3423
+ const description = entry ? entry.job.description : "root session";
3424
+ return `[agent ${selfId}${description ? ` — ${description}` : ""}]`;
3425
+ }
3426
+ /** Metadata for a turn on the root: the sender subtree's spawning round, or null from the root itself. */
3427
+ senderMetadata() {
3428
+ return this.options.directory.liveEntry(this.ownerId())?.job.metadata ?? null;
3429
+ }
3430
+ /** Delivers one stdin chunk from the attending spawner: mid-turn as a steer, otherwise as a new user turn. */
3199
3431
  async steerChild(job, message) {
3200
3432
  const content = [{
3201
3433
  type: "text",
@@ -3205,7 +3437,7 @@ var ChildSupervisor = class {
3205
3437
  await job.session.steer(content);
3206
3438
  return;
3207
3439
  } catch {}
3208
- this.watchTurn(job, job.session.send(content));
3440
+ this.trackTurn(job, job.session.send(content));
3209
3441
  }
3210
3442
  async pumpStdinSteers(id, stdinStream) {
3211
3443
  try {
@@ -3217,47 +3449,52 @@ var ChildSupervisor = class {
3217
3449
  }
3218
3450
  } catch {}
3219
3451
  }
3220
- watchChild(job, prompt) {
3221
- const opening = job.session.send([{
3222
- type: "text",
3223
- text: prompt
3224
- }], job.metadata ? { metadata: job.metadata } : {});
3225
- this.watchTurn(job, opening);
3226
- }
3227
- /** Tracks one child turn to session quiescence, then closes the job with its outcome. */
3228
- async watchTurn(job, turn) {
3229
- try {
3230
- await turn;
3231
- } catch (error) {
3452
+ /** Observes one turn promise of an own child: a non-abort failure closes the job as an error. */
3453
+ trackTurn(job, turn) {
3454
+ turn.catch((error) => {
3455
+ if (job.isClosing) return;
3232
3456
  job.failure = errorMessage(error);
3233
- await this.closeJob(job, "error");
3234
- return;
3235
- }
3236
- await this.waitForQuiescence(job);
3237
- if (job.phase === "running" && !job.isClosing) await this.closeJob(job, "completed");
3238
- }
3239
- /** Resolves when the child is idle with no pending yield wakeups (or the job closed). */
3240
- async waitForQuiescence(job) {
3241
- while (!job.isClosing && (job.session.phase() !== "idle" || job.session.hasPendingYields())) await new Promise((resolve) => {
3242
- let settled = false;
3243
- const finish = () => {
3244
- if (settled) return;
3245
- settled = true;
3246
- unsubscribe();
3247
- resolve();
3248
- };
3249
- const unsubscribe = job.session.subscribe((event) => {
3250
- if (event.type === "phase_changed") finish();
3251
- });
3252
- job.closed.then(finish);
3457
+ this.closeJob(job, "error");
3253
3458
  });
3254
3459
  }
3460
+ /**
3461
+ * The one place a child closes naturally. Loops until the child is
3462
+ * quiescent: no running or queued turn (the session action queue doubles as
3463
+ * the mailbox), no pending yield wakeups, and no live children of its own.
3464
+ * The final check-and-close is synchronous, so a send that lands before it
3465
+ * is processed and one that lands after it fails on `isClosing` — nothing
3466
+ * drops silently.
3467
+ */
3468
+ async settleJob(job) {
3469
+ while (!job.isClosing) {
3470
+ if (job.session.isSettled() && !job.session.hasPendingYields() && !job.ownSupervisor.hasLiveJobs()) {
3471
+ this.closeJob(job, "completed");
3472
+ return;
3473
+ }
3474
+ await new Promise((resolve) => {
3475
+ let settled = false;
3476
+ const finish = () => {
3477
+ if (settled) return;
3478
+ settled = true;
3479
+ job.wake = null;
3480
+ resolve();
3481
+ };
3482
+ job.wake = finish;
3483
+ job.closed.then(finish);
3484
+ if (job.session.isSettled()) return;
3485
+ job.session.waitUntilDone().then(finish);
3486
+ });
3487
+ }
3488
+ }
3255
3489
  async closeJob(job, phase) {
3256
3490
  if (job.isClosing) return;
3257
3491
  job.isClosing = true;
3258
3492
  job.phase = phase;
3259
3493
  job.unsubscribe();
3260
3494
  this.jobs.delete(job.id);
3495
+ this.options.directory.unregister(job.id);
3496
+ job.wake?.();
3497
+ await job.ownSupervisor.abortAll();
3261
3498
  const result = phase === "completed" ? boundedResultText(lastAssistantText(job.session.transcript().blocks)) : void 0;
3262
3499
  await job.session.dispose().catch(noop);
3263
3500
  await this.disposeJobShells(job);
@@ -3269,7 +3506,6 @@ var ChildSupervisor = class {
3269
3506
  closedPhase: phase,
3270
3507
  closedAt: Date.now()
3271
3508
  }).catch(noop);
3272
- await this.pruneArchive().catch(noop);
3273
3509
  const close = {
3274
3510
  phase,
3275
3511
  ...result !== void 0 ? { result } : {},
@@ -3281,6 +3517,7 @@ var ChildSupervisor = class {
3281
3517
  job: this.wireJob(job, result)
3282
3518
  });
3283
3519
  job.settleClosed(close);
3520
+ this.options.onJobsChanged?.();
3284
3521
  this.notifyIdleParent(job, close);
3285
3522
  }
3286
3523
  async disposeJobShells(job) {
@@ -3291,7 +3528,7 @@ var ChildSupervisor = class {
3291
3528
  }
3292
3529
  for (const environment of environments) await environment.disposeAllShells().catch(noop);
3293
3530
  }
3294
- /** Wakes an idle parent with a user send; a parent blocked in the spawn gets the tool result instead. */
3531
+ /** Wakes an idle owner with a user send; an owner blocked in the spawn gets the tool result instead. */
3295
3532
  notifyIdleParent(job, close) {
3296
3533
  if (this.isDisposed || !this.options.notifyParentOnIdle) return;
3297
3534
  const parent = this.parentSession;
@@ -3303,47 +3540,6 @@ var ChildSupervisor = class {
3303
3540
  text: body
3304
3541
  }], job.metadata ? { metadata: job.metadata } : {}).catch(noop);
3305
3542
  }
3306
- createChildAgentNode(childId, description) {
3307
- return {
3308
- name: "agent",
3309
- summary: "Subagent bridge to the parent session.",
3310
- subcommands: [{
3311
- name: "send-parent",
3312
- summary: "Send an interim user message to the parent session. The parent sees it only when it is not blocked waiting on this session. Your result is still the last assistant text when this session ends, not this message.",
3313
- input: { message: z.string().optional().describe("Message body; positional, or stdin/heredoc when omitted.") },
3314
- positionals: ["message"],
3315
- stdinField: "message",
3316
- output: { json: z.object({ accepted: z.boolean() }) },
3317
- run: async ({ parsed, io }) => {
3318
- const message = String(parsed.values.message ?? "").trim();
3319
- if (!message) {
3320
- await io.stderr("demi agent send-parent: message must not be empty\n");
3321
- return { exitCode: 1 };
3322
- }
3323
- this.deliverToParent(childId, description, message);
3324
- await io.stdout(parsed.json ? `${JSON.stringify({ accepted: true })}\n` : "sent\n");
3325
- return { exitCode: 0 };
3326
- }
3327
- }]
3328
- };
3329
- }
3330
- deliverToParent(childId, description, message) {
3331
- const parent = this.parentSession;
3332
- if (!parent || this.isDisposed) return;
3333
- const content = [{
3334
- type: "text",
3335
- text: `[subagent ${childId}${description ? ` — ${description}` : ""}] ${message}`
3336
- }];
3337
- const metadata = this.jobs.get(childId)?.metadata ?? null;
3338
- const sendOptions = metadata ? { metadata } : {};
3339
- if (parent.phase() !== "idle") {
3340
- parent.steer(content).catch(() => {
3341
- parent.send(content, sendOptions).catch(noop);
3342
- });
3343
- return;
3344
- }
3345
- parent.send(content, sendOptions).catch(noop);
3346
- }
3347
3543
  async childEnvironment(job, ctx) {
3348
3544
  const resolved = await this.options.agent.host({
3349
3545
  agentSessionId: job.id,
@@ -3383,8 +3579,7 @@ var ChildSupervisor = class {
3383
3579
  initialEnv: {
3384
3580
  ...prepared.initialEnv,
3385
3581
  DEMI_SUBAGENT_ID: job.id,
3386
- DEMI_PARENT_SESSION_ID: this.parentSession?.id() ?? "",
3387
- DEMI_SUBAGENT_DEPTH: "1"
3582
+ DEMI_PARENT_SESSION_ID: this.ownerId()
3388
3583
  },
3389
3584
  host,
3390
3585
  commands: job.commandRegistry
@@ -3412,9 +3607,9 @@ var ChildSupervisor = class {
3412
3607
  }
3413
3608
  subagentPreamble(childId) {
3414
3609
  return [
3415
- `You are a subagent: an isolated child agent session (id ${childId}) spawned by a parent agent session. Your transcript starts empty; the task brief in the first user message is your entire context.`,
3416
- "When you end your turn with no scheduled wakeups, the session ends and your last assistant text is returned to the parent as the result. Write it for the parent agent, in the shape the task brief asked for.",
3417
- "`demi agent send-parent <message>` sends an interim user message to the parent; it is seen only when the parent is not blocked waiting on this session.",
3610
+ `You are a subagent: an isolated child agent session (id ${childId}) spawned by parent agent session ${this.ownerId()}. Your transcript starts empty; the task brief in the first user message is your entire context.`,
3611
+ "When you end your turn with nothing pending — no queued messages, no scheduled wakeups, no running children of your own — the session ends and your last assistant text is returned to the parent as the result. Write it for the parent agent, in the shape the task brief asked for.",
3612
+ "`demi agent` spawns your own children. `demi agent send <id|parent> <message>` leaves a message any live agent sees at its next turn boundary; `demi agent steer <id> <message>` chimes into a running agent's current turn; `demi agent list` renders the whole agent tree with your position.",
3418
3613
  "You are not talking to the product user; do not address them."
3419
3614
  ].join("\n");
3420
3615
  }
@@ -3482,6 +3677,7 @@ var ChildSupervisor = class {
3482
3677
  const execution = this.executionOf(job);
3483
3678
  const base = {
3484
3679
  subagentId: job.id,
3680
+ parentSessionId: this.ownerId(),
3485
3681
  description: job.description,
3486
3682
  profile: job.profileName,
3487
3683
  phase: job.phase,
@@ -3524,6 +3720,7 @@ var ChildSupervisor = class {
3524
3720
  const execution = this.executionOf(job);
3525
3721
  const lines = [
3526
3722
  `id: ${job.id}`,
3723
+ `parent: ${this.ownerId()}`,
3527
3724
  `description: ${job.description || "(none)"}`,
3528
3725
  `profile: ${job.profileName ?? "default"}`,
3529
3726
  `phase: ${job.phase}`,
@@ -3548,7 +3745,7 @@ var ChildSupervisor = class {
3548
3745
  wireJob(job, result) {
3549
3746
  return {
3550
3747
  subagentId: job.id,
3551
- parentSessionId: this.parentSession?.id() ?? "",
3748
+ parentSessionId: this.ownerId(),
3552
3749
  description: job.description,
3553
3750
  profile: job.profileName,
3554
3751
  phase: job.phase,
@@ -3557,6 +3754,34 @@ var ChildSupervisor = class {
3557
3754
  };
3558
3755
  }
3559
3756
  };
3757
+ function renderTreeNode(node, prefix, isLast, selfId, lines) {
3758
+ const marker = node.id === selfId ? " ← you" : "";
3759
+ const body = node.kind === "root" ? `● ${node.id} (root session)${marker}` : node.kind === "archived" ? `○ ${node.id} archived (${node.phase}${node.closedAgoMs === null ? "" : ` ${formatDuration(node.closedAgoMs)} ago`}) ${node.description ? `"${node.description}"` : "(no description)"}` : `● ${node.line}${marker}`;
3760
+ if (node.parentId === null) lines.push(body);
3761
+ else lines.push(`${prefix}${isLast ? "└─" : "├─"}${body}`);
3762
+ const childPrefix = node.parentId === null ? "" : `${prefix}${isLast ? " " : "│ "}`;
3763
+ node.children.forEach((child, index) => {
3764
+ renderTreeNode(child, childPrefix, index === node.children.length - 1, selfId, lines);
3765
+ });
3766
+ }
3767
+ function flattenTree(nodes, selfId) {
3768
+ const flat = [];
3769
+ const visit = (node) => {
3770
+ flat.push({
3771
+ subagentId: node.id,
3772
+ parentSessionId: node.parentId,
3773
+ kind: node.kind,
3774
+ description: node.description,
3775
+ profile: node.profile,
3776
+ phase: node.phase,
3777
+ closedAgoMs: node.closedAgoMs,
3778
+ self: node.id === selfId
3779
+ });
3780
+ node.children.forEach(visit);
3781
+ };
3782
+ nodes.forEach(visit);
3783
+ return flat;
3784
+ }
3560
3785
  /**
3561
3786
  * Grafts the `agent` node under a `demi` root: onto an existing harness `demi`
3562
3787
  * tree, or as a new `demi` root when the harness has none.
@@ -3693,6 +3918,7 @@ var AgentServer = class {
3693
3918
  sessionOptions;
3694
3919
  prepareShell;
3695
3920
  notifyParentOnIdle;
3921
+ maxLiveSubagents;
3696
3922
  bindings = /* @__PURE__ */ new Set();
3697
3923
  sessionOwnership = new SessionOwnershipRegistry();
3698
3924
  constructor(options) {
@@ -3702,6 +3928,7 @@ var AgentServer = class {
3702
3928
  this.sessionOptions = options.session ?? {};
3703
3929
  this.prepareShell = options.prepareShell ?? null;
3704
3930
  this.notifyParentOnIdle = options.subagents?.notifyParentOnIdle ?? true;
3931
+ this.maxLiveSubagents = options.subagents?.maxLiveSubagents ?? 8;
3705
3932
  }
3706
3933
  client() {
3707
3934
  const transports = createInProcessTransportPair();
@@ -3717,6 +3944,7 @@ var AgentServer = class {
3717
3944
  session: this.sessionOptions,
3718
3945
  prepareShell: this.prepareShell,
3719
3946
  notifyParentOnIdle: this.notifyParentOnIdle,
3947
+ maxLiveSubagents: this.maxLiveSubagents,
3720
3948
  sessions: this.sessionOwnership
3721
3949
  });
3722
3950
  this.bindings.add(binding);
@@ -3767,6 +3995,7 @@ var AgentTransportBindingImpl = class {
3767
3995
  sessionOptions;
3768
3996
  prepareShell;
3769
3997
  notifyParentOnIdle;
3998
+ maxLiveSubagents;
3770
3999
  sessions;
3771
4000
  session = null;
3772
4001
  currentAgent = null;
@@ -3789,6 +4018,7 @@ var AgentTransportBindingImpl = class {
3789
4018
  this.sessionOptions = options.session ?? {};
3790
4019
  this.prepareShell = options.prepareShell;
3791
4020
  this.notifyParentOnIdle = options.notifyParentOnIdle;
4021
+ this.maxLiveSubagents = options.maxLiveSubagents;
3792
4022
  this.sessions = options.sessions;
3793
4023
  this.unsubscribeTransport = this.transport.onFrame((frame) => {
3794
4024
  this.handleFrame(frame);
@@ -4000,6 +4230,7 @@ var AgentTransportBindingImpl = class {
4000
4230
  };
4001
4231
  const harnessCommands = await agent.commands?.(harnessContext) ?? [];
4002
4232
  const profiles = await agent.agents?.(harnessContext) ?? null;
4233
+ const directory = new AgentDirectory();
4003
4234
  const supervisor = new ChildSupervisor({
4004
4235
  agent,
4005
4236
  cwd: frame.cwd,
@@ -4010,6 +4241,11 @@ var AgentTransportBindingImpl = class {
4010
4241
  sessionOptions: this.sessionOptions,
4011
4242
  notifyParentOnIdle: this.notifyParentOnIdle,
4012
4243
  store: provisionalHost.store,
4244
+ storePrefix: `agent-sessions/${agentSessionId}`,
4245
+ directory,
4246
+ maxLiveSubagents: this.maxLiveSubagents,
4247
+ canSpawn: true,
4248
+ onJobsChanged: null,
4013
4249
  emit: (subagentFrame) => this.send(subagentFrame)
4014
4250
  });
4015
4251
  const commands = injectSubagentCommand(harnessCommands, supervisor.rootCommandNode());
@@ -4062,6 +4298,7 @@ var AgentTransportBindingImpl = class {
4062
4298
  sessionRef = session;
4063
4299
  this.session = session;
4064
4300
  supervisor.attachParent(session);
4301
+ directory.attachRoot(session, supervisor);
4065
4302
  this.supervisor = supervisor;
4066
4303
  this.currentAgent = agent;
4067
4304
  this.currentCommandRegistry = commandRegistry;
@@ -4498,4 +4735,4 @@ function createProviderMap(providers) {
4498
4735
  return map;
4499
4736
  }
4500
4737
  //#endregion
4501
- export { AgentClient, AgentServer, AgentSession, ChildSupervisor, DEFAULT_MAX_MEDIA_BYTES, DEFAULT_TURN_RETRY_POLICY, MAX_ARCHIVED_SUBAGENTS, MAX_LIVE_SUBAGENTS, ProviderStreamError, RunCommandLineCommandNotRegisteredError, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError, SHELL_VIEW_MAX_CHARS, SUBAGENT_RESULT_MAX_BYTES, TranscriptLog, applyTranscriptPatches, cloneBlocks, createInProcessTransportPair, createReadonlyHost, createStandardAgentTools, createWebSocketClientTransport, createWebSocketServerTransport, estimateTranscriptBlockTokens, findResumePoint, finishShellToolResult, injectSubagentCommand, isContextLengthExceeded, isRetryableCode, resolveRetryPolicy, retryDelayMs, shellCommandHandleRequired, shellPreviewBudgetTokens, toShellToolResult };
4738
+ export { AgentClient, AgentDirectory, AgentServer, AgentSession, ChildSupervisor, DEFAULT_MAX_MEDIA_BYTES, DEFAULT_TURN_RETRY_POLICY, MAX_LIVE_SUBAGENTS, ProviderStreamError, RunCommandLineCommandNotRegisteredError, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError, SHELL_VIEW_MAX_CHARS, SUBAGENT_RESULT_MAX_BYTES, TranscriptLog, applyTranscriptPatches, cloneBlocks, createInProcessTransportPair, createReadonlyHost, createStandardAgentTools, createWebSocketClientTransport, createWebSocketServerTransport, estimateTranscriptBlockTokens, findResumePoint, finishShellToolResult, injectSubagentCommand, isContextLengthExceeded, isRetryableCode, resolveRetryPolicy, retryDelayMs, shellCommandHandleRequired, shellPreviewBudgetTokens, toShellToolResult };