@demicodes/agent 0.19.1 → 0.19.3

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 { 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";
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-CHrPzcrU.mjs";
2
+ import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-NZ5Btmb4.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 };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
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";
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-CHrPzcrU.mjs";
2
+ import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-NZ5Btmb4.mjs";
3
+ import { BashAuditEvent, BashEnvironment, BashEnvironmentOptions, Command, CommandMetadataRecord, Host, HostStore, 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
@@ -339,6 +339,7 @@ declare class AgentServer {
339
339
  //#endregion
340
340
  //#region src/subagent.d.ts
341
341
  declare const MAX_LIVE_SUBAGENTS = 8;
342
+ declare const MAX_ARCHIVED_SUBAGENTS = 16;
342
343
  declare const SUBAGENT_RESULT_MAX_BYTES: number;
343
344
  type SubagentExecution = 'idle' | 'provider_streaming' | 'tool_executing' | 'compacting' | 'finalizing' | 'pending_yield';
344
345
  interface ChildSupervisorOptions<State> {
@@ -353,14 +354,19 @@ interface ChildSupervisorOptions<State> {
353
354
  sessionOptions: AgentServerSessionOptions;
354
355
  /** When false, a child closing never wakes an idle parent; the host app orchestrates the wakeup from the `subagent closed` frame. */
355
356
  notifyParentOnIdle: boolean;
357
+ /** Backing store for child checkpoints and job metadata, keyed under the parent's session directory. */
358
+ store: HostStore;
356
359
  emit(frame: ServerFrame): void;
357
360
  }
358
361
  /**
359
362
  * Per-parent subagent supervisor. Owns every child `AgentSession` this parent
360
363
  * spawns: lifecycle (spawn / steer / abort / natural end), the `demi agent`
361
364
  * command tree, child shell environments, and the `subagent*` protocol frames
362
- * on the parent connection. Children are in-memory only: no store, no
363
- * checkpoint, gone with the parent.
365
+ * on the parent connection. Children persist exactly like the parent: each
366
+ * live child keeps a checkpoint and job record under the parent's session
367
+ * directory, and reopening the parent restores and resumes them (`restore`).
368
+ * A closed child moves to the archive: its transcript checkpoint stays on
369
+ * store and `demi agent resume` revives it with a new user message.
364
370
  */
365
371
  declare class ChildSupervisor<State = unknown> {
366
372
  private readonly options;
@@ -380,9 +386,43 @@ declare class ChildSupervisor<State = unknown> {
380
386
  } | null;
381
387
  /** Re-emits `subagent started` + transcript reset for every running child (transcript resync). */
382
388
  replay(): void;
389
+ /**
390
+ * Detaches every live child on connection teardown: aborts in-flight turns,
391
+ * flushes checkpoints, and keeps the persisted job so the next open of this
392
+ * parent restores it — the same dispose semantics as the parent session.
393
+ * No `closed` frame is emitted: the children are not done, just paused.
394
+ */
383
395
  dispose(): Promise<void>;
396
+ /**
397
+ * Rebuilds every persisted live child of this parent and finishes what it was
398
+ * doing: an interrupted turn resumes from its resume point; an already
399
+ * quiescent child closes with its result. Children share the parent's
400
+ * persistence lifecycle — a parent restore is a child restore.
401
+ */
402
+ restore(): Promise<void>;
403
+ private restoreJob;
404
+ /** Shared by restore and resume: rebuild a persisted child's job and session from its checkpoint. */
405
+ private reassembleJob;
406
+ /**
407
+ * Revives an archived child: its job record turns live again (this round's
408
+ * metadata, a fresh spawnedAt), the session rebuilds from the preserved
409
+ * checkpoint, and the message opens its next turn on top of the old
410
+ * transcript.
411
+ */
412
+ private resumeArchived;
413
+ /** Every archived (finished, revivable) child of this parent, newest first. */
414
+ private listArchivedJobs;
415
+ private pruneArchive;
384
416
  abortSubtree(id: string): Promise<void>;
385
417
  private spawn;
418
+ /** Everything spawn and restore share: the command tree, the harness runtime, and the job record (session attached separately). */
419
+ private assembleJob;
420
+ private attachSession;
421
+ private childStoreKey;
422
+ private childSessionStore;
423
+ private deletePersistedJob;
424
+ /** Shared spawn/resume foreground behaviour: announce the id, wire abort and stdin steers, wait for close, report the outcome. */
425
+ private attendChild;
386
426
  /** Delivers one steer to a child: mid-turn as a steer, idle as a new user turn. */
387
427
  private steerChild;
388
428
  private pumpStdinSteers;
@@ -392,6 +432,7 @@ declare class ChildSupervisor<State = unknown> {
392
432
  /** Resolves when the child is idle with no pending yield wakeups (or the job closed). */
393
433
  private waitForQuiescence;
394
434
  private closeJob;
435
+ private disposeJobShells;
395
436
  /** Wakes an idle parent with a user send; a parent blocked in the spawn gets the tool result instead. */
396
437
  private notifyIdleParent;
397
438
  private createChildAgentNode;
@@ -491,4 +532,4 @@ interface ShellToolView {
491
532
  declare function finishShellToolResult<State>(environment: BashEnvironment, result: ShellCommandStatus, ctx: AgentToolInvokeContext<State>): Promise<AgentToolInvokeResult>;
492
533
  declare function shellCommandHandleRequired(result: ShellCommandStatus, budgetTokens: number): boolean;
493
534
  //#endregion
494
- 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 };
535
+ 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_ARCHIVED_SUBAGENTS, 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
@@ -2653,6 +2653,7 @@ function shellCommandHandleRequired(result, budgetTokens) {
2653
2653
  //#endregion
2654
2654
  //#region src/subagent.ts
2655
2655
  const MAX_LIVE_SUBAGENTS = 8;
2656
+ const MAX_ARCHIVED_SUBAGENTS = 16;
2656
2657
  const SUBAGENT_RESULT_MAX_BYTES = 32768;
2657
2658
  const SHOW_RECENT_TOOLS = 8;
2658
2659
  const SPAWN_PROMPT_DESCRIPTION = "The child's first user message and only task brief. The child starts with an empty transcript and cannot see this conversation: do not refer to prior turns, and do not paste this conversation or the product user's message unchanged. Include the goal for this child, applicable decisions and constraints, whether to edit or only report, how to verify, and every concrete identifier it needs (paths, ids, error text, commands already tried and their key results). State the exact shape of the last assistant text it should return.";
@@ -2660,8 +2661,11 @@ const SPAWN_PROMPT_DESCRIPTION = "The child's first user message and only task b
2660
2661
  * Per-parent subagent supervisor. Owns every child `AgentSession` this parent
2661
2662
  * spawns: lifecycle (spawn / steer / abort / natural end), the `demi agent`
2662
2663
  * 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.
2664
+ * on the parent connection. Children persist exactly like the parent: each
2665
+ * live child keeps a checkpoint and job record under the parent's session
2666
+ * directory, and reopening the parent restores and resumes them (`restore`).
2667
+ * A closed child moves to the archive: its transcript checkpoint stays on
2668
+ * store and `demi agent resume` revives it with a new user message.
2665
2669
  */
2666
2670
  var ChildSupervisor = class {
2667
2671
  options;
@@ -2710,30 +2714,12 @@ var ChildSupervisor = class {
2710
2714
  await io.stderr(`demi agent: ${errorMessage(error)}\n`);
2711
2715
  return { exitCode: 1 };
2712
2716
  }
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 };
2717
+ return this.attendChild(job, {
2718
+ io,
2719
+ isJson: parsed.json === true,
2720
+ signal,
2721
+ stdinStream
2722
+ });
2737
2723
  },
2738
2724
  subcommands: [
2739
2725
  {
@@ -2792,21 +2778,73 @@ var ChildSupervisor = class {
2792
2778
  return { exitCode: 0 };
2793
2779
  }
2794
2780
  },
2781
+ {
2782
+ name: "resume",
2783
+ summary: "Revive an archived (finished) child with a new user message on top of its preserved transcript. Behaves like the spawn command afterwards: stays running until the child ends again, stdout is its new last assistant text, shell_write steers, shell_abort aborts. Archived ids are in `demi agent list`.",
2784
+ input: {
2785
+ id: z.string().describe("subagentId of an archived child"),
2786
+ message: z.string().optional().describe("The reviving user message; positional, or stdin/heredoc when omitted.")
2787
+ },
2788
+ positionals: ["id", "message"],
2789
+ stdinField: "message",
2790
+ output: { json: z.object({
2791
+ subagentId: z.string(),
2792
+ text: z.string()
2793
+ }) },
2794
+ run: async ({ parsed, io, signal, stdinStream }) => {
2795
+ const id = String(parsed.values.id);
2796
+ const message = String(parsed.values.message ?? "").trim();
2797
+ if (!message) {
2798
+ await io.stderr("demi agent resume: message must not be empty\n");
2799
+ return { exitCode: 1 };
2800
+ }
2801
+ let job;
2802
+ try {
2803
+ job = await this.resumeArchived(id, message);
2804
+ } catch (error) {
2805
+ await io.stderr(`demi agent resume: ${errorMessage(error)}\n`);
2806
+ return { exitCode: 1 };
2807
+ }
2808
+ return this.attendChild(job, {
2809
+ io,
2810
+ isJson: parsed.json === true,
2811
+ signal,
2812
+ stdinStream
2813
+ });
2814
+ }
2815
+ },
2795
2816
  {
2796
2817
  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()) }) },
2818
+ summary: "Snapshot roster of this session's running children, then its archived (finished, revivable via resume) children. One line per child with ages relative to now. A read, not a wait — not for polling loops.",
2819
+ output: { json: z.object({
2820
+ agents: z.array(z.unknown()),
2821
+ archived: z.array(z.unknown())
2822
+ }) },
2799
2823
  run: async ({ parsed, io }) => {
2800
2824
  const jobs = [...this.jobs.values()];
2825
+ const archived = await this.listArchivedJobs();
2801
2826
  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");
2827
+ await io.stdout(`${JSON.stringify({
2828
+ agents: jobs.map((job) => this.snapshot(job, false)),
2829
+ archived: archived.map(({ id, meta }) => ({
2830
+ subagentId: id,
2831
+ description: meta.description,
2832
+ profile: meta.profileName,
2833
+ phase: meta.closedPhase,
2834
+ closedAgoMs: meta.closedAt === void 0 ? null : Date.now() - meta.closedAt
2835
+ }))
2836
+ })}\n`);
2807
2837
  return { exitCode: 0 };
2808
2838
  }
2839
+ if (jobs.length === 0) await io.stdout("no running subagents\n");
2809
2840
  for (const job of jobs) await io.stdout(`${this.renderListLine(job)}\n`);
2841
+ if (archived.length > 0) {
2842
+ await io.stdout("archived (revivable with `demi agent resume <id>`):\n");
2843
+ for (const { id, meta } of archived) {
2844
+ const closedAgo = meta.closedAt === void 0 ? "" : ` closed ${formatDuration(Date.now() - meta.closedAt)} ago`;
2845
+ await io.stdout(` ${id} ${meta.closedPhase}${closedAgo} ${meta.description ? `"${meta.description}"` : "(no description)"}\n`);
2846
+ }
2847
+ }
2810
2848
  return { exitCode: 0 };
2811
2849
  }
2812
2850
  },
@@ -2860,9 +2898,129 @@ var ChildSupervisor = class {
2860
2898
  });
