@ian-pascoe/pi-minimal-subagents 0.1.0 → 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.
- package/README.md +77 -9
- package/package.json +6 -6
- package/src/minimal-subagents-capabilities.ts +8 -8
- package/src/minimal-subagents-config.ts +222 -73
- package/src/minimal-subagents-context.ts +7 -1
- package/src/minimal-subagents-coordinator.ts +943 -116
- package/src/minimal-subagents-delivery-ledger.ts +529 -0
- package/src/minimal-subagents-extension.ts +382 -64
- package/src/minimal-subagents-fork-lifecycle.ts +22 -12
- package/src/minimal-subagents-message-envelope.ts +20 -0
- package/src/minimal-subagents-registry-wire.ts +269 -0
- package/src/minimal-subagents-registry.ts +1505 -132
- package/src/minimal-subagents-render-contract.ts +301 -0
- package/src/minimal-subagents-rendering.ts +513 -372
- package/src/minimal-subagents-session-wire.ts +42 -0
- package/src/minimal-subagents-sessions.ts +437 -101
- package/src/minimal-subagents-shutdown.ts +1 -1
- package/src/minimal-subagents-tool-schemas.ts +21 -7
- package/src/minimal-subagents-tools.ts +75 -25
- package/src/minimal-subagents-types.ts +76 -21
- package/src/minimal-subagents-ui.ts +214 -23
package/README.md
CHANGED
|
@@ -45,15 +45,15 @@ Project values override global values. Run `/reload` after editing either file.
|
|
|
45
45
|
`minimalSubagents.modelRoles` gives the parent agent advisory names for
|
|
46
46
|
eligible models. The extension defines no roles itself, performs no task
|
|
47
47
|
classification, and does not route launches. The parent still passes the
|
|
48
|
-
ordinary `model`
|
|
48
|
+
ordinary `model` and `thinking_level` arguments separately.
|
|
49
49
|
|
|
50
50
|
```json
|
|
51
51
|
{
|
|
52
52
|
"minimalSubagents": {
|
|
53
53
|
"modelRoles": {
|
|
54
|
-
"budget": "opencode-go/glm-5.2",
|
|
54
|
+
"budget": "opencode-go/glm-5.2:low",
|
|
55
55
|
"design": {
|
|
56
|
-
"model": "opencode-go/kimi-k3",
|
|
56
|
+
"model": "opencode-go/kimi-k3:high",
|
|
57
57
|
"hint": "UI design, visual critique, and frontend polish"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -61,10 +61,18 @@ ordinary `model` argument and chooses `thinking_level` independently.
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
A recognized final suffix (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`,
|
|
65
|
+
or `max`) is a preferred `thinking_level`, not part of the canonical model
|
|
66
|
+
passed to `subagent`. Unsuffixed roles leave thinking selection independent.
|
|
64
67
|
Role names and hints are trimmed, single-line text. Names may be up to 64
|
|
65
68
|
characters and hints up to 500 characters. Models use canonical
|
|
66
69
|
`provider/model` IDs and must be available under the effective `enabledModels`
|
|
67
|
-
scope.
|
|
70
|
+
scope. The resolver matches the complete authored model ID first, so real
|
|
71
|
+
colon-bearing IDs—including IDs ending in `:high`—remain exact model IDs;
|
|
72
|
+
only an otherwise-unmatched recognized final suffix is treated as a thinking
|
|
73
|
+
preference. A thinking level pinned in `enabledModels` neither supplies nor
|
|
74
|
+
constrains a role preference, and normal spawn-time model-capability clamping
|
|
75
|
+
still applies.
|
|
68
76
|
|
|
69
77
|
Global and project roles merge by name in settings order. Expanded role
|
|
70
78
|
objects merge by field; a project string replaces the whole global entry. A
|
|
@@ -122,8 +130,68 @@ children: `subagent`, `agent_message`, `subagent_wait`, `subagent_status`,
|
|
|
122
130
|
only the three adjacent-coordination tools: `agent_message`, `subagent_wait`,
|
|
123
131
|
and `subagent_status`.
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
The `subagent` `tools` argument distinguishes capability presets from exact
|
|
134
|
+
lists: `"read"` grants `read`, `grep`, `find`, and `ls`; `"modify"` adds
|
|
135
|
+
`bash`, `edit`, and `write`; an array such as `["read"]` grants exactly the
|
|
136
|
+
named ordinary tool and does not expand a preset. Coordinator tools are
|
|
137
|
+
injected separately according to delegation and must not appear in `tools`;
|
|
138
|
+
misuse returns an actionable error. Use the string preset when a child needs
|
|
139
|
+
the complete read-only discovery bundle.
|
|
140
|
+
|
|
141
|
+
`agent_message` reports whether a message was delivered through an active
|
|
142
|
+
parent wait, queued for the recipient, or failed. `subagent_wait` can return an
|
|
143
|
+
intermediate Wait Event containing a Coordination Message before the child turn
|
|
144
|
+
settles; call it again for the terminal turn result. Pass optional `turn_id` to
|
|
145
|
+
address an older retained turn exactly. Without it, waits select the oldest
|
|
146
|
+
observable claimed or pending turn before the active/latest turn. A caller may
|
|
147
|
+
have only one outstanding wait for the same source turn; a concurrent duplicate
|
|
148
|
+
is rejected instead of competing for one Wait Event.
|
|
149
|
+
|
|
150
|
+
The persisted Delivery Ledger records Coordination Messages, terminal results,
|
|
151
|
+
globally increasing sequence, and wait ownership before delivery. Existing
|
|
152
|
+
items retain their sequence; gaps from skipped malformed records are valid.
|
|
153
|
+
Claims can name only active, latest, or retained turns. Once a wait returns an
|
|
154
|
+
intermediate message, that wait path owns the rest of the source turn across
|
|
155
|
+
reloads, forks, and newer turns. Automatic fallback retains its ordered queue
|
|
156
|
+
reservation, treats idle notifications as advisory, and rechecks actual
|
|
157
|
+
recipient idleness before injecting a message. Destination-session Delivery
|
|
158
|
+
Evidence settles and compacts ledger items, preventing duplicate delivery and
|
|
159
|
+
unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
|
|
160
|
+
most 20 pending wait-only terminal results per source agent; Coordination
|
|
161
|
+
Messages are not removed by that terminal-retention limit. Delivered messages
|
|
162
|
+
include stable delivery, source-agent, and source-turn identities in persisted
|
|
163
|
+
details.
|
|
164
|
+
|
|
165
|
+
Deleting a child first verifies its session header and persistent identity,
|
|
166
|
+
then uses the optional `trash` command when available and falls back to
|
|
167
|
+
unlinking its session file. Deletion prunes pending delivery state and retained
|
|
168
|
+
recent-message projections sourced from the complete deleted subtree. Restore
|
|
169
|
+
and clone perform the same ownership check and reopen the recorded child-session
|
|
170
|
+
leaf.
|
|
171
|
+
|
|
172
|
+
Registry replay and Delivery Evidence are scoped to the Root Agent's active
|
|
173
|
+
session-tree branch. Registry writes use V2 records with complete field,
|
|
174
|
+
identity, sequence, hierarchy, adjacency, destination, ordinary-tool ceiling,
|
|
175
|
+
and coordinator-tool exclusion validation. Every available V2 agent has a
|
|
176
|
+
selected leaf; only unavailable recovery placeholders may omit it. Valid V1
|
|
177
|
+
records and checkpoints migrate during replay; invalid owned records are
|
|
178
|
+
skipped with semantic diagnostic codes rather than disabling the extension.
|
|
179
|
+
Persisted message activity carries an explicit `recorded_at` from the
|
|
180
|
+
coordinator clock.
|
|
181
|
+
|
|
182
|
+
`/tree` abandons old process-local work and restores the selected branch. Fork
|
|
183
|
+
preparation is read-only; only confirmed fork shutdown interrupts work and
|
|
184
|
+
clones the selected branch, so another extension can cancel a fork without
|
|
185
|
+
freezing coordinator tools. Each clone records a new generation-specific
|
|
186
|
+
identity/provenance pair, and the destination appends ownership for that clone's
|
|
187
|
+
current session ID rather than reusing inherited ownership. If a
|
|
188
|
+
process-local fork handoff is lost, recovery reads only the destination's
|
|
189
|
+
selected branch and proceeds only when its canonical `parentSession` proves the
|
|
190
|
+
source file; it never substitutes the source session's newer head.
|
|
191
|
+
|
|
192
|
+
## Status and TUI
|
|
193
|
+
|
|
194
|
+
Visible Child Agent rows show the canonical `provider/model:thinking` Runtime
|
|
195
|
+
Profile. Status uses the live Runtime Profile while a runtime exists and falls
|
|
196
|
+
back to the immutable Launch Contract otherwise. Live changes are observational:
|
|
197
|
+
they do not rewrite persistence or change nested spawn defaults.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -29,10 +29,6 @@
|
|
|
29
29
|
"access": "public",
|
|
30
30
|
"provenance": true
|
|
31
31
|
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
34
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
35
|
-
},
|
|
36
32
|
"peerDependencies": {
|
|
37
33
|
"@earendil-works/pi-agent-core": "*",
|
|
38
34
|
"@earendil-works/pi-ai": "*",
|
|
@@ -47,5 +43,9 @@
|
|
|
47
43
|
"extensions": [
|
|
48
44
|
"./src/index.ts"
|
|
49
45
|
]
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
49
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|
|
@@ -16,8 +16,6 @@ export const COORDINATOR_TOOL_NAMES = [
|
|
|
16
16
|
|
|
17
17
|
const READ_TOOL_BUNDLE = ["read", "grep", "find", "ls"];
|
|
18
18
|
const MODIFY_TOOL_BUNDLE = [...READ_TOOL_BUNDLE, "bash", "edit", "write"];
|
|
19
|
-
const THINKING_SUFFIX_PATTERN = /:(?:off|minimal|low|medium|high|xhigh|max)$/;
|
|
20
|
-
|
|
21
19
|
interface ModelReference {
|
|
22
20
|
provider: string;
|
|
23
21
|
id: string;
|
|
@@ -28,11 +26,6 @@ interface ScopedModelReference {
|
|
|
28
26
|
thinkingLevel?: string;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
/** Remove a recognized Pi thinking suffix without changing model IDs containing other colons. */
|
|
32
|
-
export function stripThinkingSuffix(modelPattern: string): string {
|
|
33
|
-
return modelPattern.replace(THINKING_SUFFIX_PATTERN, "");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
29
|
/** Build the authenticated runtime model enum from Pi's already-resolved model scope. */
|
|
37
30
|
export function buildEligibleModelIds(input: {
|
|
38
31
|
availableModels: readonly ModelReference[];
|
|
@@ -47,7 +40,7 @@ export function buildEligibleModelIds(input: {
|
|
|
47
40
|
const result: string[] = [];
|
|
48
41
|
|
|
49
42
|
for (const model of source) {
|
|
50
|
-
const canonicalId =
|
|
43
|
+
const canonicalId = `${model.provider}/${model.id}`;
|
|
51
44
|
if (!seen.has(canonicalId)) {
|
|
52
45
|
seen.add(canonicalId);
|
|
53
46
|
result.push(canonicalId);
|
|
@@ -80,6 +73,13 @@ export function resolveOrdinaryToolSelection(
|
|
|
80
73
|
? MODIFY_TOOL_BUNDLE
|
|
81
74
|
: selection;
|
|
82
75
|
const uniqueRequested = [...new Set(requested)];
|
|
76
|
+
const coordinatorTools = new Set<string>(COORDINATOR_TOOL_NAMES);
|
|
77
|
+
const requestedCoordinatorTools = uniqueRequested.filter((name) => coordinatorTools.has(name));
|
|
78
|
+
if (requestedCoordinatorTools.length > 0) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Minimal subagents ordinary tool selection: coordinator tools are injected separately and must not appear in tools: ${requestedCoordinatorTools.join(", ")}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
83
|
const available = new Set(context.availableTools);
|
|
84
84
|
const ceiling = new Set(context.capabilityCeiling);
|
|
85
85
|
const missing = uniqueRequested.filter((name) => !available.has(name));
|
|
@@ -1,15 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "
|
|
1
|
+
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type Static, Type } from "typebox";
|
|
4
|
+
import { Value } from "typebox/value";
|
|
5
|
+
import { DEFAULT_MAX_SUBAGENT_DEPTH, THINKING_LEVELS } from "./minimal-subagents-capabilities.js";
|
|
5
6
|
|
|
6
7
|
const MODEL_ROLE_NAME_MAX_LENGTH = 64;
|
|
7
8
|
const MODEL_ROLE_HINT_MAX_LENGTH = 500;
|
|
8
9
|
|
|
10
|
+
const JsonValueSchema = Type.Unsafe<JsonValue>({});
|
|
11
|
+
const SettingsDocumentSchema = Type.Object({
|
|
12
|
+
minimalSubagents: Type.Optional(JsonValueSchema),
|
|
13
|
+
});
|
|
14
|
+
const MinimalSubagentsSettingsSchema = Type.Object({
|
|
15
|
+
maxSubagentDepth: Type.Optional(JsonValueSchema),
|
|
16
|
+
modelRoles: Type.Optional(JsonValueSchema),
|
|
17
|
+
});
|
|
18
|
+
const JsonObjectSchema = Type.Record(Type.String(), JsonValueSchema);
|
|
19
|
+
const PositiveSafeIntegerSchema = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
20
|
+
const MaxSubagentDepthSettingSchema = Type.Union([PositiveSafeIntegerSchema, Type.Null()]);
|
|
21
|
+
const ShorthandModelRoleSchema = Type.String();
|
|
22
|
+
const ExpandedModelRoleSchema = Type.Object(
|
|
23
|
+
{
|
|
24
|
+
model: Type.String(),
|
|
25
|
+
hint: Type.Optional(Type.String()),
|
|
26
|
+
},
|
|
27
|
+
{ additionalProperties: false },
|
|
28
|
+
);
|
|
29
|
+
const ModelRoleEntriesSchema = Type.Record(Type.String(), JsonValueSchema);
|
|
30
|
+
const ModelRolesSettingSchema = Type.Union([ModelRoleEntriesSchema, Type.Null()]);
|
|
31
|
+
|
|
32
|
+
type ModelRoleThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
33
|
+
|
|
9
34
|
/** Describes one user-authored advisory model role shown to subagent callers. */
|
|
10
35
|
export interface MinimalSubagentsModelRole {
|
|
11
36
|
name: string;
|
|
12
37
|
model: string;
|
|
38
|
+
thinkingLevel?: ModelRoleThinkingLevel;
|
|
13
39
|
hint?: string;
|
|
14
40
|
}
|
|
15
41
|
|
|
@@ -20,75 +46,190 @@ export interface ResolvedMinimalSubagentsConfig {
|
|
|
20
46
|
warnings: string[];
|
|
21
47
|
}
|
|
22
48
|
|
|
49
|
+
interface MinimalSubagentsSettingsDocument {
|
|
50
|
+
minimalSubagents?: JsonValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
23
53
|
interface MinimalSubagentsConfigInput {
|
|
24
|
-
globalSettings:
|
|
25
|
-
projectSettings:
|
|
54
|
+
globalSettings: MinimalSubagentsSettingsDocument;
|
|
55
|
+
projectSettings: MinimalSubagentsSettingsDocument;
|
|
26
56
|
eligibleModelIds: readonly string[];
|
|
27
57
|
}
|
|
28
58
|
|
|
59
|
+
type PiSettingsDocument = ReturnType<SettingsManager["getGlobalSettings"]>;
|
|
60
|
+
type MinimalSubagentsSettingsDocumentInput = PiSettingsDocument | MinimalSubagentsSettingsDocument;
|
|
61
|
+
|
|
29
62
|
interface MinimalSubagentsSettingsReader {
|
|
30
|
-
getGlobalSettings():
|
|
31
|
-
getProjectSettings():
|
|
63
|
+
getGlobalSettings(): MinimalSubagentsSettingsDocumentInput;
|
|
64
|
+
getProjectSettings(): MinimalSubagentsSettingsDocumentInput;
|
|
32
65
|
}
|
|
33
66
|
|
|
34
67
|
type SettingsScope = "global" | "project";
|
|
35
68
|
|
|
36
69
|
interface ScopedSettingValue {
|
|
37
70
|
scope: SettingsScope;
|
|
38
|
-
value:
|
|
71
|
+
value: ModelRoleWireValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ParsedMinimalSubagentsSettings {
|
|
75
|
+
maxSubagentDepth?: MaxSubagentDepthWireValue;
|
|
76
|
+
modelRoles?: ModelRolesWireValue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type MaxSubagentDepthWireValue =
|
|
80
|
+
| { kind: "depth"; value: number }
|
|
81
|
+
| { kind: "reset" }
|
|
82
|
+
| { kind: "invalid" };
|
|
83
|
+
|
|
84
|
+
type ModelRoleWireValue =
|
|
85
|
+
| { kind: "delete" }
|
|
86
|
+
| { kind: "shorthand"; model: string }
|
|
87
|
+
| { kind: "expanded"; fields: Static<typeof ExpandedModelRoleSchema> }
|
|
88
|
+
| { kind: "malformed-expanded"; fields: Record<string, JsonValue> }
|
|
89
|
+
| { kind: "invalid" };
|
|
90
|
+
|
|
91
|
+
type ModelRolesWireValue =
|
|
92
|
+
| { kind: "reset" }
|
|
93
|
+
| { kind: "entries"; entries: ReadonlyMap<string, ModelRoleWireValue> }
|
|
94
|
+
| { kind: "invalid" };
|
|
95
|
+
|
|
96
|
+
function parseMaxSubagentDepthWireValue(value: JsonValue): MaxSubagentDepthWireValue {
|
|
97
|
+
if (!Value.Check(MaxSubagentDepthSettingSchema, value)) return { kind: "invalid" };
|
|
98
|
+
return value === null ? { kind: "reset" } : { kind: "depth", value };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseModelRoleWireValue(value: JsonValue): ModelRoleWireValue {
|
|
102
|
+
if (value === null) return { kind: "delete" };
|
|
103
|
+
if (Value.Check(ShorthandModelRoleSchema, value)) return { kind: "shorthand", model: value };
|
|
104
|
+
if (Value.Check(JsonObjectSchema, value)) {
|
|
105
|
+
return Value.Check(ExpandedModelRoleSchema, value)
|
|
106
|
+
? { kind: "expanded", fields: value }
|
|
107
|
+
: { kind: "malformed-expanded", fields: value };
|
|
108
|
+
}
|
|
109
|
+
return { kind: "invalid" };
|
|
39
110
|
}
|
|
40
111
|
|
|
41
|
-
function
|
|
42
|
-
|
|
112
|
+
function isExpandedModelRoleWireValue(
|
|
113
|
+
value: ModelRoleWireValue,
|
|
114
|
+
): value is Extract<ModelRoleWireValue, { kind: "expanded" | "malformed-expanded" }> {
|
|
115
|
+
return value.kind === "expanded" || value.kind === "malformed-expanded";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseModelRolesWireValue(value: JsonValue): ModelRolesWireValue {
|
|
119
|
+
if (!Value.Check(ModelRolesSettingSchema, value)) return { kind: "invalid" };
|
|
120
|
+
if (value === null) return { kind: "reset" };
|
|
121
|
+
return {
|
|
122
|
+
kind: "entries",
|
|
123
|
+
entries: new Map(
|
|
124
|
+
Object.entries(value).map(([name, roleValue]) => [name, parseModelRoleWireValue(roleValue)]),
|
|
125
|
+
),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parsePiSettingsDocument(
|
|
130
|
+
settings: MinimalSubagentsSettingsDocumentInput,
|
|
131
|
+
): MinimalSubagentsSettingsDocument {
|
|
132
|
+
if (!Value.Check(SettingsDocumentSchema, settings)) return {};
|
|
133
|
+
const parsed: MinimalSubagentsSettingsDocument = {};
|
|
134
|
+
if (settings.minimalSubagents !== undefined) {
|
|
135
|
+
parsed.minimalSubagents = settings.minimalSubagents;
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
43
138
|
}
|
|
44
139
|
|
|
45
140
|
function readMinimalSubagentsSettings(
|
|
46
|
-
settings:
|
|
141
|
+
settings: MinimalSubagentsSettingsDocument,
|
|
47
142
|
scope: SettingsScope,
|
|
48
143
|
warnings: string[],
|
|
49
|
-
):
|
|
50
|
-
if (!
|
|
51
|
-
|
|
144
|
+
): ParsedMinimalSubagentsSettings {
|
|
145
|
+
if (!Value.Check(SettingsDocumentSchema, settings)) return {};
|
|
146
|
+
const minimalSubagents = settings.minimalSubagents;
|
|
147
|
+
if (minimalSubagents === undefined) return {};
|
|
148
|
+
if (Value.Check(MinimalSubagentsSettingsSchema, minimalSubagents)) {
|
|
149
|
+
const parsed: ParsedMinimalSubagentsSettings = {};
|
|
150
|
+
if (minimalSubagents.maxSubagentDepth !== undefined) {
|
|
151
|
+
parsed.maxSubagentDepth = parseMaxSubagentDepthWireValue(minimalSubagents.maxSubagentDepth);
|
|
152
|
+
}
|
|
153
|
+
if (minimalSubagents.modelRoles !== undefined) {
|
|
154
|
+
parsed.modelRoles = parseModelRolesWireValue(minimalSubagents.modelRoles);
|
|
155
|
+
}
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
52
158
|
warnings.push(`${scope} minimalSubagents: expected an object`);
|
|
53
159
|
return {};
|
|
54
160
|
}
|
|
55
161
|
|
|
56
162
|
function mergeModelRoleEntries(
|
|
57
|
-
globalValue:
|
|
58
|
-
projectValue:
|
|
163
|
+
globalValue: ModelRolesWireValue | undefined,
|
|
164
|
+
projectValue: ModelRolesWireValue | undefined,
|
|
59
165
|
warnings: string[],
|
|
60
166
|
): Map<string, ScopedSettingValue> {
|
|
61
167
|
const entries = new Map<string, ScopedSettingValue>();
|
|
62
|
-
if (globalValue
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
entries.set(name, { scope: "global", value });
|
|
66
|
-
}
|
|
67
|
-
} else if (globalValue !== null) {
|
|
68
|
-
warnings.push("global minimalSubagents.modelRoles: expected an object or null");
|
|
168
|
+
if (globalValue?.kind === "entries") {
|
|
169
|
+
for (const [name, value] of globalValue.entries) {
|
|
170
|
+
entries.set(name, { scope: "global", value });
|
|
69
171
|
}
|
|
172
|
+
} else if (globalValue?.kind === "invalid") {
|
|
173
|
+
warnings.push("global minimalSubagents.modelRoles: expected an object or null");
|
|
70
174
|
}
|
|
71
|
-
if (projectValue ===
|
|
175
|
+
if (projectValue?.kind === "reset") return new Map();
|
|
72
176
|
if (projectValue === undefined) return entries;
|
|
73
|
-
if (
|
|
177
|
+
if (projectValue.kind === "invalid") {
|
|
74
178
|
warnings.push("project minimalSubagents.modelRoles: expected an object or null");
|
|
75
179
|
return entries;
|
|
76
180
|
}
|
|
181
|
+
if (projectValue.kind !== "entries") return entries;
|
|
77
182
|
|
|
78
|
-
for (const [name, value] of
|
|
79
|
-
if (value ===
|
|
183
|
+
for (const [name, value] of projectValue.entries) {
|
|
184
|
+
if (value.kind === "delete") {
|
|
80
185
|
entries.delete(name);
|
|
81
186
|
continue;
|
|
82
187
|
}
|
|
83
188
|
const inherited = entries.get(name)?.value;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
189
|
+
const mergedValue =
|
|
190
|
+
inherited !== undefined &&
|
|
191
|
+
isExpandedModelRoleWireValue(inherited) &&
|
|
192
|
+
isExpandedModelRoleWireValue(value)
|
|
193
|
+
? parseModelRoleWireValue({ ...inherited.fields, ...value.fields })
|
|
194
|
+
: value;
|
|
195
|
+
entries.set(name, { scope: "project", value: mergedValue });
|
|
88
196
|
}
|
|
89
197
|
return entries;
|
|
90
198
|
}
|
|
91
199
|
|
|
200
|
+
interface ResolvedModelRoleReference {
|
|
201
|
+
model: string;
|
|
202
|
+
thinkingLevel?: ModelRoleThinkingLevel;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolveThinkingLevelSuffix(suffix: string): ModelRoleThinkingLevel | undefined {
|
|
206
|
+
return THINKING_LEVELS.find((thinkingLevel) => thinkingLevel === suffix);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveModelRoleReference(
|
|
210
|
+
model: string,
|
|
211
|
+
eligibleModels: ReadonlySet<string>,
|
|
212
|
+
path: string,
|
|
213
|
+
warnings: string[],
|
|
214
|
+
): ResolvedModelRoleReference | undefined {
|
|
215
|
+
if (eligibleModels.has(model)) return { model };
|
|
216
|
+
|
|
217
|
+
const separatorIndex = model.lastIndexOf(":");
|
|
218
|
+
if (separatorIndex >= 0) {
|
|
219
|
+
const prefix = model.slice(0, separatorIndex);
|
|
220
|
+
const suffix = model.slice(separatorIndex + 1);
|
|
221
|
+
if (eligibleModels.has(prefix)) {
|
|
222
|
+
const thinkingLevel = resolveThinkingLevelSuffix(suffix);
|
|
223
|
+
if (thinkingLevel !== undefined) return { model: prefix, thinkingLevel };
|
|
224
|
+
warnings.push(`${path}: unknown thinking level suffix: ${suffix}`);
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
warnings.push(`${path}: model is not eligible: ${model}`);
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
92
233
|
function parseModelRoles(
|
|
93
234
|
entries: ReadonlyMap<string, ScopedSettingValue>,
|
|
94
235
|
eligibleModelIds: readonly string[],
|
|
@@ -109,70 +250,78 @@ function parseModelRoles(
|
|
|
109
250
|
}
|
|
110
251
|
|
|
111
252
|
const value = entry.value;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
253
|
+
const expandedRoleObject = isExpandedModelRoleWireValue(value) ? value.fields : undefined;
|
|
254
|
+
if (expandedRoleObject !== undefined) {
|
|
255
|
+
const invalidFields = Object.keys(expandedRoleObject).filter(
|
|
256
|
+
(key) => key !== "model" && key !== "hint",
|
|
257
|
+
);
|
|
258
|
+
if (invalidFields.length > 0) {
|
|
259
|
+
warnings.push(`${path}: unknown field: ${invalidFields.join(", ")}`);
|
|
116
260
|
continue;
|
|
117
261
|
}
|
|
118
|
-
} else if (
|
|
262
|
+
} else if (value.kind !== "shorthand") {
|
|
119
263
|
warnings.push(`${path}: expected a model string or expanded role object`);
|
|
120
264
|
continue;
|
|
121
265
|
}
|
|
122
266
|
|
|
123
|
-
const
|
|
124
|
-
|
|
267
|
+
const modelValue =
|
|
268
|
+
expandedRoleObject === undefined && value.kind === "shorthand"
|
|
269
|
+
? value.model
|
|
270
|
+
: expandedRoleObject?.model;
|
|
271
|
+
if (
|
|
272
|
+
!Value.Check(ShorthandModelRoleSchema, modelValue) ||
|
|
273
|
+
modelValue.length === 0 ||
|
|
274
|
+
modelValue !== modelValue.trim()
|
|
275
|
+
) {
|
|
125
276
|
warnings.push(`${path}: model must be a non-empty trimmed string`);
|
|
126
277
|
continue;
|
|
127
278
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
`${path}: thinking level suffixes are not allowed; choose thinking_level per spawn`,
|
|
131
|
-
);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (!eligibleModels.has(model)) {
|
|
135
|
-
warnings.push(`${path}: model is not eligible: ${model}`);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
279
|
+
const resolvedModel = resolveModelRoleReference(modelValue, eligibleModels, path, warnings);
|
|
280
|
+
if (resolvedModel === undefined) continue;
|
|
138
281
|
|
|
139
|
-
const
|
|
282
|
+
const hintValue = expandedRoleObject?.hint;
|
|
140
283
|
if (
|
|
141
|
-
|
|
142
|
-
(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
/[\r\n]/.test(
|
|
146
|
-
|
|
284
|
+
hintValue !== undefined &&
|
|
285
|
+
(!Value.Check(ShorthandModelRoleSchema, hintValue) ||
|
|
286
|
+
hintValue.length === 0 ||
|
|
287
|
+
hintValue !== hintValue.trim() ||
|
|
288
|
+
/[\r\n]/.test(hintValue) ||
|
|
289
|
+
hintValue.length > MODEL_ROLE_HINT_MAX_LENGTH)
|
|
147
290
|
) {
|
|
148
291
|
warnings.push(`${path}.hint: expected trimmed single-line text up to 500 characters`);
|
|
149
292
|
continue;
|
|
150
293
|
}
|
|
151
|
-
|
|
294
|
+
const role: MinimalSubagentsModelRole = {
|
|
295
|
+
name,
|
|
296
|
+
model: resolvedModel.model,
|
|
297
|
+
};
|
|
298
|
+
if (resolvedModel.thinkingLevel !== undefined) {
|
|
299
|
+
role.thinkingLevel = resolvedModel.thinkingLevel;
|
|
300
|
+
}
|
|
301
|
+
if (hintValue !== undefined && Value.Check(ShorthandModelRoleSchema, hintValue)) {
|
|
302
|
+
role.hint = hintValue;
|
|
303
|
+
}
|
|
304
|
+
roles.push(role);
|
|
152
305
|
}
|
|
153
306
|
return roles;
|
|
154
307
|
}
|
|
155
308
|
|
|
156
309
|
function resolveMaxSubagentDepth(
|
|
157
|
-
globalValue:
|
|
158
|
-
projectValue:
|
|
310
|
+
globalValue: MaxSubagentDepthWireValue | undefined,
|
|
311
|
+
projectValue: MaxSubagentDepthWireValue | undefined,
|
|
159
312
|
warnings: string[],
|
|
160
313
|
): number {
|
|
161
314
|
let resolvedDepth = DEFAULT_MAX_SUBAGENT_DEPTH;
|
|
162
|
-
if (globalValue
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
169
|
-
}
|
|
315
|
+
if (globalValue?.kind === "depth") {
|
|
316
|
+
resolvedDepth = globalValue.value;
|
|
317
|
+
} else if (globalValue?.kind === "invalid") {
|
|
318
|
+
warnings.push(
|
|
319
|
+
"global minimalSubagents.maxSubagentDepth: expected a positive safe integer or null",
|
|
320
|
+
);
|
|
170
321
|
}
|
|
171
322
|
if (projectValue === undefined) return resolvedDepth;
|
|
172
|
-
if (projectValue ===
|
|
173
|
-
if (
|
|
174
|
-
return Number(projectValue);
|
|
175
|
-
}
|
|
323
|
+
if (projectValue.kind === "reset") return DEFAULT_MAX_SUBAGENT_DEPTH;
|
|
324
|
+
if (projectValue.kind === "depth") return projectValue.value;
|
|
176
325
|
warnings.push(
|
|
177
326
|
"project minimalSubagents.maxSubagentDepth: expected a positive safe integer or null",
|
|
178
327
|
);
|
|
@@ -210,8 +359,8 @@ export function resolveMinimalSubagentsSettings(
|
|
|
210
359
|
eligibleModelIds: readonly string[],
|
|
211
360
|
): ResolvedMinimalSubagentsConfig {
|
|
212
361
|
return resolveMinimalSubagentsConfig({
|
|
213
|
-
globalSettings: settings.getGlobalSettings(),
|
|
214
|
-
projectSettings: settings.getProjectSettings(),
|
|
362
|
+
globalSettings: parsePiSettingsDocument(settings.getGlobalSettings()),
|
|
363
|
+
projectSettings: parsePiSettingsDocument(settings.getProjectSettings()),
|
|
215
364
|
eligibleModelIds,
|
|
216
365
|
});
|
|
217
366
|
}
|
|
@@ -11,11 +11,17 @@ export function snapshotCommittedContext(
|
|
|
11
11
|
return structuredClone(committed);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** Carries the selected caller messages and whether child preparation should compact them. */
|
|
15
|
+
export interface ImportedSubagentContext {
|
|
16
|
+
messages: AgentMessage[];
|
|
17
|
+
compact: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
/** Select the imported message snapshot and defer expensive compact preparation to the child turn. */
|
|
15
21
|
export function assembleImportedContext(
|
|
16
22
|
mode: SessionContextMode,
|
|
17
23
|
committedMessages: AgentMessage[],
|
|
18
|
-
):
|
|
24
|
+
): ImportedSubagentContext {
|
|
19
25
|
if (mode === "omit") return { messages: [], compact: false };
|
|
20
26
|
return { messages: committedMessages, compact: mode === "compact" };
|
|
21
27
|
}
|