@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/client-entry.d.mts +2 -2
- package/dist/index.d.mts +187 -41
- package/dist/index.mjs +438 -201
- package/dist/stdio-transport.d.mts +1 -1
- package/dist/{transport-_eV2zg9M.d.mts → transport-vzA9vMxT.d.mts} +2 -0
- package/dist/{websocket-transport-BC7OstFf.d.mts → websocket-transport-CyoM_NS1.d.mts} +1 -1
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1553,11 +1553,15 @@ var AgentSession = class AgentSession {
|
|
|
1553
1553
|
};
|
|
1554
1554
|
}
|
|
1555
1555
|
waitUntilDone() {
|
|
1556
|
-
if (
|
|
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
|
-
*
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
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
|
-
|
|
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: "
|
|
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("
|
|
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
|
-
|
|
2746
|
-
|
|
2747
|
-
await io.
|
|
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
|
|
2761
|
-
input: { id: z.string().describe("subagentId
|
|
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:
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
-
|
|
2840
|
-
for (const
|
|
2841
|
-
|
|
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
|
|
2854
|
-
input: { id: z.string().describe("
|
|
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
|
|
2860
|
-
if (!
|
|
2861
|
-
await io.stderr(`demi agent show: no
|
|
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:
|
|
2865
|
-
else await io.stdout(
|
|
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
|
|
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())
|
|
2878
|
-
environment
|
|
2879
|
-
|
|
2880
|
-
|
|
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
|
|
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
|
|
2903
|
-
* flushes checkpoints, and keeps the persisted
|
|
2904
|
-
*
|
|
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
|
|
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.
|
|
2922
|
-
*
|
|
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
|
-
|
|
2926
|
-
|
|
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.
|
|
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
|
|
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
|
|
2982
|
-
if (this.isDisposed) throw new Error("
|
|
2983
|
-
if (this.jobs.has(id)) throw new Error(`subagent "${id}" is still running; steer it instead`);
|
|
2984
|
-
if (this.jobs.size >=
|
|
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.
|
|
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
|
|
3142
|
+
/** Every archived (finished, revivable) child of this owner, newest first. */
|
|
3004
3143
|
async listArchivedJobs() {
|
|
3005
|
-
|
|
3006
|
-
|
|
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
|
|
3037
|
-
if (this.
|
|
3038
|
-
if (this.
|
|
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.
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
3094
|
-
|
|
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
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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 };
|