@ian-pascoe/pi-minimal-subagents 0.1.1 → 0.2.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,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({
@@ -9,6 +9,7 @@ import {
9
9
  type ToolDefinition,
10
10
  type ToolRenderResultOptions,
11
11
  } from "@earendil-works/pi-coding-agent";
12
+ import type { Static } from "typebox";
12
13
  import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
13
14
  import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
14
15
  import {
@@ -16,8 +17,17 @@ import {
16
17
  renderCoordinatorToolResult,
17
18
  type CoordinatorToolName,
18
19
  } from "./minimal-subagents-rendering.js";
20
+ import type { CoordinatorToolCallInput } from "./minimal-subagents-render-contract.js";
19
21
  import type { createCoordinatorToolSchemas } from "./minimal-subagents-tool-schemas.js";
20
- import type { CallerSnapshot, SpawnParameters } from "./minimal-subagents-types.js";
22
+ import type {
23
+ AgentMessageResult,
24
+ CallerSnapshot,
25
+ CancelResult,
26
+ DeleteResult,
27
+ SpawnResult,
28
+ StatusResult,
29
+ WaitResult,
30
+ } from "./minimal-subagents-types.js";
21
31
 
22
32
  const ORDINARY_CHILD_COORDINATOR_TOOL_NAMES = new Set([
23
33
  "agent_message",
@@ -25,8 +35,15 @@ const ORDINARY_CHILD_COORDINATOR_TOOL_NAMES = new Set([
25
35
  "subagent_status",
26
36
  ]);
27
37
 
28
- interface CoordinatorToolDefinitionOptions {
29
- coordinator: MinimalSubagentsCoordinator;
38
+ /** Coordinator operations consumed by the six public coordinator tool definitions. */
39
+ export type CoordinatorToolOperations = Pick<
40
+ MinimalSubagentsCoordinator,
41
+ "spawn" | "inspectStatus" | "sendAgentMessage" | "wait" | "status" | "cancel" | "delete"
42
+ >;
43
+
44
+ /** Dependencies and caller policy used to create caller-bound coordinator tools. */
45
+ export interface CoordinatorToolDefinitionOptions {
46
+ coordinator: CoordinatorToolOperations;
30
47
  callerId: string;
31
48
  allowFanoutTools?: boolean;
32
49
  modelRoles?: readonly MinimalSubagentsModelRole[];
@@ -36,6 +53,27 @@ interface CoordinatorToolDefinitionOptions {
36
53
  onAttention?: (message: string) => void;
37
54
  }
38
55
 
56
+ /** Arguments consumed by the wait tool's narrow coordinator execution seam. */
57
+ export type CoordinatorWaitToolParameters = Static<
58
+ ReturnType<typeof createCoordinatorToolSchemas>["subagent_wait"]
59
+ >;
60
+
61
+ /** Forward one typed wait-tool request without requiring an unrelated Pi execution context. */
62
+ export function executeCoordinatorWaitTool(
63
+ coordinator: Pick<CoordinatorToolOperations, "wait">,
64
+ callerId: string,
65
+ parameters: CoordinatorWaitToolParameters,
66
+ signal: AbortSignal | undefined,
67
+ ): Promise<WaitResult> {
68
+ return coordinator.wait(
69
+ callerId,
70
+ parameters.agent_id,
71
+ parameters.timeout_ms,
72
+ signal,
73
+ parameters.turn_id,
74
+ );
75
+ }
76
+
39
77
  function buildModelRolePromptGuidelines(
40
78
  modelRoles: readonly MinimalSubagentsModelRole[],
41
79
  ): string[] | undefined {
@@ -61,15 +99,24 @@ async function runCoordinatorToolActivity<T>(
61
99
  }
62
100
  }
63
101
 
102
+ type CoordinatorToolResultDetails =
103
+ | SpawnResult
104
+ | AgentMessageResult
105
+ | WaitResult
106
+ | StatusResult
107
+ | CancelResult
108
+ | DeleteResult
109
+ | { agent_id: string; status: "waiting"; elapsed_ms: number };
110
+
64
111
  function createCoordinatorToolRendering(toolName: CoordinatorToolName) {
65
112
  return {
66
- renderCall: (args: Record<string, unknown>, theme: Theme) =>
113
+ renderCall: (args: CoordinatorToolCallInput, theme: Theme) =>
67
114
  renderCoordinatorToolCall(toolName, args, theme),
68
115
  renderResult: (
69
- result: AgentToolResult<unknown>,
116
+ result: AgentToolResult<CoordinatorToolResultDetails>,
70
117
  renderOptions: ToolRenderResultOptions,
71
118
  theme: Theme,
72
- context: { args: Record<string, unknown>; isError: boolean },
119
+ context: { args: CoordinatorToolCallInput; isError: boolean },
73
120
  ) =>
74
121
  renderCoordinatorToolResult(
75
122
  toolName,
@@ -82,7 +129,9 @@ function createCoordinatorToolRendering(toolName: CoordinatorToolName) {
82
129
  };
83
130
  }
84
131
 
85
- function structuredToolResult(result: unknown) {
132
+ function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
133
+ result: TDetails,
134
+ ): AgentToolResult<TDetails> {
86
135
  const json = JSON.stringify(result, null, 2);
87
136
  const truncated = truncateHead(json, {
88
137
  maxBytes: DEFAULT_MAX_BYTES,
@@ -94,7 +143,7 @@ function structuredToolResult(result: unknown) {
94
143
  };
95
144
  }
96
145
 
97
- function failedStructuredOperation(prefix: string, result: unknown): never {
146
+ function failedStructuredOperation(prefix: string, result: DeleteResult): never {
98
147
  const json = JSON.stringify(result);
99
148
  const truncated = truncateHead(json, {
100
149
  maxBytes: DEFAULT_MAX_BYTES,
@@ -104,7 +153,7 @@ function failedStructuredOperation(prefix: string, result: unknown): never {
104
153
  }
105
154
 
106
155
  function callerSourceTurnId(
107
- coordinator: MinimalSubagentsCoordinator,
156
+ coordinator: CoordinatorToolOperations,
108
157
  callerId: string,
109
158
  toolCallId: string,
110
159
  ): string {
@@ -132,17 +181,17 @@ export function createCoordinatorToolDefinitions(
132
181
  return runCoordinatorToolActivity(options, async () => {
133
182
  const result = await options.coordinator.spawn(
134
183
  options.callerId,
135
- parameters as SpawnParameters,
184
+ parameters,
136
185
  options.captureCaller(context),
137
186
  );
138
187
  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
- },
188
+ const details: SpawnResult & {
189
+ agent?: import("./minimal-subagents-types.js").AgentDetail;
190
+ } = {
191
+ ...result,
145
192
  };
193
+ if (status && "agent" in status) details.agent = status.agent;
194
+ return structuredToolResult(details);
146
195
  });
147
196
  },
148
197
  ...createCoordinatorToolRendering("subagent"),
@@ -152,7 +201,7 @@ export function createCoordinatorToolDefinitions(
152
201
  name: "agent_message",
153
202
  label: "Agent Message",
154
203
  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.",
204
+ "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
205
  promptSnippet: "Coordinate required mid-turn action with one adjacent agent",
157
206
  parameters: options.schemas.agent_message,
158
207
  async execute(toolCallId, parameters) {
@@ -175,7 +224,7 @@ export function createCoordinatorToolDefinitions(
175
224
  name: "subagent_wait",
176
225
  label: "Subagent Wait",
177
226
  description:
178
- "Wait for one direct child's exact active turn, or return its latest settled turn immediately. Timeout never cancels the child.",
227
+ "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
228
  promptSnippet: "Wait for one direct child's exact turn",
180
229
  parameters: options.schemas.subagent_wait,
181
230
  async execute(_toolCallId, parameters, signal, onUpdate) {
@@ -194,10 +243,10 @@ export function createCoordinatorToolDefinitions(
194
243
  waitingInterval.unref?.();
195
244
  try {
196
245
  return await runCoordinatorToolActivity(options, async () => {
197
- const result = await options.coordinator.wait(
246
+ const result = await executeCoordinatorWaitTool(
247
+ options.coordinator,
198
248
  options.callerId,
199
- parameters.agent_id,
200
- parameters.timeout_ms,
249
+ parameters,
201
250
  signal,
202
251
  );
203
252
  return {
@@ -58,13 +58,29 @@ export interface TurnResult {
58
58
  elapsed_ms?: number;
59
59
  }
60
60
 
61
- /** Reports delivery of one direct message to an authorized adjacent agent. */
61
+ /** Reports whether one direct message was handed to a wait, queued, or failed. */
62
+ export type AgentMessageDisposition = "delivered-via-wait" | "queued" | "failed";
63
+
62
64
  export interface AgentMessageResult {
63
65
  agent_id: string;
64
- delivered: boolean;
66
+ message_id: string;
67
+ disposition: AgentMessageDisposition;
65
68
  error?: string;
66
69
  }
67
70
 
71
+ /** Reports one coordination message returned before the source turn settles. */
72
+ export interface WaitMessageResult {
73
+ event: "message";
74
+ agent_id: string;
75
+ turn_id: string;
76
+ message_id: string;
77
+ delivery_id?: string;
78
+ message: string;
79
+ }
80
+
81
+ /** Reports one terminal child turn returned by subagent_wait. */
82
+ export type WaitResult = WaitMessageResult | ({ event: "turn" } & TurnResult);
83
+
68
84
  /** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
69
85
  export interface AgentSummary extends RuntimeProfile {
70
86
  agent_id: string;
@@ -92,7 +108,7 @@ export interface RecentAgentMessage {
92
108
  /** Extends summary status with launch, dependency, and recent-message diagnostics. */
93
109
  export interface AgentDetail extends AgentSummary {
94
110
  session_file?: string;
95
- launch_contract: Record<string, unknown>;
111
+ launch_contract: LaunchContract;
96
112
  capability_ceiling: string[];
97
113
  spawn_entry_id: string;
98
114
  recent_messages: RecentAgentMessage[];
@@ -164,6 +180,8 @@ export interface CoordinatorMessage {
164
180
  source_agent_id: string;
165
181
  destination_agent_id?: string;
166
182
  source_turn_id: string;
183
+ message_id: string;
184
+ delivery_id?: string;
167
185
  status?: TurnStatus;
168
186
  elapsed_ms?: number;
169
187
  usage?: Usage;
@@ -174,6 +192,7 @@ export interface CoordinatorMessage {
174
192
  export interface ChildAgentRuntime {
175
193
  readonly sessionFile: string;
176
194
  readonly sessionId: string;
195
+ readonly sessionLeafId: string | undefined;
177
196
  readonly isRunning: boolean;
178
197
  runPrompt(
179
198
  task: string,
@@ -182,22 +201,22 @@ export interface ChildAgentRuntime {
182
201
  callerThinkingLevel: ThinkingLevel,
183
202
  ): Promise<RuntimeTurnOutcome>;
184
203
  runMessage(message: CoordinatorMessage): Promise<RuntimeTurnOutcome>;
185
- /** Steer one typed coordinator message into the child session. */
186
- steerCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
204
+ /** Queue one typed coordinator message into the child session. */
205
+ queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
187
206
  abort(): Promise<void>;
188
207
  dispose(): void;
189
208
  /** Return the live Runtime Profile, or undefined when the SDK session has no model. */
190
209
  getRuntimeProfile(): RuntimeProfile | undefined;
191
210
  snapshotCommittedMessages(): AgentMessage[];
192
- hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string): boolean;
211
+ hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
193
212
  getUsage(): Usage | undefined;
194
- cloneSession(): Promise<{ sessionFile: string; sessionId: string }>;
195
213
  }
196
214
 
197
215
  /** Identifies one writable child JSONL session owned by a coordinator root. */
198
216
  export interface PersistedSessionIdentity {
199
217
  sessionFile: string;
200
218
  sessionId: string;
219
+ sessionLeafId?: string;
201
220
  }
202
221
 
203
222
  /** Combines a persisted agent record with first-launch imported context. */
@@ -215,15 +234,28 @@ export interface AgentSessionFactory {
215
234
  resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]>;
216
235
  resolveThinkingLevel(modelId: string, requested: ThinkingLevel): ThinkingLevel;
217
236
  modelSupportsImages(modelId: string): boolean;
237
+ /** Clone a child leaf owned by the active source root during confirmed shutdown. */
218
238
  cloneSession(agent: PersistedAgent): Promise<PersistedSessionIdentity>;
219
- trashSessionFile(sessionFile: string): Promise<void>;
239
+ /** Clone a source-owned leaf recovered from a proven destination branch. */
240
+ cloneForkSourceSession(
241
+ agent: PersistedAgent,
242
+ sourceRootSessionId: string,
243
+ ): Promise<PersistedSessionIdentity>;
244
+ /** Append and verify destination-root ownership for one fork clone. */
245
+ adoptForkSessionOwnership(
246
+ agent: PersistedAgent,
247
+ sourceRootSessionId: string,
248
+ ): Promise<PersistedSessionIdentity>;
249
+ trashSession(agent: PersistedAgent): Promise<void>;
220
250
  }
221
251
 
222
252
  /** Abstracts root message delivery and durable delivery-evidence lookup. */
223
253
  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;
254
+ /** Queue one typed coordinator message into the root conversation. */
255
+ queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
256
+ hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
257
+ /** Report whether automatic delivery can start a new root turn without racing an active wait. */
258
+ isIdle(): boolean;
227
259
  }
228
260
 
229
261
  /** Stores root-owned agent identity, launch contract, availability, and latest activity. */
@@ -237,6 +269,7 @@ export interface PersistedAgent {
237
269
  spawn_entry_id: string;
238
270
  session_file?: string;
239
271
  session_id?: string;
272
+ session_leaf_id?: string;
240
273
  clone_error?: string;
241
274
  launch_contract: LaunchContract;
242
275
  capability_ceiling: string[];
@@ -250,7 +283,7 @@ export interface PersistedAgent {
250
283
  deleted?: boolean;
251
284
  }
252
285
 
253
- /** Records whether successful output was observed through wait or automatic messaging. */
286
+ /** Records which conversation path owns one successful terminal result. */
254
287
  export type DeliveryPath = "wait" | "message";
255
288
 
256
289
  /** Stores a keyed successful result until destination evidence settles delivery. */
@@ -260,26 +293,44 @@ export interface PersistedDelivery {
260
293
  destination_agent_id: string;
261
294
  path: DeliveryPath;
262
295
  settled: boolean;
296
+ sequence?: number;
263
297
  result?: TurnResult;
264
298
  error?: string;
265
299
  }
266
300
 
267
- /** Checkpoints all live agents, deletion tombstones, and delivery records for one root. */
301
+ /** Stores one durable Coordination Message until destination evidence settles it. */
302
+ export interface PersistedCoordinationDelivery {
303
+ delivery_id: string;
304
+ sequence: number;
305
+ destination_agent_id: string;
306
+ path: DeliveryPath;
307
+ settled: boolean;
308
+ message: CoordinatorMessage;
309
+ error?: string;
310
+ }
311
+
312
+ /** Checkpoints live agents, tombstones, and only pending delivery-ledger items for one root. */
268
313
  export interface RegistrySnapshot {
269
314
  agents: PersistedAgent[];
270
315
  tombstones: string[];
271
316
  deliveries: PersistedDelivery[];
317
+ coordination_deliveries?: PersistedCoordinationDelivery[];
318
+ wait_claimed_turns?: string[];
319
+ next_delivery_sequence?: number;
272
320
  }
273
321
 
274
322
  /** Clones a registry snapshot while recording the source root session file. */
275
323
  export interface ForkSnapshot extends RegistrySnapshot {
324
+ /** Canonical Pi root session file from which the selected fork branch originated. */
276
325
  source_root_session_file: string;
326
+ /** Root identity that every cloned Child Session provenance record must match. */
327
+ source_root_session_id: string;
277
328
  }
278
329
 
279
330
  /** Appends root-owned registry events to the active root conversation branch. */
280
331
  export interface RegistryWriter {
281
332
  readonly rootSessionId: string;
282
- append(event: import("./minimal-subagents-registry.js").RegistryEventV1): void;
333
+ append(event: import("./minimal-subagents-registry.js").RegistryEventV2): void;
283
334
  }
284
335
 
285
336
  /** 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 {