@narumitw/pi-subagents 0.49.3 → 0.52.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 +362 -53
- package/package.json +10 -7
- package/src/adaptive-scheduler.ts +224 -0
- package/src/admission-benchmark.ts +95 -0
- package/src/admission-policy.ts +78 -0
- package/src/agent-projection.ts +53 -0
- package/src/agents.ts +58 -1
- package/src/auto-transport.ts +114 -0
- package/src/blocking-status.ts +63 -0
- package/src/capabilities.ts +145 -0
- package/src/capability-grant.ts +115 -0
- package/src/capability-router.ts +107 -0
- package/src/completion-delivery.ts +257 -0
- package/src/config-status.ts +221 -0
- package/src/config-ui.ts +215 -236
- package/src/consult-resources.ts +4 -27
- package/src/consult.ts +9 -1
- package/src/create-stateful-transport.ts +55 -0
- package/src/delegation-contract.ts +417 -0
- package/src/execution-plan.ts +322 -0
- package/src/execution-profiles.ts +95 -0
- package/src/execution-ui.ts +320 -0
- package/src/execution.ts +1098 -158
- package/src/in-process-transport.ts +269 -25
- package/src/inspect-render.ts +101 -1
- package/src/inspect.ts +321 -3
- package/src/integration-controller.ts +98 -0
- package/src/limits.ts +3 -0
- package/src/orchestration-metrics.ts +109 -0
- package/src/outcome.ts +61 -0
- package/src/panel-child-group.ts +35 -0
- package/src/panel-contract.ts +343 -0
- package/src/panel-evidence.ts +59 -0
- package/src/panel-execution.ts +770 -0
- package/src/panel-failure.ts +56 -0
- package/src/panel-planning.ts +175 -0
- package/src/panel-prompts.ts +132 -0
- package/src/panel-reconciliation.ts +57 -0
- package/src/panel-render.ts +103 -0
- package/src/parallel-limit-ui.ts +112 -0
- package/src/params.ts +179 -3
- package/src/persistence.ts +182 -32
- package/src/prompt-resources.ts +38 -0
- package/src/registry-types.ts +175 -0
- package/src/registry.ts +466 -143
- package/src/render.ts +72 -6
- package/src/result-contract.ts +416 -0
- package/src/retained-semantic-state.ts +100 -0
- package/src/rpc-timeout-finalization.ts +207 -0
- package/src/rpc-transport-metadata.ts +65 -0
- package/src/rpc-transport.ts +990 -0
- package/src/rpc-turn-capture.ts +142 -0
- package/src/runner-result.ts +55 -0
- package/src/runner-usage.ts +48 -0
- package/src/runner.ts +325 -73
- package/src/semantic-snapshot.ts +214 -0
- package/src/settings.ts +254 -35
- package/src/spawn-idempotency.ts +61 -0
- package/src/stateful-config.ts +13 -0
- package/src/stateful-guidance.ts +1 -0
- package/src/stateful-lifecycle.ts +45 -2
- package/src/stateful-limit-ui.ts +246 -0
- package/src/stateful-limits.ts +96 -0
- package/src/stateful-prompt.ts +11 -2
- package/src/stateful-render.ts +48 -3
- package/src/stateful.ts +467 -357
- package/src/subagents.ts +114 -46
- package/src/subprocess-transport.ts +64 -5
- package/src/supervision.ts +103 -0
- package/src/timeout-checkpoint.ts +305 -0
- package/src/timeout-finalization.ts +75 -0
- package/src/transport-types.ts +68 -0
- package/src/transport-ui.ts +169 -0
- package/src/transport.ts +16 -4
- package/src/turn-budget.ts +109 -0
- package/src/verification-policy.ts +67 -0
- package/src/work-item-ledger.ts +931 -0
- package/src/work-item-persistence.ts +223 -0
- package/src/workflow-planning.ts +162 -0
- package/src/workflow-tree-identity.ts +289 -0
- package/src/workflow-ui.ts +61 -0
- package/src/workflow-verification.ts +296 -0
- package/src/workspace.ts +69 -12
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
export const SEMANTIC_SNAPSHOT_VERSION = "pi-subagents:semantic-snapshot:v1" as const;
|
|
10
|
+
const SEMANTIC_COMPONENT_KEYS = [
|
|
11
|
+
"agent",
|
|
12
|
+
"rolePrompt",
|
|
13
|
+
"tools",
|
|
14
|
+
"model",
|
|
15
|
+
"thinkingLevel",
|
|
16
|
+
"transport",
|
|
17
|
+
"trust",
|
|
18
|
+
"repository",
|
|
19
|
+
"artifacts",
|
|
20
|
+
"workflowGeneration",
|
|
21
|
+
"schedulerPolicy",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export interface SemanticSnapshotInput {
|
|
25
|
+
agentName: string;
|
|
26
|
+
agentManifest?: unknown;
|
|
27
|
+
rolePrompt: string;
|
|
28
|
+
tools?: string[];
|
|
29
|
+
model?: string;
|
|
30
|
+
thinkingLevel?: string;
|
|
31
|
+
transport: string;
|
|
32
|
+
trust: { kind: string; projectTrusted: boolean };
|
|
33
|
+
repository: { kind: string; generation: string };
|
|
34
|
+
artifacts: Record<string, string>;
|
|
35
|
+
workflowGeneration: number;
|
|
36
|
+
schedulerPolicy: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SemanticSnapshot {
|
|
40
|
+
version: typeof SEMANTIC_SNAPSHOT_VERSION;
|
|
41
|
+
digest: string;
|
|
42
|
+
components: {
|
|
43
|
+
agent: string;
|
|
44
|
+
rolePrompt: string;
|
|
45
|
+
tools: string;
|
|
46
|
+
model: string;
|
|
47
|
+
thinkingLevel: string;
|
|
48
|
+
transport: string;
|
|
49
|
+
trust: string;
|
|
50
|
+
repository: string;
|
|
51
|
+
artifacts: string;
|
|
52
|
+
workflowGeneration: string;
|
|
53
|
+
schedulerPolicy: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RepositoryGeneration {
|
|
58
|
+
kind: "git-commit" | "git-dirty" | "filesystem";
|
|
59
|
+
generation: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SemanticCompatibility {
|
|
63
|
+
status: "compatible" | "warning" | "needs-revalidation" | "rejected";
|
|
64
|
+
changedComponents: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function captureRepositoryGeneration(cwd: string): Promise<RepositoryGeneration> {
|
|
68
|
+
const resolved = await fs.promises.realpath(path.resolve(cwd));
|
|
69
|
+
try {
|
|
70
|
+
const result = await execFileAsync(
|
|
71
|
+
"git",
|
|
72
|
+
["-C", resolved, "status", "--porcelain=v2", "--branch", "--untracked-files=all"],
|
|
73
|
+
{ encoding: "utf8", timeout: 2_000, maxBuffer: 256 * 1024 },
|
|
74
|
+
);
|
|
75
|
+
const lines = result.stdout.split("\n");
|
|
76
|
+
const head = lines
|
|
77
|
+
.find((line) => line.startsWith("# branch.oid "))
|
|
78
|
+
?.slice("# branch.oid ".length)
|
|
79
|
+
.trim();
|
|
80
|
+
if (!head || head === "(initial)") throw new Error("Repository has no stable HEAD");
|
|
81
|
+
const status = lines.filter((line) => line && !line.startsWith("# ")).join("\n");
|
|
82
|
+
return status
|
|
83
|
+
? { kind: "git-dirty", generation: digest({ head, status }) }
|
|
84
|
+
: { kind: "git-commit", generation: head };
|
|
85
|
+
} catch {
|
|
86
|
+
return {
|
|
87
|
+
kind: "filesystem",
|
|
88
|
+
generation: digest({ path: resolved, failClosedNonce: randomUUID() }),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function captureSemanticResourceGeneration(paths: string[]): Promise<string> {
|
|
94
|
+
const entries: Array<{ path: string; digest: string }> = [];
|
|
95
|
+
let totalBytes = 0;
|
|
96
|
+
const visit = async (candidate: string, label: string): Promise<void> => {
|
|
97
|
+
if (entries.length >= 256 || totalBytes > 1024 * 1024) {
|
|
98
|
+
throw new Error("Semantic resource snapshot exceeds bounds");
|
|
99
|
+
}
|
|
100
|
+
let stat: fs.Stats;
|
|
101
|
+
try {
|
|
102
|
+
stat = await fs.promises.lstat(candidate);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
if (stat.isSymbolicLink()) return;
|
|
108
|
+
if (stat.isDirectory()) {
|
|
109
|
+
const children = (await fs.promises.readdir(candidate)).sort();
|
|
110
|
+
for (const child of children) await visit(path.join(candidate, child), `${label}/${child}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!stat.isFile()) return;
|
|
114
|
+
totalBytes += stat.size;
|
|
115
|
+
if (totalBytes > 1024 * 1024) throw new Error("Semantic resource snapshot exceeds bounds");
|
|
116
|
+
entries.push({ path: label, digest: digestBytes(await fs.promises.readFile(candidate)) });
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
for (const [index, candidate] of paths.entries()) {
|
|
120
|
+
await visit(path.resolve(candidate), `resource-${index}`);
|
|
121
|
+
}
|
|
122
|
+
return digest(entries);
|
|
123
|
+
} catch {
|
|
124
|
+
return digest({ failClosedNonce: randomUUID() });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function captureSemanticSnapshot(input: SemanticSnapshotInput): SemanticSnapshot {
|
|
129
|
+
if (!Number.isSafeInteger(input.workflowGeneration) || input.workflowGeneration < 0) {
|
|
130
|
+
throw new Error("workflowGeneration must be a non-negative safe integer");
|
|
131
|
+
}
|
|
132
|
+
const components = {
|
|
133
|
+
agent: digest({ name: input.agentName, manifest: input.agentManifest ?? null }),
|
|
134
|
+
rolePrompt: digest(input.rolePrompt),
|
|
135
|
+
tools: digest([...(input.tools ?? [])].sort()),
|
|
136
|
+
model: digest(input.model ?? null),
|
|
137
|
+
thinkingLevel: digest(input.thinkingLevel ?? null),
|
|
138
|
+
transport: digest(input.transport),
|
|
139
|
+
trust: digest(input.trust),
|
|
140
|
+
repository: digest(input.repository),
|
|
141
|
+
artifacts: digest(input.artifacts),
|
|
142
|
+
workflowGeneration: digest(input.workflowGeneration),
|
|
143
|
+
schedulerPolicy: digest(input.schedulerPolicy),
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
version: SEMANTIC_SNAPSHOT_VERSION,
|
|
147
|
+
digest: digest(components),
|
|
148
|
+
components,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function isSemanticSnapshot(value: unknown): value is SemanticSnapshot {
|
|
153
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
154
|
+
const snapshot = value as Partial<SemanticSnapshot>;
|
|
155
|
+
return (
|
|
156
|
+
snapshot.version === SEMANTIC_SNAPSHOT_VERSION &&
|
|
157
|
+
typeof snapshot.digest === "string" &&
|
|
158
|
+
/^[a-f0-9]{64}$/u.test(snapshot.digest) &&
|
|
159
|
+
Boolean(snapshot.components) &&
|
|
160
|
+
Object.keys(snapshot.components ?? {}).length === SEMANTIC_COMPONENT_KEYS.length &&
|
|
161
|
+
SEMANTIC_COMPONENT_KEYS.every((key) =>
|
|
162
|
+
/^[a-f0-9]{64}$/u.test(snapshot.components?.[key] ?? ""),
|
|
163
|
+
) &&
|
|
164
|
+
digest(snapshot.components) === snapshot.digest
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function evaluateSemanticCompatibility(
|
|
169
|
+
previous: SemanticSnapshot,
|
|
170
|
+
current: SemanticSnapshot,
|
|
171
|
+
): SemanticCompatibility {
|
|
172
|
+
if (
|
|
173
|
+
previous.version !== SEMANTIC_SNAPSHOT_VERSION ||
|
|
174
|
+
current.version !== SEMANTIC_SNAPSHOT_VERSION
|
|
175
|
+
) {
|
|
176
|
+
return { status: "rejected", changedComponents: ["version"] };
|
|
177
|
+
}
|
|
178
|
+
if (!isSemanticSnapshot(previous) || !isSemanticSnapshot(current)) {
|
|
179
|
+
return { status: "rejected", changedComponents: ["invalid-snapshot"] };
|
|
180
|
+
}
|
|
181
|
+
if (previous.digest === current.digest) {
|
|
182
|
+
return { status: "compatible", changedComponents: [] };
|
|
183
|
+
}
|
|
184
|
+
const changedComponents = SEMANTIC_COMPONENT_KEYS.filter(
|
|
185
|
+
(key) => previous.components[key] !== current.components[key],
|
|
186
|
+
).sort();
|
|
187
|
+
const warningOnly = changedComponents.every((key) => key === "schedulerPolicy");
|
|
188
|
+
return {
|
|
189
|
+
status: warningOnly ? "warning" : "needs-revalidation",
|
|
190
|
+
changedComponents,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function digest(value: unknown): string {
|
|
195
|
+
return digestBytes(stableStringify(value));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function digestBytes(value: string | Buffer): string {
|
|
199
|
+
return createHash("sha256").update(value).digest("hex");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stableStringify(value: unknown): string {
|
|
203
|
+
return JSON.stringify(sortValue(value));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function sortValue(value: unknown): unknown {
|
|
207
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
208
|
+
if (!value || typeof value !== "object") return value;
|
|
209
|
+
return Object.fromEntries(
|
|
210
|
+
Object.entries(value as Record<string, unknown>)
|
|
211
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
212
|
+
.map(([key, entry]) => [key, sortValue(entry)]),
|
|
213
|
+
);
|
|
214
|
+
}
|
package/src/settings.ts
CHANGED
|
@@ -16,8 +16,21 @@ import {
|
|
|
16
16
|
type SubagentAgentConfig,
|
|
17
17
|
type SubagentSettings,
|
|
18
18
|
type SubagentThinkingLevel,
|
|
19
|
+
type SubagentTransportKind,
|
|
19
20
|
} from "./agents.js";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_MAX_PARALLEL_TASKS,
|
|
23
|
+
MAX_CONFIGURABLE_PARALLEL_TASKS,
|
|
24
|
+
MAX_SUBAGENT_TIMEOUT_MS,
|
|
25
|
+
} from "./limits.js";
|
|
26
|
+
import {
|
|
27
|
+
isValidStatefulLimit,
|
|
28
|
+
resolveStatefulLimits,
|
|
29
|
+
STATEFUL_LIMIT_FIELDS,
|
|
30
|
+
type StatefulLimitField,
|
|
31
|
+
type StatefulLimits,
|
|
32
|
+
statefulLimitDefinition,
|
|
33
|
+
} from "./stateful-limits.js";
|
|
21
34
|
|
|
22
35
|
export function hasOwn(obj: object, key: PropertyKey): boolean {
|
|
23
36
|
return Object.hasOwn(obj, key);
|
|
@@ -39,10 +52,6 @@ function isPositiveInteger(value: unknown): value is number {
|
|
|
39
52
|
return isPositiveNumber(value) && Number.isSafeInteger(value);
|
|
40
53
|
}
|
|
41
54
|
|
|
42
|
-
function isNonNegativeInteger(value: unknown): value is number {
|
|
43
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
55
|
export function normalizeAgentSettings(value: unknown): SubagentAgentConfig | undefined {
|
|
47
56
|
if (!isPlainObject(value)) return undefined;
|
|
48
57
|
|
|
@@ -100,13 +109,27 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
|
|
|
100
109
|
if (typeof value.blocking.enabled !== "boolean") return undefined;
|
|
101
110
|
blocking.enabled = value.blocking.enabled;
|
|
102
111
|
}
|
|
112
|
+
if (hasOwn(value.blocking, "maxParallelTasks")) {
|
|
113
|
+
if (
|
|
114
|
+
!isPositiveInteger(value.blocking.maxParallelTasks) ||
|
|
115
|
+
value.blocking.maxParallelTasks > MAX_CONFIGURABLE_PARALLEL_TASKS
|
|
116
|
+
) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
blocking.maxParallelTasks = value.blocking.maxParallelTasks;
|
|
120
|
+
}
|
|
103
121
|
settings.blocking = blocking;
|
|
104
122
|
}
|
|
105
123
|
if (hasOwn(value, "stateful")) {
|
|
106
124
|
if (!isPlainObject(value.stateful)) return undefined;
|
|
107
125
|
const runtime: NonNullable<SubagentSettings["stateful"]> = {};
|
|
108
126
|
if (hasOwn(value.stateful, "transport")) {
|
|
109
|
-
if (
|
|
127
|
+
if (
|
|
128
|
+
value.stateful.transport !== "subprocess" &&
|
|
129
|
+
value.stateful.transport !== "in-process" &&
|
|
130
|
+
value.stateful.transport !== "rpc" &&
|
|
131
|
+
value.stateful.transport !== "auto"
|
|
132
|
+
) {
|
|
110
133
|
return undefined;
|
|
111
134
|
}
|
|
112
135
|
runtime.transport = value.stateful.transport;
|
|
@@ -120,23 +143,17 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
|
|
|
120
143
|
}
|
|
121
144
|
runtime.completionDelivery = value.stateful.completionDelivery;
|
|
122
145
|
}
|
|
123
|
-
for (const key of
|
|
124
|
-
"maxAgents",
|
|
125
|
-
"maxActiveTurns",
|
|
126
|
-
"maxChildrenPerAgent",
|
|
127
|
-
"maxMailboxMessages",
|
|
128
|
-
"maxMailboxMessageBytes",
|
|
129
|
-
"idleTtlMs",
|
|
130
|
-
"maxStoredAgents",
|
|
131
|
-
] as const) {
|
|
146
|
+
for (const key of STATEFUL_LIMIT_FIELDS) {
|
|
132
147
|
if (hasOwn(value.stateful, key)) {
|
|
133
|
-
if (!
|
|
148
|
+
if (!isValidStatefulLimit(key, value.stateful[key])) return undefined;
|
|
134
149
|
runtime[key] = value.stateful[key];
|
|
135
150
|
}
|
|
136
151
|
}
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
152
|
+
for (const key of ["maxMailboxMessages", "maxMailboxMessageBytes", "idleTtlMs"] as const) {
|
|
153
|
+
if (hasOwn(value.stateful, key)) {
|
|
154
|
+
if (!isPositiveInteger(value.stateful[key])) return undefined;
|
|
155
|
+
runtime[key] = value.stateful[key];
|
|
156
|
+
}
|
|
140
157
|
}
|
|
141
158
|
if (hasOwn(value.stateful, "retentionDays")) {
|
|
142
159
|
if (!isPositiveNumber(value.stateful.retentionDays)) return undefined;
|
|
@@ -283,6 +300,32 @@ export interface CompletionDeliverySettingsSnapshot {
|
|
|
283
300
|
error?: string;
|
|
284
301
|
}
|
|
285
302
|
|
|
303
|
+
export interface StatefulTransportSettingsSnapshot {
|
|
304
|
+
path: string;
|
|
305
|
+
value: SubagentTransportKind;
|
|
306
|
+
source: "default" | "user settings";
|
|
307
|
+
error?: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface BlockingParallelLimitSettingsSnapshot {
|
|
311
|
+
path: string;
|
|
312
|
+
value: number;
|
|
313
|
+
source: "default" | "user settings";
|
|
314
|
+
error?: string;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export interface StatefulLimitFieldSnapshot {
|
|
318
|
+
value: number;
|
|
319
|
+
source: "default" | "user settings";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface StatefulLimitSettingsSnapshot {
|
|
323
|
+
path: string;
|
|
324
|
+
writePath: string;
|
|
325
|
+
values?: Record<StatefulLimitField, StatefulLimitFieldSnapshot>;
|
|
326
|
+
error?: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
286
329
|
export interface ConsultResourceSettingsSnapshot {
|
|
287
330
|
path: string;
|
|
288
331
|
value: ConsultResourcePolicy;
|
|
@@ -462,6 +505,71 @@ export function inspectCompletionDeliverySettings(): CompletionDeliverySettingsS
|
|
|
462
505
|
};
|
|
463
506
|
}
|
|
464
507
|
|
|
508
|
+
export function inspectStatefulTransportSettings(): StatefulTransportSettingsSnapshot {
|
|
509
|
+
const inspected = inspectSubagentSettingsDocument();
|
|
510
|
+
if (!inspected.raw || !inspected.settings) {
|
|
511
|
+
return {
|
|
512
|
+
path: inspected.path,
|
|
513
|
+
value: "subprocess",
|
|
514
|
+
source: "default",
|
|
515
|
+
...(inspected.error ? { error: inspected.error } : {}),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const explicit =
|
|
519
|
+
isPlainObject(inspected.raw.stateful) && hasOwn(inspected.raw.stateful, "transport");
|
|
520
|
+
return {
|
|
521
|
+
path: inspected.path,
|
|
522
|
+
value: inspected.settings.stateful?.transport ?? "subprocess",
|
|
523
|
+
source: explicit ? "user settings" : "default",
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function resolveBlockingMaxParallelTasks(settings?: SubagentSettings): number {
|
|
528
|
+
return settings?.blocking?.maxParallelTasks ?? DEFAULT_MAX_PARALLEL_TASKS;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function inspectBlockingParallelLimitSettings(): BlockingParallelLimitSettingsSnapshot {
|
|
532
|
+
const inspected = inspectSubagentSettingsDocument();
|
|
533
|
+
if (!inspected.raw || !inspected.settings) {
|
|
534
|
+
return {
|
|
535
|
+
path: inspected.path,
|
|
536
|
+
value: DEFAULT_MAX_PARALLEL_TASKS,
|
|
537
|
+
source: "default",
|
|
538
|
+
...(inspected.error ? { error: inspected.error } : {}),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const explicit =
|
|
542
|
+
isPlainObject(inspected.raw.blocking) && hasOwn(inspected.raw.blocking, "maxParallelTasks");
|
|
543
|
+
return {
|
|
544
|
+
path: inspected.path,
|
|
545
|
+
value: resolveBlockingMaxParallelTasks(inspected.settings),
|
|
546
|
+
source: explicit ? "user settings" : "default",
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function inspectStatefulLimitSettings(): StatefulLimitSettingsSnapshot {
|
|
551
|
+
const inspected = inspectSubagentSettingsDocument();
|
|
552
|
+
const writePath = subagentSettingsFilePath();
|
|
553
|
+
if (inspected.error) {
|
|
554
|
+
return { path: inspected.path, writePath, error: inspected.error };
|
|
555
|
+
}
|
|
556
|
+
const resolved = resolveStatefulLimits(inspected.settings?.stateful);
|
|
557
|
+
const rawStateful = isPlainObject(inspected.raw?.stateful) ? inspected.raw.stateful : undefined;
|
|
558
|
+
return {
|
|
559
|
+
path: inspected.path,
|
|
560
|
+
writePath,
|
|
561
|
+
values: Object.fromEntries(
|
|
562
|
+
STATEFUL_LIMIT_FIELDS.map((field) => [
|
|
563
|
+
field,
|
|
564
|
+
{
|
|
565
|
+
value: resolved[field],
|
|
566
|
+
source: rawStateful && hasOwn(rawStateful, field) ? "user settings" : "default",
|
|
567
|
+
},
|
|
568
|
+
]),
|
|
569
|
+
) as unknown as Record<StatefulLimitField, StatefulLimitFieldSnapshot>,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
465
573
|
export function updateDelegationWorkflowSetting(
|
|
466
574
|
value: Exclude<DelegationWorkflow, "disabled">,
|
|
467
575
|
): void {
|
|
@@ -493,6 +601,27 @@ export function updateDelegationWorkflowSetting(
|
|
|
493
601
|
});
|
|
494
602
|
}
|
|
495
603
|
|
|
604
|
+
export function updateStatefulTransportSetting(value: SubagentTransportKind): void {
|
|
605
|
+
if (!["subprocess", "in-process", "rpc", "auto"].includes(value)) {
|
|
606
|
+
throw new Error(`Unsupported stateful transport: ${value}`);
|
|
607
|
+
}
|
|
608
|
+
withSettingsMutationLock(() => {
|
|
609
|
+
const update = readSettingsObjectForUpdate();
|
|
610
|
+
const raw = update.document;
|
|
611
|
+
const stateful = raw.stateful;
|
|
612
|
+
if (stateful !== undefined && !isPlainObject(stateful)) {
|
|
613
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} stateful settings`);
|
|
614
|
+
}
|
|
615
|
+
writeSettingsObjectUnlocked(
|
|
616
|
+
{
|
|
617
|
+
...raw,
|
|
618
|
+
stateful: { ...(stateful ?? {}), transport: value },
|
|
619
|
+
},
|
|
620
|
+
update.replaceCanonical,
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
496
625
|
export function updateCompletionDeliverySetting(value: CompletionDelivery): void {
|
|
497
626
|
withSettingsMutationLock(() => {
|
|
498
627
|
const update = readSettingsObjectForUpdate();
|
|
@@ -514,6 +643,64 @@ export function updateCompletionDeliverySetting(value: CompletionDelivery): void
|
|
|
514
643
|
});
|
|
515
644
|
}
|
|
516
645
|
|
|
646
|
+
export function updateBlockingMaxParallelTasksSetting(value: number): void {
|
|
647
|
+
if (!isPositiveInteger(value) || value > MAX_CONFIGURABLE_PARALLEL_TASKS) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
`Maximum parallel tasks must be an integer between 1 and ${MAX_CONFIGURABLE_PARALLEL_TASKS}`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
withSettingsMutationLock(() => {
|
|
653
|
+
const update = readSettingsObjectForUpdate();
|
|
654
|
+
const raw = update.document;
|
|
655
|
+
const blocking = raw.blocking;
|
|
656
|
+
if (blocking !== undefined && !isPlainObject(blocking)) {
|
|
657
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} blocking settings`);
|
|
658
|
+
}
|
|
659
|
+
writeSettingsObjectUnlocked(
|
|
660
|
+
{
|
|
661
|
+
...raw,
|
|
662
|
+
blocking: {
|
|
663
|
+
...(blocking ?? {}),
|
|
664
|
+
maxParallelTasks: value,
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
update.replaceCanonical,
|
|
668
|
+
);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function updateStatefulLimitSetting(
|
|
673
|
+
field: StatefulLimitField,
|
|
674
|
+
value: number,
|
|
675
|
+
expected?: StatefulLimits,
|
|
676
|
+
): void {
|
|
677
|
+
if (!isValidStatefulLimit(field, value)) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`${field} must be a safe integer greater than or equal to ${statefulLimitDefinition(field).minimum}`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
withSettingsMutationLock(() => {
|
|
683
|
+
const update = readSettingsObjectForUpdate();
|
|
684
|
+
const raw = update.document;
|
|
685
|
+
const stateful = raw.stateful;
|
|
686
|
+
if (stateful !== undefined && !isPlainObject(stateful)) {
|
|
687
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} stateful settings`);
|
|
688
|
+
}
|
|
689
|
+
const normalized = normalizeSubagentSettings(raw);
|
|
690
|
+
if (!normalized) throw new Error(`Cannot update invalid ${SETTINGS_FILE}`);
|
|
691
|
+
if (expected && !sameStatefulLimits(resolveStatefulLimits(normalized.stateful), expected)) {
|
|
692
|
+
throw new Error("Detached limit settings changed; reopen settings and retry");
|
|
693
|
+
}
|
|
694
|
+
writeSettingsObjectUnlocked(
|
|
695
|
+
{
|
|
696
|
+
...raw,
|
|
697
|
+
stateful: { ...(stateful ?? {}), [field]: value },
|
|
698
|
+
},
|
|
699
|
+
update.replaceCanonical,
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
517
704
|
export function updateConsultResourceSetting(value: ConsultResourcePolicy): void {
|
|
518
705
|
withSettingsMutationLock(() => {
|
|
519
706
|
const update = readSettingsObjectForUpdate();
|
|
@@ -558,7 +745,18 @@ export function updateCwdPolicySetting(
|
|
|
558
745
|
});
|
|
559
746
|
}
|
|
560
747
|
|
|
748
|
+
export type AgentSettingsPatch = {
|
|
749
|
+
tools?: string[] | undefined;
|
|
750
|
+
model?: string | null | undefined;
|
|
751
|
+
thinkingLevel?: SubagentThinkingLevel | null | undefined;
|
|
752
|
+
timeoutMs?: number | null | undefined;
|
|
753
|
+
};
|
|
754
|
+
|
|
561
755
|
export function updateAgentToolsSetting(name: string, tools: string[] | undefined): void {
|
|
756
|
+
updateAgentSettingsPatch({ [name]: { tools } });
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export function updateAgentSettingsPatch(patches: Record<string, AgentSettingsPatch>): void {
|
|
562
760
|
withSettingsMutationLock(() => {
|
|
563
761
|
const update = readSettingsObjectForUpdate();
|
|
564
762
|
const raw = update.document;
|
|
@@ -567,24 +765,41 @@ export function updateAgentToolsSetting(name: string, tools: string[] | undefine
|
|
|
567
765
|
throw new Error(`Cannot update invalid ${SETTINGS_FILE} agent settings`);
|
|
568
766
|
}
|
|
569
767
|
const agents = { ...(rawAgents ?? {}) };
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
value
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
768
|
+
for (const [name, patch] of Object.entries(patches)) {
|
|
769
|
+
const rawAgent = hasOwn(agents, name) ? agents[name] : undefined;
|
|
770
|
+
if (rawAgent !== undefined && !isPlainObject(rawAgent)) {
|
|
771
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} settings for ${name}`);
|
|
772
|
+
}
|
|
773
|
+
const agent = { ...(rawAgent ?? {}) };
|
|
774
|
+
for (const field of ["tools", "model", "thinkingLevel", "timeoutMs"] as const) {
|
|
775
|
+
if (!hasOwn(patch, field)) continue;
|
|
776
|
+
const value = patch[field];
|
|
777
|
+
if (value === undefined) delete agent[field];
|
|
778
|
+
else
|
|
779
|
+
Object.defineProperty(agent, field, {
|
|
780
|
+
value,
|
|
781
|
+
enumerable: true,
|
|
782
|
+
configurable: true,
|
|
783
|
+
writable: true,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
if (Object.keys(agent).length > 0) {
|
|
787
|
+
Object.defineProperty(agents, name, {
|
|
788
|
+
value: agent,
|
|
789
|
+
enumerable: true,
|
|
790
|
+
configurable: true,
|
|
791
|
+
writable: true,
|
|
792
|
+
});
|
|
793
|
+
} else {
|
|
794
|
+
delete agents[name];
|
|
795
|
+
}
|
|
586
796
|
}
|
|
587
797
|
|
|
798
|
+
const normalized = normalizeSubagentSettings({
|
|
799
|
+
...raw,
|
|
800
|
+
...(Object.keys(agents).length > 0 ? { agents } : {}),
|
|
801
|
+
});
|
|
802
|
+
if (!normalized) throw new Error(`Cannot update invalid ${SETTINGS_FILE} agent settings`);
|
|
588
803
|
const updated = { ...raw };
|
|
589
804
|
if (Object.keys(agents).length > 0) updated.agents = agents;
|
|
590
805
|
else delete updated.agents;
|
|
@@ -659,6 +874,10 @@ function withSettingsMutationLock<T>(mutate: () => T): T {
|
|
|
659
874
|
}
|
|
660
875
|
}
|
|
661
876
|
|
|
877
|
+
function sameStatefulLimits(left: StatefulLimits, right: StatefulLimits): boolean {
|
|
878
|
+
return STATEFUL_LIMIT_FIELDS.every((field) => left[field] === right[field]);
|
|
879
|
+
}
|
|
880
|
+
|
|
662
881
|
function pathEntryExists(filePath: string): boolean {
|
|
663
882
|
try {
|
|
664
883
|
fs.lstatSync(filePath);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { AgentScope, SubagentThinkingLevel } from "./agents.js";
|
|
3
|
+
import type { DelegationContract } from "./delegation-contract.js";
|
|
4
|
+
import type { SubagentResultFormat } from "./result-contract.js";
|
|
5
|
+
|
|
6
|
+
export const MAX_SPAWN_IDEMPOTENCY_KEY_LENGTH = 256;
|
|
7
|
+
|
|
8
|
+
export interface CanonicalSpawnRequest {
|
|
9
|
+
agent: string;
|
|
10
|
+
task: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
agentScope: AgentScope;
|
|
13
|
+
thinkingLevel?: SubagentThinkingLevel;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
idleTimeoutMs?: number;
|
|
16
|
+
maxTurns?: number;
|
|
17
|
+
maxToolCalls?: number;
|
|
18
|
+
parentId?: string;
|
|
19
|
+
context?: string;
|
|
20
|
+
contextSourceIds: readonly string[];
|
|
21
|
+
workspaceMode: "shared" | "worktree";
|
|
22
|
+
allowConcurrentWrites: boolean;
|
|
23
|
+
contract?: DelegationContract;
|
|
24
|
+
resultFormat: SubagentResultFormat;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hashSpawnRequest(request: CanonicalSpawnRequest): string {
|
|
28
|
+
return createHash("sha256")
|
|
29
|
+
.update(
|
|
30
|
+
JSON.stringify({
|
|
31
|
+
agent: request.agent,
|
|
32
|
+
task: request.task,
|
|
33
|
+
cwd: request.cwd,
|
|
34
|
+
agentScope: request.agentScope,
|
|
35
|
+
thinkingLevel: request.thinkingLevel ?? null,
|
|
36
|
+
...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
|
|
37
|
+
...(request.idleTimeoutMs === undefined ? {} : { idleTimeoutMs: request.idleTimeoutMs }),
|
|
38
|
+
...(request.maxTurns === undefined ? {} : { maxTurns: request.maxTurns }),
|
|
39
|
+
...(request.maxToolCalls === undefined ? {} : { maxToolCalls: request.maxToolCalls }),
|
|
40
|
+
parentId: request.parentId ?? null,
|
|
41
|
+
contextHash: request.context
|
|
42
|
+
? createHash("sha256").update(request.context).digest("hex")
|
|
43
|
+
: null,
|
|
44
|
+
contextSourceIds: [...request.contextSourceIds],
|
|
45
|
+
workspaceMode: request.workspaceMode,
|
|
46
|
+
allowConcurrentWrites: request.allowConcurrentWrites,
|
|
47
|
+
...(request.contract === undefined ? {} : { contract: request.contract }),
|
|
48
|
+
resultFormat: request.resultFormat,
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
.digest("hex");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function assertSpawnIdempotencyKey(value: string | undefined): void {
|
|
55
|
+
if (value === undefined) return;
|
|
56
|
+
if (!value || value.length > MAX_SPAWN_IDEMPOTENCY_KEY_LENGTH) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`subagent_spawn idempotencyKey must contain 1-${MAX_SPAWN_IDEMPOTENCY_KEY_LENGTH} characters`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CompletionDelivery, SubagentTransportKind } from "./agents.js";
|
|
2
|
+
|
|
3
|
+
export function resolveStatefulTransportKind(
|
|
4
|
+
value: SubagentTransportKind | undefined,
|
|
5
|
+
): SubagentTransportKind {
|
|
6
|
+
return value ?? "subprocess";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolveCompletionDelivery(
|
|
10
|
+
value: CompletionDelivery | undefined,
|
|
11
|
+
): CompletionDelivery {
|
|
12
|
+
return value ?? "next-turn";
|
|
13
|
+
}
|
package/src/stateful-guidance.ts
CHANGED
|
@@ -19,6 +19,7 @@ export function createSpawnPromptGuidelines(
|
|
|
19
19
|
return [
|
|
20
20
|
"Do not use subagent_spawn for simple or critical-path work that the main agent can perform directly.",
|
|
21
21
|
"Set subagent_spawn thinkingLevel to the lowest sufficient thinking level for the delegated task: use off or minimal for extraction, formatting, or mechanical work; low for straightforward bounded work; medium for ordinary multi-step research or implementation; high for complex debugging, design, review, or cross-file analysis; xhigh for highly ambiguous, cross-system, or high-risk analysis; and max only for the hardest tasks when quality clearly outweighs latency and cost. Omit subagent_spawn thinkingLevel only to preserve the agent or child default.",
|
|
22
|
+
"Set subagent_spawn timeoutMs to the shortest realistic work deadline for the task difficulty; use idleTimeoutMs for stalled work and maxTurns or maxToolCalls to stop repeated work without progress. Split oversized tasks instead of extending budgets merely to compensate for broad scope. Omit these fields only to preserve the retained agent or configured defaults.",
|
|
22
23
|
deliveryGuidance,
|
|
23
24
|
"Use a single subagent_spawn only for a concrete bounded subtask that can run independently and has an isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation.",
|
|
24
25
|
...(blockingEnabled
|