@ian-pascoe/pi-minimal-subagents 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,9 +21,9 @@ export async function shutdownMinimalSubagentsSession(
21
21
  }
22
22
 
23
23
  while (true) {
24
+ if (!rootIdleGate.isRootIdle()) await rootIdleGate.waitForRootIdle();
24
25
  await coordinator.waitForSettledOperations();
25
26
  if (rootIdleGate.isRootIdle()) break;
26
- await rootIdleGate.waitForRootIdle();
27
27
  }
28
28
  await coordinator.shutdownAfterSettling();
29
29
  }
@@ -1,15 +1,25 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
- import { Type, type TSchema } from "typebox";
2
+ import { Type } from "typebox";
3
3
  import { THINKING_LEVELS } from "./minimal-subagents-capabilities.js";
4
4
 
5
5
  const SessionContextSchema = StringEnum(["inherit", "compact", "omit"] as const);
6
6
  const ProjectContextSchema = StringEnum(["inherit", "omit"] as const);
7
7
  const DelegationSchema = StringEnum(["none", "fanout"] as const);
8
8
  const ThinkingLevelSchema = StringEnum(THINKING_LEVELS);
9
- const ToolSelectionSchema = Type.Union([
10
- StringEnum(["none", "read", "modify"] as const),
11
- Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }),
12
- ]);
9
+ const ToolSelectionSchema = Type.Union(
10
+ [
11
+ StringEnum(["none", "read", "modify"] as const),
12
+ Type.Array(Type.String({ minLength: 1 }), {
13
+ uniqueItems: true,
14
+ description:
15
+ "Exact ordinary tool names. Coordinator tools are injected separately and must not appear here. Arrays are not bundle names; use the string preset `read` or `modify` for bundled capabilities.",
16
+ }),
17
+ ],
18
+ {
19
+ description:
20
+ 'Use the string preset "read" for read, grep, find, and ls; use "modify" for the read bundle plus bash, edit, and write. An array grants exactly those named tools (ordinary tools only); coordinator tools are injected separately.',
21
+ },
22
+ );
13
23
  const FRIENDLY_AGENT_ID_PATTERN = "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$";
14
24
  const CANONICAL_AGENT_ID_PATTERN =
15
25
  "^(?:root\\.)?[A-Za-z0-9][A-Za-z0-9_-]{0,63}(?:\\.[A-Za-z0-9][A-Za-z0-9_-]{0,63})*$";