2861
2899
  }
2862
2900
  }
2901
+ /**
2902
+ * Detaches every live child on connection teardown: aborts in-flight turns,
2903
+ * flushes checkpoints, and keeps the persisted job so the next open of this
2904
+ * parent restores it — the same dispose semantics as the parent session.
2905
+ * No `closed` frame is emitted: the children are not done, just paused.
2906
+ */
2863
2907
  async dispose() {
2864
2908
  this.isDisposed = true;
2865
- for (const job of [...this.jobs.values()]) await this.closeJob(job, "aborted");
2909
+ for (const job of [...this.jobs.values()]) {
2910
+ job.isClosing = true;
2911
+ job.unsubscribe();
2912
+ this.jobs.delete(job.id);
2913
+ await job.session.dispose().catch(noop);
2914
+ await this.disposeJobShells(job);
2915
+ job.settleClosed({ phase: "aborted" });
2916
+ }
2917
+ }
2918
+ /**
2919
+ * Rebuilds every persisted live child of this parent and finishes what it was
2920
+ * doing: an interrupted turn resumes from its resume point; an already
2921
+ * quiescent child closes with its result. Children share the parent's
2922
+ * persistence lifecycle — a parent restore is a child restore.
2923
+ */
2924
+ async restore() {
2925
+ const parent = this.parentSession;
2926
+ if (!parent || this.isDisposed) return;
2927
+ const prefix = `agent-sessions/${parent.id()}/subagents/`;
2928
+ const keys = await this.options.store.list(prefix).catch(() => []);
2929
+ const ids = [...new Set(keys.map((key) => key.slice(prefix.length).split("/")[0] ?? "").filter(Boolean))];
2930
+ for (const id of ids) {
2931
+ if (this.jobs.has(id)) continue;
2932
+ try {
2933
+ await this.restoreJob(id);
2934
+ } catch {
2935
+ await this.deletePersistedJob(id);
2936
+ }
2937
+ }
2938
+ }
2939
+ async restoreJob(id) {
2940
+ if (!this.parentSession) return;
2941
+ const meta = await this.options.store.readJson(this.childStoreKey(id, "job.json"));
2942
+ if (meta?.closedPhase) return;
2943
+ const checkpoint = await this.childSessionStore(id).loadCheckpoint();
2944
+ if (!meta || !checkpoint) throw new Error("incomplete persisted subagent");
2945
+ const job = this.reassembleJob(id, meta, checkpoint);
2946
+ this.watchTurn(job, job.session.resume(meta.metadata ? { metadata: meta.metadata } : {}));
2947
+ }
2948
+ /** Shared by restore and resume: rebuild a persisted child's job and session from its checkpoint. */
2949
+ reassembleJob(id, meta, checkpoint) {
2950
+ const parent = this.parentSession;
2951
+ if (!parent) throw new Error("subagent supervisor has no parent session");
2952
+ const profile = this.resolveProfile(meta.profileName ?? void 0);
2953
+ const { job, runtime } = this.assembleJob({
2954
+ id,
2955
+ description: meta.description,
2956
+ profileName: meta.profileName,
2957
+ profile,
2958
+ metadata: meta.metadata,
2959
+ spawnedAt: meta.spawnedAt
2960
+ });
2961
+ const session = AgentSession.fromCheckpoint({
2962
+ provider: parent.cloneProviderRuntime(),
2963
+ runtime,
2964
+ checkpoint
2965
+ }, {
2966
+ agentSessionId: id,
2967
+ store: this.childSessionStore(id),
2968
+ ...this.options.sessionOptions
2969
+ });
2970
+ this.attachSession(job, session);
2971
+ return job;
2972
+ }
2973
+ /**
2974
+ * Revives an archived child: its job record turns live again (this round's
2975
+ * metadata, a fresh spawnedAt), the session rebuilds from the preserved
2976
+ * checkpoint, and the message opens its next turn on top of the old
2977
+ * transcript.
2978
+ */
2979
+ async resumeArchived(id, message) {
2980
+ const parent = this.parentSession;
2981
+ if (!parent) throw new Error("subagent supervisor has no parent session");
2982
+ if (this.isDisposed) throw new Error("parent session is closing");
2983
+ if (this.jobs.has(id)) throw new Error(`subagent "${id}" is still running; steer it instead`);
2984
+ if (this.jobs.size >= 8) throw new Error(`at most 8 running subagents per session; abort one or wait for a result`);
2985
+ const meta = await this.options.store.readJson(this.childStoreKey(id, "job.json"));
2986
+ if (!meta?.closedPhase) throw new Error(`no archived subagent "${id}" (see \`demi agent list\`)`);
2987
+ const checkpoint = await this.childSessionStore(id).loadCheckpoint();
2988
+ if (!checkpoint) throw new Error(`archived subagent "${id}" has no checkpoint left`);
2989
+ const liveMeta = {
2990
+ description: meta.description,
2991
+ profileName: meta.profileName,
2992
+ metadata: parent.actionMetadata(),
2993
+ spawnedAt: Date.now()
2994
+ };
2995
+ await this.options.store.writeJson(this.childStoreKey(id, "job.json"), liveMeta);
2996
+ const job = this.reassembleJob(id, liveMeta, checkpoint);
2997
+ this.watchTurn(job, job.session.send([{
2998
+ type: "text",
2999
+ text: message
3000
+ }], liveMeta.metadata ? { metadata: liveMeta.metadata } : {}));
3001
+ return job;
3002
+ }
3003
+ /** Every archived (finished, revivable) child of this parent, newest first. */
3004
+ async listArchivedJobs() {
3005
+ const parent = this.parentSession;
3006
+ if (!parent) return [];
3007
+ const prefix = `agent-sessions/${parent.id()}/subagents/`;
3008
+ const keys = await this.options.store.list(prefix).catch(() => []);
3009
+ const ids = [...new Set(keys.map((key) => key.slice(prefix.length).split("/")[0] ?? "").filter(Boolean))];
3010
+ const archived = [];
3011
+ for (const id of ids) {
3012
+ if (this.jobs.has(id)) continue;
3013
+ const meta = await this.options.store.readJson(this.childStoreKey(id, "job.json"));
3014
+ if (meta?.closedPhase) archived.push({
3015
+ id,
3016
+ meta
3017
+ });
3018
+ }
3019
+ return archived.sort((a, b) => (b.meta.closedAt ?? 0) - (a.meta.closedAt ?? 0));
3020
+ }
3021
+ async pruneArchive() {
3022
+ const archived = await this.listArchivedJobs();
3023
+ for (const stale of archived.slice(16)) await this.deletePersistedJob(stale.id);
2866
3024
  }
