@demicodes/agent 0.21.0 → 0.22.1

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.
@@ -2652,23 +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;
2659
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.";
2660
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.";
2661
2665
  /**
2662
- * Per-parent subagent supervisor. Owns every child `AgentSession` this parent
2663
- * spawns: lifecycle (spawn / steer / abort / natural end), the `demi agent`
2664
- * command tree, child shell environments, and the `subagent*` protocol frames
2665
- * on the parent connection. Children persist exactly like the parent: each
2666
- * live child keeps a checkpoint and job record under the parent's session
2667
- * directory, and reopening the parent restores and resumes them (`restore`).
2668
- * A closed child moves to the archive: its transcript checkpoint stays on
2669
- * 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.
2670
2763
  */
2671
- var ChildSupervisor = class {
2764
+ var ChildSupervisor = class ChildSupervisor {
2672
2765
  options;
2673
2766
  jobs = /* @__PURE__ */ new Map();
2674
2767
  parentSession = null;
@@ -2679,18 +2772,35 @@ var ChildSupervisor = class {
2679
2772
  attachParent(session) {
2680
2773
  this.parentSession = session;
2681
2774
  }
2682
- /** 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. */
2683
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() {
2684
2793
  const profileNames = this.configuredProfileNames();
2685
2794
  return {
2686
2795
  name: "agent",
2687
- 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.",
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.",
2688
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",
2689
2798
  failureOutput: "non-zero exit with the abort or failure reason on stderr",
2690
2799
  input: {
2691
2800
  prompt: z.string().optional().describe(SPAWN_PROMPT_DESCRIPTION),
2692
2801
  profile: z.string().optional().describe(`Named subagent profile configured at harness assembly. Available: ${profileNames.join(", ")}.`),
2693
- 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.")
2694
2804
  },
2695
2805
  positionals: ["prompt"],
2696
2806
  stdinField: "prompt",
@@ -2710,7 +2820,8 @@ var ChildSupervisor = class {
2710
2820
  job = await this.spawn({
2711
2821
  prompt,
2712
2822
  profileName: parsed.values.profile === void 0 ? void 0 : String(parsed.values.profile),
2713
- 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
2714
2825
  });
2715
2826
  } catch (error) {
2716
2827
  await io.stderr(`demi agent: ${errorMessage(error)}\n`);
@@ -2724,11 +2835,43 @@ var ChildSupervisor = class {
2724
2835
  });
2725
2836
  },
2726
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
+ },
2727
2870
  {
2728
2871
  name: "steer",
2729
- 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`.",
2730
2873
  input: {
2731
- 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"),
2732
2875
  message: z.string().optional().describe("Message body; positional, or stdin/heredoc when omitted.")
2733
2876
  },
2734
2877
  positionals: ["id", "message"],
@@ -2738,29 +2881,28 @@ var ChildSupervisor = class {
2738
2881
  accepted: z.boolean()
2739
2882
  }) },
2740
2883
  run: async ({ parsed, io }) => {
2741
- const id = String(parsed.values.id);
2742
2884
  const message = String(parsed.values.message ?? "").trim();
2743
2885
  if (!message) {
2744
2886
  await io.stderr("demi agent steer: message must not be empty\n");
2745
2887
  return { exitCode: 1 };
2746
2888
  }
2747
- const job = this.jobs.get(id);
2748
- if (!job) {
2749
- 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`);
2750
2898
  return { exitCode: 1 };
2751
2899
  }
2752
- await this.steerChild(job, message);
2753
- await io.stdout(parsed.json ? `${JSON.stringify({
2754
- id,
2755
- accepted: true
2756
- })}\n` : `steered ${id}\n`);
2757
- return { exitCode: 0 };
2758
2900
  }
2759
2901
  },
