@demicodes/agent 0.17.4 → 0.19.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.
@@ -1,4 +1,4 @@
1
- import { b as AgentMetadata, d as TranscriptPatch, l as ServerFrame, n as AgentServerTransport, o as ClientFrame, r as AgentTransport, s as ClientSessionEvent, t as AgentClientTransport, u as ShellCommandStatusLike } from "./transport-DtBCCS_A.mjs";
2
- import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-DdfGgBjO.mjs";
1
+ import { f as TranscriptPatch, l as ServerFrame, n as AgentServerTransport, o as ClientFrame, r as AgentTransport, s as ClientSessionEvent, t as AgentClientTransport, u as ShellCommandStatusLike, x as AgentMetadata } from "./transport-BRqZoaeo.mjs";
2
+ import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-C6Y5CeLG.mjs";
3
3
  import { ProviderSelection } from "@demicodes/provider";
4
4
  export { type AgentActionOptions, AgentClient, type AgentClientListener, type AgentClientTransport, type AgentMetadata, type AgentServerTransport, type AgentTransport, type ClientFrame, type ClientSessionEvent, type JsonWebSocket, type ProviderSelection, type ServerFrame, type ShellCommandStatusLike, type TranscriptPatch, createWebSocketClientTransport, createWebSocketServerTransport };
@@ -1,2 +1,2 @@
1
- import { n as createWebSocketServerTransport, r as AgentClient, t as createWebSocketClientTransport } from "./websocket-transport-c6P3NBuf.mjs";
1
+ import { n as createWebSocketServerTransport, r as AgentClient, t as createWebSocketClientTransport } from "./websocket-transport-CIOotpd-.mjs";
2
2
  export { AgentClient, createWebSocketClientTransport, createWebSocketServerTransport };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { A as AgentTool, B as TranscriptLog, C as AgentSessionCheckpoint, D as AgentSessionRestoreParams, E as AgentSessionParams, F as ModelSwitchApply, G as isRetryableCode, H as estimateTranscriptBlockTokens, I as SessionEvent, K as resolveRetryPolicy, L as SessionEventListener, M as AgentToolInvokeContext, N as AgentToolInvokeResult, O as AgentSessionStore, P as ExternalMutationReservation, R as CompactionWindow, S as AgentReferenceResolveContext, T as AgentSessionOptions, U as DEFAULT_TURN_RETRY_POLICY, V as TranscriptOptions, W as TurnRetryPolicy, _ as AgentHarnessRuntime, a as createInProcessTransportPair, b as AgentMetadata, c as ConversationSummary, d as TranscriptPatch, f as AbortResult, g as AgentHarnessContext, h as AgentHarness, i as InProcessTransportPair, j as AgentToolContext, k as AgentSystemPromptContext, l as ServerFrame, m as AgentDisposeContext, n as AgentServerTransport, o as ClientFrame, p as AbortTarget, q as retryDelayMs, r as AgentTransport, s as ClientSessionEvent, t as AgentClientTransport, u as ShellCommandStatusLike, v as AgentHostContext, w as AgentSessionCloneParams, x as AgentPromptContext, y as AgentLifecycleEvent, z as DrainedTranscriptPatches } from "./transport-DtBCCS_A.mjs";
2
- import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-DdfGgBjO.mjs";
3
- import { BashAuditEvent, BashEnvironment, BashEnvironmentOptions, CommandMetadataRecord, Host, ShellCommandStatus, ShellOutputChunk } from "@demicodes/shell";
1
+ import { A as AgentSystemPromptContext, B as CompactionWindow, C as AgentReferenceResolveContext, D as AgentSessionParams, E as AgentSessionOptions, F as ExternalMutationReservation, G as DEFAULT_TURN_RETRY_POLICY, H as TranscriptLog, I as ModelSwitchApply, J as resolveRetryPolicy, K as TurnRetryPolicy, L as SessionEvent, M as AgentToolContext, N as AgentToolInvokeContext, O as AgentSessionRestoreParams, P as AgentToolInvokeResult, R as SessionEventListener, S as AgentPromptContext, T as AgentSessionCloneParams, U as TranscriptOptions, V as DrainedTranscriptPatches, W as estimateTranscriptBlockTokens, Y as retryDelayMs, _ as AgentHarnessContext, a as createInProcessTransportPair, b as AgentLifecycleEvent, c as ConversationSummary, d as SubagentJob, f as TranscriptPatch, g as AgentHarness, h as AgentDisposeContext, i as InProcessTransportPair, j as AgentTool, k as AgentSessionStore, l as ServerFrame, m as AbortTarget, n as AgentServerTransport, o as ClientFrame, p as AbortResult, q as isRetryableCode, r as AgentTransport, s as ClientSessionEvent, t as AgentClientTransport, u as ShellCommandStatusLike, v as AgentHarnessRuntime, w as AgentSessionCheckpoint, x as AgentMetadata, y as AgentHostContext, z as SubagentProfile } from "./transport-BRqZoaeo.mjs";
2
+ import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-C6Y5CeLG.mjs";
3
+ import { BashAuditEvent, BashEnvironment, BashEnvironmentOptions, Command, CommandMetadataRecord, Host, ShellCommandStatus, ShellOutputChunk } from "@demicodes/shell";
4
4
  import { AgentProvider, Provider } from "@demicodes/provider";
5
5
  import { Block, Model, ModelMediaKind, ModelSelection, ProviderErrorDiagnostics, QueuedMessage, SessionPhase, UserContentBlock } from "@demicodes/core";
6
6
  //#region src/recovery.d.ts