2867
3025
  async abortSubtree(id) {
2868
3026
  const job = this.jobs.get(id);
@@ -2877,6 +3035,40 @@ var ChildSupervisor = class {
2877
3035
  const profile = this.resolveProfile(input.profileName);
2878
3036
  const id = createId();
2879
3037
  const metadata = parent.actionMetadata();
3038
+ const profileName = input.profileName ?? (this.options.profiles ? profile.name : null);
3039
+ const spawnedAt = Date.now();
3040
+ const { job, runtime } = this.assembleJob({
3041
+ id,
3042
+ description: input.description,
3043
+ profileName,
3044
+ profile,
3045
+ metadata,
3046
+ spawnedAt
3047
+ });
3048
+ await this.options.store.writeJson(this.childStoreKey(id, "job.json"), {
3049
+ description: input.description,
3050
+ profileName,
3051
+ metadata,
3052
+ spawnedAt
3053
+ });
3054
+ const session = new AgentSession({
3055
+ provider: parent.cloneProviderRuntime(),
3056
+ model: profile.model ?? structuredClone(parent.modelSelection),
3057
+ cwd: this.options.cwd,
3058
+ runtime,
3059
+ state: this.options.agent.initialState()
3060
+ }, {
3061
+ agentSessionId: id,
3062
+ store: this.childSessionStore(id),
3063
+ ...this.options.sessionOptions
3064
+ });
3065
+ this.attachSession(job, session);
3066
+ this.watchChild(job, input.prompt);
3067
+ return job;
3068
+ }
3069
+ /** Everything spawn and restore share: the command tree, the harness runtime, and the job record (session attached separately). */
3070
+ assembleJob(input) {
3071
+ const { id, profile } = input;
2880
3072
  const agentNode = this.createChildAgentNode(id, input.description);
2881
3073
  const commands = injectSubagentCommand(profile.commands ? profile.commands([...this.options.parentCommands]) : [...this.options.parentCommands], agentNode);
2882
3074
  const commandRegistry = new CommandRegistry();
@@ -2891,15 +3083,15 @@ var ChildSupervisor = class {
2891
3083
  id,
2892
3084
  description: input.description,
2893
3085
  profile,
2894
- profileName: input.profileName ?? (this.options.profiles ? profile.name : null),
2895
- metadata,
3086
+ profileName: input.profileName,
3087
+ metadata: input.metadata,
2896
3088
  session: null,
2897
3089
  commandRegistry,
2898
3090
  commandNames,
2899
3091
  environments: /* @__PURE__ */ new Map(),
2900
3092
  pendingEnvironments: /* @__PURE__ */ new Map(),
2901
3093
  readonlyHosts: /* @__PURE__ */ new WeakMap(),
2902
- spawnedAt: Date.now(),
3094
+ spawnedAt: input.spawnedAt,
2903
3095
  lastEventAt: Date.now(),
2904
3096
  tools: [],
2905
3097
  lastAssistantTextAt: null,
@@ -2912,56 +3104,92 @@ var ChildSupervisor = class {
2912
3104
  };
2913
3105
  const agent = this.options.agent;
2914
3106
  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
- })
3107
+ return {
3108
+ job,
3109
+ runtime: {
3110
+ harnessName: agent.name,
3111
+ initialState: () => agent.initialState(),
3112
+ systemPrompt: (ctx) => (profile.systemPrompt ?? agent.systemPrompt).call(agent, {
3113
+ ...ctx,
3114
+ commandsPrompt
3115
+ }),
3116
+ preamble: async (ctx) => {
3117
+ const inheritedPreamble = profile.systemPrompt ? null : await agent.preamble?.(ctx) ?? null;
3118
+ return inheritedPreamble ? `${inheritedPreamble}\n\n${preamble}` : preamble;
3119
+ },
3120
+ tools: () => createStandardAgentTools({
3121
+ environment: (ctx) => this.childEnvironment(job, ctx),
3122
+ scheduleYield: (ctx, durationMs) => job.session.scheduleYieldWakeup(durationMs, ctx.metadata)
3123
+ })
3124
+ }
2930
3125
  };
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) => {
3126
+ }
3127
+ attachSession(job, session) {
3128
+ job.session = session;
3129
+ job.unsubscribe = session.subscribe((event) => {
2942
3130
  if (event.type !== "transcript_changed") return;
2943
3131
  this.recordTelemetry(job, event.patches);
2944
3132
  this.options.emit({
2945
3133
  type: "subagent_transcript_patch",
2946
- subagentId: id,
3134
+ subagentId: job.id,
2947
3135
  patches: event.patches,
2948
3136
  revision: event.revision
2949
3137
  });
2950
3138
  });
2951
- this.jobs.set(id, job);
3139
+ this.jobs.set(job.id, job);
2952
3140
  this.options.emit({
2953
3141
  type: "subagent",
2954
3142
  event: "started",
2955
3143
  job: this.wireJob(job)
2956
3144
  });
3145
+ const transcript = session.transcript();
2957
3146
  this.options.emit({
2958
3147
  type: "subagent_transcript_reset",
2959
- subagentId: id,
2960
- blocks: [],
2961
- revision: 0
3148
+ subagentId: job.id,
3149
+ blocks: structuredClone(transcript.blocks),
3150
+ revision: transcript.revision
2962
3151
  });
2963
- this.watchChild(job, input.prompt);
2964
- return job;
3152
+ }
3153
+ childStoreKey(childId, file) {
3154
+ return `agent-sessions/${this.parentSession?.id() ?? ""}/subagents/${childId}/${file}`;
3155
+ }
3156
+ childSessionStore(childId) {
3157
+ return {
3158
+ saveCheckpoint: (checkpoint) => this.options.store.writeJson(this.childStoreKey(childId, "checkpoint.json"), checkpoint),
3159
+ loadCheckpoint: () => this.options.store.readJson(this.childStoreKey(childId, "checkpoint.json"))
3160
+ };
3161
+ }
3162
+ async deletePersistedJob(id) {
3163
+ await this.options.store.delete(this.childStoreKey(id, "checkpoint.json")).catch(noop);
3164
+ await this.options.store.delete(this.childStoreKey(id, "job.json")).catch(noop);
3165
+ }
3166
+ /** Shared spawn/resume foreground behaviour: announce the id, wire abort and stdin steers, wait for close, report the outcome. */
3167
+ async attendChild(job, ctx) {
3168
+ const { io, isJson, signal, stdinStream } = ctx;
3169
+ await io.stderr(`subagentId: ${job.id}\n`);
3170
+ const onAbort = () => {
3171
+ this.abortSubtree(job.id).catch(noop);
3172
+ };
3173
+ if (signal.aborted) onAbort();
3174
+ else signal.addEventListener("abort", onAbort, { once: true });
3175
+ this.pumpStdinSteers(job.id, stdinStream);
3176
+ const close = await job.closed;
3177
+ signal.removeEventListener("abort", onAbort);
3178
+ if (close.phase === "completed") {
3179
+ const text = close.result ?? "";
3180
+ if (isJson) await io.stdout(`${JSON.stringify({
3181
+ subagentId: job.id,
3182
+ text
3183
+ })}\n`);
3184
+ else if (text.length > 0) await io.stdout(text.endsWith("\n") ? text : `${text}\n`);
3185
+ return { exitCode: 0 };
3186
+ }
3187
+ if (close.phase === "aborted") {
3188
+ await io.stderr(`demi agent: subagent ${job.id} aborted\n`);
3189
+ return { exitCode: 130 };
3190
+ }
3191
+ await io.stderr(`demi agent: subagent ${job.id} failed: ${close.failure ?? "unknown error"}\n`);
3192
+ return { exitCode: 1 };
2965
3193
  }
