@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/client-entry.d.mts +2 -2
- package/dist/index.d.mts +192 -44
- package/dist/index.mjs +435 -200
- 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.
|
|
@@ -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
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2665
|
-
*
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
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
|
-
|
|
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: "
|
|
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("
|
|
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
|
-
|
|
2748
|
-
|
|
2749
|
-
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`);
|
|
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
|
|
2763
|
-
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") },
|
|
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:
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
-
|
|
2843
|
-
for (const
|
|
2844
|
-
|
|
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
|
|
2857
|
-
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") },
|
|
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
|
|
2863
|
-
if (!
|
|
2864
|
-
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`);
|
|
2865
2988
|
return { exitCode: 1 };
|
|
2866
2989
|
}
|
|
2867
|
-
if (parsed.json) await io.stdout(`${JSON.stringify({ agent:
|
|
2868
|
-
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));
|
|
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
|
|
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())
|
|
2881
|
-
environment
|
|
2882
|
-
|
|
2883
|
-
|
|
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
|
|
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
|
|
2906
|
-
* flushes checkpoints, and keeps the persisted
|
|
2907
|
-
*
|
|
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
|
|
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.
|
|
2925
|
-
*
|
|
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
|
-
|
|
2929
|
-
|
|
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.
|
|
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
|
|
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
|
|
2985
|
-
if (this.isDisposed) throw new Error("
|
|
2986
|
-
if (this.jobs.has(id)) throw new Error(`subagent "${id}" is still running; steer it instead`);
|
|
2987
|
-
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`);
|
|
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.
|
|
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
|
|
3142
|
+
/** Every archived (finished, revivable) child of this owner, newest first. */
|
|
3007
3143
|
async listArchivedJobs() {
|
|
3008
|
-
|
|
3009
|
-
|
|
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
|
|
3040
|
-
if (this.
|
|
3041
|
-
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`);
|
|
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.
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
3097
|
-
|
|
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
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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 };
|