@@ -149,6 +149,13 @@ declare class AgentSession<State> {
149
149
  state(): State;
150
150
  id(): string;
151
151
  phase(): SessionPhase;
152
+ /** Granular in-turn phase for observers (null when no turn is active). */
153
+ turnPhase(): ActiveTurnPhase | null;
154
+ hasPendingYields(): boolean;
155
+ /** Metadata of the currently executing action, if any. */
156
+ actionMetadata(): AgentMetadata | null;
157
+ /** Independent provider runtime with this session's configuration (for child sessions). */
158
+ cloneProviderRuntime(): AgentProvider;
152
159
  queuedMessages(): QueuedMessage[];
153
160
  dequeueMessage(id: string): boolean;
154
161
  sendQueuedMessage(id: string): boolean;
@@ -321,6 +328,92 @@ declare class AgentServer {
321
328
  runCommandLine(shellId: string, name: string, args: string[], opts: RunCommandLineOptions): Promise<RunCommandLineResult>;
322
329
  }
323
330
  //#endregion
331
+ //#region src/subagent.d.ts
332
+ declare const MAX_LIVE_SUBAGENTS = 8;
333
+ declare const SUBAGENT_RESULT_MAX_BYTES: number;
334
+ type SubagentExecution = 'idle' | 'provider_streaming' | 'tool_executing' | 'compacting' | 'finalizing' | 'pending_yield';
335
+ interface ChildSupervisorOptions<State> {
336
+ agent: AgentHarness<State>;
337
+ cwd: string;
338
+ /** Harness-configured profiles; null means the implicit `default` only. */
339
+ profiles: SubagentProfile<State>[] | null;
340
+ /** Parent registered commands (harness list, before the `demi agent` injection). */
341
+ parentCommands: Command[];
342
+ shellOptions: Omit<BashEnvironmentOptions, 'host' | 'commands'>;
343
+ prepareShell: PrepareShell | null;
344
+ sessionOptions: AgentServerSessionOptions;
345
+ emit(frame: ServerFrame): void;
346
+ }
347
+ /**
348
+ * Per-parent subagent supervisor. Owns every child `AgentSession` this parent
349
+ * spawns: lifecycle (spawn / steer / abort / natural end), the `demi agent`
350
+ * command tree, child shell environments, and the `subagent*` protocol frames
351
+ * on the parent connection. Children are in-memory only: no store, no
352
+ * checkpoint, gone with the parent.
353
+ */
354
+ declare class ChildSupervisor<State = unknown> {
355
+ private readonly options;
356
+ private readonly jobs;
357
+ private parentSession;
358
+ private isDisposed;
359
+ constructor(options: ChildSupervisorOptions<State>);
360
+ attachParent(session: AgentSession<State>): void;
361
+ /** The `agent` node AgentServer grafts under the parent registry's `demi` root. */
362
+ rootCommandNode(): Command;
363
+ hasShell(shellId: string): boolean;
364
+ /** Resolves the child scope owning a shell, for the command bridge dispatch. */
365
+ environmentScopeForShell(shellId: string): {
366
+ environment: BashEnvironment;
367
+ commandNames: ReadonlySet<string>;
368
+ agentSessionId: string;
369
+ } | null;
370
+ /** Re-emits `subagent started` + transcript reset for every running child (transcript resync). */
371
+ replay(): void;
372
+ dispose(): Promise<void>;
373
+ abortSubtree(id: string): Promise<void>;
374
+ private spawn;
375
+ /** Delivers one steer to a child: mid-turn as a steer, idle as a new user turn. */
376
+ private steerChild;
377
+ private pumpStdinSteers;
378
+ private watchChild;
379
+ /** Tracks one child turn to session quiescence, then closes the job with its outcome. */
380
+ private watchTurn;
381
+ /** Resolves when the child is idle with no pending yield wakeups (or the job closed). */
382
+ private waitForQuiescence;
383
+ private closeJob;
384
+ /** Wakes an idle parent with a user send; a parent blocked in the spawn gets the tool result instead. */
385
+ private notifyIdleParent;
386
+ private createChildAgentNode;
387
+ private deliverToParent;
388
+ private childEnvironment;
389
+ private createChildEnvironment;
390
+ private resolveProfile;
391
+ private configuredProfileNames;
392
+ private subagentPreamble;
393
+ private recordTelemetry;
394
+ private executionOf;
395
+ private activityOf;
396
+ private executionForMs;
397
+ private snapshot;
398
+ private renderListLine;
399
+ private renderShow;
400
+ private wireJob;
401
+ }
402
+ /**
403
+ * Grafts the `agent` node under a `demi` root: onto an existing harness `demi`
404
+ * tree, or as a new `demi` root when the harness has none.
405
+ */
406
+ declare function injectSubagentCommand(commands: Command[], agentNode: Command): Command[];
407
+ /**
408
+ * Host wrapper for read-only subagent profiles: filesystem mutation is
409
+ * rejected outside the shell's own command-artifacts tree, and process spawn
410
+ * is rejected outright (a real process cannot be write-restricted).
411
+ *
412
+ * Every facet delegates method by method: Host facets are class instances
413
+ * whose methods live on the prototype, so object spread would drop them.
414
+ */
415
+ declare function createReadonlyHost(host: Host): Host;
416
+ //#endregion
324
417
  //#region src/tools.d.ts
325
418
  interface StandardAgentToolOptions<State = unknown> {
326
419
  environment: BashEnvironment | ((ctx: AgentToolInvokeContext<State>, handle: {
@@ -387,4 +480,4 @@ interface ShellToolView {
387
480
  declare function finishShellToolResult<State>(environment: BashEnvironment, result: ShellCommandStatus, ctx: AgentToolInvokeContext<State>): Promise<AgentToolInvokeResult>;
388
481
  declare function shellCommandHandleRequired(result: ShellCommandStatus, budgetTokens: number): boolean;
389
482
  //#endregion
390
- export { AbortResult, AbortTarget, ActiveTurnPhase, AgentActionOptions, AgentClient, AgentClientListener, AgentClientTransport, AgentDisposeContext, AgentHarness, AgentHarnessContext, AgentHarnessRuntime, AgentHostContext, AgentLifecycleEvent, AgentMetadata, AgentPromptContext, AgentReferenceResolveContext, AgentServer, AgentServerOptions, AgentServerSessionOptions, AgentServerTransport, AgentSession, AgentSessionCheckpoint, AgentSessionCloneParams, AgentSessionOptions, AgentSessionParams, AgentSessionRestoreParams, AgentSessionStore, AgentSystemPromptContext, AgentTool, AgentToolContext, AgentToolInvokeContext, AgentToolInvokeResult, AgentTransport, AgentTransportBinding, ClientFrame, ClientSessionEvent, CompactionWindow, ConversationSummary, DEFAULT_MAX_MEDIA_BYTES, DEFAULT_TURN_RETRY_POLICY, DrainedTranscriptPatches, ExternalMutationReservation, InProcessTransportPair, JsonWebSocket, ModelSwitchApply, PrepareShell, PrepareShellContext, ProviderStreamError, ResumePoint, RunCommandLineCommandNotRegisteredError, RunCommandLineOptions, RunCommandLineResult, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError, SHELL_VIEW_MAX_CHARS, ServerFrame, SessionEvent, SessionEventListener, ShellCommandStatusLike, ShellToolResultOptions, ShellToolView, StandardAgentToolOptions, TranscriptLog, TranscriptOptions, TranscriptPatch, TurnRetryPolicy, applyTranscriptPatches, cloneBlocks, createInProcessTransportPair, createStandardAgentTools, createWebSocketClientTransport, createWebSocketServerTransport, estimateTranscriptBlockTokens, findResumePoint, finishShellToolResult, isContextLengthExceeded, isRetryableCode, resolveRetryPolicy, retryDelayMs, shellCommandHandleRequired, shellPreviewBudgetTokens, toShellToolResult };
483
+ export { AbortResult, AbortTarget, ActiveTurnPhase, AgentActionOptions, AgentClient, AgentClientListener, AgentClientTransport, AgentDisposeContext, AgentHarness, AgentHarnessContext, AgentHarnessRuntime, AgentHostContext, AgentLifecycleEvent, AgentMetadata, AgentPromptContext, AgentReferenceResolveContext, AgentServer, AgentServerOptions, AgentServerSessionOptions, AgentServerTransport, AgentSession, AgentSessionCheckpoint, AgentSessionCloneParams, AgentSessionOptions, AgentSessionParams, AgentSessionRestoreParams, AgentSessionStore, AgentSystemPromptContext, AgentTool, AgentToolContext, AgentToolInvokeContext, AgentToolInvokeResult, AgentTransport, AgentTransportBinding, ChildSupervisor, ChildSupervisorOptions, ClientFrame, ClientSessionEvent, CompactionWindow, ConversationSummary, DEFAULT_MAX_MEDIA_BYTES, DEFAULT_TURN_RETRY_POLICY, DrainedTranscriptPatches, ExternalMutationReservation, InProcessTransportPair, JsonWebSocket, MAX_LIVE_SUBAGENTS, ModelSwitchApply, PrepareShell, PrepareShellContext, ProviderStreamError, ResumePoint, RunCommandLineCommandNotRegisteredError, RunCommandLineOptions, RunCommandLineResult, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError, SHELL_VIEW_MAX_CHARS, SUBAGENT_RESULT_MAX_BYTES, ServerFrame, SessionEvent, SessionEventListener, ShellCommandStatusLike, ShellToolResultOptions, ShellToolView, StandardAgentToolOptions, SubagentExecution, SubagentJob, SubagentProfile, TranscriptLog, TranscriptOptions, TranscriptPatch, TurnRetryPolicy, applyTranscriptPatches, cloneBlocks, createInProcessTransportPair, createReadonlyHost, createStandardAgentTools, createWebSocketClientTransport, createWebSocketServerTransport, estimateTranscriptBlockTokens, findResumePoint, finishShellToolResult, injectSubagentCommand, isContextLengthExceeded, isRetryableCode, resolveRetryPolicy, retryDelayMs, shellCommandHandleRequired, shellPreviewBudgetTokens, toShellToolResult };
package/dist/index.mjs CHANGED
@@ -1,8 +1,9 @@
1
- import { a as cloneBlocks, i as applyTranscriptPatches, n as createWebSocketServerTransport, o as ProviderStreamError, r as AgentClient, s as isContextLengthExceeded, t as createWebSocketClientTransport } from "./websocket-transport-c6P3NBuf.mjs";
2
- import { AbortError, abortable, asError, asRecord, asString, bytesToBase64, createId, delay, isAbortError, isRecord, noop, parseJsonOrString, safeJsonStringify, sliceHead, sliceTail, throwIfAborted, toWellFormedText, truncate } from "@demicodes/utils";
1
+ import { a as cloneBlocks, i as applyTranscriptPatches, n as createWebSocketServerTransport, o as ProviderStreamError, r as AgentClient, s as isContextLengthExceeded, t as createWebSocketClientTransport } from "./websocket-transport-CIOotpd-.mjs";
2
+ import { AbortError, abortable, asError, asRecord, asString, bytesToBase64, createId, decodeUtf8, delay, errorMessage, isAbortError, isRecord, noop, parseJsonOrString, safeJsonStringify, sliceHead, sliceTail, throwIfAborted, toWellFormedText, truncate, utf8Slice } from "@demicodes/utils";
3
3
  import { BashEnvironment, CommandRegistry, MAX_TIMEOUT_MS, heredocDelimiter, shellQuote } from "@demicodes/shell";
4
4
  import { providerRuntime } from "@demicodes/provider";
5
5
  import { modelAcceptsMediaType, sniffModelMediaType } from "@demicodes/core";
6
+ import { z } from "zod";
6
7
  //#region src/transcript.ts
7
8
  const DEFAULT_MODEL_TEXT_HEAD_CHARS = 8e3;
8
9
  const DEFAULT_MODEL_TEXT_TAIL_CHARS = 8e3;
@@ -912,7 +913,7 @@ var CompactionController = class {
912
913
  text: COMPACTION_SUMMARY_INSTRUCTION
913
914
  }]);
914
915
  throwIfAborted(parentSignal);
915
- return lastAssistantText(clone.transcript(), baseBlockCount).trim();
916
+ return lastAssistantText$1(clone.transcript(), baseBlockCount).trim();
916
917
  } finally {
917
918
  parentSignal.removeEventListener("abort", abortClone);
918
919
  unsubscribe();
@@ -920,7 +921,7 @@ var CompactionController = class {
920
921
  }
921
922
  }
922
923
  };
923
- function lastAssistantText(transcript, startIndex) {
924
+ function lastAssistantText$1(transcript, startIndex) {
924
925
  for (let index = transcript.blocks.length - 1; index >= startIndex; index -= 1) {
925
926
  const block = transcript.blocks[index];
926
927
  if (block?.type === "text") return block.text;
@@ -1583,6 +1584,21 @@ var AgentSession = class AgentSession {
1583
1584
  phase() {
1584
1585
  return this.currentPhase;
1585
1586
  }
1587
+ /** Granular in-turn phase for observers (null when no turn is active). */
1588
+ turnPhase() {
1589
+ return this.activeTurnPhase;
1590
+ }
1591
+ hasPendingYields() {
1592
+ return this.yields.hasPending;
1593
+ }
1594
+ /** Metadata of the currently executing action, if any. */
1595
+ actionMetadata() {
1596
+ return this.activeMetadata;
1597
+ }
1598
+ /** Independent provider runtime with this session's configuration (for child sessions). */
1599
+ cloneProviderRuntime() {
1600
+ return this.provider.clone();
1601
+ }
1586
1602
  queuedMessages() {
1587
1603
  return this.queued.map((message) => ({
1588
1604
  ...message,
@@ -2635,6 +2651,765 @@ function shellCommandHandleRequired(result, budgetTokens) {
2635
2651
  return preview.truncated || result.output.truncated || result.output.bytes > maxChars || result.stdout.truncated || result.stderr.truncated;
2636
2652
  }
2637
2653
  //#endregion
2654
+ //#region src/subagent.ts
2655
+ const MAX_LIVE_SUBAGENTS = 8;
2656
+ const SUBAGENT_RESULT_MAX_BYTES = 32768;
2657
+ const SHOW_RECENT_TOOLS = 8;
2658
+ 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.";
2659
+ /**
2660
+ * Per-parent subagent supervisor. Owns every child `AgentSession` this parent
2661
+ * spawns: lifecycle (spawn / steer / abort / natural end), the `demi agent`
2662
+ * command tree, child shell environments, and the `subagent*` protocol frames
2663
+ * on the parent connection. Children are in-memory only: no store, no
2664
+ * checkpoint, gone with the parent.
2665
+ */
2666
+ var ChildSupervisor = class {
2667
+ options;
2668
+ jobs = /* @__PURE__ */ new Map();
2669
+ parentSession = null;
2670
+ isDisposed = false;
2671
+ constructor(options) {
2672
+ this.options = options;
2673
+ }
2674
+ attachParent(session) {
2675
+ this.parentSession = session;
2676
+ }
2677
+ /** The `agent` node AgentServer grafts under the parent registry's `demi` root. */
2678
+ rootCommandNode() {
2679
+ const profileNames = this.configuredProfileNames();
2680
+ return {
2681
+ name: "agent",
2682
+ 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.",
2683
+ successOutput: "first stderr line is \"subagentId: <id>\" at start; stdout is the child's last assistant text (empty is valid), written only at exit",
2684
+ failureOutput: "non-zero exit with the abort or failure reason on stderr",
2685
+ input: {
2686
+ prompt: z.string().optional().describe(SPAWN_PROMPT_DESCRIPTION),
2687
+ profile: z.string().optional().describe(`Named subagent profile configured at harness assembly. Available: ${profileNames.join(", ")}.`),
2688
+ description: z.string().optional().describe("Short UI title distinguishing concurrent children.")
2689
+ },
2690
+ positionals: ["prompt"],
2691
+ stdinField: "prompt",
2692
+ output: { json: z.object({
2693
+ subagentId: z.string(),
2694
+ text: z.string()
2695
+ }) },
2696
+ run: async ({ parsed, io, signal, stdinStream }) => {
2697
+ const prompt = String(parsed.values.prompt ?? "").trim();
2698
+ if (!prompt) {
2699
+ await io.stderr("demi agent: prompt must not be empty\n");
2700
+ return { exitCode: 1 };
2701
+ }
2702
+ let job;
2703
+ try {
2704
+ job = await this.spawn({
2705
+ prompt,
2706
+ profileName: parsed.values.profile === void 0 ? void 0 : String(parsed.values.profile),
2707
+ description: parsed.values.description === void 0 ? "" : String(parsed.values.description)
2708
+ });
2709
+ } catch (error) {
2710
+ await io.stderr(`demi agent: ${errorMessage(error)}\n`);
2711
+ return { exitCode: 1 };
2712
+ }
2713
+ await io.stderr(`subagentId: ${job.id}\n`);
2714
+ const onAbort = () => {
2715
+ this.abortSubtree(job.id).catch(noop);
2716
+ };
2717
+ if (signal.aborted) onAbort();
2718
+ else signal.addEventListener("abort", onAbort, { once: true });
2719
+ this.pumpStdinSteers(job.id, stdinStream);
2720
+ const close = await job.closed;
2721
+ signal.removeEventListener("abort", onAbort);
2722
+ if (close.phase === "completed") {
2723
+ const text = close.result ?? "";
2724
+ if (parsed.json) await io.stdout(`${JSON.stringify({
2725
+ subagentId: job.id,
2726
+ text
2727
+ })}\n`);
2728
+ else if (text.length > 0) await io.stdout(text.endsWith("\n") ? text : `${text}\n`);
2729
+ return { exitCode: 0 };
2730
+ }
2731
+ if (close.phase === "aborted") {
2732
+ await io.stderr(`demi agent: subagent ${job.id} aborted\n`);
2733
+ return { exitCode: 130 };
2734
+ }
2735
+ await io.stderr(`demi agent: subagent ${job.id} failed: ${close.failure ?? "unknown error"}\n`);
2736
+ return { exitCode: 1 };
2737
+ },
2738
+ subcommands: [
2739
+ {
2740
+ name: "steer",
2741
+ summary: "Send a user steer to a running child. Queues until the child can take it; does not wait.",
2742
+ input: {
2743
+ id: z.string().describe("subagentId from spawn stderr"),
2744
+ message: z.string().optional().describe("Message body; positional, or stdin/heredoc when omitted.")
2745
+ },
2746
+ positionals: ["id", "message"],
2747
+ stdinField: "message",
2748
+ output: { json: z.object({
2749
+ id: z.string(),
2750
+ accepted: z.boolean()
2751
+ }) },
2752
+ run: async ({ parsed, io }) => {
2753
+ const id = String(parsed.values.id);
2754
+ const message = String(parsed.values.message ?? "").trim();
2755
+ if (!message) {
2756
+ await io.stderr("demi agent steer: message must not be empty\n");
2757
+ return { exitCode: 1 };
2758
+ }
2759
+ const job = this.jobs.get(id);
2760
+ if (!job) {
2761
+ await io.stderr(`demi agent steer: no running subagent "${id}"\n`);
2762
+ return { exitCode: 1 };
2763
+ }
2764
+ await this.steerChild(job, message);
2765
+ await io.stdout(parsed.json ? `${JSON.stringify({
2766
+ id,
2767
+ accepted: true
2768
+ })}\n` : `steered ${id}\n`);
2769
+ return { exitCode: 0 };
2770
+ }
2771
+ },
2772
+ {
2773
+ name: "abort",
2774
+ summary: "Abort a running child and its subtree. Siblings are untouched.",
2775
+ input: { id: z.string().describe("subagentId from spawn stderr") },
2776
+ positionals: ["id"],
2777
+ output: { json: z.object({
2778
+ id: z.string(),
2779
+ aborted: z.boolean()
2780
+ }) },
2781
+ run: async ({ parsed, io }) => {
2782
+ const id = String(parsed.values.id);
2783
+ if (!this.jobs.has(id)) {
2784
+ await io.stderr(`demi agent abort: no running subagent "${id}"\n`);
2785
+ return { exitCode: 1 };
2786
+ }
2787
+ await this.abortSubtree(id);
2788
+ await io.stdout(parsed.json ? `${JSON.stringify({
2789
+ id,
2790
+ aborted: true
2791
+ })}\n` : `aborted ${id}\n`);
2792
+ return { exitCode: 0 };
2793
+ }
2794
+ },
2795
+ {
2796
+ name: "list",
2797
+ summary: "Snapshot roster of this session's running children (finished children are absent). One line per child with ages relative to now. A read, not a wait — not for polling loops.",
2798
+ output: { json: z.object({ agents: z.array(z.unknown()) }) },
2799
+ run: async ({ parsed, io }) => {
2800
+ const jobs = [...this.jobs.values()];
2801
+ if (parsed.json) {
2802
+ await io.stdout(`${JSON.stringify({ agents: jobs.map((job) => this.snapshot(job, false)) })}\n`);
2803
+ return { exitCode: 0 };
2804
+ }
2805
+ if (jobs.length === 0) {
2806
+ await io.stdout("no running subagents\n");
2807
+ return { exitCode: 0 };
2808
+ }
2809
+ for (const job of jobs) await io.stdout(`${this.renderListLine(job)}\n`);
2810
+ return { exitCode: 0 };
2811
+ }
2812
+ },
2813
+ {
2814
+ name: "show",
2815
+ summary: "Bounded snapshot of one running child: execution state, recent tool titles with durations, last assistant text. Every duration is relative to now — use the ages to tell motion from stall. Omits tool outputs, file contents, and older turns. A read, not a wait — not for polling loops.",
2816
+ input: { id: z.string().describe("subagentId from spawn stderr") },
2817
+ positionals: ["id"],
2818
+ output: { json: z.object({ agent: z.unknown() }) },
2819
+ run: async ({ parsed, io }) => {
2820
+ const id = String(parsed.values.id);
2821
+ const job = this.jobs.get(id);
2822
+ if (!job) {
2823
+ await io.stderr(`demi agent show: no running subagent "${id}"\n`);
2824
+ return { exitCode: 1 };
2825
+ }
2826
+ if (parsed.json) await io.stdout(`${JSON.stringify({ agent: this.snapshot(job, true) })}\n`);
2827
+ else await io.stdout(this.renderShow(job));
2828
+ return { exitCode: 0 };
2829
+ }
2830
+ }
2831
+ ]
2832
+ };
2833
+ }
2834
+ hasShell(shellId) {
2835
+ return this.environmentScopeForShell(shellId) !== null;
2836
+ }
2837
+ /** Resolves the child scope owning a shell, for the command bridge dispatch. */
2838
+ environmentScopeForShell(shellId) {
2839
+ for (const job of this.jobs.values()) for (const environment of job.environments.values()) if (environment.getShell(shellId)) return {
2840
+ environment,
2841
+ commandNames: new Set(job.commandNames),
2842
+ agentSessionId: job.id
2843
+ };
2844
+ return null;
2845
+ }
2846
+ /** Re-emits `subagent started` + transcript reset for every running child (transcript resync). */
2847
+ replay() {
2848
+ for (const job of this.jobs.values()) {
2849
+ this.options.emit({
2850
+ type: "subagent",
2851
+ event: "started",
2852
+ job: this.wireJob(job)
2853
+ });
2854
+ const transcript = job.session.transcript();
2855
+ this.options.emit({
2856
+ type: "subagent_transcript_reset",
2857
+ subagentId: job.id,
2858
+ blocks: structuredClone(transcript.blocks),
2859
+ revision: transcript.revision
2860
+ });
2861
+ }
2862
+ }
2863
+ async dispose() {
2864
+ this.isDisposed = true;
2865
+ for (const job of [...this.jobs.values()]) await this.closeJob(job, "aborted");
2866
+ }
2867
+ async abortSubtree(id) {
2868
+ const job = this.jobs.get(id);
2869
+ if (!job) return;
2870
+ await this.closeJob(job, "aborted");
2871
+ }
2872
+ async spawn(input) {
2873
+ const parent = this.parentSession;
2874
+ if (!parent) throw new Error("subagent supervisor has no parent session");
2875
+ if (this.isDisposed) throw new Error("parent session is closing");
2876
+ if (this.jobs.size >= 8) throw new Error(`at most 8 running subagents per session; abort one or wait for a result`);
2877
+ const profile = this.resolveProfile(input.profileName);
2878
+ const id = createId();
2879
+ const metadata = parent.actionMetadata();
2880
+ const agentNode = this.createChildAgentNode(id, input.description);
2881
+ const commands = injectSubagentCommand(profile.commands ? profile.commands([...this.options.parentCommands]) : [...this.options.parentCommands], agentNode);
2882
+ const commandRegistry = new CommandRegistry();
2883
+ for (const command of commands) commandRegistry.register(command);
2884
+ const commandNames = commandRegistry.list().map((command) => command.name);
2885
+ const commandsPrompt = commandRegistry.renderHelp();
2886
+ let settleClosed;
2887
+ const closed = new Promise((resolve) => {
2888
+ settleClosed = resolve;
2889
+ });
2890
+ const job = {
2891
+ id,
2892
+ description: input.description,
2893
+ profile,
2894
+ profileName: input.profileName ?? (this.options.profiles ? profile.name : null),
2895
+ metadata,
2896
+ session: null,
2897
+ commandRegistry,
2898
+ commandNames,
2899
+ environments: /* @__PURE__ */ new Map(),
2900
+ pendingEnvironments: /* @__PURE__ */ new Map(),
2901
+ readonlyHosts: /* @__PURE__ */ new WeakMap(),
2902
+ spawnedAt: Date.now(),
2903
+ lastEventAt: Date.now(),
2904
+ tools: [],
2905
+ lastAssistantTextAt: null,
2906
+ phase: "running",
2907
+ failure: null,
2908
+ unsubscribe: noop,
2909
+ closed,
2910
+ settleClosed,
2911
+ isClosing: false
2912
+ };
2913
+ const agent = this.options.agent;
2914
+ const preamble = this.subagentPreamble(id);
2915
+ const runtime = {
2916
+ harnessName: agent.name,
2917
+ initialState: () => agent.initialState(),
2918
+ systemPrompt: (ctx) => (profile.systemPrompt ?? agent.systemPrompt).call(agent, {
2919
+ ...ctx,
2920
+ commandsPrompt
2921
+ }),
2922
+ preamble: async (ctx) => {
2923
+ const inheritedPreamble = profile.systemPrompt ? null : await agent.preamble?.(ctx) ?? null;
2924
+ return inheritedPreamble ? `${inheritedPreamble}\n\n${preamble}` : preamble;
2925
+ },
2926
+ tools: () => createStandardAgentTools({
2927
+ environment: (ctx) => this.childEnvironment(job, ctx),
2928
+ scheduleYield: (ctx, durationMs) => job.session.scheduleYieldWakeup(durationMs, ctx.metadata)
2929
+ })
2930
+ };
2931
+ job.session = new AgentSession({
2932
+ provider: parent.cloneProviderRuntime(),
2933
+ model: profile.model ?? structuredClone(parent.modelSelection),
2934
+ cwd: this.options.cwd,
2935
+ runtime,
2936
+ state: agent.initialState()
2937
+ }, {
2938
+ agentSessionId: id,
2939
+ ...this.options.sessionOptions
2940
+ });
2941
+ job.unsubscribe = job.session.subscribe((event) => {
2942
+ if (event.type !== "transcript_changed") return;
2943
+ this.recordTelemetry(job, event.patches);
2944
+ this.options.emit({
2945
+ type: "subagent_transcript_patch",
2946
+ subagentId: id,
2947
+ patches: event.patches,
2948
+ revision: event.revision
2949
+ });
2950
+ });
2951
+ this.jobs.set(id, job);
2952
+ this.options.emit({
2953
+ type: "subagent",
2954
+ event: "started",
2955
+ job: this.wireJob(job)
2956
+ });
2957
+ this.options.emit({
2958
+ type: "subagent_transcript_reset",
2959
+ subagentId: id,
2960
+ blocks: [],
2961
+ revision: 0
2962
+ });
2963
+ this.watchChild(job, input.prompt);
2964
+ return job;
2965
+ }
2966
+ /** Delivers one steer to a child: mid-turn as a steer, idle as a new user turn. */
2967
+ async steerChild(job, message) {
2968
+ const content = [{
2969
+ type: "text",
2970
+ text: message
2971
+ }];
2972
+ if (job.session.phase() !== "idle") try {
2973
+ await job.session.steer(content);
2974
+ return;
2975
+ } catch {}
2976
+ this.watchTurn(job, job.session.send(content));
2977
+ }
2978
+ async pumpStdinSteers(id, stdinStream) {
2979
+ try {
2980
+ for await (const chunk of stdinStream) {
2981
+ const job = this.jobs.get(id);
2982
+ if (!job) return;
2983
+ const message = decodeUtf8(chunk).trim();
2984
+ if (message) await this.steerChild(job, message);
2985
+ }
2986
+ } catch {}
2987
+ }
2988
+ watchChild(job, prompt) {
2989
+ const opening = job.session.send([{
2990
+ type: "text",
2991
+ text: prompt
2992
+ }], job.metadata ? { metadata: job.metadata } : {});
2993
+ this.watchTurn(job, opening);
2994
+ }
2995
+ /** Tracks one child turn to session quiescence, then closes the job with its outcome. */
2996
+ async watchTurn(job, turn) {
2997
+ try {
2998
+ await turn;
2999
+ } catch (error) {
3000
+ job.failure = errorMessage(error);
3001
+ await this.closeJob(job, "error");
3002
+ return;
3003
+ }
3004
+ await this.waitForQuiescence(job);
3005
+ if (job.phase === "running" && !job.isClosing) await this.closeJob(job, "completed");
3006
+ }
3007
+ /** Resolves when the child is idle with no pending yield wakeups (or the job closed). */
3008
+ async waitForQuiescence(job) {
3009
+ while (!job.isClosing && (job.session.phase() !== "idle" || job.session.hasPendingYields())) await new Promise((resolve) => {
3010
+ let settled = false;
3011
+ const finish = () => {
3012
+ if (settled) return;
3013
+ settled = true;
3014
+ unsubscribe();
3015
+ resolve();
3016
+ };
3017
+ const unsubscribe = job.session.subscribe((event) => {
3018
+ if (event.type === "phase_changed") finish();
3019
+ });
3020
+ job.closed.then(finish);
3021
+ });
3022
+ }
3023
+ async closeJob(job, phase) {
3024
+ if (job.isClosing) return;
3025
+ job.isClosing = true;
3026
+ job.phase = phase;
3027
+ job.unsubscribe();
3028
+ this.jobs.delete(job.id);
3029
+ const result = phase === "completed" ? boundedResultText(lastAssistantText(job.session.transcript().blocks)) : void 0;
3030
+ await job.session.dispose().catch(noop);
3031
+ const environments = /* @__PURE__ */ new Set([...job.environments.values()]);
3032
+ for (const pending of job.pendingEnvironments.values()) {
3033
+ const environment = await pending.catch(() => null);
3034
+ if (environment) environments.add(environment);
3035
+ }
3036
+ for (const environment of environments) await environment.disposeAllShells().catch(noop);
3037
+ const close = {
3038
+ phase,
3039
+ ...result !== void 0 ? { result } : {},
3040
+ ...job.failure ? { failure: job.failure } : {}
3041
+ };
3042
+ this.options.emit({
3043
+ type: "subagent",
3044
+ event: "closed",
3045
+ job: this.wireJob(job, result)
3046
+ });
3047
+ job.settleClosed(close);
3048
+ this.notifyIdleParent(job, close);
3049
+ }
3050
+ /** Wakes an idle parent with a user send; a parent blocked in the spawn gets the tool result instead. */
3051
+ notifyIdleParent(job, close) {
3052
+ if (this.isDisposed) return;
3053
+ const parent = this.parentSession;
3054
+ if (!parent || parent.phase() !== "idle") return;
3055
+ const label = `subagent ${job.id}${job.description ? ` — ${job.description}` : ""}`;
3056
+ const body = close.phase === "completed" ? `[${label}] completed.\nResult:\n${close.result || "(empty)"}` : close.phase === "aborted" ? `[${label}] aborted.` : `[${label}] failed: ${close.failure ?? "unknown error"}`;
3057
+ parent.send([{
3058
+ type: "text",
3059
+ text: body
3060
+ }]).catch(noop);
3061
+ }
3062
+ createChildAgentNode(childId, description) {
3063
+ return {
3064
+ name: "agent",
3065
+ summary: "Subagent bridge to the parent session.",
3066
+ subcommands: [{
3067
+ name: "send-parent",
3068
+ 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.",
3069
+ input: { message: z.string().optional().describe("Message body; positional, or stdin/heredoc when omitted.") },
3070
+ positionals: ["message"],
3071
+ stdinField: "message",
3072
+ output: { json: z.object({ accepted: z.boolean() }) },
3073
+ run: async ({ parsed, io }) => {
3074
+ const message = String(parsed.values.message ?? "").trim();
3075
+ if (!message) {
3076
+ await io.stderr("demi agent send-parent: message must not be empty\n");
3077
+ return { exitCode: 1 };
3078
+ }
3079
+ this.deliverToParent(childId, description, message);
3080
+ await io.stdout(parsed.json ? `${JSON.stringify({ accepted: true })}\n` : "sent\n");
3081
+ return { exitCode: 0 };
3082
+ }
3083
+ }]
3084
+ };
3085
+ }
3086
+ deliverToParent(childId, description, message) {
3087
+ const parent = this.parentSession;
3088
+ if (!parent || this.isDisposed) return;
3089
+ const content = [{
3090
+ type: "text",
3091
+ text: `[subagent ${childId}${description ? ` — ${description}` : ""}] ${message}`
3092
+ }];
3093
+ if (parent.phase() !== "idle") {
3094
+ parent.steer(content).catch(() => {
3095
+ parent.send(content).catch(noop);
3096
+ });
3097
+ return;
3098
+ }
3099
+ parent.send(content).catch(noop);
3100
+ }
3101
+ async childEnvironment(job, ctx) {
3102
+ const resolved = await this.options.agent.host({
3103
+ agentSessionId: job.id,
3104
+ state: ctx.state,
3105
+ cwd: this.options.cwd,
3106
+ metadata: ctx.metadata
3107
+ });
3108
+ let host = resolved;
3109
+ if (job.profile.readonly) {
3110
+ const wrapped = job.readonlyHosts.get(resolved) ?? createReadonlyHost(resolved);
3111
+ job.readonlyHosts.set(resolved, wrapped);
3112
+ host = wrapped;
3113
+ }
3114
+ const existing = job.environments.get(host);
3115
+ if (existing) return existing;
3116
+ const pending = job.pendingEnvironments.get(host);
3117
+ if (pending) return pending;
3118
+ const creation = this.createChildEnvironment(job, host);
3119
+ job.pendingEnvironments.set(host, creation);
3120
+ try {
3121
+ const environment = await creation;
3122
+ job.environments.set(host, environment);
3123
+ return environment;
3124
+ } finally {
3125
+ job.pendingEnvironments.delete(host);
3126
+ }
3127
+ }
3128
+ async createChildEnvironment(job, host) {
3129
+ const prepared = this.options.prepareShell && !job.profile.readonly ? await this.options.prepareShell({
3130
+ agentSessionId: job.id,
3131
+ host,
3132
+ commandNames: job.commandNames,
3133
+ shell: this.options.shellOptions
3134
+ }) : this.options.shellOptions;
3135
+ return new BashEnvironment({
3136
+ ...prepared,
3137
+ initialEnv: {
3138
+ ...prepared.initialEnv,
3139
+ DEMI_SUBAGENT_ID: job.id,
3140
+ DEMI_PARENT_SESSION_ID: this.parentSession?.id() ?? "",
3141
+ DEMI_SUBAGENT_DEPTH: "1"
3142
+ },
3143
+ host,
3144
+ commands: job.commandRegistry
3145
+ });
3146
+ }
3147
+ resolveProfile(name) {
3148
+ const profiles = this.options.profiles;
3149
+ const implicitDefault = {
3150
+ name: "default",
3151
+ description: "Inherits the parent harness, model, Host, and commands."
3152
+ };
3153
+ if (!profiles || profiles.length === 0) {
3154
+ if (name !== void 0 && name !== "default") throw new Error(`unknown profile "${name}" (available: default)`);
3155
+ return implicitDefault;
3156
+ }
3157
+ const target = name ?? "default";
3158
+ const profile = profiles.find((candidate) => candidate.name === target);
3159
+ if (profile) return profile;
3160
+ if (name === void 0) return implicitDefault;
3161
+ throw new Error(`unknown profile "${name}" (available: ${this.configuredProfileNames().join(", ")})`);
3162
+ }
3163
+ configuredProfileNames() {
3164
+ const names = (this.options.profiles ?? []).map((profile) => profile.name);
3165
+ return names.includes("default") ? names : ["default", ...names];
3166
+ }
3167
+ subagentPreamble(childId) {
3168
+ return [
3169
+ `You are a subagent: an isolated child agent session (id ${childId}) spawned by a parent agent session. Your transcript starts empty; the task brief in the first user message is your entire context.`,
3170
+ "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.",
3171
+ "`demi agent send-parent <message>` sends an interim user message to the parent; it is seen only when the parent is not blocked waiting on this session.",
3172
+ "You are not talking to the product user; do not address them."
3173
+ ].join("\n");
3174
+ }
3175
+ recordTelemetry(job, patches) {
3176
+ const now = Date.now();
3177
+ for (const patch of patches) {
3178
+ if (patch.op === "add") {
3179
+ const block = patch.value;
3180
+ if (block.type === "tool_call") {
3181
+ job.tools.push({
3182
+ toolUseId: block.toolUseId,
3183
+ title: toolCallTitle(block),
3184
+ startedAt: now,
3185
+ endedAt: null,
3186
+ status: "executing"
3187
+ });
3188
+ trimToolRecords(job.tools);
3189
+ job.lastEventAt = now;
3190
+ } else if (block.type === "text") {
3191
+ job.lastAssistantTextAt = now;
3192
+ job.lastEventAt = now;
3193
+ }
3194
+ continue;
3195
+ }
3196
+ if (patch.op === "append_text") {
3197
+ if (job.session.transcript().blocks[patch.path[1]]?.type === "text") {
3198
+ job.lastAssistantTextAt = now;
3199
+ job.lastEventAt = now;
3200
+ }
3201
+ continue;
3202
+ }
3203
+ if (patch.op === "replace_block") {
3204
+ const block = patch.value;
3205
+ if (block.type !== "tool_call" || block.status === "executing") continue;
3206
+ const record = job.tools.find((tool) => tool.toolUseId === block.toolUseId && tool.endedAt === null);
3207
+ if (record) {
3208
+ record.endedAt = now;
3209
+ record.status = block.status === "error" ? "error" : "completed";
3210
+ }
3211
+ job.lastEventAt = now;
3212
+ }
3213
+ }
3214
+ }
3215
+ executionOf(job) {
3216
+ const phase = job.session.phase();
3217
+ if (phase === "compacting") return "compacting";
3218
+ if (phase === "idle") return job.session.hasPendingYields() ? "pending_yield" : "idle";
3219
+ return job.session.turnPhase() ?? "provider_streaming";
3220
+ }
3221
+ activityOf(job, execution) {
3222
+ const inflight = [...job.tools].reverse().find((tool) => tool.endedAt === null);
3223
+ if (execution === "tool_executing" && inflight) return inflight.title;
3224
+ if (execution === "provider_streaming") return "streaming";
3225
+ return execution;
3226
+ }
3227
+ executionForMs(job, execution, now) {
3228
+ if (execution === "tool_executing") {
3229
+ const inflight = [...job.tools].reverse().find((tool) => tool.endedAt === null);
3230
+ if (inflight) return now - inflight.startedAt;
3231
+ }
3232
+ return now - job.lastEventAt;
3233
+ }
3234
+ snapshot(job, detailed) {
3235
+ const now = Date.now();
3236
+ const execution = this.executionOf(job);
3237
+ const base = {
3238
+ subagentId: job.id,
3239
+ description: job.description,
3240
+ profile: job.profileName,
3241
+ phase: job.phase,
3242
+ elapsedMs: now - job.spawnedAt,
3243
+ lastEventMs: now - job.lastEventAt,
3244
+ execution,
3245
+ activity: this.activityOf(job, execution)
3246
+ };
3247
+ if (!detailed) return base;
3248
+ const text = lastAssistantText(job.session.transcript().blocks);
3249
+ return {
3250
+ ...base,
3251
+ executionForMs: this.executionForMs(job, execution, now),
3252
+ tools: job.tools.slice(-8).map((tool) => ({
3253
+ title: tool.title,
3254
+ status: tool.status,
3255
+ durationMs: (tool.endedAt ?? now) - tool.startedAt,
3256
+ endedAgoMs: tool.endedAt === null ? null : now - tool.endedAt
3257
+ })),
3258
+ lastAssistantText: boundedResultText(text),
3259
+ lastAssistantTextAgoMs: job.lastAssistantTextAt === null ? null : now - job.lastAssistantTextAt
3260
+ };
3261
+ }
3262
+ renderListLine(job) {
3263
+ const now = Date.now();
3264
+ const execution = this.executionOf(job);
3265
+ return [
3266
+ job.id,
3267
+ job.phase,
3268
+ `up ${formatDuration(now - job.spawnedAt)}`,
3269
+ `last-event ${formatDuration(now - job.lastEventAt)} ago`,
3270
+ `profile=${job.profileName ?? "default"}`,
3271
+ job.description ? `"${job.description}"` : "(no description)",
3272
+ `execution=${execution}`,
3273
+ `activity=${this.activityOf(job, execution)}`
3274
+ ].join(" ");
3275
+ }
3276
+ renderShow(job) {
3277
+ const now = Date.now();
3278
+ const execution = this.executionOf(job);
3279
+ const lines = [
3280
+ `id: ${job.id}`,
3281
+ `description: ${job.description || "(none)"}`,
3282
+ `profile: ${job.profileName ?? "default"}`,
3283
+ `phase: ${job.phase}`,
3284
+ `elapsed: ${formatDuration(now - job.spawnedAt)}`,
3285
+ `execution: ${execution} (for ${formatDuration(this.executionForMs(job, execution, now))})`,
3286
+ `last-event: ${formatDuration(now - job.lastEventAt)} ago`,
3287
+ `activity: ${this.activityOf(job, execution)}`
3288
+ ];
3289
+ const recent = job.tools.slice(-8);
3290
+ if (recent.length > 0) {
3291
+ lines.push(`recent tool calls (last ${recent.length}):`);
3292
+ for (const tool of recent) if (tool.endedAt === null) lines.push(` [executing for ${formatDuration(now - tool.startedAt)}] ${tool.title}`);
3293
+ else lines.push(` [${tool.status} in ${formatDuration(tool.endedAt - tool.startedAt)}, ended ${formatDuration(now - tool.endedAt)} ago] ${tool.title}`);
3294
+ }
3295
+ const text = boundedResultText(lastAssistantText(job.session.transcript().blocks));
3296
+ if (text && job.lastAssistantTextAt !== null) {
3297
+ lines.push(`last assistant text (${formatDuration(now - job.lastAssistantTextAt)} ago):`);
3298
+ lines.push(text);
3299
+ } else lines.push("last assistant text: (none yet)");
3300
+ return `${lines.join("\n")}\n`;
3301
+ }
3302
+ wireJob(job, result) {
3303
+ return {
3304
+ subagentId: job.id,
3305
+ parentSessionId: this.parentSession?.id() ?? "",
3306
+ description: job.description,
3307
+ profile: job.profileName,
3308
+ phase: job.phase,
3309
+ ...result !== void 0 ? { result } : {}
3310
+ };
3311
+ }
3312
+ };
3313
+ /**
3314
+ * Grafts the `agent` node under a `demi` root: onto an existing harness `demi`
3315
+ * tree, or as a new `demi` root when the harness has none.
3316
+ */
3317
+ function injectSubagentCommand(commands, agentNode) {
3318
+ const demiIndex = commands.findIndex((command) => command.name === "demi");
3319
+ if (demiIndex === -1) return [...commands, {
3320
+ name: "demi",
3321
+ summary: "Demi agent runtime commands.",
3322
+ subcommands: [agentNode]
3323
+ }];
3324
+ const demi = commands[demiIndex];
3325
+ const subcommands = [...(demi.subcommands ?? []).filter((command) => command.name !== "agent"), agentNode];
3326
+ const next = [...commands];
3327
+ next[demiIndex] = {
3328
+ ...demi,
3329
+ subcommands
3330
+ };
3331
+ return next;
3332
+ }
3333
+ /**
3334
+ * Host wrapper for read-only subagent profiles: filesystem mutation is
3335
+ * rejected outside the shell's own command-artifacts tree, and process spawn
3336
+ * is rejected outright (a real process cannot be write-restricted).
3337
+ *
3338
+ * Every facet delegates method by method: Host facets are class instances
3339
+ * whose methods live on the prototype, so object spread would drop them.
3340
+ */
3341
+ function createReadonlyHost(host) {
3342
+ const artifactsRoot = host.commandArtifactsDir.replace(/\/+$/, "");
3343
+ const isArtifactPath = (path, cwd) => {
3344
+ const resolved = path.startsWith("/") ? path : `${(cwd ?? host.defaultCwd).replace(/\/+$/, "")}/${path}`;
3345
+ return resolved === artifactsRoot || resolved.startsWith(`${artifactsRoot}/`);
3346
+ };
3347
+ const deny = (operation) => Promise.reject(/* @__PURE__ */ new Error(`read-only subagent: ${operation} is not permitted on this Host`));
3348
+ return {
3349
+ defaultCwd: host.defaultCwd,
3350
+ commandArtifactsDir: host.commandArtifactsDir,
3351
+ identity: host.identity,
3352
+ store: host.store,
3353
+ fs: {
3354
+ readFile: (path, options) => host.fs.readFile(path, options),
3355
+ exists: (path, options) => host.fs.exists(path, options),
3356
+ stat: (path, options) => host.fs.stat(path, options),
3357
+ lstat: (path, options) => host.fs.lstat(path, options),
3358
+ readdir: ((path, options) => host.fs.readdir(path, options)),
3359
+ readlink: (path, options) => host.fs.readlink(path, options),
3360
+ realpath: (path, options) => host.fs.realpath(path, options),
3361
+ writeFile: (path, data, options) => isArtifactPath(path, options?.cwd) ? host.fs.writeFile(path, data, options) : deny(`write ${path}`),
3362
+ appendFile: (path, data, options) => isArtifactPath(path, options?.cwd) ? host.fs.appendFile(path, data, options) : deny(`append ${path}`),
3363
+ mkdir: (path, options) => isArtifactPath(path, options?.cwd) ? host.fs.mkdir(path, options) : deny(`mkdir ${path}`),
3364
+ rm: (path) => deny(`rm ${path}`),
3365
+ cp: (path, destination, options) => isArtifactPath(destination, options?.cwd) ? host.fs.cp(path, destination, options) : deny(`cp to ${destination}`),
3366
+ mv: (path, destination) => deny(`mv ${path} ${destination}`),
3367
+ chmod: (path) => deny(`chmod ${path}`),
3368
+ symlink: (_target, path) => deny(`symlink ${path}`),
3369
+ link: (_existingPath, path) => deny(`link ${path}`),
3370
+ utimes: (path) => deny(`utimes ${path}`)
3371
+ },
3372
+ process: {
3373
+ openCwd: (path) => host.process.openCwd(path),
3374
+ spawn: () => deny("process spawn")
3375
+ }
3376
+ };
3377
+ }
3378
+ function trimToolRecords(tools) {
3379
+ while (tools.length > SHOW_RECENT_TOOLS) {
3380
+ const index = tools.findIndex((tool) => tool.endedAt !== null);
3381
+ if (index === -1) return;
3382
+ tools.splice(index, 1);
3383
+ }
3384
+ }
3385
+ function toolCallTitle(block) {
3386
+ try {
3387
+ const input = JSON.parse(block.input);
3388
+ if (typeof input.description === "string" && input.description.trim()) return input.description.trim();
3389
+ } catch {}
3390
+ return block.toolName;
3391
+ }
3392
+ function lastAssistantText(blocks) {
3393
+ for (let i = blocks.length - 1; i >= 0; i -= 1) {
3394
+ const block = blocks[i];
3395
+ if (block.type === "text") return block.text;
3396
+ }
3397
+ return "";
3398
+ }
3399
+ function boundedResultText(text) {
3400
+ return utf8Slice(text, 0, SUBAGENT_RESULT_MAX_BYTES);
3401
+ }
3402
+ function formatDuration(ms) {
3403
+ const totalSeconds = Math.max(0, Math.round(ms / 1e3));
3404
+ if (totalSeconds < 60) return `${totalSeconds}s`;
3405
+ const minutes = Math.floor(totalSeconds / 60);
3406
+ const seconds = totalSeconds % 60;
3407
+ if (minutes < 60) return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
3408
+ const hours = Math.floor(minutes / 60);
3409
+ const remMinutes = minutes % 60;
3410
+ return remMinutes > 0 ? `${hours}h${remMinutes}m` : `${hours}h`;
3411
+ }
3412
+ //#endregion
2638
3413
  //#region src/server.ts
2639
3414
  var RunCommandLineShellNotFoundError = class extends Error {
2640
3415
  shellId;
@@ -2751,6 +3526,7 @@ var AgentTransportBindingImpl = class {
2751
3526
  currentProviderId = null;
2752
3527
  currentSessionId = null;
2753
3528
  currentCommandNames = /* @__PURE__ */ new Set();
3529
+ supervisor = null;
2754
3530
  unsubscribeSession = null;
2755
3531
  unsubscribeTransport = null;
2756
3532
  closed = false;
@@ -2928,6 +3704,7 @@ var AgentTransportBindingImpl = class {
2928
3704
  const session = this.sessionFor("sync_transcript");
2929
3705
  if (!session) return;
2930
3706
  this.sendTranscriptReset(session);
3707
+ this.supervisor?.replay();
2931
3708
  return;
2932
3709
  }
2933
3710
  case "close":
@@ -2965,7 +3742,19 @@ var AgentTransportBindingImpl = class {
2965
3742
  state,
2966
3743
  cwd: frame.cwd
2967
3744
  };
2968
- const commands = await agent.commands?.(harnessContext) ?? [];
3745
+ const harnessCommands = await agent.commands?.(harnessContext) ?? [];
3746
+ const profiles = await agent.agents?.(harnessContext) ?? null;
3747
+ const supervisor = new ChildSupervisor({
3748
+ agent,
3749
+ cwd: frame.cwd,
3750
+ profiles,
3751
+ parentCommands: harnessCommands,
3752
+ shellOptions: this.shellOptions,
3753
+ prepareShell: this.prepareShell,
3754
+ sessionOptions: this.sessionOptions,
3755
+ emit: (subagentFrame) => this.send(subagentFrame)
3756
+ });
3757
+ const commands = injectSubagentCommand(harnessCommands, supervisor.rootCommandNode());
2969
3758
  const commandRegistry = new CommandRegistry();
2970
3759
  for (const command of commands) commandRegistry.register(command);
2971
3760
  const commandNames = commandRegistry.list().map((command) => command.name);
@@ -3014,6 +3803,8 @@ var AgentTransportBindingImpl = class {
3014
3803
  });
3015
3804
  sessionRef = session;
3016
3805
  this.session = session;
3806
+ supervisor.attachParent(session);
3807
+ this.supervisor = supervisor;
3017
3808
  this.currentAgent = agent;
3018
3809
  this.currentCommandRegistry = commandRegistry;
3019
3810
  this.currentCwd = frame.cwd;
@@ -3107,6 +3898,7 @@ var AgentTransportBindingImpl = class {
3107
3898
  const agent = this.currentAgent;
3108
3899
  const cwd = this.currentCwd;
3109
3900
  try {
3901
+ if (this.supervisor) await this.supervisor.dispose();
3110
3902
  if (session) await session.dispose();
3111
3903
  const pendingEnvironments = await Promise.allSettled(this.pendingEnvironmentsByHost.values());
3112
3904
  const environments = new Set(this.environmentsByHost.values());
@@ -3122,6 +3914,7 @@ var AgentTransportBindingImpl = class {
3122
3914
  this.unsubscribeSession?.();
3123
3915
  this.unsubscribeSession = null;
3124
3916
  this.session = null;
3917
+ this.supervisor = null;
3125
3918
  this.currentAgent = null;
3126
3919
  this.environmentsByHost.clear();
3127
3920
  this.pendingEnvironmentsByHost.clear();
@@ -3170,10 +3963,15 @@ var AgentTransportBindingImpl = class {
3170
3963
  }
3171
3964
  /** Backs `AgentServer.runCommandLine` — see its doc comment for the contract. */
3172
3965
  async runCommandLine(shellId, name, args, opts) {
3173
- const environment = this.environmentForShell(shellId);
3174
- const agentSessionId = this.currentSessionId;
3175
- if (!environment || !agentSessionId) throw new Error("runCommandLine: session has no active shell environment");
3176
- if (!this.currentCommandNames.has(name)) throw new RunCommandLineCommandNotRegisteredError(name);
3966
+ const parentEnvironment = this.environmentForShell(shellId);
3967
+ const scope = parentEnvironment && this.currentSessionId ? {
3968
+ environment: parentEnvironment,
3969
+ commandNames: this.currentCommandNames,
3970
+ agentSessionId: this.currentSessionId
3971
+ } : this.supervisor?.environmentScopeForShell(shellId) ?? null;
3972
+ if (!scope) throw new Error("runCommandLine: session has no active shell environment");
3973
+ const { environment, agentSessionId } = scope;
3974
+ if (!scope.commandNames.has(name)) throw new RunCommandLineCommandNotRegisteredError(name);
3177
3975
  const words = [name, ...args].map(shellQuote).join(" ");
3178
3976
  let script = words;
3179
3977
  if (opts.stdin.length > 0) {
@@ -3213,7 +4011,7 @@ var AgentTransportBindingImpl = class {
3213
4011
  }
3214
4012
  }
3215
4013
  hasShell(shellId) {
3216
- return this.environmentForShell(shellId) !== null;
4014
+ return this.environmentForShell(shellId) !== null || (this.supervisor?.hasShell(shellId) ?? false);
3217
4015
  }
3218
4016
  async resolveEnvironment(ctx, handle) {
3219
4017
  const agent = this.currentAgent;
@@ -3441,4 +4239,4 @@ function createProviderMap(providers) {
3441
4239
  return map;
3442
4240
  }
3443
4241
  //#endregion
3444
- export { AgentClient, AgentServer, AgentSession, DEFAULT_MAX_MEDIA_BYTES, DEFAULT_TURN_RETRY_POLICY, ProviderStreamError, RunCommandLineCommandNotRegisteredError, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError, SHELL_VIEW_MAX_CHARS, TranscriptLog, applyTranscriptPatches, cloneBlocks, createInProcessTransportPair, createStandardAgentTools, createWebSocketClientTransport, createWebSocketServerTransport, estimateTranscriptBlockTokens, findResumePoint, finishShellToolResult, isContextLengthExceeded, isRetryableCode, resolveRetryPolicy, retryDelayMs, shellCommandHandleRequired, shellPreviewBudgetTokens, toShellToolResult };
4242
+ export { AgentClient, 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 };
@@ -1,4 +1,4 @@
1
- import { n as AgentServerTransport, t as AgentClientTransport } from "./transport-DtBCCS_A.mjs";
1
+ import { n as AgentServerTransport, t as AgentClientTransport } from "./transport-BRqZoaeo.mjs";
2
2
  import { Readable, Writable } from "node:stream";
3
3
  //#region src/stdio-transport.d.ts
4
4
  declare function createStdioClientTransport(readable: Readable, writable: Writable): AgentClientTransport;
@@ -180,12 +180,33 @@ interface AgentHostContext<State> extends AgentHarnessContext<State> {
180
180
  agentSessionId: string;
181
181
  metadata: AgentMetadata | null;
182
182
  }
183
+ /**
184
+ * A named subagent configuration. Every field is an override on the parent's
185
+ * setup: omitted fields inherit the parent harness (system prompt), the
186
+ * parent's registered commands, the parent Host, and the parent model.
187
+ */
188
+ interface SubagentProfile<State = unknown> {
189
+ name: string;
190
+ /** Shown in the spawn command help so the parent model can pick a profile. */
191
+ description: string;
192
+ systemPrompt?(ctx: AgentSystemPromptContext<State>): Promise<string> | string;
193
+ /** Derives the child's registered commands from the parent's list. */
194
+ commands?(parentCommands: Command[]): Command[];
195
+ /** Reject filesystem writes and process spawns on the child's Host. */
196
+ readonly?: boolean;
197
+ model?: ModelSelection;
198
+ }
183
199
  interface AgentHarness<State = unknown> {
184
200
  name: string;
185
201
  initialState(): State;
186
202
  /** Return the same Host object for calls that target the same execution environment. */
187
203
  host(ctx: AgentHarnessContext<State> | AgentHostContext<State>): Host | Promise<Host>;
188
204
  commands?(ctx: AgentHarnessContext<State>): Promise<Command[]> | Command[];
205
+ /**
206
+ * Named subagent profiles for `demi agent`. Omitted: one implicit profile
207
+ * named `default` that fully inherits the parent's setup.
208
+ */
209
+ agents?(ctx: AgentHarnessContext<State>): Promise<SubagentProfile<State>[]> | SubagentProfile<State>[];
189
210
  systemPrompt(ctx: AgentSystemPromptContext<State>): Promise<string> | string;
190
211
  preamble?(ctx: AgentPromptContext<State>): Promise<string | null> | string | null;
191
212
  resolveReferences?(ctx: AgentReferenceResolveContext<State>, content: UserContentBlock[]): Promise<UserContentBlock[]> | UserContentBlock[];
@@ -370,6 +391,16 @@ interface ConversationSummary {
370
391
  createdAt: string;
371
392
  updatedAt: string;
372
393
  }
394
+ /** One child agent session as seen on the parent connection. */
395
+ interface SubagentJob {
396
+ subagentId: string;
397
+ parentSessionId: string;
398
+ description: string;
399
+ profile: string | null;
400
+ phase: 'running' | 'completed' | 'aborted' | 'error';
401
+ /** Present on `closed`: the child's last assistant text, at most 32 KiB. */
402
+ result?: string;
403
+ }
373
404
  type ClientFrame = {
374
405
  type: 'open';
375
406
  provider: ProviderSelection;
@@ -489,6 +520,20 @@ type ServerFrame = {
489
520
  message: string;
490
521
  code?: string;
491
522
  diagnostics?: ProviderErrorDiagnostics;
523
+ } | {
524
+ type: 'subagent';
525
+ event: 'started' | 'closed';
526
+ job: SubagentJob;
527
+ } | {
528
+ type: 'subagent_transcript_reset';
529
+ subagentId: string;
530
+ blocks: Block[];
531
+ revision: number;
532
+ } | {
533
+ type: 'subagent_transcript_patch';
534
+ subagentId: string;
535
+ patches: TranscriptPatch[];
536
+ revision: number;
492
537
  } | {
493
538
  type: 'closed';
494
539
  };
@@ -575,6 +620,18 @@ type ClientSessionEvent = {
575
620
  message: string;
576
621
  code?: string;
577
622
  diagnostics?: ProviderErrorDiagnostics;
623
+ } | {
624
+ type: 'subagent';
625
+ event: 'started' | 'closed';
626
+ job: SubagentJob;
627
+ } | {
628
+ type: 'subagent_transcript_reset';
629
+ subagentId: string;
630
+ blocks: Block[];
631
+ } | {
632
+ type: 'subagent_transcript_patch';
633
+ subagentId: string;
634
+ patches: TranscriptPatch[];
578
635
  } | {
579
636
  type: 'opened';
580
637
  } | {
@@ -595,4 +652,4 @@ interface InProcessTransportPair {
595
652
  }
596
653
  declare function createInProcessTransportPair(): InProcessTransportPair;
597
654
  //#endregion
598
- export { AgentTool as A, TranscriptLog as B, AgentSessionCheckpoint as C, AgentSessionRestoreParams as D, AgentSessionParams as E, ModelSwitchApply as F, isRetryableCode as G, estimateTranscriptBlockTokens as H, SessionEvent as I, resolveRetryPolicy as K, SessionEventListener as L, AgentToolInvokeContext as M, AgentToolInvokeResult as N, AgentSessionStore as O, ExternalMutationReservation as P, CompactionWindow as R, AgentReferenceResolveContext as S, AgentSessionOptions as T, DEFAULT_TURN_RETRY_POLICY as U, TranscriptOptions as V, TurnRetryPolicy as W, AgentHarnessRuntime as _, createInProcessTransportPair as a, AgentMetadata as b, ConversationSummary as c, TranscriptPatch as d, AbortResult as f, AgentHarnessContext as g, AgentHarness as h, InProcessTransportPair as i, AgentToolContext as j, AgentSystemPromptContext as k, ServerFrame as l, AgentDisposeContext as m, AgentServerTransport as n, ClientFrame as o, AbortTarget as p, retryDelayMs as q, AgentTransport as r, ClientSessionEvent as s, AgentClientTransport as t, ShellCommandStatusLike as u, AgentHostContext as v, AgentSessionCloneParams as w, AgentPromptContext as x, AgentLifecycleEvent as y, DrainedTranscriptPatches as z };
655
+ export { AgentSystemPromptContext as A, CompactionWindow as B, AgentReferenceResolveContext as C, AgentSessionParams as D, AgentSessionOptions as E, ExternalMutationReservation as F, DEFAULT_TURN_RETRY_POLICY as G, TranscriptLog as H, ModelSwitchApply as I, resolveRetryPolicy as J, TurnRetryPolicy as K, SessionEvent as L, AgentToolContext as M, AgentToolInvokeContext as N, AgentSessionRestoreParams as O, AgentToolInvokeResult as P, SessionEventListener as R, AgentPromptContext as S, AgentSessionCloneParams as T, TranscriptOptions as U, DrainedTranscriptPatches as V, estimateTranscriptBlockTokens as W, retryDelayMs as Y, AgentHarnessContext as _, createInProcessTransportPair as a, AgentLifecycleEvent as b, ConversationSummary as c, SubagentJob as d, TranscriptPatch as f, AgentHarness as g, AgentDisposeContext as h, InProcessTransportPair as i, AgentTool as j, AgentSessionStore as k, ServerFrame as l, AbortTarget as m, AgentServerTransport as n, ClientFrame as o, AbortResult as p, isRetryableCode as q, AgentTransport as r, ClientSessionEvent as s, AgentClientTransport as t, ShellCommandStatusLike as u, AgentHarnessRuntime as v, AgentSessionCheckpoint as w, AgentMetadata as x, AgentHostContext as y, SubagentProfile as z };
@@ -1,4 +1,4 @@
1
- import { F as ModelSwitchApply, b as AgentMetadata, c as ConversationSummary, f as AbortResult, n as AgentServerTransport, s as ClientSessionEvent, t as AgentClientTransport } from "./transport-DtBCCS_A.mjs";
1
+ import { I as ModelSwitchApply, c as ConversationSummary, n as AgentServerTransport, p as AbortResult, s as ClientSessionEvent, t as AgentClientTransport, x as AgentMetadata } from "./transport-BRqZoaeo.mjs";
2
2
  import { ProviderSelection } from "@demicodes/provider";
3
3
  import { Block, UserContentBlock } from "@demicodes/core";
4
4
  //#region src/client.d.ts
@@ -304,8 +304,23 @@ var AgentClient = class {
304
304
  case "shell_write_result":
305
305
  case "audit":
306
306
  case "retry_scheduled":
307
+ case "subagent":
307
308
  this.emit(frame);
308
309
  return;
310
+ case "subagent_transcript_reset":
311
+ this.emit({
312
+ type: "subagent_transcript_reset",
313
+ subagentId: frame.subagentId,
314
+ blocks: frame.blocks
315
+ });
316
+ return;
317
+ case "subagent_transcript_patch":
318
+ this.emit({
319
+ type: "subagent_transcript_patch",
320
+ subagentId: frame.subagentId,
321
+ patches: frame.patches
322
+ });
323
+ return;
309
324
  case "conversations":
310
325
  this.pendingConversationWaiters.shift()?.(frame.conversations);
311
326
  return;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/agent",
3
3
  "description": "Session runtime and transport-neutral client and server protocol for Demi.",
4
- "version": "0.17.4",
4
+ "version": "0.19.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -19,10 +19,11 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@demicodes/core": "^0.17.4",
23
- "@demicodes/provider": "^0.17.4",
24
- "@demicodes/shell": "^0.17.4",
25
- "@demicodes/utils": "^0.17.4"
22
+ "@demicodes/core": "^0.19.0",
23
+ "@demicodes/provider": "^0.19.0",
24
+ "@demicodes/shell": "^0.19.0",
25
+ "@demicodes/utils": "^0.19.0",
26
+ "zod": "^4.0.0"
26
27
  },
27
28
  "license": "Apache-2.0",
28
29
  "main": "./dist/index.mjs",
@@ -43,8 +44,5 @@
43
44
  "directory": "packages/agent"
44
45
  },
45
46
  "homepage": "https://github.com/wspl/demi/tree/main/packages/agent#readme",
46
- "bugs": "https://github.com/wspl/demi/issues",
47
- "devDependencies": {
48
- "zod": "^4.0.0"
49
- }
47
+ "bugs": "https://github.com/wspl/demi/issues"
50
48
  }