2966
3194
  /** Delivers one steer to a child: mid-turn as a steer, idle as a new user turn. */
2967
3195
  async steerChild(job, message) {
@@ -3028,12 +3256,16 @@ var ChildSupervisor = class {
3028
3256
  this.jobs.delete(job.id);
3029
3257
  const result = phase === "completed" ? boundedResultText(lastAssistantText(job.session.transcript().blocks)) : void 0;
3030
3258
  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);
3259
+ await this.disposeJobShells(job);
3260
+ await this.options.store.writeJson(this.childStoreKey(job.id, "job.json"), {
3261
+ description: job.description,
3262
+ profileName: job.profileName,
3263
+ metadata: job.metadata,
3264
+ spawnedAt: job.spawnedAt,
3265
+ closedPhase: phase,
3266
+ closedAt: Date.now()
3267
+ }).catch(noop);
3268
+ await this.pruneArchive().catch(noop);
3037
3269
  const close = {
3038
3270
  phase,
3039
3271
  ...result !== void 0 ? { result } : {},
@@ -3047,6 +3279,14 @@ var ChildSupervisor = class {
3047
3279
  job.settleClosed(close);
3048
3280
  this.notifyIdleParent(job, close);
3049
3281
  }
3282
+ async disposeJobShells(job) {
3283
+ const environments = /* @__PURE__ */ new Set([...job.environments.values()]);
3284
+ for (const pending of job.pendingEnvironments.values()) {
3285
+ const environment = await pending.catch(() => null);
3286
+ if (environment) environments.add(environment);
3287
+ }
3288
+ for (const environment of environments) await environment.disposeAllShells().catch(noop);
3289
+ }
3050
3290
  /** Wakes an idle parent with a user send; a parent blocked in the spawn gets the tool result instead. */
3051
3291
  notifyIdleParent(job, close) {
3052
3292
  if (this.isDisposed || !this.options.notifyParentOnIdle) return;
@@ -3308,6 +3548,7 @@ var ChildSupervisor = class {
3308
3548
  description: job.description,
3309
3549
  profile: job.profileName,
3310
3550
  phase: job.phase,
3551
+ metadata: job.metadata,
3311
3552
  ...result !== void 0 ? { result } : {}
3312
3553
  };
3313
3554
  }
@@ -3738,10 +3979,11 @@ var AgentTransportBindingImpl = class {
3738
3979
  await this.sessions.claim(agentSessionId, this);
3739
3980
  this.currentSessionId = agentSessionId;
3740
3981
  const initialState = agent.initialState();
3741
- const store = new HostAgentSessionStore((await agent.host({
3982
+ const provisionalHost = await agent.host({
3742
3983
  state: initialState,
3743
3984
  cwd: frame.cwd
3744
- })).store, agentSessionId);
3985
+ });
3986
+ const store = new HostAgentSessionStore(provisionalHost.store, agentSessionId);
3745
3987
  const checkpoint = await store.loadCheckpoint();
3746
3988
  const restoring = checkpoint !== null && checkpoint.harnessName === agent.name;
3747
3989
  const state = restoring ? structuredClone(checkpoint.state) : initialState;
@@ -3760,6 +4002,7 @@ var AgentTransportBindingImpl = class {
3760
4002
  prepareShell: this.prepareShell,
3761
4003
  sessionOptions: this.sessionOptions,
3762
4004
  notifyParentOnIdle: this.notifyParentOnIdle,
4005
+ store: provisionalHost.store,
3763
4006
  emit: (subagentFrame) => this.send(subagentFrame)
3764
4007
  });
3765
4008
  const commands = injectSubagentCommand(harnessCommands, supervisor.rootCommandNode());
@@ -3830,6 +4073,7 @@ var AgentTransportBindingImpl = class {
3830
4073
  type: "queue",
3831
4074
  queue: this.session.queuedMessages()
3832
4075
  });
4076
+ if (restoring) await supervisor.restore();
3833
4077
  }
