@howarewoo/pi-subagents 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +265 -94
- package/docs/advanced-policies.md +4 -2
- package/docs/delegation-contract.md +57 -28
- package/docs/images/subagent-hub.svg +1 -1
- package/docs/images/subagent-profiles.svg +1 -1
- package/docs/images/subagent-widget.svg +1 -1
- package/docs/releases.md +111 -23
- package/docs/verification.md +1 -1
- package/package.json +4 -4
- package/src/child-execution.ts +459 -0
- package/src/coordinator.ts +608 -664
- package/src/delegation-types.ts +111 -0
- package/src/hub.ts +38 -267
- package/src/pi-extended-subagents.ts +83 -189
- package/src/profile-viewer.ts +109 -38
- package/src/scheduling.ts +1 -1
- package/src/subagent-widget.ts +115 -0
- package/src/supported-host.ts +26 -0
- package/src/task-input.ts +3 -2
- package/src/task-presentation.ts +165 -0
- package/src/tool-schemas.ts +165 -0
package/src/coordinator.ts
CHANGED
|
@@ -1,42 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
toolProvenance,
|
|
4
|
-
type ToolMetadata,
|
|
5
|
-
type CapabilityDenial,
|
|
6
|
-
} from "./capabilities.ts";
|
|
1
|
+
import { requireSupportedHost } from "./supported-host.ts";
|
|
2
|
+
import { leafDenial, toolProvenance, type CapabilityDenial } from "./capabilities.ts";
|
|
7
3
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { mkdir } from "node:fs/promises";
|
|
9
|
-
import { join } from "node:path";
|
|
10
4
|
import {
|
|
11
|
-
createAgentSession,
|
|
12
|
-
createExtensionRuntime,
|
|
13
|
-
DefaultResourceLoader,
|
|
14
5
|
SessionManager,
|
|
15
|
-
SettingsManager,
|
|
16
6
|
type AgentSession,
|
|
17
7
|
type AgentSessionEvent,
|
|
18
|
-
type ExtensionToolContext,
|
|
19
|
-
type ResourceLoader,
|
|
20
|
-
type Skill,
|
|
21
|
-
type ToolDefinition,
|
|
22
8
|
} from "@earendil-works/pi-coding-agent";
|
|
23
|
-
import {
|
|
24
|
-
resultValidator,
|
|
25
|
-
submissionParameters,
|
|
26
|
-
saveOutcomeArtifact,
|
|
27
|
-
type StructuredOutcome,
|
|
28
|
-
} from "./outcomes.ts";
|
|
29
|
-
import type { Validator } from "@earendil-works/pi-schema";
|
|
9
|
+
import { resultValidator } from "./outcomes.ts";
|
|
30
10
|
import {
|
|
31
11
|
allowsModel,
|
|
32
12
|
sameSelection,
|
|
33
13
|
validateModelSelection,
|
|
34
14
|
type ModelSelection,
|
|
35
15
|
} from "./model-policy.ts";
|
|
36
|
-
import {
|
|
37
|
-
import { resolveWorkspace, workspaceArguments } from "./workspace.ts";
|
|
16
|
+
import { resolveWorkspace } from "./workspace.ts";
|
|
38
17
|
import {
|
|
39
18
|
DEFAULT_POLICY,
|
|
19
|
+
resolveScheduling,
|
|
40
20
|
narrowLimits,
|
|
41
21
|
validatePolicy,
|
|
42
22
|
type ExecutionLimits,
|
|
@@ -59,111 +39,44 @@ const MAX_WAITERS = 64;
|
|
|
59
39
|
const MAX_WAIT_MS = 60000;
|
|
60
40
|
const MAX_GENERATIONS = 64;
|
|
61
41
|
|
|
62
|
-
export type
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
export
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
parentId: string;
|
|
83
|
-
parentGeneration?: number;
|
|
84
|
-
depth: number;
|
|
85
|
-
writerLease: boolean;
|
|
86
|
-
limits: ExecutionLimits;
|
|
87
|
-
limitReason?: string;
|
|
88
|
-
toolCalls: number;
|
|
89
|
-
status: "queued" | "setup" | "running" | "draining" | "settled" | "failed" | "cancelled";
|
|
90
|
-
profile: string;
|
|
91
|
-
tools: readonly BuiltinTool[];
|
|
92
|
-
leafTools?: readonly string[];
|
|
93
|
-
skills?: readonly string[];
|
|
94
|
-
capabilityDenials?: readonly CapabilityDenial[];
|
|
95
|
-
cwd?: string;
|
|
96
|
-
assignment: string;
|
|
97
|
-
activity: string;
|
|
98
|
-
progress: string;
|
|
99
|
-
result: string;
|
|
100
|
-
model: { provider: string; id: string };
|
|
101
|
-
effort: string | null;
|
|
102
|
-
prewalk?: {
|
|
103
|
-
target: ModelSelection;
|
|
104
|
-
status: "armed" | "handed-off" | "suppressed-exact" | "no-op";
|
|
105
|
-
};
|
|
106
|
-
telemetry: {
|
|
107
|
-
requests: number | null;
|
|
108
|
-
tokens: ReturnType<AgentSession["getSessionStats"]>["tokens"] | null;
|
|
109
|
-
cost: number | null;
|
|
110
|
-
context: ReturnType<AgentSession["getContextUsage"]> | null;
|
|
111
|
-
};
|
|
112
|
-
outcome?: StructuredOutcome;
|
|
113
|
-
artifacts?: { text?: string; structured?: string; error?: string };
|
|
114
|
-
error?: string;
|
|
115
|
-
transcript?: string;
|
|
116
|
-
disposed: boolean;
|
|
117
|
-
createdAt: number;
|
|
118
|
-
endedAt: number | null;
|
|
119
|
-
}
|
|
120
|
-
>;
|
|
121
|
-
export type RegistryEvent = Readonly<{
|
|
122
|
-
type: "changed" | "sdk";
|
|
123
|
-
child: ChildSnapshot;
|
|
124
|
-
event?: AgentSessionEvent;
|
|
125
|
-
}>;
|
|
126
|
-
export type StartTask = {
|
|
127
|
-
cwd?: string;
|
|
128
|
-
model?: ModelSelection;
|
|
129
|
-
resultSchema?: unknown;
|
|
130
|
-
assignment: string;
|
|
131
|
-
profile: Profile;
|
|
132
|
-
skills: Skill[];
|
|
133
|
-
context: ExtensionToolContext;
|
|
134
|
-
assertTools: (names: readonly string[]) => void;
|
|
135
|
-
toolMetadata?: () => readonly ToolMetadata[];
|
|
136
|
-
availableSkills?: () => readonly Skill[];
|
|
137
|
-
capabilityDenials?: CapabilityDenial[];
|
|
138
|
-
leafProvenance?: Readonly<Record<string, string>>;
|
|
139
|
-
callerId: string;
|
|
140
|
-
parent?: ChildRef;
|
|
141
|
-
limits?: Partial<ExecutionLimits>;
|
|
142
|
-
coordinationTools?: (actor: ChildRef) => ToolDefinition[];
|
|
143
|
-
nestedTool?: (parent: ChildRef) => ToolDefinition;
|
|
144
|
-
};
|
|
145
|
-
type ChildSelection = Pick<
|
|
146
|
-
ExtensionToolContext,
|
|
147
|
-
"model" | "modelRegistry" | "scopedModels" | "thinkingLevel" | "cwd"
|
|
148
|
-
>;
|
|
149
|
-
type Entry = {
|
|
150
|
-
validator?: Validator;
|
|
151
|
-
input: StartTask;
|
|
152
|
-
selection: ChildSelection;
|
|
42
|
+
export type {
|
|
43
|
+
DelegationMessage,
|
|
44
|
+
CoordinationWait,
|
|
45
|
+
ChildRef,
|
|
46
|
+
ChildSnapshot,
|
|
47
|
+
RegistryEvent,
|
|
48
|
+
StartTask,
|
|
49
|
+
} from "./delegation-types.ts";
|
|
50
|
+
import type {
|
|
51
|
+
DelegationMessage,
|
|
52
|
+
CoordinationWait,
|
|
53
|
+
ChildRef,
|
|
54
|
+
ChildSnapshot,
|
|
55
|
+
RegistryEvent,
|
|
56
|
+
StartTask,
|
|
57
|
+
} from "./delegation-types.ts";
|
|
58
|
+
import { createChildExecution, type ExecutionRecord } from "./child-execution.ts";
|
|
59
|
+
export { childResources } from "./child-execution.ts";
|
|
60
|
+
|
|
61
|
+
type Entry = ExecutionRecord & {
|
|
153
62
|
done: Promise<void>;
|
|
154
63
|
finish: () => void;
|
|
155
|
-
timer?: ReturnType<typeof setTimeout>;
|
|
156
64
|
spawned: number;
|
|
157
|
-
state: ChildSnapshot;
|
|
158
65
|
finalizedMessages?: AgentSession["messages"];
|
|
159
66
|
setup?: Promise<void>;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
unsubscribe?: () => void;
|
|
163
|
-
cancelled: boolean;
|
|
67
|
+
resuming?: boolean;
|
|
68
|
+
pauseDrain?: Promise<void>;
|
|
164
69
|
callerId: string;
|
|
165
70
|
};
|
|
166
71
|
|
|
72
|
+
function broadenedGrant(profile: Profile, grant: Profile) {
|
|
73
|
+
return (["tools", "skills", "leafTools", "spawn", "coordination"] as const).find((key) =>
|
|
74
|
+
(profile[key] ?? []).some(
|
|
75
|
+
(value) => !(grant[key] as readonly string[] | undefined)?.includes(value),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
167
80
|
function admittedModel(entry: Entry, selection: ModelSelection): boolean {
|
|
168
81
|
return entry.input.profile.models === undefined
|
|
169
82
|
? sameSelection(selection, {
|
|
@@ -173,31 +86,15 @@ function admittedModel(entry: Entry, selection: ModelSelection): boolean {
|
|
|
173
86
|
: allowsModel(entry.input.profile, selection);
|
|
174
87
|
}
|
|
175
88
|
|
|
176
|
-
export function childResources(prompt: string, skills: Skill[] = []): ResourceLoader {
|
|
177
|
-
const extensions = { extensions: [], errors: [], runtime: createExtensionRuntime() };
|
|
178
|
-
return {
|
|
179
|
-
getExtensions: () => extensions,
|
|
180
|
-
getSkills: () => ({ skills, diagnostics: [] }),
|
|
181
|
-
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
182
|
-
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
183
|
-
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
184
|
-
getSystemPrompt: () =>
|
|
185
|
-
prompt +
|
|
186
|
-
"\nYou are a delegated child. Complete only this assignment; use only explicitly granted tools and spawn roles.",
|
|
187
|
-
getSystemPromptSource: () => undefined,
|
|
188
|
-
getAppendSystemPrompt: () => [],
|
|
189
|
-
getAppendSystemPromptSources: () => [],
|
|
190
|
-
extendResources: () => {},
|
|
191
|
-
reload: async () => {},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
89
|
/** Authoritative parent-lifetime registry. IDs are never reused; controls require their generation. */
|
|
196
90
|
export class DelegationCoordinator {
|
|
197
91
|
private entries = new Map<string, Entry>();
|
|
198
92
|
private queue = new Set<Entry>();
|
|
199
93
|
private history = new Map<string, { state: ChildSnapshot; messages: AgentSession["messages"] }>();
|
|
200
|
-
private presence = new Map<
|
|
94
|
+
private presence = new Map<
|
|
95
|
+
string,
|
|
96
|
+
"active" | "pausing" | "paused" | "idle" | "parked" | "stopped"
|
|
97
|
+
>();
|
|
201
98
|
private coordinationListeners = new Set<() => void>();
|
|
202
99
|
private coordinationMessages: DelegationMessage[] = [];
|
|
203
100
|
private mailboxes = new Map<string, DelegationMessage[]>();
|
|
@@ -291,10 +188,11 @@ export class DelegationCoordinator {
|
|
|
291
188
|
}
|
|
292
189
|
private active(): Entry[] {
|
|
293
190
|
return [...this.entries.values()].filter(
|
|
294
|
-
(entry) =>
|
|
191
|
+
(entry) =>
|
|
192
|
+
!entry.state.disposed && entry.state.status !== "queued" && entry.state.status !== "paused",
|
|
295
193
|
);
|
|
296
194
|
}
|
|
297
|
-
private validate(input: StartTask):
|
|
195
|
+
private validate(input: StartTask): ExecutionRecord["selection"] {
|
|
298
196
|
if (this.closing) throw new Error("Parent shut down");
|
|
299
197
|
if (!input.assignment.trim()) throw new Error("A nonempty assignment is required");
|
|
300
198
|
const selection = {
|
|
@@ -334,27 +232,14 @@ export class DelegationCoordinator {
|
|
|
334
232
|
throw new Error("Shared checkout writer conflict: in-flight parent mutation");
|
|
335
233
|
if (input.parent) {
|
|
336
234
|
const parent = this.entry(input.parent);
|
|
337
|
-
if (parent.cancelled || parent.state.disposed)
|
|
235
|
+
if (parent.cancelled || parent.paused || parent.state.disposed)
|
|
338
236
|
throw new Error("Delegating parent is no longer active");
|
|
339
|
-
|
|
340
|
-
!admittedModel(parent, effective) ||
|
|
341
|
-
input.profile.models?.some((model) => !admittedModel(parent, model)) ||
|
|
342
|
-
(input.profile.prewalk && !input.model && !admittedModel(parent, input.profile.prewalk))
|
|
343
|
-
)
|
|
344
|
-
throw new Error("Descendant cannot broaden ancestor model grants");
|
|
237
|
+
this.assertDescendantModels(parent, input.profile, effective, input.model);
|
|
345
238
|
if (!parent.input.profile.spawn?.includes(input.profile.name))
|
|
346
239
|
throw new Error("Ancestor spawn role denied");
|
|
347
240
|
if (parent.state.depth >= Math.min(this.policy.depth, parent.state.limits.depth))
|
|
348
241
|
throw new Error("Delegation depth limit");
|
|
349
|
-
if (
|
|
350
|
-
input.profile.tools.some((tool) => !parent.input.profile.tools.includes(tool)) ||
|
|
351
|
-
input.profile.skills.some((skill) => !parent.input.profile.skills.includes(skill)) ||
|
|
352
|
-
input.profile.leafTools?.some((name) => !parent.input.profile.leafTools?.includes(name)) ||
|
|
353
|
-
input.profile.spawn?.some((role) => !parent.input.profile.spawn?.includes(role)) ||
|
|
354
|
-
input.profile.coordination?.some(
|
|
355
|
-
(action) => !parent.input.profile.coordination?.includes(action),
|
|
356
|
-
)
|
|
357
|
-
)
|
|
242
|
+
if (broadenedGrant(input.profile, parent.input.profile))
|
|
358
243
|
throw new Error("Descendant cannot broaden ancestor grants");
|
|
359
244
|
let ancestor: Entry | undefined = parent;
|
|
360
245
|
while (ancestor) {
|
|
@@ -365,8 +250,142 @@ export class DelegationCoordinator {
|
|
|
365
250
|
}
|
|
366
251
|
return selection;
|
|
367
252
|
}
|
|
253
|
+
private assertDescendantModels(
|
|
254
|
+
parent: Entry,
|
|
255
|
+
profile: Profile,
|
|
256
|
+
effective: ModelSelection,
|
|
257
|
+
explicit?: ModelSelection,
|
|
258
|
+
): void {
|
|
259
|
+
if (
|
|
260
|
+
!admittedModel(parent, effective) ||
|
|
261
|
+
profile.models?.some((model) => !admittedModel(parent, model)) ||
|
|
262
|
+
(profile.prewalk && !explicit && !admittedModel(parent, profile.prewalk))
|
|
263
|
+
)
|
|
264
|
+
throw new Error("Descendant cannot broaden ancestor model grants");
|
|
265
|
+
}
|
|
266
|
+
private assertActive(entry: Entry): void {
|
|
267
|
+
let current: Entry | undefined = entry;
|
|
268
|
+
while (current) {
|
|
269
|
+
if (
|
|
270
|
+
this.entry(current.state) !== current ||
|
|
271
|
+
current.cancelled ||
|
|
272
|
+
current.paused ||
|
|
273
|
+
current.state.disposed ||
|
|
274
|
+
this.closing
|
|
275
|
+
)
|
|
276
|
+
throw new Error("Delegating parent is no longer active");
|
|
277
|
+
current = current.input.parent ? this.entry(current.input.parent) : undefined;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Each action is bounded by its admission and fresh policy for the whole lineage. */
|
|
281
|
+
private async authorize(
|
|
282
|
+
ref: ChildRef,
|
|
283
|
+
allows: (profile: Profile, index: number) => boolean,
|
|
284
|
+
denial: string,
|
|
285
|
+
): Promise<void> {
|
|
286
|
+
const lineage: Entry[] = [];
|
|
287
|
+
let current: Entry | undefined = this.entry(ref);
|
|
288
|
+
while (current) {
|
|
289
|
+
this.assertActive(current);
|
|
290
|
+
if (!allows(current.input.profile, lineage.length)) throw new Error(denial);
|
|
291
|
+
lineage.push(current);
|
|
292
|
+
current = current.input.parent ? this.entry(current.input.parent) : undefined;
|
|
293
|
+
}
|
|
294
|
+
for (const [index, entry] of lineage.entries()) {
|
|
295
|
+
const { profile } = await resolveProfile(
|
|
296
|
+
entry.input.profile.name,
|
|
297
|
+
entry.input.context.cwd,
|
|
298
|
+
entry.input.context.isProjectTrusted(),
|
|
299
|
+
undefined,
|
|
300
|
+
entry.input.availableSkills?.() ?? entry.input.skills,
|
|
301
|
+
);
|
|
302
|
+
if (!allows(profile, index)) throw new Error(denial);
|
|
303
|
+
}
|
|
304
|
+
// Policy loading yields: cancellation, pause, shutdown and generation changes win.
|
|
305
|
+
this.assertActive(lineage[0]);
|
|
306
|
+
}
|
|
307
|
+
private effectiveCapabilities(
|
|
308
|
+
profile: Profile,
|
|
309
|
+
source: Pick<StartTask, "context" | "toolMetadata">,
|
|
310
|
+
cwd: string,
|
|
311
|
+
) {
|
|
312
|
+
const callable = new Set(source.context.tools.map((tool) => tool.name));
|
|
313
|
+
const capabilityDenials: CapabilityDenial[] = [];
|
|
314
|
+
const leafTools: string[] = [];
|
|
315
|
+
const leafProvenance: Record<string, string> = {};
|
|
316
|
+
if (profile.name === "general") {
|
|
317
|
+
const metadata = source.toolMetadata?.() ?? [];
|
|
318
|
+
const sameWorkspace = cwd === resolveWorkspace(undefined, source.context.cwd);
|
|
319
|
+
for (const tool of metadata) {
|
|
320
|
+
if (profile.tools.includes(tool.name as BuiltinTool)) continue;
|
|
321
|
+
const reason = !callable.has(tool.name)
|
|
322
|
+
? "tool is not live callable"
|
|
323
|
+
: leafDenial(tool, profile.leafTools ?? [], sameWorkspace);
|
|
324
|
+
if (reason) capabilityDenials.push({ name: tool.name, reason });
|
|
325
|
+
else {
|
|
326
|
+
leafTools.push(tool.name);
|
|
327
|
+
leafProvenance[tool.name] = toolProvenance(tool);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (const name of profile.leafTools ?? [])
|
|
331
|
+
if (!metadata.some((tool) => tool.name === name))
|
|
332
|
+
capabilityDenials.push({ name, reason: "tool metadata unavailable" });
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
profile: { ...profile, leafTools, tools: profile.tools.filter((name) => callable.has(name)) },
|
|
336
|
+
capabilityDenials,
|
|
337
|
+
leafProvenance,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
async authorizeSpawn(
|
|
341
|
+
ref: ChildRef,
|
|
342
|
+
profile: Profile,
|
|
343
|
+
model?: ModelSelection,
|
|
344
|
+
source?: Pick<StartTask, "context" | "toolMetadata" | "cwd">,
|
|
345
|
+
): Promise<void> {
|
|
346
|
+
const parent = this.entry(ref);
|
|
347
|
+
this.assertDescendantModels(
|
|
348
|
+
parent,
|
|
349
|
+
profile,
|
|
350
|
+
model ??
|
|
351
|
+
profile.model ?? {
|
|
352
|
+
...parent.state.model,
|
|
353
|
+
effort: parent.state.effort as ModelSelection["effort"],
|
|
354
|
+
},
|
|
355
|
+
model,
|
|
356
|
+
);
|
|
357
|
+
// Compare effective capabilities, not unavailable configured ceilings.
|
|
358
|
+
profile = this.effectiveCapabilities(
|
|
359
|
+
profile,
|
|
360
|
+
source ?? parent.input,
|
|
361
|
+
resolveWorkspace(source?.cwd ?? parent.selection.cwd, (source ?? parent.input).context.cwd),
|
|
362
|
+
).profile;
|
|
363
|
+
const roles = [profile.name];
|
|
364
|
+
let current: Entry | undefined = this.entry(ref);
|
|
365
|
+
while (current) {
|
|
366
|
+
roles.push(current.input.profile.name);
|
|
367
|
+
current = current.input.parent ? this.entry(current.input.parent) : undefined;
|
|
368
|
+
}
|
|
369
|
+
await this.authorize(
|
|
370
|
+
ref,
|
|
371
|
+
(grant, index) => !!grant.spawn?.includes(roles[index]) && !broadenedGrant(profile, grant),
|
|
372
|
+
"Current or admitted ancestor spawn grant denied; descendant cannot broaden ancestor grants",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
async admit(
|
|
376
|
+
inputs: StartTask[],
|
|
377
|
+
replacement?: ChildRef,
|
|
378
|
+
signal?: AbortSignal,
|
|
379
|
+
): Promise<ChildRef[]> {
|
|
380
|
+
requireSupportedHost();
|
|
381
|
+
for (const input of inputs)
|
|
382
|
+
if (input.parent) await this.authorizeSpawn(input.parent, input.profile, input.model, input);
|
|
383
|
+
signal?.throwIfAborted();
|
|
384
|
+
for (const input of inputs) if (input.parent) this.assertActive(this.entry(input.parent));
|
|
385
|
+
return this.admitAuthorized(inputs, replacement);
|
|
386
|
+
}
|
|
368
387
|
/** Whole-batch synchronous validation precedes registry insertion, disk setup or dispatch. */
|
|
369
|
-
|
|
388
|
+
private admitAuthorized(inputs: StartTask[], replacement?: ChildRef): ChildRef[] {
|
|
370
389
|
const previous = replacement ? this.entry(replacement) : undefined;
|
|
371
390
|
inputs = inputs.map((input) => {
|
|
372
391
|
const cwd = resolveWorkspace(
|
|
@@ -379,39 +398,28 @@ export class DelegationCoordinator {
|
|
|
379
398
|
throw new Error("Follow-up must retain its original workspace");
|
|
380
399
|
if (typeof input.assertTools !== "function")
|
|
381
400
|
throw new Error("Delegation requires a parent builtin provenance validator");
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const metadata = input.toolMetadata?.() ?? [];
|
|
388
|
-
const sameWorkspace = cwd === resolveWorkspace(undefined, input.context.cwd);
|
|
389
|
-
for (const tool of metadata) {
|
|
390
|
-
if (input.profile.tools.includes(tool.name as BuiltinTool)) continue;
|
|
391
|
-
const reason = !callable.has(tool.name)
|
|
392
|
-
? "tool is not live callable"
|
|
393
|
-
: leafDenial(tool, input.profile.leafTools ?? [], sameWorkspace);
|
|
394
|
-
if (reason) capabilityDenials.push({ name: tool.name, reason });
|
|
395
|
-
else {
|
|
396
|
-
leafTools.push(tool.name);
|
|
397
|
-
leafProvenance[tool.name] = toolProvenance(tool);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
for (const name of input.profile.leafTools ?? [])
|
|
401
|
-
if (!metadata.some((tool) => tool.name === name))
|
|
402
|
-
capabilityDenials.push({ name, reason: "tool metadata unavailable" });
|
|
403
|
-
}
|
|
404
|
-
const profile = {
|
|
405
|
-
...input.profile,
|
|
406
|
-
leafTools,
|
|
407
|
-
tools: input.profile.tools.filter((name) => callable.has(name)),
|
|
408
|
-
};
|
|
401
|
+
const { profile, capabilityDenials, leafProvenance } = this.effectiveCapabilities(
|
|
402
|
+
input.profile,
|
|
403
|
+
input,
|
|
404
|
+
cwd,
|
|
405
|
+
);
|
|
409
406
|
if (!profile.tools.length)
|
|
410
407
|
throw new Error(
|
|
411
408
|
`Profile ${profile.name} has no callable parent builtin tools; enable an allowed parent tool or choose a usable profile`,
|
|
412
409
|
);
|
|
413
410
|
input.assertTools(profile.tools);
|
|
414
|
-
|
|
411
|
+
// Capture the admitted ceiling, but keep root/ancestor revocation live. A nested
|
|
412
|
+
// task uses the root dispatcher context, not its immediate parent's SDK context.
|
|
413
|
+
const inheritedCodemode = input.parent
|
|
414
|
+
? this.entry(input.parent).input.codemodeEnabled
|
|
415
|
+
: undefined;
|
|
416
|
+
const enabled =
|
|
417
|
+
!!input.codemodeEnabled?.() &&
|
|
418
|
+
(!input.parent || !!inheritedCodemode?.()) &&
|
|
419
|
+
(!previous || !!previous.input.codemodeEnabled?.());
|
|
420
|
+
const codemodeEnabled = () =>
|
|
421
|
+
enabled && !!input.codemodeEnabled?.() && (!input.parent || !!inheritedCodemode?.());
|
|
422
|
+
return { ...input, profile, cwd, capabilityDenials, leafProvenance, codemodeEnabled };
|
|
415
423
|
});
|
|
416
424
|
if (previous) {
|
|
417
425
|
if (
|
|
@@ -434,13 +442,7 @@ export class DelegationCoordinator {
|
|
|
434
442
|
throw new Error("Follow-up cannot broaden previous model grants");
|
|
435
443
|
if (
|
|
436
444
|
profile.name !== previous.input.profile.name ||
|
|
437
|
-
profile
|
|
438
|
-
profile.skills.some((skill) => !previous.input.profile.skills.includes(skill)) ||
|
|
439
|
-
profile.leafTools?.some((name) => !previous.input.profile.leafTools?.includes(name)) ||
|
|
440
|
-
profile.spawn?.some((role) => !previous.input.profile.spawn?.includes(role)) ||
|
|
441
|
-
profile.coordination?.some(
|
|
442
|
-
(action) => !previous.input.profile.coordination?.includes(action),
|
|
443
|
-
)
|
|
445
|
+
broadenedGrant(profile, previous.input.profile)
|
|
444
446
|
)
|
|
445
447
|
throw new Error("Follow-up cannot broaden previous grants");
|
|
446
448
|
}
|
|
@@ -465,18 +467,7 @@ export class DelegationCoordinator {
|
|
|
465
467
|
return narrowLimits(maximum, input.limits ?? {});
|
|
466
468
|
});
|
|
467
469
|
const queued = this.queue.size;
|
|
468
|
-
|
|
469
|
-
let slots = Math.max(0, this.policy.concurrency - this.active().length);
|
|
470
|
-
let writerBusy = !!this.writer || !!this.parentMutations.size;
|
|
471
|
-
let dispatchable = 0;
|
|
472
|
-
for (const input of pending) {
|
|
473
|
-
if (!slots || (isWriter(input.profile) && writerBusy)) break;
|
|
474
|
-
slots--;
|
|
475
|
-
dispatchable++;
|
|
476
|
-
if (isWriter(input.profile)) writerBusy = true;
|
|
477
|
-
}
|
|
478
|
-
if (pending.length - dispatchable > this.policy.queue)
|
|
479
|
-
throw new Error("Delegation queue capacity limit");
|
|
470
|
+
this.assertQueueCapacity(inputs);
|
|
480
471
|
const parents = new Map<Entry, number>();
|
|
481
472
|
for (const input of inputs)
|
|
482
473
|
if (input.parent) {
|
|
@@ -523,6 +514,9 @@ export class DelegationCoordinator {
|
|
|
523
514
|
writerLease: false,
|
|
524
515
|
limits: limits[index],
|
|
525
516
|
toolCalls: 0,
|
|
517
|
+
spawned: previous?.spawned ?? 0,
|
|
518
|
+
window: 1,
|
|
519
|
+
retainedContext: false,
|
|
526
520
|
profile: input.profile.name,
|
|
527
521
|
tools: [...input.profile.tools],
|
|
528
522
|
leafTools: [...(input.profile.leafTools ?? [])],
|
|
@@ -558,7 +552,10 @@ export class DelegationCoordinator {
|
|
|
558
552
|
};
|
|
559
553
|
return entry;
|
|
560
554
|
});
|
|
561
|
-
for (const [parent, count] of parents)
|
|
555
|
+
for (const [parent, count] of parents) {
|
|
556
|
+
parent.spawned += count;
|
|
557
|
+
this.update(parent, { spawned: parent.spawned });
|
|
558
|
+
}
|
|
562
559
|
if (previous)
|
|
563
560
|
this.history.set(this.key(previous.state), {
|
|
564
561
|
state: structuredClone(previous.state),
|
|
@@ -579,7 +576,7 @@ export class DelegationCoordinator {
|
|
|
579
576
|
return entries.map((entry) => ({ id: entry.state.id, generation: entry.state.generation }));
|
|
580
577
|
}
|
|
581
578
|
async start(input: StartTask): Promise<ChildRef> {
|
|
582
|
-
const [ref] = this.admit([input]);
|
|
579
|
+
const [ref] = await this.admit([input]);
|
|
583
580
|
await this.entry(ref).setup;
|
|
584
581
|
return ref;
|
|
585
582
|
}
|
|
@@ -592,7 +589,8 @@ export class DelegationCoordinator {
|
|
|
592
589
|
}
|
|
593
590
|
return parent && { id: parent.id, generation: parent.generation };
|
|
594
591
|
}
|
|
595
|
-
renew(ref: ChildRef, input: StartTask): ChildRef {
|
|
592
|
+
async renew(ref: ChildRef, input: StartTask, signal?: AbortSignal): Promise<ChildRef> {
|
|
593
|
+
requireSupportedHost();
|
|
596
594
|
const previous = this.entry(ref);
|
|
597
595
|
this.renewalParent(ref);
|
|
598
596
|
const profile = intersectProfileGrants(input.profile, previous.input.profile);
|
|
@@ -600,24 +598,41 @@ export class DelegationCoordinator {
|
|
|
600
598
|
previous.state.prewalk && ["handed-off", "no-op"].includes(previous.state.prewalk.status)
|
|
601
599
|
? previous.state.prewalk.target
|
|
602
600
|
: previous.input.model;
|
|
603
|
-
const next =
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
601
|
+
const next = (
|
|
602
|
+
await this.admit(
|
|
603
|
+
[
|
|
604
|
+
{
|
|
605
|
+
...input,
|
|
606
|
+
model: retained ?? input.model,
|
|
607
|
+
profile,
|
|
608
|
+
skills: input.skills.filter((skill) => profile.skills.includes(skill.filePath)),
|
|
609
|
+
resultSchema:
|
|
610
|
+
input.resultSchema === undefined ? previous.input.resultSchema : input.resultSchema,
|
|
611
|
+
limits: narrowLimits(previous.state.limits, input.limits ?? {}),
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
ref,
|
|
615
|
+
signal,
|
|
616
|
+
)
|
|
616
617
|
)[0];
|
|
617
618
|
if (previous.state.prewalk && previous.state.prewalk.status !== "armed")
|
|
618
619
|
this.update(this.entry(next), { prewalk: previous.state.prewalk });
|
|
619
620
|
return next;
|
|
620
621
|
}
|
|
622
|
+
private assertQueueCapacity(inputs: StartTask[]): void {
|
|
623
|
+
const pending = [...this.queue].map((entry) => entry.input).concat(inputs);
|
|
624
|
+
let slots = Math.max(0, this.policy.concurrency - this.active().length);
|
|
625
|
+
let writerBusy = !!this.writer || !!this.parentMutations.size;
|
|
626
|
+
let dispatchable = 0;
|
|
627
|
+
for (const input of pending) {
|
|
628
|
+
if (!slots || (isWriter(input.profile) && writerBusy)) break;
|
|
629
|
+
slots--;
|
|
630
|
+
dispatchable++;
|
|
631
|
+
if (isWriter(input.profile)) writerBusy = true;
|
|
632
|
+
}
|
|
633
|
+
if (pending.length - dispatchable > this.policy.queue)
|
|
634
|
+
throw new Error("Delegation queue capacity limit");
|
|
635
|
+
}
|
|
621
636
|
private pump(): void {
|
|
622
637
|
if (this.closing) return;
|
|
623
638
|
for (const entry of this.queue) {
|
|
@@ -633,445 +648,346 @@ export class DelegationCoordinator {
|
|
|
633
648
|
writerLease: writer,
|
|
634
649
|
activity: "Preparing SDK child",
|
|
635
650
|
};
|
|
636
|
-
entry.setup =
|
|
651
|
+
entry.setup = entry.session
|
|
652
|
+
? Promise.resolve().then(() => {
|
|
653
|
+
if (!entry.paused && !entry.cancelled) return entry.prompt!(entry.resumeText!);
|
|
654
|
+
})
|
|
655
|
+
: this.create(entry);
|
|
637
656
|
this.update(entry, {});
|
|
638
657
|
void entry.setup.catch(() => {});
|
|
639
658
|
}
|
|
640
659
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
660
|
+
/** Mark the entire active subtree before aborting any SDK or releasing any permit. */
|
|
661
|
+
private pause(ref: ChildRef): Promise<void> {
|
|
662
|
+
const root = this.entry(ref);
|
|
663
|
+
if (root.pauseDrain) return root.pauseDrain;
|
|
664
|
+
const entries: Entry[] = [];
|
|
665
|
+
const collect = (entry: Entry) => {
|
|
666
|
+
if (entry.cancelled || entry.state.disposed || entry.paused) return;
|
|
667
|
+
entries.push(entry);
|
|
668
|
+
for (const child of this.children(entry)) collect(child);
|
|
669
|
+
};
|
|
670
|
+
collect(root);
|
|
671
|
+
for (const entry of entries) {
|
|
672
|
+
entry.paused = true;
|
|
673
|
+
this.presence.set(entry.state.id, "pausing");
|
|
674
|
+
clearTimeout(entry.timer);
|
|
675
|
+
this.queue.delete(entry);
|
|
676
|
+
}
|
|
677
|
+
for (const entry of entries)
|
|
678
|
+
this.update(entry, {
|
|
679
|
+
status: "pausing",
|
|
680
|
+
pauseReason: entry === root ? "deadline" : "ancestor deadline",
|
|
681
|
+
activity: "Deadline reached; aborting and draining before pause",
|
|
682
|
+
});
|
|
683
|
+
const drain = async (entry: Entry) => {
|
|
684
|
+
await entry.setup?.catch(() => {});
|
|
685
|
+
await entry.session?.abort();
|
|
686
|
+
await entry.run;
|
|
687
|
+
};
|
|
688
|
+
// Start aborts concurrently: an ancestor may be awaiting its descendant's outcome.
|
|
689
|
+
const drains = new Map(entries.map((entry) => [entry, drain(entry)]));
|
|
690
|
+
const completions = new Map<Entry, Promise<void>>();
|
|
691
|
+
const complete = (entry: Entry): Promise<void> => {
|
|
692
|
+
const existing = completions.get(entry);
|
|
693
|
+
if (existing) return existing;
|
|
694
|
+
const completion = Promise.all([
|
|
695
|
+
drains.get(entry),
|
|
696
|
+
// Independently pausing children already own their abort and subtree drain.
|
|
697
|
+
...this.children(entry).map((child) =>
|
|
698
|
+
drains.has(child) ? complete(child) : child.pauseDrain,
|
|
699
|
+
),
|
|
700
|
+
]).then(() => {
|
|
701
|
+
if (entry.cancelled || this.closing || entry.state.disposed) return;
|
|
702
|
+
if (this.writer === entry.state.id) this.writer = undefined;
|
|
703
|
+
this.presence.set(entry.state.id, "paused");
|
|
704
|
+
this.update(entry, {
|
|
705
|
+
status: "paused",
|
|
706
|
+
writerLease: false,
|
|
707
|
+
retainedContext: !!entry.session,
|
|
708
|
+
activity: "Paused; explicitly resume to continue retained conversation",
|
|
709
|
+
});
|
|
710
|
+
entry.finish();
|
|
711
|
+
this.pump();
|
|
712
|
+
});
|
|
713
|
+
completions.set(entry, completion);
|
|
714
|
+
entry.pauseDrain = completion;
|
|
715
|
+
return completion;
|
|
716
|
+
};
|
|
717
|
+
const paused = Promise.all(entries.map(complete)).then(() => {});
|
|
718
|
+
return paused;
|
|
651
719
|
}
|
|
652
|
-
|
|
720
|
+
/** Continue a drained window without replacing the identity or SDK conversation. */
|
|
721
|
+
async resume(ref: ChildRef, text: string): Promise<ChildRef> {
|
|
722
|
+
requireSupportedHost();
|
|
723
|
+
const entry = this.entry(ref);
|
|
724
|
+
if (entry.resuming || entry.state.status !== "paused" || entry.cancelled || this.closing)
|
|
725
|
+
throw new Error("Resume requires a drained paused assignment; duplicate resume rejected");
|
|
726
|
+
if (!text.trim()) throw new Error("Nonempty continuation text is required");
|
|
727
|
+
entry.resuming = true;
|
|
653
728
|
try {
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
let policyError: string | undefined;
|
|
669
|
-
const effective = (): ModelSelection => ({
|
|
729
|
+
const parent = this.renewalParent(ref);
|
|
730
|
+
const input = entry.input;
|
|
731
|
+
const policy = await resolveScheduling(input.context.cwd, input.context.isProjectTrusted());
|
|
732
|
+
this.configure(policy);
|
|
733
|
+
const limits = { ...entry.state.limits };
|
|
734
|
+
for (const key of Object.keys(limits) as (keyof ExecutionLimits)[])
|
|
735
|
+
limits[key] = Math.min(
|
|
736
|
+
limits[key],
|
|
737
|
+
policy[key],
|
|
738
|
+
parent ? this.entry(parent).state.limits[key] : Infinity,
|
|
739
|
+
);
|
|
740
|
+
let current: Entry | undefined = entry;
|
|
741
|
+
let descendant: Entry | undefined;
|
|
742
|
+
const selected: ModelSelection = {
|
|
670
743
|
...entry.state.model,
|
|
671
744
|
effort: entry.state.effort as ModelSelection["effort"],
|
|
672
|
-
});
|
|
673
|
-
const revalidate = async (selected: ModelSelection, requirePrewalk: boolean) => {
|
|
674
|
-
let current: Entry | undefined = entry;
|
|
675
|
-
while (current) {
|
|
676
|
-
const policy = (
|
|
677
|
-
await resolveProfile(
|
|
678
|
-
current.input.profile.name,
|
|
679
|
-
input.context.cwd,
|
|
680
|
-
input.context.isProjectTrusted(),
|
|
681
|
-
undefined,
|
|
682
|
-
current.input.availableSkills?.() ?? current.input.skills,
|
|
683
|
-
)
|
|
684
|
-
).profile;
|
|
685
|
-
if (
|
|
686
|
-
(current.input.profile.models !== undefined && policy.models === undefined) ||
|
|
687
|
-
!allowsModel(policy, selected) ||
|
|
688
|
-
!admittedModel(current, selected)
|
|
689
|
-
)
|
|
690
|
-
throw new Error("Current or admitted model policy denied selection");
|
|
691
|
-
if (
|
|
692
|
-
current === entry &&
|
|
693
|
-
requirePrewalk &&
|
|
694
|
-
(!policy.prewalk || !sameSelection(policy.prewalk, entry.state.prewalk!.target))
|
|
695
|
-
)
|
|
696
|
-
throw new Error("Current prewalk grant revoked");
|
|
697
|
-
current = current.input.parent ? this.entry(current.input.parent) : undefined;
|
|
698
|
-
}
|
|
699
|
-
validateModelSelection(input.context, selected);
|
|
700
745
|
};
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
746
|
+
while (current) {
|
|
747
|
+
const { profile } = await resolveProfile(
|
|
748
|
+
current.input.profile.name,
|
|
749
|
+
input.context.cwd,
|
|
750
|
+
input.context.isProjectTrusted(),
|
|
751
|
+
undefined,
|
|
752
|
+
current.input.availableSkills?.() ?? current.input.skills,
|
|
753
|
+
);
|
|
754
|
+
if (
|
|
755
|
+
(current.input.profile.models !== undefined && profile.models === undefined) ||
|
|
756
|
+
!allowsModel(profile, selected) ||
|
|
757
|
+
!admittedModel(current, selected)
|
|
758
|
+
)
|
|
759
|
+
throw new Error("Resume denied: current or admitted model policy revoked");
|
|
760
|
+
if (descendant && !profile.spawn?.includes(descendant.input.profile.name))
|
|
761
|
+
throw new Error("Resume denied: ancestor spawn role revoked");
|
|
762
|
+
// Reject revoked grants instead of rebuilding an SDK tool/resource graph. This never
|
|
763
|
+
// broadens the original admission and leaves the paused conversation recoverable.
|
|
764
|
+
const revoked = broadenedGrant(input.profile, profile);
|
|
765
|
+
if (revoked) throw new Error(`Resume denied: current ${revoked} grant revoked`);
|
|
766
|
+
if (
|
|
767
|
+
current === entry &&
|
|
768
|
+
entry.state.prewalk?.status === "armed" &&
|
|
769
|
+
(!profile.prewalk || !sameSelection(profile.prewalk, entry.state.prewalk.target))
|
|
770
|
+
)
|
|
771
|
+
throw new Error("Resume denied: prewalk grant revoked");
|
|
772
|
+
descendant = current;
|
|
773
|
+
current = current.input.parent ? this.entry(current.input.parent) : undefined;
|
|
774
|
+
}
|
|
775
|
+
validateModelSelection(input.context, selected);
|
|
776
|
+
const auth = await input.context.modelRegistry.getApiKeyAndHeaders(
|
|
777
|
+
validateModelSelection(input.context, selected),
|
|
731
778
|
);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
noSkills: true,
|
|
738
|
-
noPromptTemplates: true,
|
|
739
|
-
noThemes: true,
|
|
740
|
-
noContextFiles: true,
|
|
741
|
-
extensionFactories: [
|
|
742
|
-
(pi) => {
|
|
743
|
-
pi.on("turn_start", async () => {
|
|
744
|
-
try {
|
|
745
|
-
await revalidate(effective(), entry.state.prewalk?.status === "armed");
|
|
746
|
-
} catch (error) {
|
|
747
|
-
policyError = String(error);
|
|
748
|
-
}
|
|
749
|
-
});
|
|
750
|
-
pi.on("turn_end", async (event) => {
|
|
751
|
-
if (
|
|
752
|
-
!entry.cancelled &&
|
|
753
|
-
!this.closing &&
|
|
754
|
-
entry.state.prewalk?.status === "armed" &&
|
|
755
|
-
event.toolResults.some(
|
|
756
|
-
(result) =>
|
|
757
|
-
(result.toolName === "edit" || result.toolName === "write") &&
|
|
758
|
-
input.profile.tools.includes(result.toolName),
|
|
759
|
-
)
|
|
760
|
-
) {
|
|
761
|
-
try {
|
|
762
|
-
const next = entry.state.prewalk.target;
|
|
763
|
-
await revalidate(next, true);
|
|
764
|
-
if (entry.cancelled || this.closing) return;
|
|
765
|
-
await entry.session!.setModel(target!);
|
|
766
|
-
entry.session!.setThinkingLevel(next.effort);
|
|
767
|
-
if (entry.session!.thinkingLevel !== next.effort)
|
|
768
|
-
throw new Error("SDK coerced prewalk effort");
|
|
769
|
-
this.update(entry, {
|
|
770
|
-
model: { provider: next.provider, id: next.id },
|
|
771
|
-
effort: next.effort,
|
|
772
|
-
prewalk: { target: next, status: "handed-off" },
|
|
773
|
-
});
|
|
774
|
-
} catch (error) {
|
|
775
|
-
policyError = String(error);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
},
|
|
780
|
-
],
|
|
781
|
-
});
|
|
782
|
-
await loader.reload();
|
|
783
|
-
resources.getExtensions = () => loader.getExtensions();
|
|
784
|
-
const tools: ToolDefinition[] = [
|
|
785
|
-
...input.profile.tools,
|
|
786
|
-
...(input.profile.leafTools ?? []),
|
|
787
|
-
].map((name) => {
|
|
788
|
-
const tool = input.context.tools.find((tool) => tool.name === name);
|
|
789
|
-
if (!tool) throw new Error(`Admitted parent tool is no longer callable: ${name}`);
|
|
790
|
-
return {
|
|
791
|
-
name,
|
|
792
|
-
label: name,
|
|
793
|
-
description: tool.description,
|
|
794
|
-
parameters: tool.parameters,
|
|
795
|
-
execute: async (_id, args, signal, onUpdate) => {
|
|
796
|
-
if (entry.cancelled || this.closing) throw new Error("Delegation cancelled");
|
|
797
|
-
if (!input.context.tools.some((tool) => tool.name === name))
|
|
798
|
-
throw new Error(`Admitted parent tool is no longer callable: ${name}`);
|
|
799
|
-
const builtin = input.profile.tools.includes(name as BuiltinTool);
|
|
800
|
-
// Re-read configuration for this generation and every ancestor before dispatch.
|
|
801
|
-
let current: Entry | undefined = entry;
|
|
802
|
-
while (current) {
|
|
803
|
-
const { profile } = await resolveProfile(
|
|
804
|
-
current.input.profile.name,
|
|
805
|
-
input.context.cwd,
|
|
806
|
-
input.context.isProjectTrusted(),
|
|
807
|
-
undefined,
|
|
808
|
-
current.input.availableSkills?.() ?? current.input.skills,
|
|
809
|
-
);
|
|
810
|
-
if (
|
|
811
|
-
builtin
|
|
812
|
-
? !profile.tools.includes(name as BuiltinTool)
|
|
813
|
-
: !profile.leafTools?.includes(name)
|
|
814
|
-
)
|
|
815
|
-
throw new Error(`Current capability grant revoked: ${name}`);
|
|
816
|
-
current = current.input.parent ? this.entry(current.input.parent) : undefined;
|
|
817
|
-
}
|
|
818
|
-
if (entry.cancelled || this.closing) throw new Error("Delegation cancelled");
|
|
819
|
-
if (builtin) input.assertTools([name]);
|
|
820
|
-
else {
|
|
821
|
-
const metadata = input.toolMetadata?.().find((tool) => tool.name === name);
|
|
822
|
-
if (
|
|
823
|
-
!metadata ||
|
|
824
|
-
leafDenial(
|
|
825
|
-
metadata,
|
|
826
|
-
input.profile.leafTools ?? [],
|
|
827
|
-
selection.cwd === resolveWorkspace(undefined, input.context.cwd),
|
|
828
|
-
) ||
|
|
829
|
-
toolProvenance(metadata) !== input.leafProvenance?.[name]
|
|
830
|
-
)
|
|
831
|
-
throw new Error(`Leaf capability revoked or reclassified: ${name}`);
|
|
832
|
-
}
|
|
833
|
-
if (!input.context.tools.some((tool) => tool.name === name))
|
|
834
|
-
throw new Error(`Admitted parent tool is no longer callable: ${name}`);
|
|
835
|
-
// The public parent dispatcher preserves validation and permission hooks. Do not
|
|
836
|
-
// call a raw create*Tool.execute here: that would bypass operator policy.
|
|
837
|
-
const outcome = await input.context.executeTool(
|
|
838
|
-
name,
|
|
839
|
-
builtin
|
|
840
|
-
? workspaceArguments(name as BuiltinTool, args, selection.cwd, input.context.cwd)
|
|
841
|
-
: args,
|
|
842
|
-
{ signal, onUpdate },
|
|
843
|
-
);
|
|
844
|
-
return { ...outcome.result, isError: outcome.isError };
|
|
845
|
-
},
|
|
846
|
-
};
|
|
847
|
-
});
|
|
848
|
-
if (entry.validator) {
|
|
849
|
-
const submission: ToolDefinition<typeof submissionParameters> = {
|
|
850
|
-
name: "submit_result",
|
|
851
|
-
label: "Submit result",
|
|
852
|
-
exposure: "model-only",
|
|
853
|
-
description: `Submit the assignment's required structured JSON value exactly once. Invalid values return errors and can be corrected within normal limits. Submission is provisional until prompt/tools/descendants drain; it never overrides cancellation or failure. Required schema: ${JSON.stringify(input.resultSchema)}`,
|
|
854
|
-
parameters: submissionParameters,
|
|
855
|
-
execute: async (_id, args, signal) => {
|
|
856
|
-
signal?.throwIfAborted();
|
|
857
|
-
if (
|
|
858
|
-
entry.cancelled ||
|
|
859
|
-
this.closing ||
|
|
860
|
-
entry.state.disposed ||
|
|
861
|
-
entry.state.status !== "running"
|
|
862
|
-
)
|
|
863
|
-
throw new Error("Submission identity is no longer running");
|
|
864
|
-
const outcome = entry.state.outcome!;
|
|
865
|
-
if (outcome.validation === "submitted")
|
|
866
|
-
throw new Error("A result was already submitted; duplicate submission rejected");
|
|
867
|
-
const checked = entry.validator!.validate(args.value);
|
|
868
|
-
if (!checked.ok) {
|
|
869
|
-
const errors = checked.errors
|
|
870
|
-
.map((error) => `${error.path || "/"}: ${error.message}`)
|
|
871
|
-
.slice(0, 32);
|
|
872
|
-
this.update(entry, {
|
|
873
|
-
outcome: {
|
|
874
|
-
validation: "invalid",
|
|
875
|
-
attempts: outcome.attempts + 1,
|
|
876
|
-
errors,
|
|
877
|
-
finalized: false,
|
|
878
|
-
},
|
|
879
|
-
});
|
|
880
|
-
throw new Error(
|
|
881
|
-
"Invalid structured result; correct and resubmit: " + errors.join("; "),
|
|
882
|
-
);
|
|
883
|
-
}
|
|
884
|
-
this.update(entry, {
|
|
885
|
-
outcome: {
|
|
886
|
-
validation: "submitted",
|
|
887
|
-
attempts: outcome.attempts + 1,
|
|
888
|
-
errors: [],
|
|
889
|
-
value: structuredClone(args.value),
|
|
890
|
-
finalized: false,
|
|
891
|
-
},
|
|
892
|
-
});
|
|
893
|
-
return {
|
|
894
|
-
content: [
|
|
895
|
-
{
|
|
896
|
-
type: "text",
|
|
897
|
-
text: "Structured value submitted; final outcome awaits prompt/tool/descendant drain.",
|
|
898
|
-
},
|
|
899
|
-
],
|
|
900
|
-
details: {},
|
|
901
|
-
};
|
|
902
|
-
},
|
|
903
|
-
};
|
|
904
|
-
tools.push(submission);
|
|
779
|
+
if (!auth.ok) throw new Error(auth.error);
|
|
780
|
+
if (entry.state.prewalk?.status === "armed") {
|
|
781
|
+
const target = validateModelSelection(input.context, entry.state.prewalk.target);
|
|
782
|
+
const targetAuth = await input.context.modelRegistry.getApiKeyAndHeaders(target);
|
|
783
|
+
if (!targetAuth.ok) throw new Error(targetAuth.error);
|
|
905
784
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
785
|
+
input.assertTools(input.profile.tools);
|
|
786
|
+
for (const name of [...input.profile.tools, ...(input.profile.leafTools ?? [])]) {
|
|
787
|
+
if (!input.context.tools.some((tool) => tool.name === name))
|
|
788
|
+
throw new Error(`Resume denied: admitted tool no longer callable: ${name}`);
|
|
789
|
+
if (input.profile.leafTools?.includes(name)) {
|
|
790
|
+
const metadata = input.toolMetadata?.().find((tool) => tool.name === name);
|
|
791
|
+
if (
|
|
792
|
+
!metadata ||
|
|
793
|
+
leafDenial(
|
|
794
|
+
metadata,
|
|
795
|
+
input.profile.leafTools,
|
|
796
|
+
entry.selection.cwd === resolveWorkspace(undefined, input.context.cwd),
|
|
797
|
+
) ||
|
|
798
|
+
toolProvenance(metadata) !== input.leafProvenance?.[name]
|
|
799
|
+
)
|
|
800
|
+
throw new Error(`Resume denied: leaf capability revoked or reclassified: ${name}`);
|
|
801
|
+
}
|
|
923
802
|
}
|
|
924
|
-
const { session, modelFallbackMessage } = await createAgentSession({
|
|
925
|
-
cwd: selection.cwd,
|
|
926
|
-
agentDir,
|
|
927
|
-
modelRuntime: runtime,
|
|
928
|
-
model: selection.model,
|
|
929
|
-
thinkingLevel: selection.thinkingLevel,
|
|
930
|
-
scopedModels: [
|
|
931
|
-
{ model: selection.model!, thinkingLevel: selection.thinkingLevel },
|
|
932
|
-
...(target ? [{ model: target, thinkingLevel: input.profile.prewalk!.effort }] : []),
|
|
933
|
-
],
|
|
934
|
-
resourceLoader: resources,
|
|
935
|
-
tools: tools.map((tool) => tool.name),
|
|
936
|
-
customTools: tools,
|
|
937
|
-
settingsManager: SettingsManager.inMemory({
|
|
938
|
-
compaction: { enabled: false },
|
|
939
|
-
retry: { enabled: false },
|
|
940
|
-
}),
|
|
941
|
-
sessionManager: SessionManager.create(selection.cwd, join(taskDir, "transcripts")),
|
|
942
|
-
});
|
|
943
|
-
entry.session = session;
|
|
944
|
-
if (session.thinkingLevel !== selection.thinkingLevel)
|
|
945
|
-
throw new Error("SDK did not preserve selected effort");
|
|
946
803
|
if (
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
804
|
+
(entry.state.telemetry.requests ?? 0) >= limits.requests ||
|
|
805
|
+
entry.state.toolCalls >= limits.tools ||
|
|
806
|
+
entry.spawned > limits.spawns
|
|
950
807
|
)
|
|
951
|
-
throw new Error("
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
.join("");
|
|
974
|
-
for (const message of this.coordinationMessages)
|
|
975
|
-
if (
|
|
976
|
-
message.recipient !== "root" &&
|
|
977
|
-
this.key(message.recipient) === this.key(entry.state) &&
|
|
978
|
-
text ===
|
|
979
|
-
`Delegation message ${message.id} from ${message.sender === "root" ? "root" : this.key(message.sender)}: ${message.text}`
|
|
980
|
-
)
|
|
981
|
-
message.status = "delivered";
|
|
982
|
-
this.notifyCoordination();
|
|
983
|
-
}
|
|
984
|
-
if (event.type === "tool_execution_start") patch.activity = `Tool: ${event.toolName}`;
|
|
985
|
-
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
986
|
-
const stats = session.getSessionStats();
|
|
987
|
-
patch.telemetry = {
|
|
988
|
-
...entry.state.telemetry,
|
|
989
|
-
tokens: stats.tokens,
|
|
990
|
-
cost: stats.cost,
|
|
991
|
-
context: session.getContextUsage() ?? null,
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
this.update(entry, patch, event);
|
|
808
|
+
throw new Error("Resume denied: cumulative execution budget exhausted");
|
|
809
|
+
if (entry.state.depth > limits.depth)
|
|
810
|
+
throw new Error("Resume denied: delegation depth limit");
|
|
811
|
+
// Reuse the original admission's writer and nested no-lending checks, without
|
|
812
|
+
// admitting another task or charging another spawn.
|
|
813
|
+
this.validate(input);
|
|
814
|
+
this.renewalParent(ref);
|
|
815
|
+
const available =
|
|
816
|
+
this.active().length < policy.concurrency &&
|
|
817
|
+
(!isWriter(input.profile) || (!this.writer && !this.parentMutations.size));
|
|
818
|
+
if (parent && (this.queue.size || !available))
|
|
819
|
+
throw new Error("Nested capacity unavailable; no permit lending");
|
|
820
|
+
this.assertQueueCapacity([input]);
|
|
821
|
+
if (
|
|
822
|
+
entry.cancelled ||
|
|
823
|
+
this.closing ||
|
|
824
|
+
entry.state.disposed ||
|
|
825
|
+
entry.state.status !== "paused"
|
|
826
|
+
)
|
|
827
|
+
throw new Error("Resume cancelled by stop or shutdown");
|
|
828
|
+
entry.done = new Promise<void>((resolve) => {
|
|
829
|
+
entry.finish = resolve;
|
|
995
830
|
});
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
entry
|
|
1003
|
-
.prompt(input.assignment, { expandPromptTemplates: false })
|
|
1004
|
-
.then(async () => {
|
|
1005
|
-
await this.stopMany(this.children(entry).map((child) => child.state));
|
|
1006
|
-
const last = session.messages.filter((message) => message.role === "assistant").at(-1);
|
|
1007
|
-
const failed = !last || last.stopReason === "error";
|
|
1008
|
-
const stats = session.getSessionStats();
|
|
1009
|
-
this.update(entry, { status: "draining", activity: "Finalizing outcome artifacts" });
|
|
1010
|
-
const result = session.getLastAssistantText() ?? "";
|
|
1011
|
-
const artifacts: NonNullable<ChildSnapshot["artifacts"]> = {};
|
|
1012
|
-
try {
|
|
1013
|
-
artifacts.text = await saveOutcomeArtifact(
|
|
1014
|
-
taskDir,
|
|
1015
|
-
"result.txt",
|
|
1016
|
-
last?.content
|
|
1017
|
-
.filter((block) => block.type === "text")
|
|
1018
|
-
.map((block) => block.text)
|
|
1019
|
-
.join("") ?? "",
|
|
1020
|
-
);
|
|
1021
|
-
if (entry.state.outcome?.validation === "submitted")
|
|
1022
|
-
artifacts.structured = await saveOutcomeArtifact(
|
|
1023
|
-
taskDir,
|
|
1024
|
-
"structured.json",
|
|
1025
|
-
JSON.stringify(entry.state.outcome.value),
|
|
1026
|
-
);
|
|
1027
|
-
} catch (error) {
|
|
1028
|
-
artifacts.error = `Outcome artifact persistence failed: ${String(error)}`;
|
|
1029
|
-
}
|
|
1030
|
-
const missing = entry.validator && entry.state.outcome?.validation !== "submitted";
|
|
1031
|
-
this.update(entry, {
|
|
1032
|
-
artifacts,
|
|
1033
|
-
outcome: entry.state.outcome ? { ...entry.state.outcome, finalized: true } : undefined,
|
|
1034
|
-
status:
|
|
1035
|
-
entry.cancelled || last?.stopReason === "aborted"
|
|
1036
|
-
? "cancelled"
|
|
1037
|
-
: failed || missing || artifacts.error
|
|
1038
|
-
? "failed"
|
|
1039
|
-
: "settled",
|
|
1040
|
-
result,
|
|
1041
|
-
error: entry.state.limitReason
|
|
1042
|
-
? entry.state.error
|
|
1043
|
-
: failed
|
|
1044
|
-
? (last?.errorMessage ?? "No final assistant outcome")
|
|
1045
|
-
: missing
|
|
1046
|
-
? "Required structured result was not submitted successfully"
|
|
1047
|
-
: artifacts.error,
|
|
1048
|
-
activity: "Prompt completed",
|
|
1049
|
-
telemetry: {
|
|
1050
|
-
requests: entry.state.telemetry.requests,
|
|
1051
|
-
tokens: stats.assistantMessages ? stats.tokens : null,
|
|
1052
|
-
cost: stats.assistantMessages ? stats.cost : null,
|
|
1053
|
-
context: session.getContextUsage() ?? null,
|
|
1054
|
-
},
|
|
1055
|
-
});
|
|
1056
|
-
})
|
|
1057
|
-
.catch(async (error) => {
|
|
1058
|
-
await this.stopMany(this.children(entry).map((child) => child.state));
|
|
1059
|
-
this.update(entry, {
|
|
1060
|
-
status: entry.cancelled ? "cancelled" : "failed",
|
|
1061
|
-
error: entry.state.limitReason ? entry.state.error : String(error),
|
|
1062
|
-
});
|
|
1063
|
-
})
|
|
1064
|
-
.finally(() => this.dispose(entry));
|
|
1065
|
-
// prompt preparation precedes abort-signal ownership; acknowledge only abortable work.
|
|
1066
|
-
await Promise.race([started, entry.run]);
|
|
1067
|
-
} catch (error) {
|
|
831
|
+
entry.paused = false;
|
|
832
|
+
this.presence.set(entry.state.id, "active");
|
|
833
|
+
entry.pauseDrain = undefined;
|
|
834
|
+
entry.resumeText =
|
|
835
|
+
!entry.prompted && entry.resumeText ? entry.resumeText + "\n\n" + text : text;
|
|
836
|
+
entry.run = undefined;
|
|
837
|
+
this.queue.add(entry);
|
|
1068
838
|
this.update(entry, {
|
|
1069
|
-
status:
|
|
1070
|
-
|
|
839
|
+
status: "queued",
|
|
840
|
+
pauseReason: undefined,
|
|
841
|
+
error: undefined,
|
|
842
|
+
limits,
|
|
843
|
+
window: (entry.state.window ?? 1) + 1,
|
|
844
|
+
activity: "Continuation queued for admission",
|
|
1071
845
|
});
|
|
1072
|
-
|
|
1073
|
-
|
|
846
|
+
const window = entry.state.window;
|
|
847
|
+
entry.timer = setTimeout(() => {
|
|
848
|
+
if (entry.state.window === window) this.limit(entry, "deadline");
|
|
849
|
+
}, limits.deadlineMs);
|
|
850
|
+
this.pump();
|
|
851
|
+
return { id: ref.id, generation: ref.generation };
|
|
852
|
+
} finally {
|
|
853
|
+
entry.resuming = false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
private limit(entry: Entry, reason: string): void {
|
|
857
|
+
if (
|
|
858
|
+
entry.cancelled ||
|
|
859
|
+
entry.state.disposed ||
|
|
860
|
+
entry.state.limitReason ||
|
|
861
|
+
["settled", "failed", "cancelled"].includes(entry.state.status)
|
|
862
|
+
)
|
|
863
|
+
return;
|
|
864
|
+
if (reason === "deadline") {
|
|
865
|
+
void this.pause(entry.state);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
this.update(entry, { limitReason: reason, error: `Execution limit: ${reason}` });
|
|
869
|
+
void this.stop(entry.state);
|
|
870
|
+
}
|
|
871
|
+
private chargeToolCall(entry: Entry): void {
|
|
872
|
+
if (entry.state.toolCalls >= entry.state.limits.tools) {
|
|
873
|
+
this.limit(entry, "tools");
|
|
874
|
+
throw new Error("Tool call limit");
|
|
875
|
+
}
|
|
876
|
+
this.update(entry, { toolCalls: entry.state.toolCalls + 1 });
|
|
877
|
+
}
|
|
878
|
+
private async revalidateExecutionModel(
|
|
879
|
+
entry: Entry,
|
|
880
|
+
selected: ModelSelection,
|
|
881
|
+
requirePrewalk: boolean,
|
|
882
|
+
): Promise<void> {
|
|
883
|
+
const { input } = entry;
|
|
884
|
+
let current: Entry | undefined = entry;
|
|
885
|
+
while (current) {
|
|
886
|
+
const policy = (
|
|
887
|
+
await resolveProfile(
|
|
888
|
+
current.input.profile.name,
|
|
889
|
+
input.context.cwd,
|
|
890
|
+
input.context.isProjectTrusted(),
|
|
891
|
+
undefined,
|
|
892
|
+
current.input.availableSkills?.() ?? current.input.skills,
|
|
893
|
+
)
|
|
894
|
+
).profile;
|
|
895
|
+
if (
|
|
896
|
+
(current.input.profile.models !== undefined && policy.models === undefined) ||
|
|
897
|
+
!allowsModel(policy, selected) ||
|
|
898
|
+
!admittedModel(current, selected)
|
|
899
|
+
)
|
|
900
|
+
throw new Error("Current or admitted model policy denied selection");
|
|
901
|
+
if (
|
|
902
|
+
current === entry &&
|
|
903
|
+
requirePrewalk &&
|
|
904
|
+
(!policy.prewalk || !sameSelection(policy.prewalk, entry.state.prewalk!.target))
|
|
905
|
+
)
|
|
906
|
+
throw new Error("Current prewalk grant revoked");
|
|
907
|
+
current = current.input.parent ? this.entry(current.input.parent) : undefined;
|
|
908
|
+
}
|
|
909
|
+
validateModelSelection(input.context, selected);
|
|
910
|
+
}
|
|
911
|
+
private assertExecutionTool(
|
|
912
|
+
entry: Entry,
|
|
913
|
+
name: string,
|
|
914
|
+
builtin: boolean,
|
|
915
|
+
signal?: AbortSignal,
|
|
916
|
+
): void {
|
|
917
|
+
const { input, selection } = entry;
|
|
918
|
+
this.assertActive(entry);
|
|
919
|
+
signal?.throwIfAborted();
|
|
920
|
+
if (builtin) input.assertTools([name]);
|
|
921
|
+
else {
|
|
922
|
+
const metadata = input.toolMetadata?.().find((tool) => tool.name === name);
|
|
923
|
+
if (
|
|
924
|
+
!metadata ||
|
|
925
|
+
leafDenial(
|
|
926
|
+
metadata,
|
|
927
|
+
input.profile.leafTools ?? [],
|
|
928
|
+
selection.cwd === resolveWorkspace(undefined, input.context.cwd),
|
|
929
|
+
) ||
|
|
930
|
+
toolProvenance(metadata) !== input.leafProvenance?.[name]
|
|
931
|
+
)
|
|
932
|
+
throw new Error(`Leaf capability revoked or reclassified: ${name}`);
|
|
933
|
+
}
|
|
934
|
+
if (!input.context.tools.some((tool) => tool.name === name))
|
|
935
|
+
throw new Error(`Admitted parent tool is no longer callable: ${name}`);
|
|
936
|
+
}
|
|
937
|
+
private chargeExecutionRequest(entry: Entry): void {
|
|
938
|
+
const { input } = entry;
|
|
939
|
+
validateModelSelection(input.context, {
|
|
940
|
+
...entry.state.model,
|
|
941
|
+
effort: entry.state.effort as ModelSelection["effort"],
|
|
942
|
+
});
|
|
943
|
+
if (entry.cancelled || entry.paused || this.closing) throw new Error("Delegation cancelled");
|
|
944
|
+
if ((entry.state.telemetry.requests ?? 0) >= entry.state.limits.requests) {
|
|
945
|
+
this.limit(entry, "requests");
|
|
946
|
+
throw new Error("Direct request limit");
|
|
1074
947
|
}
|
|
948
|
+
this.update(entry, {
|
|
949
|
+
telemetry: {
|
|
950
|
+
...entry.state.telemetry,
|
|
951
|
+
requests: (entry.state.telemetry.requests ?? 0) + 1,
|
|
952
|
+
},
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
private observeExecutionMessage(entry: Entry, text: string): void {
|
|
956
|
+
for (const message of this.coordinationMessages)
|
|
957
|
+
if (
|
|
958
|
+
message.recipient !== "root" &&
|
|
959
|
+
this.key(message.recipient) === this.key(entry.state) &&
|
|
960
|
+
text ===
|
|
961
|
+
`Delegation message ${message.id} from ${message.sender === "root" ? "root" : this.key(message.sender)}: ${message.text}`
|
|
962
|
+
)
|
|
963
|
+
message.status = "delivered";
|
|
964
|
+
this.notifyCoordination();
|
|
965
|
+
}
|
|
966
|
+
private create(entry: Entry): Promise<void> {
|
|
967
|
+
const isClosing = () => this.closing;
|
|
968
|
+
return createChildExecution(this.root, entry, {
|
|
969
|
+
get closing() {
|
|
970
|
+
return isClosing();
|
|
971
|
+
},
|
|
972
|
+
update: (patch, event) => this.update(entry, patch, event),
|
|
973
|
+
revalidateModel: (selected, requirePrewalk) =>
|
|
974
|
+
this.revalidateExecutionModel(entry, selected, requirePrewalk),
|
|
975
|
+
authorizeTool: (name, builtin) =>
|
|
976
|
+
this.authorize(
|
|
977
|
+
entry.state,
|
|
978
|
+
(profile) =>
|
|
979
|
+
builtin
|
|
980
|
+
? profile.tools.includes(name as BuiltinTool)
|
|
981
|
+
: !!profile.leafTools?.includes(name),
|
|
982
|
+
`Current capability grant revoked: ${name}`,
|
|
983
|
+
),
|
|
984
|
+
assertTool: (name, builtin, signal) => this.assertExecutionTool(entry, name, builtin, signal),
|
|
985
|
+
chargeRequest: () => this.chargeExecutionRequest(entry),
|
|
986
|
+
chargeToolCall: () => this.chargeToolCall(entry),
|
|
987
|
+
observeUserMessage: (text) => this.observeExecutionMessage(entry, text),
|
|
988
|
+
drainDescendants: () => this.stopMany(this.children(entry).map((child) => child.state)),
|
|
989
|
+
dispose: () => this.dispose(entry),
|
|
990
|
+
});
|
|
1075
991
|
}
|
|
1076
992
|
private closeDescendantPresence(entry: Entry): void {
|
|
1077
993
|
for (const child of this.entries.values())
|
|
@@ -1093,11 +1009,17 @@ export class DelegationCoordinator {
|
|
|
1093
1009
|
// Retain immutable evidence, not the disposed SDK session graph, while idle/parked.
|
|
1094
1010
|
entry.unsubscribe = undefined;
|
|
1095
1011
|
entry.session = undefined;
|
|
1012
|
+
entry.prompt = undefined;
|
|
1013
|
+
entry.resumeText = undefined;
|
|
1014
|
+
entry.setup = undefined;
|
|
1015
|
+
entry.run = undefined;
|
|
1016
|
+
entry.pauseDrain = undefined;
|
|
1096
1017
|
if (this.writer === entry.state.id) this.writer = undefined;
|
|
1097
1018
|
this.closeDescendantPresence(entry);
|
|
1098
1019
|
this.presence.set(entry.state.id, entry.cancelled || this.closing ? "stopped" : "idle");
|
|
1099
1020
|
this.update(entry, {
|
|
1100
1021
|
disposed: true,
|
|
1022
|
+
retainedContext: false,
|
|
1101
1023
|
endedAt: Date.now(),
|
|
1102
1024
|
writerLease: false,
|
|
1103
1025
|
outcome: entry.state.outcome ? { ...entry.state.outcome, finalized: true } : undefined,
|
|
@@ -1108,7 +1030,10 @@ export class DelegationCoordinator {
|
|
|
1108
1030
|
private key(ref?: ChildRef): string {
|
|
1109
1031
|
return ref ? `${ref.id}/${ref.generation}` : "root";
|
|
1110
1032
|
}
|
|
1111
|
-
private actor(
|
|
1033
|
+
private async actor(
|
|
1034
|
+
ref: ChildRef | undefined,
|
|
1035
|
+
action: CoordinationAction,
|
|
1036
|
+
): Promise<Entry | undefined> {
|
|
1112
1037
|
if (this.closing) throw new Error("Parent shut down");
|
|
1113
1038
|
if (!ref) return;
|
|
1114
1039
|
const actor = this.entry(ref);
|
|
@@ -1116,6 +1041,12 @@ export class DelegationCoordinator {
|
|
|
1116
1041
|
throw new Error("Sender is no longer running");
|
|
1117
1042
|
if (!actor.input.profile.coordination?.includes(action))
|
|
1118
1043
|
throw new Error(`Coordination action denied: ${action}`);
|
|
1044
|
+
await this.authorize(
|
|
1045
|
+
ref,
|
|
1046
|
+
(profile) => !!profile.coordination?.includes(action),
|
|
1047
|
+
`Coordination action denied: ${action}`,
|
|
1048
|
+
);
|
|
1049
|
+
if (actor.state.status !== "running") throw new Error("Sender is no longer running");
|
|
1119
1050
|
return actor;
|
|
1120
1051
|
}
|
|
1121
1052
|
private target(actor: Entry | undefined, ref: ChildRef): Entry {
|
|
@@ -1140,8 +1071,11 @@ export class DelegationCoordinator {
|
|
|
1140
1071
|
throw new Error("Coordination target outside granted family");
|
|
1141
1072
|
return target;
|
|
1142
1073
|
}
|
|
1143
|
-
inbox(actor?: ChildRef): DelegationMessage[] {
|
|
1144
|
-
this.actor(actor, "inbox");
|
|
1074
|
+
async inbox(actor?: ChildRef, signal?: AbortSignal): Promise<DelegationMessage[]> {
|
|
1075
|
+
const sender = await this.actor(actor, "inbox");
|
|
1076
|
+
if (sender) this.assertActive(sender);
|
|
1077
|
+
if (this.closing) throw new Error("Parent shut down");
|
|
1078
|
+
signal?.throwIfAborted();
|
|
1145
1079
|
return this.takeInbox(actor);
|
|
1146
1080
|
}
|
|
1147
1081
|
async message(
|
|
@@ -1149,8 +1083,13 @@ export class DelegationCoordinator {
|
|
|
1149
1083
|
target: ChildRef | undefined,
|
|
1150
1084
|
text: string,
|
|
1151
1085
|
mode: "inbox" | "steer" | "followUp" = "inbox",
|
|
1086
|
+
signal?: AbortSignal,
|
|
1152
1087
|
): Promise<DelegationMessage> {
|
|
1153
|
-
|
|
1088
|
+
if (mode !== "inbox") requireSupportedHost();
|
|
1089
|
+
const sender = await this.actor(actor, "message");
|
|
1090
|
+
if (sender) this.assertActive(sender);
|
|
1091
|
+
if (this.closing) throw new Error("Parent shut down");
|
|
1092
|
+
signal?.throwIfAborted();
|
|
1154
1093
|
if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > MAX_MESSAGE_BYTES)
|
|
1155
1094
|
throw new Error("Message requires 1–4096 UTF-8 bytes");
|
|
1156
1095
|
if (!target && !sender) throw new Error("Root cannot message itself");
|
|
@@ -1215,7 +1154,10 @@ export class DelegationCoordinator {
|
|
|
1215
1154
|
signal?: AbortSignal,
|
|
1216
1155
|
messageId?: string,
|
|
1217
1156
|
): Promise<CoordinationWait> {
|
|
1218
|
-
const sender = this.actor(actor, "wait");
|
|
1157
|
+
const sender = await this.actor(actor, "wait");
|
|
1158
|
+
if (sender) this.assertActive(sender);
|
|
1159
|
+
if (signal?.aborted) return { status: "cancelled" };
|
|
1160
|
+
if (this.closing) return { status: "shutdown" };
|
|
1219
1161
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_WAIT_MS)
|
|
1220
1162
|
throw new Error("Wait timeout must be 1–60000ms");
|
|
1221
1163
|
if (this.waiterCount >= MAX_WAITERS) throw new Error("Parent waiter limit");
|
|
@@ -1297,6 +1239,7 @@ export class DelegationCoordinator {
|
|
|
1297
1239
|
if (
|
|
1298
1240
|
sender &&
|
|
1299
1241
|
(sender.cancelled ||
|
|
1242
|
+
sender.paused ||
|
|
1300
1243
|
sender.state.disposed ||
|
|
1301
1244
|
this.entries.get(sender.state.id) !== sender)
|
|
1302
1245
|
)
|
|
@@ -1318,7 +1261,7 @@ export class DelegationCoordinator {
|
|
|
1318
1261
|
});
|
|
1319
1262
|
}
|
|
1320
1263
|
} else if (destination) {
|
|
1321
|
-
if (destination.state.disposed)
|
|
1264
|
+
if (destination.state.disposed || destination.state.status === "paused")
|
|
1322
1265
|
return finish({
|
|
1323
1266
|
status: "outcome",
|
|
1324
1267
|
outcome: {
|
|
@@ -1326,6 +1269,9 @@ export class DelegationCoordinator {
|
|
|
1326
1269
|
generation: destination.state.generation,
|
|
1327
1270
|
status: destination.state.status,
|
|
1328
1271
|
limitReason: destination.state.limitReason,
|
|
1272
|
+
...(destination.state.pauseReason
|
|
1273
|
+
? { pauseReason: destination.state.pauseReason }
|
|
1274
|
+
: {}),
|
|
1329
1275
|
},
|
|
1330
1276
|
});
|
|
1331
1277
|
} else if ((this.mailboxes.get(sourceKey)?.length ?? 0) > 0)
|
|
@@ -1347,22 +1293,19 @@ export class DelegationCoordinator {
|
|
|
1347
1293
|
this.notifyCoordination();
|
|
1348
1294
|
return structuredClone(messages);
|
|
1349
1295
|
}
|
|
1350
|
-
taskInputRead(ref: ChildRef): void {
|
|
1296
|
+
async taskInputRead(ref: ChildRef, signal?: AbortSignal): Promise<void> {
|
|
1297
|
+
await this.authorize(
|
|
1298
|
+
ref,
|
|
1299
|
+
(profile) => profile.tools.includes("read"),
|
|
1300
|
+
"Delegating parent has no active read grant",
|
|
1301
|
+
);
|
|
1351
1302
|
const entry = this.entry(ref);
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
this.closing ||
|
|
1356
|
-
!entry.input.profile.tools.includes("read")
|
|
1357
|
-
)
|
|
1358
|
-
throw new Error("Delegating parent has no active read grant");
|
|
1359
|
-
if (entry.state.toolCalls >= entry.state.limits.tools) {
|
|
1360
|
-
this.limit(entry, "tools");
|
|
1361
|
-
throw new Error("Tool call limit");
|
|
1362
|
-
}
|
|
1363
|
-
this.update(entry, { toolCalls: entry.state.toolCalls + 1 });
|
|
1303
|
+
this.assertActive(entry);
|
|
1304
|
+
signal?.throwIfAborted();
|
|
1305
|
+
this.chargeToolCall(entry);
|
|
1364
1306
|
}
|
|
1365
1307
|
async steer(ref: ChildRef, text: string): Promise<"queued" | "handled"> {
|
|
1308
|
+
requireSupportedHost();
|
|
1366
1309
|
const entry = this.entry(ref);
|
|
1367
1310
|
if (!text.trim()) throw new Error("Nonempty steering text is required");
|
|
1368
1311
|
if (entry.state.status !== "running" || entry.cancelled) throw new Error("No running child");
|
|
@@ -1401,6 +1344,7 @@ export class DelegationCoordinator {
|
|
|
1401
1344
|
await Promise.all([descendants, entry.state.disposed ? undefined : entry.session?.abort()]);
|
|
1402
1345
|
if (entry.state.disposed) return;
|
|
1403
1346
|
await entry.run;
|
|
1347
|
+
if (!entry.state.disposed) this.update(entry, { status: "cancelled" });
|
|
1404
1348
|
this.dispose(entry);
|
|
1405
1349
|
}
|
|
1406
1350
|
async wait(ref: ChildRef): Promise<ChildSnapshot> {
|