@ian-pascoe/pi-minimal-subagents 0.6.4 → 0.6.6
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/package.json +1 -1
- package/src/minimal-subagents-config.ts +18 -14
- package/src/minimal-subagents-extension.ts +32 -3
- package/src/minimal-subagents-fork-lifecycle.ts +1 -1
- package/src/minimal-subagents-registry-wire.ts +0 -4
- package/src/minimal-subagents-registry.ts +2 -6
- package/src/minimal-subagents-render-contract.ts +5 -9
- package/src/minimal-subagents-settings-writer.ts +9 -8
package/package.json
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
1
|
import type { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import { type Static, Type } from "typebox";
|
|
4
3
|
import { Value } from "typebox/value";
|
|
@@ -7,16 +6,18 @@ import { DEFAULT_MAX_SUBAGENT_DEPTH, THINKING_LEVELS } from "./minimal-subagents
|
|
|
7
6
|
const MODEL_ROLE_NAME_MAX_LENGTH = 64;
|
|
8
7
|
const MODEL_ROLE_HINT_MAX_LENGTH = 500;
|
|
9
8
|
|
|
10
|
-
const JsonValueSchema = Type.Unsafe<JsonValue>({});
|
|
11
9
|
const SettingsDocumentSchema = Type.Object({
|
|
12
|
-
minimalSubagents: Type.Optional(
|
|
10
|
+
minimalSubagents: Type.Optional(Type.Unknown()),
|
|
13
11
|
});
|
|
14
12
|
const MinimalSubagentsSettingsSchema = Type.Object({
|
|
15
|
-
enabled: Type.Optional(
|
|
16
|
-
maxSubagentDepth: Type.Optional(
|
|
17
|
-
modelRoles: Type.Optional(
|
|
13
|
+
enabled: Type.Optional(Type.Unknown()),
|
|
14
|
+
maxSubagentDepth: Type.Optional(Type.Unknown()),
|
|
15
|
+
modelRoles: Type.Optional(Type.Unknown()),
|
|
16
|
+
});
|
|
17
|
+
const ModelRoleObjectSchema = Type.Object({
|
|
18
|
+
model: Type.Optional(Type.Unknown()),
|
|
19
|
+
hint: Type.Optional(Type.Unknown()),
|
|
18
20
|
});
|
|
19
|
-
const JsonObjectSchema = Type.Record(Type.String(), JsonValueSchema);
|
|
20
21
|
const EnabledSettingSchema = Type.Boolean();
|
|
21
22
|
const PositiveSafeIntegerSchema = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
22
23
|
const MaxSubagentDepthSettingSchema = Type.Union([PositiveSafeIntegerSchema, Type.Null()]);
|
|
@@ -28,7 +29,7 @@ const ExpandedModelRoleSchema = Type.Object(
|
|
|
28
29
|
},
|
|
29
30
|
{ additionalProperties: false },
|
|
30
31
|
);
|
|
31
|
-
const ModelRoleEntriesSchema = Type.Record(Type.String(),
|
|
32
|
+
const ModelRoleEntriesSchema = Type.Record(Type.String(), Type.Unknown());
|
|
32
33
|
const ModelRolesSettingSchema = Type.Union([ModelRoleEntriesSchema, Type.Null()]);
|
|
33
34
|
|
|
34
35
|
type ModelRoleThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
@@ -61,7 +62,7 @@ export interface ResolvedMinimalSubagentsConfig {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
interface MinimalSubagentsSettingsDocument {
|
|
64
|
-
minimalSubagents?:
|
|
65
|
+
minimalSubagents?: unknown;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
interface MinimalSubagentsConfigInput {
|
|
@@ -102,7 +103,7 @@ type ModelRoleWireValue =
|
|
|
102
103
|
| { kind: "delete" }
|
|
103
104
|
| { kind: "shorthand"; model: string }
|
|
104
105
|
| { kind: "expanded"; fields: Static<typeof ExpandedModelRoleSchema> }
|
|
105
|
-
| { kind: "malformed-expanded"; fields:
|
|
106
|
+
| { kind: "malformed-expanded"; fields: Static<typeof ModelRoleObjectSchema> }
|
|
106
107
|
| { kind: "invalid" };
|
|
107
108
|
|
|
108
109
|
type ModelRolesWireValue =
|
|
@@ -110,15 +111,17 @@ type ModelRolesWireValue =
|
|
|
110
111
|
| { kind: "entries"; entries: ReadonlyMap<string, ModelRoleWireValue> }
|
|
111
112
|
| { kind: "invalid" };
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Authored settings remain unparsed until the depth schema below validates their value.
|
|
115
|
+
function parseMaxSubagentDepthWireValue(value: unknown): MaxSubagentDepthWireValue {
|
|
114
116
|
if (!Value.Check(MaxSubagentDepthSettingSchema, value)) return { kind: "invalid" };
|
|
115
117
|
return value === null ? { kind: "reset" } : { kind: "depth", value };
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Role entries may contain arbitrary settings data; schemas classify them before model/hint validation.
|
|
121
|
+
function parseModelRoleWireValue(value: unknown): ModelRoleWireValue {
|
|
119
122
|
if (value === null) return { kind: "delete" };
|
|
120
123
|
if (Value.Check(ShorthandModelRoleSchema, value)) return { kind: "shorthand", model: value };
|
|
121
|
-
if (Value.Check(
|
|
124
|
+
if (Value.Check(ModelRoleObjectSchema, value)) {
|
|
122
125
|
return Value.Check(ExpandedModelRoleSchema, value)
|
|
123
126
|
? { kind: "expanded", fields: value }
|
|
124
127
|
: { kind: "malformed-expanded", fields: value };
|
|
@@ -132,7 +135,8 @@ function isExpandedModelRoleWireValue(
|
|
|
132
135
|
return value.kind === "expanded" || value.kind === "malformed-expanded";
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
|
|
138
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Validate the authored role collection before parsing each untrusted entry.
|
|
139
|
+
function parseModelRolesWireValue(value: unknown): ModelRolesWireValue {
|
|
136
140
|
if (!Value.Check(ModelRolesSettingSchema, value)) return { kind: "invalid" };
|
|
137
141
|
if (value === null) return { kind: "reset" };
|
|
138
142
|
return {
|
|
@@ -65,7 +65,10 @@ import {
|
|
|
65
65
|
MinimalSubagentsStatusPanelController,
|
|
66
66
|
type MinimalSubagentsStatusAccess,
|
|
67
67
|
} from "./minimal-subagents-status-panel.js";
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
createCoordinatorToolDefinitions,
|
|
70
|
+
type CoordinatorToolOperations,
|
|
71
|
+
} from "./minimal-subagents-tools.js";
|
|
69
72
|
import {
|
|
70
73
|
renderMinimalSubagentsMessage,
|
|
71
74
|
renderMinimalSubagentsResult,
|
|
@@ -393,6 +396,15 @@ const productionLifecycleEffects: MinimalSubagentsLifecycleEffects = {
|
|
|
393
396
|
/** Own coordinator, UI, and prepared-fork state for one root Pi session lifecycle. */
|
|
394
397
|
export class MinimalSubagentsLifecycleController {
|
|
395
398
|
private coordinator: MinimalSubagentsCoordinator | undefined;
|
|
399
|
+
private readonly coordinatorOperations: CoordinatorToolOperations = {
|
|
400
|
+
spawn: (...args) => this.requireCoordinator().spawn(...args),
|
|
401
|
+
inspectStatus: (...args) => this.requireCoordinator().inspectStatus(...args),
|
|
402
|
+
sendAgentMessage: (...args) => this.requireCoordinator().sendAgentMessage(...args),
|
|
403
|
+
wait: (...args) => this.requireCoordinator().wait(...args),
|
|
404
|
+
status: (...args) => this.requireCoordinator().status(...args),
|
|
405
|
+
cancel: (...args) => this.requireCoordinator().cancel(...args),
|
|
406
|
+
delete: (...args) => this.requireCoordinator().delete(...args),
|
|
407
|
+
};
|
|
396
408
|
private uiController: MinimalSubagentsUiController | undefined;
|
|
397
409
|
private statusPanelController: MinimalSubagentsStatusPanelController | undefined;
|
|
398
410
|
private accessSession: ActiveSubagentAccessSession | undefined;
|
|
@@ -406,8 +418,18 @@ export class MinimalSubagentsLifecycleController {
|
|
|
406
418
|
private readonly effects: MinimalSubagentsLifecycleEffects,
|
|
407
419
|
) {}
|
|
408
420
|
|
|
409
|
-
/** Register renderers and the six Pi lifecycle event handlers
|
|
421
|
+
/** Register stable tools, renderers, and the six Pi lifecycle event handlers. */
|
|
410
422
|
register(): void {
|
|
423
|
+
const rootTools = createCoordinatorToolDefinitions({
|
|
424
|
+
coordinator: this.coordinatorOperations,
|
|
425
|
+
callerId: "root",
|
|
426
|
+
allowFanoutTools: true,
|
|
427
|
+
schemas: createCoordinatorToolSchemas([]),
|
|
428
|
+
captureCaller: (context) => rootCallerSnapshot(this.pi, context),
|
|
429
|
+
onActivity: () => this.uiController?.refresh(),
|
|
430
|
+
onAttention: (message) => this.accessSession?.context.ui.notify(message, "error"),
|
|
431
|
+
});
|
|
432
|
+
for (const tool of rootTools) this.pi.registerTool(tool);
|
|
411
433
|
this.pi.registerMessageRenderer("minimal-subagents.message", renderMinimalSubagentsMessage);
|
|
412
434
|
this.pi.registerMessageRenderer("minimal-subagents.result", renderMinimalSubagentsResult);
|
|
413
435
|
this.pi.registerCommand("subagents", {
|
|
@@ -573,7 +595,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
573
595
|
this.uiController.refresh();
|
|
574
596
|
|
|
575
597
|
const rootTools = createCoordinatorToolDefinitions({
|
|
576
|
-
coordinator:
|
|
598
|
+
coordinator: this.coordinatorOperations,
|
|
577
599
|
callerId: "root",
|
|
578
600
|
allowFanoutTools: true,
|
|
579
601
|
modelRoles: minimalSubagentsConfig.modelRoles,
|
|
@@ -605,6 +627,13 @@ export class MinimalSubagentsLifecycleController {
|
|
|
605
627
|
}
|
|
606
628
|
}
|
|
607
629
|
|
|
630
|
+
private requireCoordinator(): MinimalSubagentsCoordinator {
|
|
631
|
+
if (!this.coordinator) {
|
|
632
|
+
throw new Error("Minimal subagents lifecycle: coordinator is not initialized");
|
|
633
|
+
}
|
|
634
|
+
return this.coordinator;
|
|
635
|
+
}
|
|
636
|
+
|
|
608
637
|
private async prepareSessionFork(
|
|
609
638
|
event: SessionBeforeForkEvent,
|
|
610
639
|
context: ExtensionContext,
|
|
@@ -2,7 +2,7 @@ import type { ForkSnapshot } from "./minimal-subagents-types.js";
|
|
|
2
2
|
import { canonicalPath } from "./minimal-subagents-paths.js";
|
|
3
3
|
|
|
4
4
|
declare global {
|
|
5
|
-
//
|
|
5
|
+
// A process-global handoff must be visible to replacement extension instances.
|
|
6
6
|
var minimalSubagentsForkSnapshots: Map<string, ForkSnapshot> | undefined;
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
1
|
import { Type, type Static } from "typebox";
|
|
3
2
|
|
|
4
|
-
/** Establishes Pi's recursive JSON owner type before Registry envelope parsing. */
|
|
5
|
-
export const RegistryJsonValueWireSchema = Type.Unsafe<JsonValue>({});
|
|
6
|
-
|
|
7
3
|
const NonnegativeNumberSchema = Type.Number({ minimum: 0 });
|
|
8
4
|
const NonEmptyStringSchema = Type.String({ minLength: 1 });
|
|
9
5
|
const TurnStatusSchema = Type.Union([
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
RegistryDeliveryTurnEventWireSchema,
|
|
29
29
|
RegistryEnvelopeWireSchema,
|
|
30
30
|
RegistryEventDiscriminantWireSchema,
|
|
31
|
-
RegistryJsonValueWireSchema,
|
|
32
31
|
RegistryLooseEnvelopeWireSchema,
|
|
33
32
|
RegistryMessageRecordedEventWireSchema,
|
|
34
33
|
RegistryRootProbeWireSchema,
|
|
@@ -846,7 +845,8 @@ function validParsedEvent(event: RegistryEventV2): ParsedRegistryEvent {
|
|
|
846
845
|
type RegistryParseInput = JsonValue | RegistryEventV2;
|
|
847
846
|
|
|
848
847
|
function parseRegistryEventRecord(
|
|
849
|
-
|
|
848
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Registry entry data is unparsed; ownership is checked first, then envelope and event schemas validate every consumed field.
|
|
849
|
+
value: unknown,
|
|
850
850
|
rootSessionId: string,
|
|
851
851
|
): ParsedRegistryEvent {
|
|
852
852
|
if (Value.Check(RegistryRootProbeWireSchema, value) && value.root_session_id !== rootSessionId) {
|
|
@@ -1191,10 +1191,6 @@ export function replayRegistryEntries(
|
|
|
1191
1191
|
const diagnostics: RegistryReplayDiagnostic[] = [];
|
|
1192
1192
|
entries.forEach((entry, entryIndex) => {
|
|
1193
1193
|
if (entry.type !== "custom" || entry.customType !== REGISTRY_ENTRY_TYPE) return;
|
|
1194
|
-
if (!Value.Check(RegistryJsonValueWireSchema, entry.data)) {
|
|
1195
|
-
reportDiagnostic(diagnostics, entryIndex, "invalid-envelope", "record must be JSON");
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
1194
|
const parsed = parseRegistryEventRecord(entry.data, rootSessionId);
|
|
1199
1195
|
if (parsed.kind === "foreign-root") return;
|
|
1200
1196
|
if (parsed.kind === "event") {
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentToolResult,
|
|
3
|
-
MessageRenderer,
|
|
4
|
-
ToolDefinition,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
2
|
import { Type, type Static } from "typebox";
|
|
7
3
|
import { Value } from "typebox/value";
|
|
8
4
|
import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
|
|
@@ -339,8 +335,6 @@ export type RenderStatusAgent = Static<typeof RenderStatusAgentSchema>;
|
|
|
339
335
|
/** Raw tool arguments supplied by Pi's tool-rendering interface. */
|
|
340
336
|
export type CoordinatorToolCallInput = Parameters<NonNullable<ToolDefinition["renderCall"]>>[0];
|
|
341
337
|
|
|
342
|
-
type CoordinatorMessageInput = Parameters<MessageRenderer>[0];
|
|
343
|
-
|
|
344
338
|
/** Parsed tool-call arguments tagged by their coordinator tool name. */
|
|
345
339
|
export type ParsedCoordinatorToolCall =
|
|
346
340
|
| { toolName: "subagent"; args: SpawnCallArguments }
|
|
@@ -383,7 +377,8 @@ export function parseCoordinatorToolCall(
|
|
|
383
377
|
/** Parse one historical tool result exactly once at the transcript rendering boundary. */
|
|
384
378
|
export function parseCoordinatorToolResult(
|
|
385
379
|
toolName: CoordinatorToolName,
|
|
386
|
-
|
|
380
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Historical tool details are unparsed until the selected per-tool schema checks them below.
|
|
381
|
+
details: unknown,
|
|
387
382
|
): ParsedCoordinatorToolResult | undefined {
|
|
388
383
|
switch (toolName) {
|
|
389
384
|
case "subagent":
|
|
@@ -417,7 +412,8 @@ export type CoordinatorMessageRenderDetails = Static<typeof CoordinatorMessageRe
|
|
|
417
412
|
|
|
418
413
|
/** Parse optional custom-message details while tolerating legacy field names. */
|
|
419
414
|
export function parseCoordinatorMessageDetails(
|
|
420
|
-
|
|
415
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Historical custom-message metadata is validated by its owning schema below.
|
|
416
|
+
details: unknown,
|
|
421
417
|
): CoordinatorMessageRenderDetails | undefined {
|
|
422
418
|
return Value.Check(CoordinatorMessageRenderDetailsSchema, details) ? details : undefined;
|
|
423
419
|
}
|
|
@@ -4,8 +4,6 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import lockfile from "proper-lockfile";
|
|
7
|
-
import { Type } from "typebox";
|
|
8
|
-
import { Value } from "typebox/value";
|
|
9
7
|
|
|
10
8
|
/** Identifies the standard Pi settings file changed by a Subagent Access command. */
|
|
11
9
|
export type MinimalSubagentsSettingsScope = "global" | "project";
|
|
@@ -64,8 +62,10 @@ interface ExistingSettingsDocument {
|
|
|
64
62
|
readonly mode: number;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
function isSettingsJsonObject(value: JsonValue | undefined): value is SettingsJsonObject {
|
|
66
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- JSON.parse already established JSON data; distinguish object roots and settings blocks from primitives and arrays.
|
|
67
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
68
|
+
}
|
|
69
69
|
const SETTINGS_LOCK_RETRY_DELAY_MS = 20;
|
|
70
70
|
const SETTINGS_LOCK_RETRIES = 100;
|
|
71
71
|
const NEW_SETTINGS_FILE_MODE = 0o600;
|
|
@@ -92,7 +92,8 @@ function parseSettingsDocument(
|
|
|
92
92
|
scope: MinimalSubagentsSettingsScope,
|
|
93
93
|
path: string,
|
|
94
94
|
): ParsedSettingsDocument {
|
|
95
|
-
|
|
95
|
+
// JSON.parse is the provenance for this JSON type; the object shape is checked below.
|
|
96
|
+
let parsed: JsonValue;
|
|
96
97
|
try {
|
|
97
98
|
parsed = JSON.parse(stripUtf8Bom(content));
|
|
98
99
|
} catch (cause) {
|
|
@@ -109,11 +110,11 @@ function parseSettingsDocument(
|
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
if (!
|
|
113
|
+
if (!isSettingsJsonObject(parsed)) {
|
|
113
114
|
return { ok: false, error: settingsContractError(scope, path, "expected an object root") };
|
|
114
115
|
}
|
|
115
116
|
const minimalSubagents = parsed.minimalSubagents;
|
|
116
|
-
if (minimalSubagents !== undefined && !
|
|
117
|
+
if (minimalSubagents !== undefined && !isSettingsJsonObject(minimalSubagents)) {
|
|
117
118
|
return {
|
|
118
119
|
ok: false,
|
|
119
120
|
error: settingsContractError(
|
|
@@ -131,7 +132,7 @@ function mutateMinimalSubagentsEnabled(
|
|
|
131
132
|
enabled: boolean | undefined,
|
|
132
133
|
): void {
|
|
133
134
|
const currentMinimalSubagents = settings.minimalSubagents;
|
|
134
|
-
const minimalSubagents =
|
|
135
|
+
const minimalSubagents = isSettingsJsonObject(currentMinimalSubagents)
|
|
135
136
|
? currentMinimalSubagents
|
|
136
137
|
: {};
|
|
137
138
|
|