3834
4078
  sendTranscriptReset(session) {
3835
4079
  const transcript = session.transcript();
@@ -4247,4 +4491,4 @@ function createProviderMap(providers) {
4247
4491
  return map;
4248
4492
  }
4249
4493
  //#endregion
4250
- 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 };
4494
+ export { AgentClient, AgentServer, AgentSession, ChildSupervisor, DEFAULT_MAX_MEDIA_BYTES, DEFAULT_TURN_RETRY_POLICY, MAX_ARCHIVED_SUBAGENTS, MAX_LIVE_SUBAGENTS, ProviderStreamError, RunCommandLineCommandNotRegisteredError, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError, SHELL_VIEW_MAX_CHARS, SUBAGENT_RESULT_MAX_BYTES, TranscriptLog, applyTranscriptPatches, cloneBlocks, createInProcessTransportPair, createReadonlyHost, createStandardAgentTools, createWebSocketClientTransport, createWebSocketServerTransport, estimateTranscriptBlockTokens, findResumePoint, finishShellToolResult, injectSubagentCommand, isContextLengthExceeded, isRetryableCode, resolveRetryPolicy, retryDelayMs, shellCommandHandleRequired, shellPreviewBudgetTokens, toShellToolResult };
@@ -1,4 +1,4 @@
1
- import { n as AgentServerTransport, t as AgentClientTransport } from "./transport-BRqZoaeo.mjs";
1
+ import { n as AgentServerTransport, t as AgentClientTransport } from "./transport-CHrPzcrU.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;
@@ -398,6 +398,8 @@ interface SubagentJob {
398
398
  description: string;
399
399
  profile: string | null;
400
400
  phase: 'running' | 'completed' | 'aborted' | 'error';
401
+ /** Action metadata of the round that spawned the child. */
402
+ metadata: AgentMetadata | null;
401
403
  /** Present on `closed`: the child's last assistant text, at most 32 KiB. */
402
404
  result?: string;
403
405
  }
@@ -1,4 +1,4 @@
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";
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-CHrPzcrU.mjs";
2
2
  import { ProviderSelection } from "@demicodes/provider";
3
3
  import { Block, UserContentBlock } from "@demicodes/core";
4
4
  //#region src/client.d.ts
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.19.1",
4
+ "version": "0.19.3",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -19,10 +19,10 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@demicodes/core": "^0.19.1",
23
- "@demicodes/provider": "^0.19.1",
24
- "@demicodes/shell": "^0.19.1",
25
- "@demicodes/utils": "^0.19.1",
22
+ "@demicodes/core": "^0.19.3",
23
+ "@demicodes/provider": "^0.19.3",
24
+ "@demicodes/shell": "^0.19.3",
25
+ "@demicodes/utils": "^0.19.3",
26
26
  "zod": "^4.0.0"
27
27
  },
28
28
  "license": "Apache-2.0",