2760
2902
  {
2761
2903
  name: "abort",
2762
- summary: "Abort a running child and its subtree. Siblings are untouched.",
2763
- 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") },
2764
2906
  positionals: ["id"],
2765
2907
  output: { json: z.object({
2766
2908
  id: z.string(),
@@ -2769,7 +2911,7 @@ var ChildSupervisor = class {
2769
2911
  run: async ({ parsed, io }) => {
2770
2912
  const id = String(parsed.values.id);
2771
2913
  if (!this.jobs.has(id)) {
2772
- 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`);
2773
2915
  return { exitCode: 1 };
2774
2916
  }
2775
2917
  await this.abortSubtree(id);
@@ -2782,9 +2924,9 @@ var ChildSupervisor = class {
2782
2924
  },
2783
2925
  {
2784
2926
  name: "resume",
2785
- 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.",
2786
2928
  input: {
2787
- id: z.string().describe("subagentId of an archived child"),
2929
+ id: z.string().describe("subagentId of one of your archived children"),
2788
2930
  message: z.string().optional().describe("The reviving user message; positional, or stdin/heredoc when omitted.")
2789
2931
  },
2790
2932
  positionals: ["id", "message"],
@@ -2818,54 +2960,35 @@ var ChildSupervisor = class {
2818
2960
  },
2819
2961
  {
2820
2962
  name: "list",
2821
- 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.",
2822
- output: { json: z.object({
2823
- agents: z.array(z.unknown()),
2824
- archived: z.array(z.unknown())
2825
- }) },
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()) }) },
2826
2965
  run: async ({ parsed, io }) => {
2827
- const jobs = [...this.jobs.values()];
2828
- const archived = await this.listArchivedJobs();
2966
+ const nodes = await this.options.directory.tree();
2829
2967
  if (parsed.json) {
2830
- await io.stdout(`${JSON.stringify({
2831
- agents: jobs.map((job) => this.snapshot(job, false)),
2832
- archived: archived.map(({ id, meta }) => ({
2833
- subagentId: id,
2834
- description: meta.description,
2835
- profile: meta.profileName,
2836
- phase: meta.closedPhase,
2837
- closedAgoMs: meta.closedAt === void 0 ? null : Date.now() - meta.closedAt
2838
- }))
2839
- })}\n`);
2968
+ await io.stdout(`${JSON.stringify({ tree: flattenTree(nodes, this.ownerId()) })}\n`);
2840
2969
  return { exitCode: 0 };
2841
2970
  }
2842
- if (jobs.length === 0) await io.stdout("no running subagents\n");
2843
- for (const job of jobs) await io.stdout(`${this.renderListLine(job)}\n`);
2844
- if (archived.length > 0) {
2845
- await io.stdout("archived (revivable with `demi agent resume <id>`):\n");
2846
- for (const { id, meta } of archived) {
2847
- const closedAgo = meta.closedAt === void 0 ? "" : ` closed ${formatDuration(Date.now() - meta.closedAt)} ago`;
2848
- await io.stdout(` ${id} ${meta.closedPhase}${closedAgo} ${meta.description ? `"${meta.description}"` : "(no description)"}\n`);
2849
- }
2850
- }
2971
+ const lines = [];
2972
+ for (const node of nodes) renderTreeNode(node, "", true, this.ownerId(), lines);
2973
+ await io.stdout(`${lines.join("\n")}\n`);
2851
2974
  return { exitCode: 0 };
2852
2975
  }
2853
2976
  },
2854
2977
  {
2855
2978
  name: "show",
2856
- 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.",
2857
- 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") },
2858
2981
  positionals: ["id"],
2859
2982
  output: { json: z.object({ agent: z.unknown() }) },
2860
2983
  run: async ({ parsed, io }) => {
2861
2984
  const id = String(parsed.values.id);
2862
- const job = this.jobs.get(id);
2863
- if (!job) {
2864
- 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`);
2865
2988
  return { exitCode: 1 };
2866
2989
  }
2867
- if (parsed.json) await io.stdout(`${JSON.stringify({ agent: this.snapshot(job, true) })}\n`);
2868
- 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));
2869
2992
  return { exitCode: 0 };
2870
2993
  }
2871
2994
  }
@@ -2875,16 +2998,20 @@ var ChildSupervisor = class {
2875
2998
  hasShell(shellId) {
2876
2999
  return this.environmentScopeForShell(shellId) !== null;
2877
3000
  }
2878
- /** 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. */
2879
3002
  environmentScopeForShell(shellId) {
2880
- for (const job of this.jobs.values()) for (const environment of job.environments.values()) if (environment.getShell(shellId)) return {
2881
- environment,
2882
- commandNames: new Set(job.commandNames),
2883
- agentSessionId: job.id
2884
- };
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
+ }
2885
3012
  return null;
2886
3013
  }
2887
- /** 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). */
2888
3015
  replay() {
2889
3016
  for (const job of this.jobs.values()) {
2890
3017
  this.options.emit({
@@ -2899,12 +3026,13 @@ var ChildSupervisor = class {
2899
3026
  blocks: structuredClone(transcript.blocks),
2900
3027
  revision: transcript.revision
2901
3028
  });
3029
+ job.ownSupervisor.replay();
2902
3030
  }
2903
3031
  }
2904
3032
  /**
2905
- * Detaches every live child on connection teardown: aborts in-flight turns,
2906
- * flushes checkpoints, and keeps the persisted job so the next open of this
2907
- * 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.
2908
3036
  * No `closed` frame is emitted: the children are not done, just paused.
2909
3037
  */
2910
3038
  async dispose() {
@@ -2913,21 +3041,24 @@ var ChildSupervisor = class {
2913
3041
  job.isClosing = true;
2914
3042
  job.unsubscribe();
2915
3043
  this.jobs.delete(job.id);
3044
+ this.options.directory.unregister(job.id);
3045
+ await job.ownSupervisor.dispose();
2916
3046
  await job.session.dispose().catch(noop);
2917
3047
  await this.disposeJobShells(job);
2918
3048
  job.settleClosed({ phase: "aborted" });
3049
+ job.wake?.();
2919
3050
  }
2920
3051
  }
2921
3052
  /**
2922
- * 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
2923
3054
  * doing: an interrupted turn resumes from its resume point; an already
2924
- * quiescent child closes with its result. Children share the parent's
2925
- * 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.
2926
3058
  */
2927
3059
  async restore() {
2928
- const parent = this.parentSession;
2929
- if (!parent || this.isDisposed) return;
2930
- const prefix = `agent-sessions/${parent.id()}/subagents/`;
3060
+ if (!this.parentSession || this.isDisposed) return;
3061
+ const prefix = `${this.options.storePrefix}/subagents/`;
2931
3062
  const keys = await this.options.store.list(prefix).catch(() => []);
2932
3063
  const ids = [...new Set(keys.map((key) => key.slice(prefix.length).split("/")[0] ?? "").filter(Boolean))];
2933
3064
  for (const id of ids) {
@@ -2946,12 +3077,14 @@ var ChildSupervisor = class {
2946
3077
  const checkpoint = await this.childSessionStore(id).loadCheckpoint();
2947
3078
  if (!meta || !checkpoint) throw new Error("incomplete persisted subagent");
2948
3079
  const job = this.reassembleJob(id, meta, checkpoint);
2949
- 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();
2950
3083
  }
2951
3084
  /** Shared by restore and resume: rebuild a persisted child's job and session from its checkpoint. */
2952
3085
  reassembleJob(id, meta, checkpoint) {
2953
3086
  const parent = this.parentSession;
2954
- if (!parent) throw new Error("subagent supervisor has no parent session");
3087
+ if (!parent) throw new Error("subagent supervisor has no owner session");
2955
3088
  const profile = this.resolveProfile(meta.profileName ?? void 0);
2956
3089
  const { job, runtime } = this.assembleJob({
2957
3090
  id,
@@ -2959,7 +3092,8 @@ var ChildSupervisor = class {
2959
3092
  profileName: meta.profileName,
2960
3093
  profile,
2961
3094
  metadata: meta.metadata,
2962
- spawnedAt: meta.spawnedAt
3095
+ spawnedAt: meta.spawnedAt,
3096
+ canSpawnSubagents: meta.canSpawnSubagents !== false
2963
3097
  });
2964
3098
  const session = AgentSession.fromCheckpoint({
2965
3099
  provider: parent.cloneProviderRuntime(),
@@ -2981,10 +3115,10 @@ var ChildSupervisor = class {
2981
3115
  */
2982
3116
  async resumeArchived(id, message) {
2983
3117
  const parent = this.parentSession;
2984
- if (!parent) throw new Error("subagent supervisor has no parent session");
2985
- if (this.isDisposed) throw new Error("parent session is closing");
2986
- if (this.jobs.has(id)) throw new Error(`subagent "${id}" is still running; steer it instead`);
2987
- 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`);
2988
3122
  const meta = await this.options.store.readJson(this.childStoreKey(id, "job.json"));
2989
3123
  if (!meta?.closedPhase) throw new Error(`no archived subagent "${id}" (see \`demi agent list\`)`);
2990
3124
  const checkpoint = await this.childSessionStore(id).loadCheckpoint();
@@ -2993,21 +3127,22 @@ var ChildSupervisor = class {
2993
3127
  description: meta.description,
2994
3128
  profileName: meta.profileName,
2995
3129
  metadata: parent.actionMetadata(),
2996
- spawnedAt: Date.now()
3130
+ spawnedAt: Date.now(),
3131
+ ...meta.canSpawnSubagents === false ? { canSpawnSubagents: false } : {}
2997
3132
  };
2998
3133
  await this.options.store.writeJson(this.childStoreKey(id, "job.json"), liveMeta);
2999
3134
  const job = this.reassembleJob(id, liveMeta, checkpoint);
3000
- this.watchTurn(job, job.session.send([{
3135
+ this.trackTurn(job, job.session.send([{
3001
3136
  type: "text",
3002
3137
  text: message
3003
3138
  }], liveMeta.metadata ? { metadata: liveMeta.metadata } : {}));
3139
+ this.settleJob(job);
3004
3140
  return job;
3005
3141
  }
3006
- /** Every archived (finished, revivable) child of this parent, newest first. */
3142
+ /** Every archived (finished, revivable) child of this owner, newest first. */
3007
3143
  async listArchivedJobs() {
3008
- const parent = this.parentSession;
3009
- if (!parent) return [];
3010
- const prefix = `agent-sessions/${parent.id()}/subagents/`;
3144
+ if (!this.parentSession) return [];
3145
+ const prefix = `${this.options.storePrefix}/subagents/`;
3011
3146
  const keys = await this.options.store.list(prefix).catch(() => []);
3012
3147
  const ids = [...new Set(keys.map((key) => key.slice(prefix.length).split("/")[0] ?? "").filter(Boolean))];
3013
3148
  const archived = [];
@@ -3021,42 +3156,42 @@ var ChildSupervisor = class {
3021
3156
  }
3022
3157
  return archived.sort((a, b) => (b.meta.closedAt ?? 0) - (a.meta.closedAt ?? 0));
3023
3158
  }
3024
- async pruneArchive() {
3025
- const archived = await this.listArchivedJobs();
3026
- for (const stale of archived.slice(16)) await this.deletePersistedJob(stale.id);
3027
- }
3028
3159
  async abortSubtree(id) {
3029
3160
  const job = this.jobs.get(id);
3030
3161
  if (!job) return;
3031
3162
  await this.closeJob(job, "aborted");
3032
3163
  }
3033
- /** Aborts every live child; the archive is untouched. */
3164
+ /** Aborts every live child (each with its subtree); the archive is untouched. */
3034
3165
  async abortAll() {
3035
3166
  for (const id of [...this.jobs.keys()]) await this.abortSubtree(id);
3036
3167
  }
3037
3168
  async spawn(input) {
3038
3169
  const parent = this.parentSession;
3039
- if (!parent) throw new Error("subagent supervisor has no parent session");
3040
- if (this.isDisposed) throw new Error("parent session is closing");
3041
- 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`);
3042
3174
  const profile = this.resolveProfile(input.profileName);
3043
3175
  const id = createId();
3044
3176
  const metadata = parent.actionMetadata();
3045
3177
  const profileName = input.profileName ?? (this.options.profiles ? profile.name : null);
3046
3178
  const spawnedAt = Date.now();
3179
+ const canSpawnSubagents = !input.isSpawnForbidden && profile.canSpawnSubagents !== false;
3047
3180
  const { job, runtime } = this.assembleJob({
3048
3181
  id,
3049
3182
  description: input.description,
3050
3183
  profileName,
3051
3184
  profile,
3052
3185
  metadata,
3053
- spawnedAt
3186
+ spawnedAt,
3187
+ canSpawnSubagents
3054
3188
  });
3055
3189
  await this.options.store.writeJson(this.childStoreKey(id, "job.json"), {
3056
3190
  description: input.description,
3057
3191
  profileName,
3058
3192
  metadata,
3059
- spawnedAt
3193
+ spawnedAt,
3194
+ ...canSpawnSubagents ? {} : { canSpawnSubagents }
3060
3195
  });
3061
3196
  const session = new AgentSession({
3062
3197
  provider: parent.cloneProviderRuntime(),
@@ -3070,18 +3205,22 @@ var ChildSupervisor = class {
3070
3205
  ...this.options.sessionOptions
3071
3206
  });
3072
3207
  this.attachSession(job, session);
3073
- 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);
3074
3213
  return job;
3075
3214
  }
3076
- /** 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
+ */
3077
3221
  assembleJob(input) {
3078
3222
  const { id, profile } = input;
3079
- const agentNode = this.createChildAgentNode(id, input.description);
3080
- const commands = injectSubagentCommand(profile.commands ? profile.commands([...this.options.parentCommands]) : [...this.options.parentCommands], agentNode);
3081
- const commandRegistry = new CommandRegistry();
3082
- for (const command of commands) commandRegistry.register(command);
3083
- const commandNames = commandRegistry.list().map((command) => command.name);
3084
- const commandsPrompt = commandRegistry.renderHelp();
3223
+ const inherited = profile.commands ? profile.commands([...this.options.parentCommands]) : [...this.options.parentCommands];
3085
3224
  let settleClosed;
3086
3225
  const closed = new Promise((resolve) => {
3087
3226
  settleClosed = resolve;
@@ -3093,8 +3232,9 @@ var ChildSupervisor = class {
3093
3232
  profileName: input.profileName,
3094
3233
  metadata: input.metadata,
3095
3234
  session: null,
3096
- commandRegistry,
3097
- commandNames,
3235
+ ownSupervisor: null,
3236
+ commandRegistry: new CommandRegistry(),
3237
+ commandNames: [],
3098
3238
  environments: /* @__PURE__ */ new Map(),
3099
3239
  pendingEnvironments: /* @__PURE__ */ new Map(),
3100
3240
  readonlyHosts: /* @__PURE__ */ new WeakMap(),
@@ -3107,8 +3247,21 @@ var ChildSupervisor = class {
3107
3247
  unsubscribe: noop,
3108
3248
  closed,
3109
3249
  settleClosed,
3110
- isClosing: false
3250
+ isClosing: false,
3251
+ wake: null
3111
3252
  };
3253
+ job.ownSupervisor = new ChildSupervisor({
3254
+ ...this.options,
3255
+ parentCommands: inherited,
3256
+ storePrefix: `${this.options.storePrefix}/subagents/${id}`,
3257
+ canSpawn: input.canSpawnSubagents,
3258
+ notifyParentOnIdle: true,
3259
+ onJobsChanged: () => job.wake?.()
3260
+ });
3261
+ const commands = injectSubagentCommand(inherited, job.ownSupervisor.rootCommandNode());
3262
+ for (const command of commands) job.commandRegistry.register(command);
3263
+ job.commandNames = job.commandRegistry.list().map((command) => command.name);
3264
+ const commandsPrompt = job.commandRegistry.renderHelp();
3112
3265
  const agent = this.options.agent;
3113
3266
  const preamble = this.subagentPreamble(id);
3114
3267
  return {
@@ -3133,7 +3286,12 @@ var ChildSupervisor = class {
3133
3286
  }
3134
3287
  attachSession(job, session) {
3135
3288
  job.session = session;
3289
+ job.ownSupervisor.attachParent(session);
3136
3290
  job.unsubscribe = session.subscribe((event) => {
3291
+ if (event.type === "phase_changed") {
3292
+ job.wake?.();
3293
+ return;
3294
+ }
3137
3295
  if (event.type !== "transcript_changed") return;
3138
3296
  this.recordTelemetry(job, event.patches);
3139
3297
  this.options.emit({
@@ -3144,6 +3302,8 @@ var ChildSupervisor = class {
3144
3302
  });
3145
3303
  });
3146
3304
  this.jobs.set(job.id, job);
3305
+ this.options.directory.register(job, this);
3306
+ this.options.onJobsChanged?.();
3147
3307
  this.options.emit({
3148
3308
  type: "subagent",
3149
3309
  event: "started",
@@ -3158,7 +3318,7 @@ var ChildSupervisor = class {
3158
3318
  });
3159
3319
  }
3160
3320
  childStoreKey(childId, file) {
3161
- return `agent-sessions/${this.parentSession?.id() ?? ""}/subagents/${childId}/${file}`;
3321
+ return `${this.options.storePrefix}/subagents/${childId}/${file}`;
3162
3322
  }
3163
3323
  childSessionStore(childId) {
3164
3324
  return {
@@ -3198,7 +3358,77 @@ var ChildSupervisor = class {
3198
3358
  await io.stderr(`demi agent: subagent ${job.id} failed: ${close.failure ?? "unknown error"}\n`);
3199
3359
  return { exitCode: 1 };
3200
3360
  }
3201
- /** Delivers one steer to a child: mid-turn as a steer, idle as a new user turn. */
3361
+ /**
3362
+ * Resolves a send/steer target against the directory. `parent` is the
3363
+ * session that spawned the caller; a caller cannot message itself.
3364
+ */
3365
+ resolveTarget(rawId) {
3366
+ const selfId = this.ownerId();
3367
+ let id = rawId;
3368
+ if (id === "parent") {
3369
+ const parentId = this.options.directory.parentIdOf(selfId);
3370
+ if (parentId === null) throw new Error("the root session has no parent");
3371
+ if (parentId === void 0) throw new Error("this session is not in the agent directory");
3372
+ id = parentId;
3373
+ }
3374
+ if (id === selfId) throw new Error("cannot message your own session");
3375
+ if (id === this.options.directory.rootId()) return {
3376
+ id,
3377
+ session: this.options.directory.rootSession(),
3378
+ job: null,
3379
+ owner: null
3380
+ };
3381
+ const entry = this.options.directory.liveEntry(id);
3382
+ 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)`);
3383
+ return {
3384
+ id,
3385
+ session: entry.job.session,
3386
+ job: entry.job,
3387
+ owner: entry.owner
3388
+ };
3389
+ }
3390
+ /**
3391
+ * Mailbox delivery: an ordinary user send on the target session. The
3392
+ * session's own action queue is the inbox — a busy target sees it as a new
3393
+ * user turn after the current one; an idle root wakes; a finishing subagent
3394
+ * is kept open for one more turn by its settle loop (the enqueue lands
3395
+ * before the loop's synchronous close check, so nothing drops silently).
3396
+ */
3397
+ deliverSend(rawId, message) {
3398
+ const target = this.resolveTarget(rawId);
3399
+ const content = [{
3400
+ type: "text",
3401
+ text: `${this.senderPrefix()} ${message}`
3402
+ }];
3403
+ const metadata = target.job ? target.job.metadata : this.senderMetadata();
3404
+ if (target.job && target.owner) {
3405
+ target.owner.trackTurn(target.job, target.session.send(content, metadata ? { metadata } : {}));
3406
+ target.job.wake?.();
3407
+ } else target.session.send(content, metadata ? { metadata } : {}).catch(noop);
3408
+ return target.id;
3409
+ }
3410
+ /** Chime-in delivery: injects into the target's running turn; no fallback when idle. */
3411
+ async deliverSteer(rawId, message) {
3412
+ const target = this.resolveTarget(rawId);
3413
+ if (target.session.phase() === "idle") throw new Error(`agent "${target.id}" has no running turn to steer; use \`demi agent send\``);
3414
+ const content = [{
3415
+ type: "text",
3416
+ text: `${this.senderPrefix()} ${message}`
3417
+ }];
3418
+ await target.session.steer(content);
3419
+ return target.id;
3420
+ }
3421
+ senderPrefix() {
3422
+ const selfId = this.ownerId();
3423
+ const entry = this.options.directory.liveEntry(selfId);
3424
+ const description = entry ? entry.job.description : "root session";
3425
+ return `[agent ${selfId}${description ? ` — ${description}` : ""}]`;
3426
+ }
3427
+ /** Metadata for a turn on the root: the sender subtree's spawning round, or null from the root itself. */
3428
+ senderMetadata() {
3429
+ return this.options.directory.liveEntry(this.ownerId())?.job.metadata ?? null;
3430
+ }
3431
+ /** Delivers one stdin chunk from the attending spawner: mid-turn as a steer, otherwise as a new user turn. */
3202
3432
  async steerChild(job, message) {
3203
3433
  const content = [{
3204
3434
  type: "text",
@@ -3208,7 +3438,7 @@ var ChildSupervisor = class {
3208
3438
  await job.session.steer(content);
3209
3439
  return;
3210
3440
  } catch {}
3211
- this.watchTurn(job, job.session.send(content));
3441
+ this.trackTurn(job, job.session.send(content));
3212
3442
  }
3213
3443
  async pumpStdinSteers(id, stdinStream) {
3214
3444
  try {
@@ -3220,47 +3450,52 @@ var ChildSupervisor = class {
3220
3450
  }
3221
3451
  } catch {}
3222
3452
  }
3223
- watchChild(job, prompt) {
3224
- const opening = job.session.send([{
3225
- type: "text",
3226
- text: prompt
3227
- }], job.metadata ? { metadata: job.metadata } : {});
3228
- this.watchTurn(job, opening);
3229
- }
3230
- /** Tracks one child turn to session quiescence, then closes the job with its outcome. */
3231
- async watchTurn(job, turn) {
3232
- try {
3233
- await turn;
3234
- } catch (error) {
3453
+ /** Observes one turn promise of an own child: a non-abort failure closes the job as an error. */
3454
+ trackTurn(job, turn) {
3455
+ turn.catch((error) => {
3456
+ if (job.isClosing) return;
3235
3457
  job.failure = errorMessage(error);
3236
- await this.closeJob(job, "error");
3237
- return;
3238
- }
3239
- await this.waitForQuiescence(job);
3240
- if (job.phase === "running" && !job.isClosing) await this.closeJob(job, "completed");
3241
- }
3242
- /** Resolves when the child is idle with no pending yield wakeups (or the job closed). */
3243
- async waitForQuiescence(job) {
3244
- while (!job.isClosing && (job.session.phase() !== "idle" || job.session.hasPendingYields())) await new Promise((resolve) => {
3245
- let settled = false;
3246
- const finish = () => {
3247
- if (settled) return;
3248
- settled = true;
3249
- unsubscribe();
3250
- resolve();
3251
- };
3252
- const unsubscribe = job.session.subscribe((event) => {
3253
- if (event.type === "phase_changed") finish();
3254
- });
3255
- job.closed.then(finish);
3458
+ this.closeJob(job, "error");
3256
3459
  });
3257
3460
  }
3461
+ /**
3462
+ * The one place a child closes naturally. Loops until the child is
3463
+ * quiescent: no running or queued turn (the session action queue doubles as
3464
+ * the mailbox), no pending yield wakeups, and no live children of its own.
3465
+ * The final check-and-close is synchronous, so a send that lands before it
3466
+ * is processed and one that lands after it fails on `isClosing` — nothing
3467
+ * drops silently.
3468
+ */
3469
+ async settleJob(job) {
3470
+ while (!job.isClosing) {
3471
+ if (job.session.isSettled() && !job.session.hasPendingYields() && !job.ownSupervisor.hasLiveJobs()) {
3472
+ this.closeJob(job, "completed");
3473
+ return;
3474
+ }
3475
+ await new Promise((resolve) => {
3476
+ let settled = false;
3477
+ const finish = () => {
3478
+ if (settled) return;
3479
+ settled = true;
3480
+ job.wake = null;
3481
+ resolve();
3482
+ };
3483
+ job.wake = finish;
3484
+ job.closed.then(finish);
3485
+ if (job.session.isSettled()) return;
3486
+ job.session.waitUntilDone().then(finish);
3487
+ });
3488
+ }
3489
+ }
3258
3490
  async closeJob(job, phase) {
3259
3491
  if (job.isClosing) return;
3260
3492
  job.isClosing = true;
3261
3493
  job.phase = phase;
3262
3494
  job.unsubscribe();
3263
3495
  this.jobs.delete(job.id);
3496
+ this.options.directory.unregister(job.id);
3497
+ job.wake?.();
3498
+ await job.ownSupervisor.abortAll();
3264
3499
  const result = phase === "completed" ? boundedResultText(lastAssistantText(job.session.transcript().blocks)) : void 0;
3265
3500
  await job.session.dispose().catch(noop);
3266
3501
  await this.disposeJobShells(job);
@@ -3272,7 +3507,6 @@ var ChildSupervisor = class {
3272
3507
  closedPhase: phase,
3273
3508
  closedAt: Date.now()
3274
3509
  }).catch(noop);
3275
- await this.pruneArchive().catch(noop);
3276
3510
  const close = {
3277
3511
  phase,
3278
3512
  ...result !== void 0 ? { result } : {},
@@ -3284,6 +3518,7 @@ var ChildSupervisor = class {
3284
3518
  job: this.wireJob(job, result)
3285
3519
  });
3286
3520
  job.settleClosed(close);
3521
+ this.options.onJobsChanged?.();
3287
3522
  this.notifyIdleParent(job, close);
3288
3523
  }
3289
3524
  async disposeJobShells(job) {
@@ -3294,7 +3529,7 @@ var ChildSupervisor = class {
3294
3529
  }
3295
3530
  for (const environment of environments) await environment.disposeAllShells().catch(noop);
3296
3531
  }
3297
- /** Wakes an idle parent with a user send; a parent blocked in the spawn gets the tool result instead. */
3532
+ /** Wakes an idle owner with a user send; an owner blocked in the spawn gets the tool result instead. */
3298
3533
  notifyIdleParent(job, close) {
3299
3534
  if (this.isDisposed || !this.options.notifyParentOnIdle) return;
3300
3535
  const parent = this.parentSession;
@@ -3306,47 +3541,6 @@ var ChildSupervisor = class {
3306
3541
  text: body
3307
3542
  }], job.metadata ? { metadata: job.metadata } : {}).catch(noop);
3308
3543
  }
3309
- createChildAgentNode(childId, description) {
3310
- return {
3311
- name: "agent",
3312
- summary: "Subagent bridge to the parent session.",
3313
- subcommands: [{
3314
- name: "send-parent",
3315
- 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.",
3316
- input: { message: z.string().optional().describe("Message body; positional, or stdin/heredoc when omitted.") },
3317
- positionals: ["message"],
3318
- stdinField: "message",
3319
- output: { json: z.object({ accepted: z.boolean() }) },
3320
- run: async ({ parsed, io }) => {
3321
- const message = String(parsed.values.message ?? "").trim();
3322
- if (!message) {
3323
- await io.stderr("demi agent send-parent: message must not be empty\n");
3324
- return { exitCode: 1 };
3325
- }
3326
- this.deliverToParent(childId, description, message);
3327
- await io.stdout(parsed.json ? `${JSON.stringify({ accepted: true })}\n` : "sent\n");
3328
- return { exitCode: 0 };
3329
- }
3330
- }]
3331
- };
3332
- }
3333
- deliverToParent(childId, description, message) {
3334
- const parent = this.parentSession;
3335
- if (!parent || this.isDisposed) return;
3336
- const content = [{
3337
- type: "text",
3338
- text: `[subagent ${childId}${description ? ` — ${description}` : ""}] ${message}`
3339
- }];
3340
- const metadata = this.jobs.get(childId)?.metadata ?? null;
3341
- const sendOptions = metadata ? { metadata } : {};
3342
- if (parent.phase() !== "idle") {
3343
- parent.steer(content).catch(() => {
3344
- parent.send(content, sendOptions).catch(noop);
3345
- });
3346
- return;
3347
- }
3348
- parent.send(content, sendOptions).catch(noop);
3349
- }
3350
3544
  async childEnvironment(job, ctx) {
3351
3545
  const resolved = await this.options.agent.host({
3352
3546
  agentSessionId: job.id,
@@ -3386,8 +3580,7 @@ var ChildSupervisor = class {
3386
3580
  initialEnv: {
3387
3581
  ...prepared.initialEnv,
3388
3582
  DEMI_SUBAGENT_ID: job.id,
3389
- DEMI_PARENT_SESSION_ID: this.parentSession?.id() ?? "",
3390
- DEMI_SUBAGENT_DEPTH: "1"
3583
+ DEMI_PARENT_SESSION_ID: this.ownerId()
3391
3584
  },
3392
3585
  host,
3393
3586
  commands: job.commandRegistry
@@ -3415,9 +3608,9 @@ var ChildSupervisor = class {
3415
3608
  }
3416
3609
  subagentPreamble(childId) {
3417
3610
  return [
3418
- `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.`,
3419
- "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.",
3420
- "`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.",
3611
+ `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.`,
3612
+ "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.",
3613
+ "`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.",
3421
3614
  "You are not talking to the product user; do not address them."
3422
3615
  ].join("\n");
3423
3616
  }
@@ -3485,6 +3678,7 @@ var ChildSupervisor = class {
3485
3678
  const execution = this.executionOf(job);
3486
3679
  const base = {
3487
3680
  subagentId: job.id,
3681
+ parentSessionId: this.ownerId(),
3488
3682
  description: job.description,
3489
3683
  profile: job.profileName,
3490
3684
  phase: job.phase,
@@ -3527,6 +3721,7 @@ var ChildSupervisor = class {
3527
3721
  const execution = this.executionOf(job);
3528
3722
  const lines = [
3529
3723
  `id: ${job.id}`,
3724
+ `parent: ${this.ownerId()}`,
3530
3725
  `description: ${job.description || "(none)"}`,
3531
3726
  `profile: ${job.profileName ?? "default"}`,
3532
3727
  `phase: ${job.phase}`,
@@ -3551,7 +3746,7 @@ var ChildSupervisor = class {
3551
3746
  wireJob(job, result) {
3552
3747
  return {
3553
3748
  subagentId: job.id,
3554
- parentSessionId: this.parentSession?.id() ?? "",
3749
+ parentSessionId: this.ownerId(),
3555
3750
  description: job.description,
3556
3751
  profile: job.profileName,
3557
3752
  phase: job.phase,
@@ -3560,6 +3755,34 @@ var ChildSupervisor = class {
3560
3755
  };
3561
3756
  }
3562
3757
  };
3758
+ function renderTreeNode(node, prefix, isLast, selfId, lines) {
3759
+ const marker = node.id === selfId ? " ← you" : "";
3760
+ 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}`;
3761
+ if (node.parentId === null) lines.push(body);
3762
+ else lines.push(`${prefix}${isLast ? "└─" : "├─"}${body}`);
3763
+ const childPrefix = node.parentId === null ? "" : `${prefix}${isLast ? " " : "│ "}`;
3764
+ node.children.forEach((child, index) => {
3765
+ renderTreeNode(child, childPrefix, index === node.children.length - 1, selfId, lines);
3766
+ });
3767
+ }
3768
+ function flattenTree(nodes, selfId) {
3769
+ const flat = [];
3770
+ const visit = (node) => {
3771
+ flat.push({
3772
+ subagentId: node.id,
3773
+ parentSessionId: node.parentId,
3774
+ kind: node.kind,
3775
+ description: node.description,
3776
+ profile: node.profile,
3777
+ phase: node.phase,
3778
+ closedAgoMs: node.closedAgoMs,
3779
+ self: node.id === selfId
3780
+ });
3781
+ node.children.forEach(visit);
3782
+ };
3783
+ nodes.forEach(visit);
3784
+ return flat;
3785
+ }
3563
3786
  /**
3564
3787
  * Grafts the `agent` node under a `demi` root: onto an existing harness `demi`
3565
3788
  * tree, or as a new `demi` root when the harness has none.
@@ -3696,6 +3919,7 @@ var AgentServer = class {
3696
3919
  sessionOptions;
3697
3920
  prepareShell;
3698
3921
  notifyParentOnIdle;
3922
+ maxLiveSubagents;
3699
3923
  bindings = /* @__PURE__ */ new Set();
3700
3924
  sessionOwnership = new SessionOwnershipRegistry();
3701
3925
  constructor(options) {
@@ -3705,6 +3929,7 @@ var AgentServer = class {
3705
3929
  this.sessionOptions = options.session ?? {};
3706
3930
  this.prepareShell = options.prepareShell ?? null;
3707
3931
  this.notifyParentOnIdle = options.subagents?.notifyParentOnIdle ?? true;
3932
+ this.maxLiveSubagents = options.subagents?.maxLiveSubagents ?? 8;
3708
3933
  }
3709
3934
  client() {
3710
3935
  const transports = createInProcessTransportPair();
@@ -3720,6 +3945,7 @@ var AgentServer = class {
3720
3945
  session: this.sessionOptions,
3721
3946
  prepareShell: this.prepareShell,
3722
3947
  notifyParentOnIdle: this.notifyParentOnIdle,
3948
+ maxLiveSubagents: this.maxLiveSubagents,
3723
3949
  sessions: this.sessionOwnership
3724
3950
  });
3725
3951
  this.bindings.add(binding);
@@ -3770,6 +3996,7 @@ var AgentTransportBindingImpl = class {
3770
3996
  sessionOptions;
3771
3997
  prepareShell;
3772
3998
  notifyParentOnIdle;
3999
+ maxLiveSubagents;
3773
4000
  sessions;
3774
4001
  session = null;
3775
4002
  currentAgent = null;
@@ -3792,6 +4019,7 @@ var AgentTransportBindingImpl = class {
3792
4019
  this.sessionOptions = options.session ?? {};
3793
4020
  this.prepareShell = options.prepareShell;
3794
4021
  this.notifyParentOnIdle = options.notifyParentOnIdle;
4022
+ this.maxLiveSubagents = options.maxLiveSubagents;
3795
4023
  this.sessions = options.sessions;
3796
4024
  this.unsubscribeTransport = this.transport.onFrame((frame) => {
3797
4025
  this.handleFrame(frame);
@@ -4003,6 +4231,7 @@ var AgentTransportBindingImpl = class {
4003
4231
  };
4004
4232
  const harnessCommands = await agent.commands?.(harnessContext) ?? [];
4005
4233
  const profiles = await agent.agents?.(harnessContext) ?? null;
4234
+ const directory = new AgentDirectory();
4006
4235
  const supervisor = new ChildSupervisor({
4007
4236
  agent,
4008
4237
  cwd: frame.cwd,
@@ -4013,6 +4242,11 @@ var AgentTransportBindingImpl = class {
4013
4242
  sessionOptions: this.sessionOptions,
4014
4243
  notifyParentOnIdle: this.notifyParentOnIdle,
4015
4244
  store: provisionalHost.store,
4245
+ storePrefix: `agent-sessions/${agentSessionId}`,
4246
+ directory,
4247
+ maxLiveSubagents: this.maxLiveSubagents,
4248
+ canSpawn: true,
4249
+ onJobsChanged: null,
4016
4250
  emit: (subagentFrame) => this.send(subagentFrame)
4017
4251
  });
4018
4252
  const commands = injectSubagentCommand(harnessCommands, supervisor.rootCommandNode());
@@ -4065,6 +4299,7 @@ var AgentTransportBindingImpl = class {
4065
4299
  sessionRef = session;
4066
4300
  this.session = session;
4067
4301
  supervisor.attachParent(session);
4302
+ directory.attachRoot(session, supervisor);
4068
4303
  this.supervisor = supervisor;
4069
4304
  this.currentAgent = agent;
4070
4305
  this.currentCommandRegistry = commandRegistry;
@@ -4501,4 +4736,4 @@ function createProviderMap(providers) {
4501
4736
  return map;
4502
4737
  }
4503
4738
  //#endregion
4504
- 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 };
4739
+ 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 };