@ian-pascoe/pi-minimal-subagents 0.12.0 → 0.14.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.
- package/README.md +115 -26
- package/package.json +2 -2
- package/src/minimal-subagents-capabilities.ts +12 -1
- package/src/minimal-subagents-config.ts +11 -6
- package/src/minimal-subagents-context.ts +180 -4
- package/src/minimal-subagents-coordinator.ts +303 -32
- package/src/minimal-subagents-delivery-ledger.ts +14 -13
- package/src/minimal-subagents-extension.ts +62 -2
- package/src/minimal-subagents-registry-wire.ts +1 -0
- package/src/minimal-subagents-registry.ts +6 -1
- package/src/minimal-subagents-render-contract.ts +33 -2
- package/src/minimal-subagents-rendering.ts +321 -52
- package/src/minimal-subagents-sessions.ts +3 -1
- package/src/minimal-subagents-status-panel.ts +26 -203
- package/src/minimal-subagents-tool-schemas.ts +20 -3
- package/src/minimal-subagents-tools.ts +109 -35
- package/src/minimal-subagents-transcript.ts +296 -0
- package/src/minimal-subagents-types.ts +28 -5
- package/src/minimal-subagents-ui.ts +1 -1
- package/src/minimal-subagents-usage.ts +13 -2
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type SessionShutdownEvent,
|
|
18
18
|
type SessionStartEvent,
|
|
19
19
|
type SessionTreeEvent,
|
|
20
|
+
type TurnEndEvent,
|
|
20
21
|
} from "@earendil-works/pi-coding-agent";
|
|
21
22
|
import { isKeyRelease, isKeyRepeat, matchesKey } from "@earendil-works/pi-tui";
|
|
22
23
|
import {
|
|
@@ -182,9 +183,12 @@ function rootCallerSnapshot(pi: ExtensionAPI, context: ExtensionContext): Caller
|
|
|
182
183
|
function createRootConversationEndpoint(
|
|
183
184
|
pi: ExtensionAPI,
|
|
184
185
|
context: ExtensionContext,
|
|
186
|
+
onSteer: () => void,
|
|
185
187
|
): RootConversationEndpoint {
|
|
186
188
|
return {
|
|
187
189
|
async queueCoordinatorMessage(message) {
|
|
190
|
+
// A running root takes the message as a clearable steer; an idle root starts a turn with it.
|
|
191
|
+
if (!context.isIdle()) onSteer();
|
|
188
192
|
pi.sendMessage(
|
|
189
193
|
{
|
|
190
194
|
customType: message.customType,
|
|
@@ -505,6 +509,9 @@ export class MinimalSubagentsLifecycleController {
|
|
|
505
509
|
private readonly coordinatorOperations: CoordinatorToolOperations = {
|
|
506
510
|
spawn: (...args) => this.requireCoordinator().spawn(...args),
|
|
507
511
|
inspectStatus: (...args) => this.requireCoordinator().inspectStatus(...args),
|
|
512
|
+
previewActiveTurn: (...args) => this.requireCoordinator().previewActiveTurn(...args),
|
|
513
|
+
inspectActiveTurnTranscript: (...args) =>
|
|
514
|
+
this.requireCoordinator().inspectActiveTurnTranscript(...args),
|
|
508
515
|
sendAgentMessage: (...args) => this.requireCoordinator().sendAgentMessage(...args),
|
|
509
516
|
wait: (...args) => this.requireCoordinator().wait(...args),
|
|
510
517
|
status: (...args) => this.requireCoordinator().status(...args),
|
|
@@ -514,6 +521,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
514
521
|
private uiController: MinimalSubagentsUiController | undefined;
|
|
515
522
|
private statusPanelController: MinimalSubagentsStatusPanelController | undefined;
|
|
516
523
|
private viewerShortcut: ReturnType<typeof installViewerShortcut> | undefined;
|
|
524
|
+
private unwatchRootTurnEnds: (() => void) | undefined;
|
|
517
525
|
private accessSession: ActiveSubagentAccessSession | undefined;
|
|
518
526
|
private preparedFork:
|
|
519
527
|
| { sourceSessionFile: string; selectedBranchSnapshot: RegistrySnapshot }
|
|
@@ -525,7 +533,10 @@ export class MinimalSubagentsLifecycleController {
|
|
|
525
533
|
private readonly effects: MinimalSubagentsLifecycleEffects,
|
|
526
534
|
) {}
|
|
527
535
|
|
|
528
|
-
/**
|
|
536
|
+
/**
|
|
537
|
+
* Register stable tools, renderers, and the six static Pi lifecycle event handlers. `turn_end` is
|
|
538
|
+
* registered only while a result waits in the root's steer queue (`watchRootTurnEnds`).
|
|
539
|
+
*/
|
|
529
540
|
register(): void {
|
|
530
541
|
const rootTools = createCoordinatorToolDefinitions({
|
|
531
542
|
coordinator: this.coordinatorOperations,
|
|
@@ -549,6 +560,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
549
560
|
this.pi.on("session_before_fork", (event, context) => this.prepareSessionFork(event, context));
|
|
550
561
|
this.pi.on("session_tree", (event, context) => this.restoreSessionTree(event, context));
|
|
551
562
|
this.pi.on("message_end", (event, context) => this.reconcileMessageDelivery(event, context));
|
|
563
|
+
this.pi.on("agent_start", () => this.deliverDiscardedRootResults());
|
|
552
564
|
this.pi.on("session_shutdown", (event, context) => this.shutdownSession(event, context));
|
|
553
565
|
}
|
|
554
566
|
|
|
@@ -642,9 +654,10 @@ export class MinimalSubagentsLifecycleController {
|
|
|
642
654
|
});
|
|
643
655
|
activeCoordinator = new MinimalSubagentsCoordinator({
|
|
644
656
|
sessions: sessionFactory,
|
|
645
|
-
root: createRootConversationEndpoint(this.pi, context),
|
|
657
|
+
root: createRootConversationEndpoint(this.pi, context, () => this.watchRootTurnEnds()),
|
|
646
658
|
maxSubagentDepth: minimalSubagentsConfig.maxSubagentDepth,
|
|
647
659
|
toolsets: minimalSubagentsConfig.toolsets,
|
|
660
|
+
modelRoles: minimalSubagentsConfig.modelRoles,
|
|
648
661
|
registry: {
|
|
649
662
|
rootSessionId,
|
|
650
663
|
append: (registryEvent) => this.pi.appendEntry(REGISTRY_ENTRY_TYPE, registryEvent),
|
|
@@ -922,16 +935,63 @@ export class MinimalSubagentsLifecycleController {
|
|
|
922
935
|
_context: ExtensionContext,
|
|
923
936
|
): Promise<void> {
|
|
924
937
|
if (!this.coordinator) return;
|
|
938
|
+
this.coordinator.recordRootMessageEnd();
|
|
925
939
|
if (event.message.role === "toolResult" || event.message.role === "custom") {
|
|
926
940
|
await this.coordinator.reconcileDeliveries();
|
|
927
941
|
this.uiController?.refresh();
|
|
928
942
|
}
|
|
929
943
|
}
|
|
930
944
|
|
|
945
|
+
/**
|
|
946
|
+
* Watch root turn boundaries while a result waits in the root's steer queue. Pi builds a boundary
|
|
947
|
+
* preview for every `turn_end` while any handler listens, so the handler is removed again once no
|
|
948
|
+
* queued result remains.
|
|
949
|
+
*/
|
|
950
|
+
private watchRootTurnEnds(): void {
|
|
951
|
+
this.unwatchRootTurnEnds ??= this.pi.on("turn_end", (event, context) =>
|
|
952
|
+
this.requeueDiscardedRootResults(event, context),
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private stopWatchingRootTurnEnds(): void {
|
|
957
|
+
this.unwatchRootTurnEnds?.();
|
|
958
|
+
this.unwatchRootTurnEnds = undefined;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Pi gives extensions no queue-cleared event, so Esc's discarded steers are found here: the turn
|
|
963
|
+
* boundary's pending-message preview is empty. Returns nothing, so the transcript, system prompt,
|
|
964
|
+
* and tools are unchanged.
|
|
965
|
+
*
|
|
966
|
+
* A queue clear without an abort (dequeue, RPC `clear_queue`) leaves the run going, so released
|
|
967
|
+
* results are steered back at once and Pi's poll after this boundary takes them in the same run.
|
|
968
|
+
* Esc aborts the run, so its released results wait for the root's next run.
|
|
969
|
+
*/
|
|
970
|
+
private requeueDiscardedRootResults(event: TurnEndEvent, context: ExtensionContext): void {
|
|
971
|
+
if (!this.coordinator) {
|
|
972
|
+
this.stopWatchingRootTurnEnds();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (event.context.pendingMessages.length === 0) {
|
|
976
|
+
this.coordinator.requeueDiscardedRootResults();
|
|
977
|
+
if (context.signal && !context.signal.aborted) {
|
|
978
|
+
this.coordinator.deliverDiscardedRootResults();
|
|
979
|
+
}
|
|
980
|
+
this.uiController?.refresh();
|
|
981
|
+
}
|
|
982
|
+
if (!this.coordinator.hasQueuedRootHandOffs()) this.stopWatchingRootTurnEnds();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** Re-send discarded results into a root run that just started; recovery never starts one. */
|
|
986
|
+
private deliverDiscardedRootResults(): void {
|
|
987
|
+
this.coordinator?.deliverDiscardedRootResults();
|
|
988
|
+
}
|
|
989
|
+
|
|
931
990
|
private async shutdownSession(
|
|
932
991
|
event: SessionShutdownEvent,
|
|
933
992
|
context: ExtensionContext,
|
|
934
993
|
): Promise<void> {
|
|
994
|
+
this.stopWatchingRootTurnEnds();
|
|
935
995
|
this.viewerShortcut?.dispose();
|
|
936
996
|
this.viewerShortcut = undefined;
|
|
937
997
|
this.statusPanelController?.dispose();
|
|
@@ -56,6 +56,7 @@ const ToolSelectionWireSchema = Type.Union([
|
|
|
56
56
|
|
|
57
57
|
/** Parses persisted immutable Launch Contracts without coercion. */
|
|
58
58
|
export const RegistryLaunchContractWireSchema = Type.Object({
|
|
59
|
+
role: Type.Optional(NonEmptyStringSchema),
|
|
59
60
|
session_context: Type.Union([
|
|
60
61
|
Type.Literal("inherit"),
|
|
61
62
|
Type.Literal("compact"),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
|
|
4
|
+
import { isModelRoleName } from "./minimal-subagents-config.js";
|
|
5
|
+
import { normalizeMinimalSubagentsUsage } from "./minimal-subagents-usage.js";
|
|
4
6
|
import {
|
|
5
7
|
createDeliveryLedger,
|
|
6
8
|
deliveryLedgerSnapshot,
|
|
@@ -280,7 +282,8 @@ function cloneRegistryTurnResult(value: RegistryTurnResultWire): TurnResult {
|
|
|
280
282
|
output: value.output,
|
|
281
283
|
};
|
|
282
284
|
if (value.error !== undefined) result.error = value.error;
|
|
283
|
-
|
|
285
|
+
// Records from older versions may lack optional usage counters.
|
|
286
|
+
if (value.usage !== undefined) result.usage = normalizeMinimalSubagentsUsage(value.usage);
|
|
284
287
|
if (value.elapsed_ms !== undefined) result.elapsed_ms = value.elapsed_ms;
|
|
285
288
|
return result;
|
|
286
289
|
}
|
|
@@ -302,6 +305,7 @@ function parseRegistryLaunchContract(
|
|
|
302
305
|
): LaunchContract | undefined {
|
|
303
306
|
if (
|
|
304
307
|
!isCanonicalModelId(value.model) ||
|
|
308
|
+
(value.role !== undefined && !isModelRoleName(value.role)) ||
|
|
305
309
|
!isUniqueNonEmptyStringArray(value.ordinary_tools) ||
|
|
306
310
|
!excludesCoordinatorToolNames(value.ordinary_tools) ||
|
|
307
311
|
(Array.isArray(value.tools) &&
|
|
@@ -318,6 +322,7 @@ function parseRegistryLaunchContract(
|
|
|
318
322
|
ordinary_tools: [...value.ordinary_tools],
|
|
319
323
|
};
|
|
320
324
|
if (value.delegation !== undefined) launchContract.delegation = value.delegation;
|
|
325
|
+
if (value.role !== undefined) launchContract.role = value.role;
|
|
321
326
|
return launchContract;
|
|
322
327
|
}
|
|
323
328
|
|
|
@@ -23,6 +23,7 @@ const RenderUsageSchema = Type.Object({
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const RenderLaunchContractSchema = Type.Object({
|
|
26
|
+
role: Type.Optional(Type.String()),
|
|
26
27
|
session_context: Type.Optional(Type.String()),
|
|
27
28
|
project_context: Type.Optional(Type.String()),
|
|
28
29
|
model: Type.Optional(Type.String()),
|
|
@@ -87,9 +88,11 @@ const SpawnCallArgumentsSchema = Type.Object({
|
|
|
87
88
|
agent_id: Type.Optional(Type.String()),
|
|
88
89
|
session_context: Type.Optional(Type.String()),
|
|
89
90
|
project_context: Type.Optional(Type.String()),
|
|
91
|
+
role: Type.Optional(Type.String()),
|
|
90
92
|
model: Type.Optional(Type.String()),
|
|
91
93
|
thinking_level: Type.Optional(Type.String()),
|
|
92
94
|
delegation: Type.Optional(Type.String()),
|
|
95
|
+
tools: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
|
|
93
96
|
});
|
|
94
97
|
const MessageCallArgumentsSchema = Type.Object({
|
|
95
98
|
agent_id: Type.Optional(Type.String()),
|
|
@@ -113,6 +116,7 @@ const CurrentMessageDetailsSchema = Type.Object({
|
|
|
113
116
|
agent_id: Type.String(),
|
|
114
117
|
message_id: Type.Optional(Type.String()),
|
|
115
118
|
disposition: Type.String(),
|
|
119
|
+
turn_id: Type.Optional(Type.String()),
|
|
116
120
|
behavior: Type.Optional(Type.String()),
|
|
117
121
|
error: Type.Optional(Type.String()),
|
|
118
122
|
});
|
|
@@ -209,7 +213,7 @@ const CoordinatorAgentSummaryOutputSchema = Type.Object({
|
|
|
209
213
|
latest_activity_at: Type.Optional(Type.String()),
|
|
210
214
|
task: Type.Optional(Type.String()),
|
|
211
215
|
child_count: Type.Number(),
|
|
212
|
-
children: Type.Array(Type.Unknown()),
|
|
216
|
+
children: Type.Optional(Type.Array(Type.Unknown())),
|
|
213
217
|
model: Type.String(),
|
|
214
218
|
thinking_level: Type.String(),
|
|
215
219
|
});
|
|
@@ -217,6 +221,7 @@ const CoordinatorAgentDetailOutputSchema = Type.Object({
|
|
|
217
221
|
...CoordinatorAgentSummaryOutputSchema.properties,
|
|
218
222
|
session_file: Type.Optional(Type.String()),
|
|
219
223
|
launch_contract: Type.Object({
|
|
224
|
+
role: Type.Optional(Type.String()),
|
|
220
225
|
session_context: Type.String(),
|
|
221
226
|
project_context: Type.String(),
|
|
222
227
|
model: Type.String(),
|
|
@@ -266,7 +271,11 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
266
271
|
agent_id: Type.String(),
|
|
267
272
|
turn_id: Type.String(),
|
|
268
273
|
status: Type.Literal("running"),
|
|
269
|
-
|
|
274
|
+
model: Type.String(),
|
|
275
|
+
thinking_level: Type.String(),
|
|
276
|
+
tools: Type.Array(Type.String()),
|
|
277
|
+
delegation: Type.Union([Type.Literal("none"), Type.Literal("fanout")]),
|
|
278
|
+
warnings: Type.Optional(Type.Array(Type.String())),
|
|
270
279
|
}),
|
|
271
280
|
agent_message: Type.Object({
|
|
272
281
|
agent_id: Type.String(),
|
|
@@ -274,8 +283,10 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
274
283
|
disposition: Type.Union([
|
|
275
284
|
Type.Literal("delivered-via-wait"),
|
|
276
285
|
Type.Literal("queued"),
|
|
286
|
+
Type.Literal("started-turn"),
|
|
277
287
|
Type.Literal("failed"),
|
|
278
288
|
]),
|
|
289
|
+
turn_id: Type.Optional(Type.String()),
|
|
279
290
|
error: Type.Optional(Type.String()),
|
|
280
291
|
}),
|
|
281
292
|
subagent_wait: Type.Union([
|
|
@@ -319,6 +330,25 @@ const WaitRenderDetailsSchema = Type.Union([
|
|
|
319
330
|
WaitTimeoutDetailsSchema,
|
|
320
331
|
]);
|
|
321
332
|
|
|
333
|
+
/** Partial `subagent_wait` updates: elapsed wait time and, while the child runs, its progress. */
|
|
334
|
+
const WaitProgressRenderDetailsSchema = Type.Object({
|
|
335
|
+
agent_id: Type.String(),
|
|
336
|
+
status: Type.Literal("waiting"),
|
|
337
|
+
elapsed_ms: Type.Number(),
|
|
338
|
+
turn_id: Type.Optional(Type.String()),
|
|
339
|
+
tool_calls: Type.Optional(Type.Number()),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
export type WaitProgressRenderDetails = Static<typeof WaitProgressRenderDetailsSchema>;
|
|
343
|
+
|
|
344
|
+
/** Parse one partial `subagent_wait` progress update. */
|
|
345
|
+
export function parseWaitProgressDetails(
|
|
346
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Partial tool details are unparsed until the progress schema checks them below.
|
|
347
|
+
details: unknown,
|
|
348
|
+
): WaitProgressRenderDetails | undefined {
|
|
349
|
+
return Value.Check(WaitProgressRenderDetailsSchema, details) ? details : undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
322
352
|
export type SpawnCallArguments = Static<typeof SpawnCallArgumentsSchema>;
|
|
323
353
|
export type MessageCallArguments = Static<typeof MessageCallArgumentsSchema>;
|
|
324
354
|
export type WaitCallArguments = Static<typeof WaitCallArgumentsSchema>;
|
|
@@ -331,6 +361,7 @@ export type StatusRenderDetails = Static<typeof StatusDetailsSchema>;
|
|
|
331
361
|
export type CancelRenderDetails = Static<typeof CancelDetailsSchema>;
|
|
332
362
|
export type DeleteRenderDetails = Static<typeof DeleteDetailsSchema>;
|
|
333
363
|
export type RenderStatusAgent = Static<typeof RenderStatusAgentSchema>;
|
|
364
|
+
export type RenderRecentActivity = Static<typeof RenderRecentActivitySchema>;
|
|
334
365
|
|
|
335
366
|
/** Raw tool arguments supplied by Pi's tool-rendering interface. */
|
|
336
367
|
export type CoordinatorToolCallInput = Parameters<NonNullable<ToolDefinition["renderCall"]>>[0];
|