@@ -20,8 +30,9 @@ function canonicalAgentIdSchema(description?: string) {
20
30
 
21
31
  /** Build all six strict TypeBox schemas, including the refreshed runtime model enum. */
22
32
  export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
23
- const explicitModelSchema: TSchema =
24
- modelIds.length > 0 ? StringEnum(modelIds as [string, ...string[]]) : Type.Never();
33
+ const [firstModelId, ...remainingModelIds] = modelIds;
34
+ const explicitModelSchema =
35
+ firstModelId === undefined ? Type.Never() : StringEnum([firstModelId, ...remainingModelIds]);
25
36
  return {
26
37
  subagent: Type.Object({
27
38
  task: Type.String({ minLength: 1, description: "Task for the persistent child agent" }),
@@ -49,6 +60,9 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
49
60
  }),
50
61
  subagent_wait: Type.Object({
51
62
  agent_id: canonicalAgentIdSchema("Direct child canonical agent ID"),
63
+ turn_id: Type.Optional(
64
+ Type.String({ minLength: 1, description: "Exact retained child turn ID" }),
65
+ ),
52
66
  timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
53
67
  }),
54
68
  subagent_status: Type.Object({
@@ -16,8 +16,17 @@ import {
16
16
  renderCoordinatorToolResult,
17
17
  type CoordinatorToolName,
18
18
  } from "./minimal-subagents-rendering.js";
19
+ import type { CoordinatorToolCallInput } from "./minimal-subagents-render-contract.js";
19
20
  import type { createCoordinatorToolSchemas } from "./minimal-subagents-tool-schemas.js";
20
- import type { CallerSnapshot, SpawnParameters } from "./minimal-subagents-types.js";
21
+ import type {
22
+ AgentMessageResult,
23
+ CallerSnapshot,
24
+ CancelResult,
25
+ DeleteResult,
26
+ SpawnResult,
27
+ StatusResult,
28
+ WaitResult,
29
+ } from "./minimal-subagents-types.js";
21
30
 
22
31
  const ORDINARY_CHILD_COORDINATOR_TOOL_NAMES = new Set([
23
32
  "agent_message",
@@ -25,8 +34,15 @@ const ORDINARY_CHILD_COORDINATOR_TOOL_NAMES = new Set([
25
34
  "subagent_status",
26
35
  ]);
27
36
 
28
- interface CoordinatorToolDefinitionOptions {
29
- coordinator: MinimalSubagentsCoordinator;
37
+ /** Coordinator operations consumed by the six public coordinator tool definitions. */
38
+ export type CoordinatorToolOperations = Pick<
39
+ MinimalSubagentsCoordinator,
40
+ "spawn" | "inspectStatus" | "sendAgentMessage" | "wait" | "status" | "cancel" | "delete"
41
+ >;
42
+
43
+ /** Dependencies and caller policy used to create caller-bound coordinator tools. */
44
+ export interface CoordinatorToolDefinitionOptions {
45
+ coordinator: CoordinatorToolOperations;
30
46
  callerId: string;
31
47
  allowFanoutTools?: boolean;
32
48
  modelRoles?: readonly MinimalSubagentsModelRole[];
@@ -61,15 +77,24 @@ async function runCoordinatorToolActivity<T>(
61
77
  }
62
78
  }
63
79
 
80
+ type CoordinatorToolResultDetails =
81
+ | SpawnResult
82
+ | AgentMessageResult
83
+ | WaitResult
84
+ | StatusResult
85
+ | CancelResult
86
+ | DeleteResult
87
+ | { agent_id: string; status: "waiting"; elapsed_ms: number };
88
+
64
89
  function createCoordinatorToolRendering(toolName: CoordinatorToolName) {
65
90
  return {
66
- renderCall: (args: Record<string, unknown>, theme: Theme) =>
91
+ renderCall: (args: CoordinatorToolCallInput, theme: Theme) =>
67
92
  renderCoordinatorToolCall(toolName, args, theme),
68
93
  renderResult: (
69
- result: AgentToolResult<unknown>,
94
+ result: AgentToolResult<CoordinatorToolResultDetails>,
70
95
  renderOptions: ToolRenderResultOptions,
71
96
  theme: Theme,
72
- context: { args: Record<string, unknown>; isError: boolean },
97
+ context: { args: CoordinatorToolCallInput; isError: boolean },
73
98
  ) =>
74
99
  renderCoordinatorToolResult(
75
100
  toolName,
@@ -82,7 +107,9 @@ function createCoordinatorToolRendering(toolName: CoordinatorToolName) {
82
107
  };
83
108
  }
84
109
 
85
- function structuredToolResult(result: unknown) {
110
+ function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
111
+ result: TDetails,
112
+ ): AgentToolResult<TDetails> {
86
113
  const json = JSON.stringify(result, null, 2);
87
114
  const truncated = truncateHead(json, {
88
115
  maxBytes: DEFAULT_MAX_BYTES,
@@ -94,7 +121,7 @@ function structuredToolResult(result: unknown) {
94
121
  };
95
122
  }
96
123
 
97
- function failedStructuredOperation(prefix: string, result: unknown): never {
124
+ function failedStructuredOperation(prefix: string, result: DeleteResult): never {
98
125
  const json = JSON.stringify(result);
99
126
  const truncated = truncateHead(json, {
100
127
  maxBytes: DEFAULT_MAX_BYTES,
@@ -104,7 +131,7 @@ function failedStructuredOperation(prefix: string, result: unknown): never {
104
131
  }
105
132
 
106
133
  function callerSourceTurnId(
107
- coordinator: MinimalSubagentsCoordinator,
134
+ coordinator: CoordinatorToolOperations,
108
135
  callerId: string,
109
136
  toolCallId: string,
110
137
  ): string {
@@ -132,17 +159,17 @@ export function createCoordinatorToolDefinitions(
132
159
  return runCoordinatorToolActivity(options, async () => {
133
160
  const result = await options.coordinator.spawn(
134
161
  options.callerId,
135
- parameters as SpawnParameters,
162
+ parameters,
136
163
  options.captureCaller(context),
137
164
  );
138
165
  const status = options.coordinator.inspectStatus(result.agent_id);
139
- return {
140
- ...structuredToolResult(result),
141
- details: {
142
- ...result,
143
- ...(status && "agent" in status ? { agent: status.agent } : {}),
144
- },
166
+ const details: SpawnResult & {
167
+ agent?: import("./minimal-subagents-types.js").AgentDetail;
168
+ } = {
169
+ ...result,
145
170
  };
171
+ if (status && "agent" in status) details.agent = status.agent;
172
+ return structuredToolResult(details);
146
173
  });
147
174
  },
148
175
  ...createCoordinatorToolRendering("subagent"),
@@ -152,7 +179,7 @@ export function createCoordinatorToolDefinitions(
152
179
  name: "agent_message",
153
180
  label: "Agent Message",
154
181
  description:
155
- "Send one mid-turn coordination message to a direct parent, direct sibling, or direct child when the recipient must act before the caller's turn finishes.",
182
+ "Send one mid-turn coordination message to a direct parent, direct sibling, or direct child. The result says whether it was delivered through an active wait, queued for the recipient, or failed.",
156
183
  promptSnippet: "Coordinate required mid-turn action with one adjacent agent",
157
184
  parameters: options.schemas.agent_message,
158
185
  async execute(toolCallId, parameters) {
@@ -175,7 +202,7 @@ export function createCoordinatorToolDefinitions(
175
202
  name: "subagent_wait",
176
203
  label: "Subagent Wait",
177
204
  description:
178
- "Wait for one direct child's exact active turn, or return its latest settled turn immediately. Timeout never cancels the child.",
205
+ "Wait for one direct child's oldest observable turn, or select an exact retained turn_id. A Wait Event containing a Coordination Message may arrive first as event=message; call again for the terminal turn result. A successful wait durably claims that turn so later messages and its terminal result return through wait without duplicate automatic delivery. Timeout never cancels the child.",
179
206
  promptSnippet: "Wait for one direct child's exact turn",
180
207
  parameters: options.schemas.subagent_wait,
181
208
  async execute(_toolCallId, parameters, signal, onUpdate) {
@@ -199,6 +226,7 @@ export function createCoordinatorToolDefinitions(
199
226
  parameters.agent_id,
200
227
  parameters.timeout_ms,
201
228
  signal,
229
+ parameters.turn_id,
202
230
  );
203
231
  return {
204
232
  ...structuredToolResult(result),
@@ -1,9 +1,6 @@
1
1
  import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { Usage } from "@earendil-works/pi-ai";
3
3
 
4
- /** Canonical path-like identity for one persistent subagent. */
5
- export type AgentId = string & { readonly __agentId: unique symbol };
6
-
7
4
  /** Stable identity for one prompt and its complete assistant/tool loop. */
8
5
  export type TurnId = string & { readonly __turnId: unique symbol };
9
6
 
@@ -58,13 +55,29 @@ export interface TurnResult {
58
55
  elapsed_ms?: number;
59
56
  }
60
57
 
61
- /** Reports delivery of one direct message to an authorized adjacent agent. */
58
+ /** Reports whether one direct message was handed to a wait, queued, or failed. */
59
+ export type AgentMessageDisposition = "delivered-via-wait" | "queued" | "failed";
60
+
62
61
  export interface AgentMessageResult {
63
62
  agent_id: string;
64
- delivered: boolean;
63
+ message_id: string;
64
+ disposition: AgentMessageDisposition;
65
65
  error?: string;
66
66
  }
67
67
 
68
+ /** Reports one coordination message returned before the source turn settles. */
69
+ export interface WaitMessageResult {
70
+ event: "message";
71
+ agent_id: string;
72
+ turn_id: string;
73
+ message_id: string;
74
+ delivery_id?: string;
75
+ message: string;
76
+ }
77
+
78
+ /** Reports one terminal child turn returned by subagent_wait. */
79
+ export type WaitResult = WaitMessageResult | ({ event: "turn" } & TurnResult);
80
+
68
81
  /** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
69
82
  export interface AgentSummary extends RuntimeProfile {
70
83
  agent_id: string;
@@ -92,7 +105,7 @@ export interface RecentAgentMessage {
92
105
  /** Extends summary status with launch, dependency, and recent-message diagnostics. */
93
106
  export interface AgentDetail extends AgentSummary {
94
107
  session_file?: string;
95
- launch_contract: Record<string, unknown>;
108
+ launch_contract: LaunchContract;
96
109
  capability_ceiling: string[];
97
110
  spawn_entry_id: string;
98
111
  recent_messages: RecentAgentMessage[];
@@ -164,6 +177,8 @@ export interface CoordinatorMessage {
164
177
  source_agent_id: string;
165
178
  destination_agent_id?: string;
166
179
  source_turn_id: string;
180
+ message_id: string;
181
+ delivery_id?: string;
167
182
  status?: TurnStatus;
168
183
  elapsed_ms?: number;
169
184
  usage?: Usage;
@@ -172,8 +187,7 @@ export interface CoordinatorMessage {
172
187
 
173
188
  /** Process-local adapter around one SDK-created Pi child session. */
174
189
  export interface ChildAgentRuntime {
175
- readonly sessionFile: string;
176
- readonly sessionId: string;
190
+ readonly sessionLeafId: string | undefined;
177
191
  readonly isRunning: boolean;
178
192
  runPrompt(
179
193
  task: string,
@@ -182,48 +196,55 @@ export interface ChildAgentRuntime {
182
196
  callerThinkingLevel: ThinkingLevel,
183
197
  ): Promise<RuntimeTurnOutcome>;
184
198
  runMessage(message: CoordinatorMessage): Promise<RuntimeTurnOutcome>;
185
- /** Steer one typed coordinator message into the child session. */
186
- steerCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
199
+ /** Queue one typed coordinator message into the child session. */
200
+ queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
187
201
  abort(): Promise<void>;
188
202
  dispose(): void;
189
203
  /** Return the live Runtime Profile, or undefined when the SDK session has no model. */
190
204
  getRuntimeProfile(): RuntimeProfile | undefined;
191
205
  snapshotCommittedMessages(): AgentMessage[];
192
- hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string): boolean;
206
+ hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
193
207
  getUsage(): Usage | undefined;
194
- cloneSession(): Promise<{ sessionFile: string; sessionId: string }>;
195
208
  }
196
209
 
197
210
  /** Identifies one writable child JSONL session owned by a coordinator root. */
198
211
  export interface PersistedSessionIdentity {
199
212
  sessionFile: string;
200
213
  sessionId: string;
201
- }
202
-
203
- /** Combines a persisted agent record with first-launch imported context. */
204
- export interface RuntimeCreationRequest {
205
- agent: PersistedAgent;
206
- importedMessages: AgentMessage[];
214
+ sessionLeafId?: string;
207
215
  }
208
216
 
209
217
  /** Pi-specific session operations injected into the pure coordinator. */
210
218
  export interface AgentSessionFactory {
211
219
  createIdentity(agent: PersistedAgent, importedMessages: AgentMessage[]): PersistedSessionIdentity;
212
- createRuntime(request: RuntimeCreationRequest): Promise<ChildAgentRuntime>;
213
- restoreRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime>;
220
+ /** Open one verified persisted Child Agent runtime for launch or restoration. */
221
+ openRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime>;
214
222
  resolveLaunchMissingDependencies(agent: PersistedAgent): Promise<string[]>;
215
223
  resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]>;
216
224
  resolveThinkingLevel(modelId: string, requested: ThinkingLevel): ThinkingLevel;
217
225
  modelSupportsImages(modelId: string): boolean;
226
+ /** Clone a child leaf owned by the active source root during confirmed shutdown. */
218
227
  cloneSession(agent: PersistedAgent): Promise<PersistedSessionIdentity>;
219
- trashSessionFile(sessionFile: string): Promise<void>;
228
+ /** Clone a source-owned leaf recovered from a proven destination branch. */
229
+ cloneForkSourceSession(
230
+ agent: PersistedAgent,
231
+ sourceRootSessionId: string,
232
+ ): Promise<PersistedSessionIdentity>;
233
+ /** Append and verify destination-root ownership for one fork clone. */
234
+ adoptForkSessionOwnership(
235
+ agent: PersistedAgent,
236
+ sourceRootSessionId: string,
237
+ ): Promise<PersistedSessionIdentity>;
238
+ trashSession(agent: PersistedAgent): Promise<void>;
220
239
  }
221
240
 
222
241
  /** Abstracts root message delivery and durable delivery-evidence lookup. */
223
242
  export interface RootConversationEndpoint {
224
- /** Steer one typed coordinator message into the root conversation. */
225
- steerCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
226
- hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string): boolean;
243
+ /** Queue one typed coordinator message into the root conversation. */
244
+ queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
245
+ hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
246
+ /** Report whether automatic delivery can start a new root turn without racing an active wait. */
247
+ isIdle(): boolean;
227
248
  }
228
249
 
229
250
  /** Stores root-owned agent identity, launch contract, availability, and latest activity. */
@@ -237,6 +258,7 @@ export interface PersistedAgent {
237
258
  spawn_entry_id: string;
238
259
  session_file?: string;
239
260
  session_id?: string;
261
+ session_leaf_id?: string;
240
262
  clone_error?: string;
241
263
  launch_contract: LaunchContract;
242
264
  capability_ceiling: string[];
@@ -250,7 +272,7 @@ export interface PersistedAgent {
250
272
  deleted?: boolean;
251
273
  }
252
274
 
253
- /** Records whether successful output was observed through wait or automatic messaging. */
275
+ /** Records which conversation path owns one successful terminal result. */
254
276
  export type DeliveryPath = "wait" | "message";
255
277
 
256
278
  /** Stores a keyed successful result until destination evidence settles delivery. */
@@ -260,26 +282,44 @@ export interface PersistedDelivery {
260
282
  destination_agent_id: string;
261
283
  path: DeliveryPath;
262
284
  settled: boolean;
285
+ sequence?: number;
263
286
  result?: TurnResult;
264
287
  error?: string;
265
288
  }
266
289
 
267
- /** Checkpoints all live agents, deletion tombstones, and delivery records for one root. */
290
+ /** Stores one durable Coordination Message until destination evidence settles it. */
291
+ export interface PersistedCoordinationDelivery {
292
+ delivery_id: string;
293
+ sequence: number;
294
+ destination_agent_id: string;
295
+ path: DeliveryPath;
296
+ settled: boolean;
297
+ message: CoordinatorMessage;
298
+ error?: string;
299
+ }
300
+
301
+ /** Checkpoints live agents, tombstones, and only pending delivery-ledger items for one root. */
268
302
  export interface RegistrySnapshot {
269
303
  agents: PersistedAgent[];
270
304
  tombstones: string[];
271
305
  deliveries: PersistedDelivery[];
306
+ coordination_deliveries?: PersistedCoordinationDelivery[];
307
+ wait_claimed_turns?: string[];
308
+ next_delivery_sequence?: number;
272
309
  }
273
310
 
274
311
  /** Clones a registry snapshot while recording the source root session file. */
275
312
  export interface ForkSnapshot extends RegistrySnapshot {
313
+ /** Canonical Pi root session file from which the selected fork branch originated. */
276
314
  source_root_session_file: string;
315
+ /** Root identity that every cloned Child Session provenance record must match. */
316
+ source_root_session_id: string;
277
317
  }
278
318
 
279
319
  /** Appends root-owned registry events to the active root conversation branch. */
280
320
  export interface RegistryWriter {
281
321
  readonly rootSessionId: string;
282
- append(event: import("./minimal-subagents-registry.js").RegistryEventV1): void;
322
+ append(event: import("./minimal-subagents-registry.js").RegistryEventV2): void;
283
323
  }
284
324
 
285
325
  /** Describes concise lifecycle notices surfaced through Pi UI notifications. */
@@ -1,4 +1,7 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
+
3
+ /** Theme operations used by the Minimal Subagents widget renderer. */
4
+ export type MinimalSubagentsWidgetTheme = Pick<Theme, "fg" | "bold">;
2
5
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
6
  import {
4
7
  sliceByColumn,
@@ -195,7 +198,7 @@ function renderMinimalSubagentsWidgetRowParts(
195
198
  task: string | undefined,
196
199
  duration: string | undefined,
197
200
  profile: string,
198
- theme: Theme,
201
+ theme: MinimalSubagentsWidgetTheme,
199
202
  ): MinimalSubagentsWidgetRowParts {
200
203
  const branch = row.depth > 0 ? `${" ".repeat(row.depth)}╰─ ` : " ";
201
204
  const styledBranch = theme.fg("borderMuted", branch);
@@ -225,7 +228,7 @@ function minimalSubagentsWidgetProfileBudget(
225
228
  task: string | undefined,
226
229
  duration: string | undefined,
227
230
  separator: string,
228
- theme: Theme,
231
+ theme: MinimalSubagentsWidgetTheme,
229
232
  width: number,
230
233
  ): number {
231
234
  const fixedParts = renderMinimalSubagentsWidgetRowParts(row, task, duration, "", theme);
@@ -235,7 +238,7 @@ function minimalSubagentsWidgetProfileBudget(
235
238
  function renderMinimalSubagentsWidgetRow(
236
239
  row: MinimalSubagentsWidgetRow,
237
240
  width: number,
238
- theme: Theme,
241
+ theme: MinimalSubagentsWidgetTheme,
239
242
  ): string {
240
243
  const separator = theme.fg("dim", MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT);
241
244
  const task = row.task?.replace(/\s+/g, " ").trim() || undefined;
@@ -336,7 +339,7 @@ function renderMinimalSubagentsWidgetRow(
336
339
  export function renderMinimalSubagentsWidgetLines(
337
340
  view: MinimalSubagentsWidgetView,
338
341
  width: number,
339
- theme: Theme,
342
+ theme: MinimalSubagentsWidgetTheme,
340
343
  ): string[] {
341
344
  if (width <= 0) return [];
342
345
  const separator = theme.fg("dim", MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT);
@@ -376,7 +379,7 @@ class MinimalSubagentsWidgetComponent implements Component {
376
379
  constructor(
377
380
  private view: MinimalSubagentsWidgetView,
378
381
  private readonly tui: TUI,
379
- private readonly theme: Theme,
382
+ private readonly theme: MinimalSubagentsWidgetTheme,
380
383
  ) {}
381
384
 
382
385
  update(view: MinimalSubagentsWidgetView): void {