@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
package/src/stateful.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateful registration remains one lifecycle owner so session replacement, persistence,
|
|
3
|
+
* capability revocation, workspace cleanup, and completion delivery cannot reorder.
|
|
4
|
+
*/
|
|
1
5
|
import { randomUUID } from "node:crypto";
|
|
2
6
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
|
-
import {
|
|
4
|
-
defineTool,
|
|
5
|
-
type ExtensionAPI,
|
|
6
|
-
type ExtensionContext,
|
|
7
|
-
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Type } from "typebox";
|
|
9
9
|
import {
|
|
10
10
|
type AgentScope,
|
|
@@ -13,38 +13,54 @@ import {
|
|
|
13
13
|
isThinkingLevel,
|
|
14
14
|
type SubagentRuntimeSettings,
|
|
15
15
|
type SubagentSettings,
|
|
16
|
+
type SubagentTransportKind,
|
|
16
17
|
THINKING_LEVELS,
|
|
17
18
|
} from "./agents.js";
|
|
19
|
+
import { issueCapabilityGrant } from "./capability-grant.js";
|
|
20
|
+
import { CompletionDeliveryBroker } from "./completion-delivery.js";
|
|
18
21
|
import { buildContextSnapshot, type ContextMode, redactPrivateText } from "./context.js";
|
|
22
|
+
import { createStatefulTransport } from "./create-stateful-transport.js";
|
|
19
23
|
import {
|
|
20
24
|
assertDelegationTargetAllowed,
|
|
21
25
|
resolveSubagentTarget,
|
|
22
26
|
targetPolicyAudit,
|
|
23
27
|
} from "./cwd-policy.js";
|
|
28
|
+
import { DelegationContractSchema, normalizeDelegationContract } from "./delegation-contract.js";
|
|
24
29
|
import { assertSubagentDepthAllowed } from "./execution.js";
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
InProcessTransport,
|
|
28
|
-
type ParentRuntimeSnapshot,
|
|
29
|
-
} from "./in-process-transport.js";
|
|
30
|
-
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
30
|
+
import type { ChildSessionFactory, ParentRuntimeSnapshot } from "./in-process-transport.js";
|
|
31
|
+
import { DEFAULT_MAX_CONTEXT_BYTES, MAX_SUBAGENT_TIMEOUT_MS, truncateUtf8 } from "./limits.js";
|
|
31
32
|
import { AgentPersistence } from "./persistence.js";
|
|
32
33
|
import {
|
|
33
34
|
AgentRegistry,
|
|
34
35
|
type AgentRunInspectionDetail,
|
|
35
36
|
type AgentRunInspectionSummary,
|
|
36
|
-
type AgentTurnCompletion,
|
|
37
37
|
type ManagedAgent,
|
|
38
38
|
} from "./registry.js";
|
|
39
|
+
import { SUBAGENT_RESULT_FORMATS, type SubagentResultFormat } from "./result-contract.js";
|
|
40
|
+
import { buildRetainedSemanticState } from "./retained-semantic-state.js";
|
|
41
|
+
import { evaluateSemanticCompatibility } from "./semantic-snapshot.js";
|
|
39
42
|
import { DEFAULT_DELEGATION_CWD_POLICY, readSubagentSettings } from "./settings.js";
|
|
43
|
+
import {
|
|
44
|
+
assertSpawnIdempotencyKey,
|
|
45
|
+
hashSpawnRequest,
|
|
46
|
+
MAX_SPAWN_IDEMPOTENCY_KEY_LENGTH,
|
|
47
|
+
} from "./spawn-idempotency.js";
|
|
48
|
+
import { resolveCompletionDelivery, resolveStatefulTransportKind } from "./stateful-config.js";
|
|
40
49
|
import { createSpawnPromptGuidelines } from "./stateful-guidance.js";
|
|
41
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
assertCurrentSpawn,
|
|
52
|
+
cleanupPersistedWorkspaces,
|
|
53
|
+
disposeStatefulRuntime,
|
|
54
|
+
waitForOwnedSpawn,
|
|
55
|
+
} from "./stateful-lifecycle.js";
|
|
56
|
+
import { resolveStatefulLimits, type StatefulLimits } from "./stateful-limits.js";
|
|
42
57
|
import { createStatefulToolRenderer } from "./stateful-render.js";
|
|
43
58
|
import {
|
|
44
59
|
assertFollowUpWriteAllowed,
|
|
45
60
|
assertNoSharedWriteConflict,
|
|
46
61
|
confirmProjectAgent,
|
|
47
62
|
} from "./stateful-safety.js";
|
|
63
|
+
import { MAX_SUBAGENT_TOOL_CALLS, MAX_SUBAGENT_TURNS } from "./turn-budget.js";
|
|
48
64
|
|
|
49
65
|
export {
|
|
50
66
|
assertFollowUpWriteAllowed,
|
|
@@ -58,7 +74,6 @@ import {
|
|
|
58
74
|
validateMailboxParams,
|
|
59
75
|
validateManageParams,
|
|
60
76
|
} from "./stateful-tool-params.js";
|
|
61
|
-
import { SubprocessTransport } from "./subprocess-transport.js";
|
|
62
77
|
import { WorkspaceManager } from "./workspace.js";
|
|
63
78
|
|
|
64
79
|
const ContextModeSchema = Type.Union([
|
|
@@ -74,10 +89,37 @@ const StatefulThinkingLevelSchema = StringEnum(THINKING_LEVELS, {
|
|
|
74
89
|
description:
|
|
75
90
|
"Optional requested Pi thinking level selected for this task difficulty; retained for every turn of the spawned agent.",
|
|
76
91
|
});
|
|
92
|
+
const StatefulTimeoutSchema = Type.Integer({
|
|
93
|
+
minimum: 1,
|
|
94
|
+
maximum: MAX_SUBAGENT_TIMEOUT_MS,
|
|
95
|
+
description:
|
|
96
|
+
"Work deadline in milliseconds selected for the task difficulty. On expiry, Pi aborts the work and makes one separately bounded summary attempt. Retained as the agent default.",
|
|
97
|
+
});
|
|
98
|
+
const StatefulTurnLimitFields = {
|
|
99
|
+
idleTimeoutMs: Type.Optional(
|
|
100
|
+
Type.Integer({
|
|
101
|
+
minimum: 1,
|
|
102
|
+
maximum: MAX_SUBAGENT_TIMEOUT_MS,
|
|
103
|
+
description:
|
|
104
|
+
"Maximum milliseconds without a completed assistant turn or tool result; retained as the agent default.",
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
maxTurns: Type.Optional(
|
|
108
|
+
Type.Integer({
|
|
109
|
+
minimum: 1,
|
|
110
|
+
maximum: MAX_SUBAGENT_TURNS,
|
|
111
|
+
description: "Maximum unfinished assistant turns; retained as the agent default.",
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
maxToolCalls: Type.Optional(
|
|
115
|
+
Type.Integer({
|
|
116
|
+
minimum: 1,
|
|
117
|
+
maximum: MAX_SUBAGENT_TOOL_CALLS,
|
|
118
|
+
description: "Maximum tool calls; retained as the agent default.",
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
};
|
|
77
122
|
const MAX_TOOL_MESSAGE_BYTES = 2 * 1024;
|
|
78
|
-
const MAX_COMPLETION_ERROR_BYTES = 512;
|
|
79
|
-
const MAX_COMPLETIONS_PER_MESSAGE = 16;
|
|
80
|
-
const COMPLETION_BATCH_DELAY_MS = 10;
|
|
81
123
|
|
|
82
124
|
export interface StatefulSubagentDependencies {
|
|
83
125
|
blockingEnabled?: boolean;
|
|
@@ -90,8 +132,9 @@ export interface StatefulSubagentDependencies {
|
|
|
90
132
|
export interface StatefulSubagentRuntimeStatus {
|
|
91
133
|
enabled: boolean;
|
|
92
134
|
initialized: boolean;
|
|
93
|
-
transport:
|
|
135
|
+
transport: SubagentTransportKind;
|
|
94
136
|
completionDelivery: CompletionDelivery;
|
|
137
|
+
limits: StatefulLimits;
|
|
95
138
|
activeAgents: number;
|
|
96
139
|
retainedAgents: number;
|
|
97
140
|
}
|
|
@@ -124,6 +167,7 @@ export function registerStatefulSubagents(
|
|
|
124
167
|
const enabled = settings.enabled !== false;
|
|
125
168
|
const transportKind = resolveStatefulTransportKind(settings.transport);
|
|
126
169
|
let completionDelivery = resolveCompletionDelivery(settings.completionDelivery);
|
|
170
|
+
let runtimeLimits = resolveStatefulLimits(settings);
|
|
127
171
|
let agentCatalog = "";
|
|
128
172
|
let completionBroker: CompletionDeliveryBroker | undefined;
|
|
129
173
|
let refreshSpawnToolRegistration: (() => void) | undefined;
|
|
@@ -135,9 +179,17 @@ export function registerStatefulSubagents(
|
|
|
135
179
|
const workspaceManager = dependencies.workspaceManager ?? new WorkspaceManager();
|
|
136
180
|
const isolatedAgents = new Map<string, string>();
|
|
137
181
|
const seenMessageIds = new Set<string>();
|
|
182
|
+
type PendingIdempotentSpawn = {
|
|
183
|
+
generation: number;
|
|
184
|
+
requestHash: string;
|
|
185
|
+
promise: Promise<ManagedAgent>;
|
|
186
|
+
};
|
|
187
|
+
const pendingIdempotentSpawns = new Map<string, PendingIdempotentSpawn>();
|
|
138
188
|
const parentRuntime: ParentRuntimeSnapshot = { model: undefined, thinkingLevel: "off" };
|
|
139
189
|
const getCurrentSettings = () =>
|
|
140
190
|
dependencies.getSettings ? dependencies.getSettings() : readSubagentSettings();
|
|
191
|
+
const getCurrentStatefulSettings = () =>
|
|
192
|
+
dependencies.getSettings ? (dependencies.getSettings()?.stateful ?? {}) : settings;
|
|
141
193
|
|
|
142
194
|
const clearAgents = async (): Promise<number> => {
|
|
143
195
|
const generation = runtimeGeneration;
|
|
@@ -181,6 +233,7 @@ export function registerStatefulSubagents(
|
|
|
181
233
|
initialized: registry !== undefined,
|
|
182
234
|
transport: transportKind,
|
|
183
235
|
completionDelivery,
|
|
236
|
+
limits: { ...runtimeLimits },
|
|
184
237
|
...counts,
|
|
185
238
|
};
|
|
186
239
|
},
|
|
@@ -201,12 +254,6 @@ export function registerStatefulSubagents(
|
|
|
201
254
|
if (!registry) throw new Error("Stateful subagents are not initialized for this session");
|
|
202
255
|
return registry;
|
|
203
256
|
};
|
|
204
|
-
const requireAgent = (agentId: string) => {
|
|
205
|
-
const agent = requireRegistry().get(agentId);
|
|
206
|
-
if (!agent) throw new Error(`Unknown subagent: ${agentId}`);
|
|
207
|
-
return agent;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
257
|
pi.on("session_start", async (_event, ctx) => {
|
|
211
258
|
const generation = ++runtimeGeneration;
|
|
212
259
|
completionBroker?.close();
|
|
@@ -218,6 +265,7 @@ export function registerStatefulSubagents(
|
|
|
218
265
|
persistence = undefined;
|
|
219
266
|
isolatedAgents.clear();
|
|
220
267
|
seenMessageIds.clear();
|
|
268
|
+
pendingIdempotentSpawns.clear();
|
|
221
269
|
const initialize = async () => {
|
|
222
270
|
const cleanupErrors = await disposeStatefulRuntime(previousRegistry, workspaceManager);
|
|
223
271
|
if (generation !== runtimeGeneration) return;
|
|
@@ -229,44 +277,47 @@ export function registerStatefulSubagents(
|
|
|
229
277
|
}
|
|
230
278
|
parentRuntime.model = ctx.model;
|
|
231
279
|
parentRuntime.thinkingLevel = normalizeRuntimeThinkingLevel(pi.getThinkingLevel());
|
|
280
|
+
const sessionSettings = getCurrentStatefulSettings();
|
|
281
|
+
const nextLimits = resolveStatefulLimits(sessionSettings);
|
|
232
282
|
const owner =
|
|
233
283
|
ctx.sessionManager.getSessionId?.() ??
|
|
234
284
|
ctx.sessionManager.getSessionFile?.() ??
|
|
235
285
|
`ephemeral:${ctx.cwd}`;
|
|
236
286
|
const sessionPersistence = new AgentPersistence(owner, {
|
|
237
|
-
retentionDays:
|
|
238
|
-
maxStoredAgents:
|
|
287
|
+
retentionDays: sessionSettings.retentionDays,
|
|
288
|
+
maxStoredAgents: nextLimits.maxStoredAgents,
|
|
239
289
|
});
|
|
290
|
+
let nextRegistry: AgentRegistry;
|
|
240
291
|
const sessionBroker = new CompletionDeliveryBroker(pi, ctx, completionDelivery, {
|
|
241
292
|
onDeliveryError: (error) => {
|
|
242
293
|
if (!ctx.hasUI) return;
|
|
243
294
|
const reason = error instanceof Error ? error.message : String(error);
|
|
244
295
|
ctx.ui.notify(`Subagent completion delivery failed: ${reason}`, "warning");
|
|
245
296
|
},
|
|
297
|
+
onDelivered: (completions, deliveredAt) => {
|
|
298
|
+
if (generation !== runtimeGeneration) return;
|
|
299
|
+
for (const completion of completions) {
|
|
300
|
+
nextRegistry.markCompletionDelivered(completion.agent.id, deliveredAt);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
246
303
|
});
|
|
247
|
-
const transport =
|
|
248
|
-
transportKind
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
maxAgents: settings.maxAgents,
|
|
263
|
-
maxActiveTurns: settings.maxActiveTurns,
|
|
264
|
-
maxDepth: settings.maxDepth,
|
|
265
|
-
maxChildrenPerAgent: settings.maxChildrenPerAgent,
|
|
266
|
-
maxMailboxMessages: settings.maxMailboxMessages,
|
|
267
|
-
maxMailboxMessageBytes: settings.maxMailboxMessageBytes,
|
|
268
|
-
idleTtlMs: settings.idleTtlMs,
|
|
304
|
+
const transport = createStatefulTransport({
|
|
305
|
+
kind: transportKind,
|
|
306
|
+
modelRegistry: ctx.modelRegistry,
|
|
307
|
+
getParentRuntime: () => ({ ...parentRuntime }),
|
|
308
|
+
getSettings: getCurrentSettings,
|
|
309
|
+
createInProcessSession: dependencies.createInProcessSession,
|
|
310
|
+
});
|
|
311
|
+
nextRegistry = new AgentRegistry(transport, {
|
|
312
|
+
maxAgents: nextLimits.maxAgents,
|
|
313
|
+
maxActiveTurns: nextLimits.maxActiveTurns,
|
|
314
|
+
maxDepth: nextLimits.maxDepth,
|
|
315
|
+
maxChildrenPerAgent: nextLimits.maxChildrenPerAgent,
|
|
316
|
+
maxMailboxMessages: sessionSettings.maxMailboxMessages,
|
|
317
|
+
maxMailboxMessageBytes: sessionSettings.maxMailboxMessageBytes,
|
|
318
|
+
idleTtlMs: sessionSettings.idleTtlMs,
|
|
269
319
|
onChange: async (agents) => {
|
|
320
|
+
if (generation !== runtimeGeneration) return;
|
|
270
321
|
await sessionPersistence.save(agents);
|
|
271
322
|
if (generation !== runtimeGeneration) return;
|
|
272
323
|
for (const agent of agents) {
|
|
@@ -285,8 +336,12 @@ export function registerStatefulSubagents(
|
|
|
285
336
|
if (generation === runtimeGeneration) sessionBroker.enqueue(completion);
|
|
286
337
|
},
|
|
287
338
|
});
|
|
288
|
-
const
|
|
289
|
-
|
|
339
|
+
const persisted = sessionPersistence.load();
|
|
340
|
+
const orphanCleanupFailures = await cleanupPersistedWorkspaces(persisted, workspaceManager);
|
|
341
|
+
if (ctx.hasUI && orphanCleanupFailures > 0) {
|
|
342
|
+
ctx.ui.notify("Some orphaned subagent worktrees could not be cleaned", "warning");
|
|
343
|
+
}
|
|
344
|
+
const restored = persisted
|
|
290
345
|
.filter(
|
|
291
346
|
(agent) =>
|
|
292
347
|
agent.workspaceMode !== "worktree" &&
|
|
@@ -317,7 +372,12 @@ export function registerStatefulSubagents(
|
|
|
317
372
|
registry = nextRegistry;
|
|
318
373
|
persistence = sessionPersistence;
|
|
319
374
|
completionBroker = sessionBroker;
|
|
320
|
-
|
|
375
|
+
runtimeLimits = nextLimits;
|
|
376
|
+
refreshSpawnToolRegistration?.();
|
|
377
|
+
const sweepEveryMs = Math.max(
|
|
378
|
+
1_000,
|
|
379
|
+
Math.min(sessionSettings.idleTtlMs ?? 60 * 60 * 1000, 60_000),
|
|
380
|
+
);
|
|
321
381
|
sweepTimer = setInterval(() => {
|
|
322
382
|
void nextRegistry.sweepExpired().catch((error: unknown) => {
|
|
323
383
|
if (!ctx.hasUI || generation !== runtimeGeneration) return;
|
|
@@ -359,6 +419,7 @@ export function registerStatefulSubagents(
|
|
|
359
419
|
persistence = undefined;
|
|
360
420
|
isolatedAgents.clear();
|
|
361
421
|
seenMessageIds.clear();
|
|
422
|
+
pendingIdempotentSpawns.clear();
|
|
362
423
|
const shutdown = async () => {
|
|
363
424
|
const errors = await disposeStatefulRuntime(previousRegistry, workspaceManager);
|
|
364
425
|
if (errors.length > 0 && ctx.hasUI) {
|
|
@@ -371,7 +432,7 @@ export function registerStatefulSubagents(
|
|
|
371
432
|
});
|
|
372
433
|
|
|
373
434
|
const baseSpawnDescription = () =>
|
|
374
|
-
`Start an addressable background subagent with an optional thinking level chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously. Working-directory target policy: ${dependencies.getSettings?.()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`;
|
|
435
|
+
`Start an addressable background subagent with an optional thinking level and execution budgets chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously. Detached capacity: ${runtimeLimits.maxAgents} retained agents, ${runtimeLimits.maxActiveTurns} active turns, ${runtimeLimits.maxChildrenPerAgent} direct children per agent, and depth ${runtimeLimits.maxDepth}. Working-directory target policy: ${dependencies.getSettings?.()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`;
|
|
375
436
|
const spawnTool = defineTool({
|
|
376
437
|
name: "subagent_spawn",
|
|
377
438
|
label: "Spawn Subagent",
|
|
@@ -382,6 +443,8 @@ export function registerStatefulSubagents(
|
|
|
382
443
|
agent: Type.String({ minLength: 1 }),
|
|
383
444
|
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
384
445
|
thinkingLevel: Type.Optional(StatefulThinkingLevelSchema),
|
|
446
|
+
timeoutMs: Type.Optional(StatefulTimeoutSchema),
|
|
447
|
+
...StatefulTurnLimitFields,
|
|
385
448
|
cwd: Type.Optional(Type.String()),
|
|
386
449
|
agentScope: Type.Optional(ScopeSchema),
|
|
387
450
|
confirmProjectAgents: Type.Optional(Type.Boolean({ default: true })),
|
|
@@ -398,11 +461,33 @@ export function registerStatefulSubagents(
|
|
|
398
461
|
description: "Use the shared workspace or an opt-in disposable Git worktree.",
|
|
399
462
|
}),
|
|
400
463
|
),
|
|
464
|
+
contract: Type.Optional(DelegationContractSchema),
|
|
465
|
+
idempotencyKey: Type.Optional(
|
|
466
|
+
Type.String({
|
|
467
|
+
minLength: 1,
|
|
468
|
+
maxLength: MAX_SPAWN_IDEMPOTENCY_KEY_LENGTH,
|
|
469
|
+
description: "Reuse the same retained agent for an exact accepted spawn retry.",
|
|
470
|
+
}),
|
|
471
|
+
),
|
|
472
|
+
resultFormat: Type.Optional(
|
|
473
|
+
StringEnum(SUBAGENT_RESULT_FORMATS, {
|
|
474
|
+
description:
|
|
475
|
+
"Use text (default), structured-v1, or the evidence-preserving structured-v2 completion contract.",
|
|
476
|
+
}),
|
|
477
|
+
),
|
|
401
478
|
}),
|
|
402
479
|
...createStatefulToolRenderer("spawn"),
|
|
403
480
|
async execute(_id, params, signal, _update, ctx) {
|
|
404
481
|
const scope = (params.agentScope ?? "user") as AgentScope;
|
|
482
|
+
const resultFormat = (params.resultFormat ?? "text") as SubagentResultFormat;
|
|
483
|
+
const contract = normalizeDelegationContract(params.contract);
|
|
484
|
+
if (params.contract !== undefined && !contract) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
"subagent_spawn contract must be a valid pi-subagents:delegation:v2 object",
|
|
487
|
+
);
|
|
488
|
+
}
|
|
405
489
|
assertSubagentDepthAllowed();
|
|
490
|
+
assertSpawnIdempotencyKey(params.idempotencyKey);
|
|
406
491
|
const generation = runtimeGeneration;
|
|
407
492
|
const currentSettings = getCurrentSettings();
|
|
408
493
|
const target = resolveSubagentTarget({
|
|
@@ -415,22 +500,6 @@ export function registerStatefulSubagents(
|
|
|
415
500
|
currentSettings?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
|
|
416
501
|
);
|
|
417
502
|
const cwd = target.cwd;
|
|
418
|
-
await confirmProjectAgent(
|
|
419
|
-
params.agent,
|
|
420
|
-
scope,
|
|
421
|
-
params.confirmProjectAgents ?? true,
|
|
422
|
-
ctx,
|
|
423
|
-
cwd,
|
|
424
|
-
currentSettings,
|
|
425
|
-
);
|
|
426
|
-
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
427
|
-
const ownedRegistry = requireRegistry();
|
|
428
|
-
const resolvedAgent = discoverAgents(cwd, scope, currentSettings).agents.find(
|
|
429
|
-
(agent) => agent.name === params.agent,
|
|
430
|
-
);
|
|
431
|
-
if (params.workspaceMode === "worktree" && resolvedAgent?.source === "project") {
|
|
432
|
-
throw new Error("Project-local subagent definitions cannot run in a detached worktree");
|
|
433
|
-
}
|
|
434
503
|
const mode = resolveSpawnContextMode(params.context, params.contextEntryIds);
|
|
435
504
|
const snapshot = buildContextSnapshot(
|
|
436
505
|
ctx.sessionManager.getBranch(),
|
|
@@ -438,59 +507,181 @@ export function registerStatefulSubagents(
|
|
|
438
507
|
DEFAULT_MAX_CONTEXT_BYTES,
|
|
439
508
|
params.contextEntryIds,
|
|
440
509
|
);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
510
|
+
if ((scope === "project" || scope === "both") && !ctx.isProjectTrusted()) {
|
|
511
|
+
throw new Error("Project-local subagent definitions require a trusted project");
|
|
512
|
+
}
|
|
513
|
+
const resolvedAgents = discoverAgents(cwd, scope, currentSettings).agents;
|
|
514
|
+
const resolvedAgent = resolvedAgents.find((agent) => agent.name === params.agent);
|
|
515
|
+
if (!resolvedAgent) {
|
|
516
|
+
const available = resolvedAgents.map((agent) => agent.name).join(", ") || "none";
|
|
517
|
+
throw new Error(`Unknown subagent ${params.agent}. Available agents: ${available}`);
|
|
518
|
+
}
|
|
519
|
+
const targetSnapshot = targetPolicyAudit(target);
|
|
520
|
+
const { executionPlan, semanticSnapshot } = await buildRetainedSemanticState({
|
|
521
|
+
agent: resolvedAgent,
|
|
522
|
+
contract,
|
|
523
|
+
target: targetSnapshot,
|
|
524
|
+
cwd,
|
|
525
|
+
workspaceMode: params.workspaceMode === "worktree" ? "worktree" : "shared",
|
|
526
|
+
transport: transportKind,
|
|
527
|
+
resultFormat,
|
|
528
|
+
thinkingLevel: params.thinkingLevel,
|
|
529
|
+
timeoutMs: params.timeoutMs,
|
|
530
|
+
taskGeneration: 1,
|
|
531
|
+
});
|
|
532
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
533
|
+
const requestHash = hashSpawnRequest({
|
|
534
|
+
agent: params.agent,
|
|
535
|
+
task: params.task,
|
|
536
|
+
cwd,
|
|
537
|
+
agentScope: scope,
|
|
538
|
+
thinkingLevel: params.thinkingLevel,
|
|
539
|
+
timeoutMs: params.timeoutMs,
|
|
540
|
+
idleTimeoutMs: params.idleTimeoutMs,
|
|
541
|
+
maxTurns: params.maxTurns,
|
|
542
|
+
maxToolCalls: params.maxToolCalls,
|
|
543
|
+
parentId: params.parentId,
|
|
544
|
+
context: snapshot.text || undefined,
|
|
545
|
+
contextSourceIds: snapshot.sourceIds,
|
|
546
|
+
workspaceMode: params.workspaceMode ?? "shared",
|
|
547
|
+
allowConcurrentWrites: params.allowConcurrentWrites ?? false,
|
|
548
|
+
contract,
|
|
549
|
+
resultFormat,
|
|
550
|
+
});
|
|
551
|
+
const ownedRegistry = requireRegistry();
|
|
552
|
+
const retained = ownedRegistry.findBySpawnIdempotencyKey(params.idempotencyKey, requestHash);
|
|
553
|
+
if (retained) return result(retained, `Reused ${retained.agent} as ${retained.id}.`);
|
|
554
|
+
const foundPending = params.idempotencyKey
|
|
555
|
+
? pendingIdempotentSpawns.get(params.idempotencyKey)
|
|
556
|
+
: undefined;
|
|
557
|
+
if (
|
|
558
|
+
params.idempotencyKey &&
|
|
559
|
+
foundPending &&
|
|
560
|
+
foundPending.generation !== generation &&
|
|
561
|
+
pendingIdempotentSpawns.get(params.idempotencyKey) === foundPending
|
|
562
|
+
) {
|
|
563
|
+
pendingIdempotentSpawns.delete(params.idempotencyKey);
|
|
564
|
+
}
|
|
565
|
+
const pending = foundPending?.generation === generation ? foundPending : undefined;
|
|
566
|
+
if (pending) {
|
|
567
|
+
if (pending.requestHash !== requestHash) {
|
|
568
|
+
throw new Error("The subagent_spawn idempotencyKey is pending with different parameters");
|
|
569
|
+
}
|
|
570
|
+
const agent = await waitForOwnedSpawn(pending.promise, signal);
|
|
571
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
572
|
+
return result(agent, `Reused ${agent.agent} as ${agent.id}.`);
|
|
573
|
+
}
|
|
574
|
+
let resolvePending: ((agent: ManagedAgent) => void) | undefined;
|
|
575
|
+
let rejectPending: ((error: unknown) => void) | undefined;
|
|
576
|
+
let ownedPending: PendingIdempotentSpawn | undefined;
|
|
577
|
+
if (params.idempotencyKey) {
|
|
578
|
+
const promise = new Promise<ManagedAgent>((resolve, reject) => {
|
|
579
|
+
resolvePending = resolve;
|
|
580
|
+
rejectPending = reject;
|
|
581
|
+
});
|
|
582
|
+
void promise.catch(() => undefined);
|
|
583
|
+
ownedPending = { generation, requestHash, promise };
|
|
584
|
+
pendingIdempotentSpawns.set(params.idempotencyKey, ownedPending);
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
await confirmProjectAgent(
|
|
445
588
|
params.agent,
|
|
446
|
-
requestedCwd,
|
|
447
589
|
scope,
|
|
590
|
+
params.confirmProjectAgents ?? true,
|
|
591
|
+
ctx,
|
|
592
|
+
cwd,
|
|
448
593
|
currentSettings,
|
|
449
594
|
);
|
|
450
|
-
}
|
|
451
|
-
const workspaceOwner = `pending-${randomUUID()}`;
|
|
452
|
-
const workspace =
|
|
453
|
-
params.workspaceMode === "worktree"
|
|
454
|
-
? await workspaceManager.create(workspaceOwner, requestedCwd)
|
|
455
|
-
: undefined;
|
|
456
|
-
try {
|
|
457
595
|
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
596
|
+
if (params.workspaceMode === "worktree" && resolvedAgent.source === "project") {
|
|
597
|
+
throw new Error("Project-local subagent definitions cannot run in a detached worktree");
|
|
598
|
+
}
|
|
599
|
+
const requestedCwd = cwd;
|
|
600
|
+
if ((params.workspaceMode ?? "shared") === "shared" && !params.allowConcurrentWrites) {
|
|
601
|
+
assertNoSharedWriteConflict(
|
|
602
|
+
ownedRegistry,
|
|
603
|
+
params.agent,
|
|
604
|
+
requestedCwd,
|
|
605
|
+
scope,
|
|
606
|
+
currentSettings,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const workspaceOwner = `pending-${randomUUID()}`;
|
|
610
|
+
const workspace =
|
|
611
|
+
params.workspaceMode === "worktree"
|
|
612
|
+
? await workspaceManager.create(workspaceOwner, requestedCwd)
|
|
613
|
+
: undefined;
|
|
614
|
+
try {
|
|
615
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
616
|
+
} catch (error) {
|
|
617
|
+
if (workspace) await workspaceManager.cleanup(workspaceOwner);
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
let agent: ManagedAgent | undefined;
|
|
621
|
+
try {
|
|
622
|
+
const capabilityGrant = issueCapabilityGrant(
|
|
623
|
+
executionPlan,
|
|
624
|
+
Date.now(),
|
|
625
|
+
Math.max(1, (params.timeoutMs ?? resolvedAgent.timeoutMs ?? 600_000) + 60_000),
|
|
626
|
+
);
|
|
627
|
+
agent = await ownedRegistry.spawn({
|
|
628
|
+
agent: params.agent,
|
|
629
|
+
task: params.task,
|
|
630
|
+
cwd: workspace?.path ?? requestedCwd,
|
|
631
|
+
agentScope: scope,
|
|
632
|
+
thinkingLevel: params.thinkingLevel,
|
|
633
|
+
timeoutMs: params.timeoutMs,
|
|
634
|
+
idleTimeoutMs: params.idleTimeoutMs,
|
|
635
|
+
maxTurns: params.maxTurns,
|
|
636
|
+
maxToolCalls: params.maxToolCalls,
|
|
637
|
+
parentId: params.parentId,
|
|
638
|
+
context: snapshot.text || undefined,
|
|
639
|
+
contextSourceIds: snapshot.sourceIds,
|
|
640
|
+
contextTruncated: snapshot.truncated,
|
|
641
|
+
contextTurns: snapshot.turns,
|
|
642
|
+
contextBytes: Buffer.byteLength(snapshot.text, "utf8"),
|
|
643
|
+
workspaceMode: workspace ? "worktree" : undefined,
|
|
644
|
+
spawnIdempotencyKey: params.idempotencyKey,
|
|
645
|
+
spawnRequestHash: params.idempotencyKey ? requestHash : undefined,
|
|
646
|
+
contract,
|
|
647
|
+
resultFormat: resultFormat === "text" ? undefined : resultFormat,
|
|
648
|
+
executionPlan,
|
|
649
|
+
capabilityGrant,
|
|
650
|
+
semanticSnapshot,
|
|
651
|
+
semanticCompatibility: { status: "compatible", changedComponents: [] },
|
|
652
|
+
target: targetSnapshot,
|
|
653
|
+
});
|
|
654
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if (agent) await ownedRegistry.closeTree(agent.id).catch(() => undefined);
|
|
657
|
+
if (workspace) await workspaceManager.cleanup(workspaceOwner);
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
if (!agent) throw new Error("Subagent spawn completed without a retained agent");
|
|
661
|
+
if (workspace && agent.cwd === workspace.path) isolatedAgents.set(agent.id, workspaceOwner);
|
|
662
|
+
else if (workspace) await workspaceManager.cleanup(workspaceOwner);
|
|
478
663
|
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
664
|
+
resolvePending?.(agent);
|
|
665
|
+
const deliveryNote =
|
|
666
|
+
completionDelivery === "auto-resume"
|
|
667
|
+
? "If no useful local work remains, briefly tell the user what was launched and end the response; auto-resume will request synthesis after completion."
|
|
668
|
+
: "End the response without the result only when the current response does not depend on it; next-turn delivery will not wake an idle root.";
|
|
669
|
+
return result(
|
|
670
|
+
agent,
|
|
671
|
+
`Spawned ${agent.agent} as ${agent.id}. Do useful non-overlapping work immediately. ${deliveryNote} Do not poll for progress.`,
|
|
672
|
+
);
|
|
479
673
|
} catch (error) {
|
|
480
|
-
|
|
481
|
-
if (workspace) await workspaceManager.cleanup(workspaceOwner);
|
|
674
|
+
rejectPending?.(error);
|
|
482
675
|
throw error;
|
|
676
|
+
} finally {
|
|
677
|
+
if (
|
|
678
|
+
params.idempotencyKey &&
|
|
679
|
+
ownedPending &&
|
|
680
|
+
pendingIdempotentSpawns.get(params.idempotencyKey) === ownedPending
|
|
681
|
+
) {
|
|
682
|
+
pendingIdempotentSpawns.delete(params.idempotencyKey);
|
|
683
|
+
}
|
|
483
684
|
}
|
|
484
|
-
if (!agent) throw new Error("Subagent spawn completed without a retained agent");
|
|
485
|
-
if (workspace) isolatedAgents.set(agent.id, workspaceOwner);
|
|
486
|
-
const deliveryNote =
|
|
487
|
-
completionDelivery === "auto-resume"
|
|
488
|
-
? "If no useful local work remains, briefly tell the user what was launched and end the response; auto-resume will request synthesis after completion."
|
|
489
|
-
: "End the response without the result only when the current response does not depend on it; next-turn delivery will not wake an idle root.";
|
|
490
|
-
return result(
|
|
491
|
-
agent,
|
|
492
|
-
`Spawned ${agent.agent} as ${agent.id}. Do useful non-overlapping work immediately. ${deliveryNote} Do not poll for progress.`,
|
|
493
|
-
);
|
|
494
685
|
},
|
|
495
686
|
});
|
|
496
687
|
refreshSpawnToolRegistration = () => {
|
|
@@ -504,11 +695,26 @@ export function registerStatefulSubagents(
|
|
|
504
695
|
name: "subagent_send",
|
|
505
696
|
label: "Send Subagent Follow-up",
|
|
506
697
|
description:
|
|
507
|
-
"Send follow-up work to
|
|
698
|
+
"Send follow-up work to a reusable retained subagent and start a new turn. Semantic resource skew requires explicit revalidation. Use subagent_mailbox for queue-only messages.",
|
|
508
699
|
promptSnippet: "Start a new detached follow-up turn on a retained subagent",
|
|
509
700
|
parameters: Type.Object({
|
|
510
701
|
agentId: Type.String(),
|
|
511
702
|
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
703
|
+
timeoutMs: Type.Optional(
|
|
704
|
+
Type.Integer({
|
|
705
|
+
minimum: 1,
|
|
706
|
+
maximum: MAX_SUBAGENT_TIMEOUT_MS,
|
|
707
|
+
description:
|
|
708
|
+
"Optional work deadline for this follow-up turn. On expiry, Pi aborts the work and makes one separately bounded summary attempt.",
|
|
709
|
+
}),
|
|
710
|
+
),
|
|
711
|
+
...StatefulTurnLimitFields,
|
|
712
|
+
revalidate: Type.Optional(
|
|
713
|
+
Type.Boolean({
|
|
714
|
+
description:
|
|
715
|
+
"Accept the current semantic resource snapshot after a reported mismatch before starting this follow-up.",
|
|
716
|
+
}),
|
|
717
|
+
),
|
|
512
718
|
allowConcurrentWrites: Type.Optional(
|
|
513
719
|
Type.Boolean({ description: "Override the shared-workspace write conflict guard." }),
|
|
514
720
|
),
|
|
@@ -520,6 +726,64 @@ export function registerStatefulSubagents(
|
|
|
520
726
|
const currentSettings = getCurrentSettings();
|
|
521
727
|
const existing = ownedRegistry.get(params.agentId);
|
|
522
728
|
if (!existing) throw new Error(`Unknown subagent: ${params.agentId}`);
|
|
729
|
+
const currentAgent = discoverAgents(
|
|
730
|
+
existing.cwd,
|
|
731
|
+
existing.agentScope ?? "user",
|
|
732
|
+
currentSettings,
|
|
733
|
+
).agents.find((agent) => agent.name === existing.agent);
|
|
734
|
+
if (!currentAgent) throw new Error(`Unknown retained subagent definition: ${existing.agent}`);
|
|
735
|
+
const resolvedFollowUpTarget =
|
|
736
|
+
existing.workspaceMode === "worktree"
|
|
737
|
+
? undefined
|
|
738
|
+
: resolveSubagentTarget({
|
|
739
|
+
workspace: ctx.cwd,
|
|
740
|
+
requestedCwd: existing.cwd,
|
|
741
|
+
currentProjectTrusted: ctx.isProjectTrusted(),
|
|
742
|
+
});
|
|
743
|
+
if (resolvedFollowUpTarget) {
|
|
744
|
+
assertDelegationTargetAllowed(
|
|
745
|
+
resolvedFollowUpTarget,
|
|
746
|
+
currentSettings?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
if (existing.workspaceMode === "worktree" && !existing.target) {
|
|
750
|
+
throw new Error("Retained worktree is missing its trusted target snapshot");
|
|
751
|
+
}
|
|
752
|
+
const currentTarget =
|
|
753
|
+
existing.workspaceMode === "worktree" && existing.target
|
|
754
|
+
? existing.target
|
|
755
|
+
: targetPolicyAudit(resolvedFollowUpTarget as NonNullable<typeof resolvedFollowUpTarget>);
|
|
756
|
+
const { executionPlan: currentPlan, semanticSnapshot: currentSnapshot } =
|
|
757
|
+
await buildRetainedSemanticState({
|
|
758
|
+
agent: currentAgent,
|
|
759
|
+
contract: existing.contract,
|
|
760
|
+
target: currentTarget,
|
|
761
|
+
cwd: existing.cwd,
|
|
762
|
+
workspaceMode: existing.workspaceMode === "worktree" ? "worktree" : "shared",
|
|
763
|
+
transport: transportKind,
|
|
764
|
+
resultFormat: existing.resultFormat ?? "text",
|
|
765
|
+
thinkingLevel: existing.thinkingLevel,
|
|
766
|
+
timeoutMs: params.timeoutMs ?? existing.timeoutMs,
|
|
767
|
+
taskGeneration: (existing.executionPlan?.taskGeneration ?? 0) + 1,
|
|
768
|
+
cancellationLineage: [
|
|
769
|
+
...(existing.executionPlan?.cancellationLineage ?? []),
|
|
770
|
+
...(existing.state === "interrupted" && existing.executionPlan?.id
|
|
771
|
+
? [existing.executionPlan.id]
|
|
772
|
+
: []),
|
|
773
|
+
],
|
|
774
|
+
});
|
|
775
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
776
|
+
const compatibility = existing.semanticSnapshot
|
|
777
|
+
? evaluateSemanticCompatibility(existing.semanticSnapshot, currentSnapshot)
|
|
778
|
+
: { status: "warning" as const, changedComponents: ["legacy-missing-snapshot"] };
|
|
779
|
+
if (
|
|
780
|
+
(compatibility.status === "needs-revalidation" || compatibility.status === "rejected") &&
|
|
781
|
+
params.revalidate !== true
|
|
782
|
+
) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
`Subagent semantic resources changed; retry with revalidate: true after reviewing: ${compatibility.changedComponents.join(", ")}`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
523
787
|
await confirmProjectAgent(
|
|
524
788
|
existing.agent,
|
|
525
789
|
existing.agentScope ?? "user",
|
|
@@ -536,7 +800,26 @@ export function registerStatefulSubagents(
|
|
|
536
800
|
isolatedAgents.has(existing.id),
|
|
537
801
|
currentSettings,
|
|
538
802
|
);
|
|
539
|
-
const
|
|
803
|
+
const currentGrant = issueCapabilityGrant(
|
|
804
|
+
currentPlan,
|
|
805
|
+
Date.now(),
|
|
806
|
+
Math.max(1, (params.timeoutMs ?? existing.timeoutMs ?? 600_000) + 60_000),
|
|
807
|
+
);
|
|
808
|
+
await ownedRegistry.updateSemanticState(
|
|
809
|
+
existing.id,
|
|
810
|
+
currentPlan,
|
|
811
|
+
currentGrant,
|
|
812
|
+
currentSnapshot,
|
|
813
|
+
params.revalidate === true && compatibility.status !== "compatible"
|
|
814
|
+
? { status: "warning", changedComponents: compatibility.changedComponents }
|
|
815
|
+
: compatibility,
|
|
816
|
+
);
|
|
817
|
+
const agent = await ownedRegistry.followUp(params.agentId, params.task, {
|
|
818
|
+
timeoutMs: params.timeoutMs,
|
|
819
|
+
idleTimeoutMs: params.idleTimeoutMs,
|
|
820
|
+
maxTurns: params.maxTurns,
|
|
821
|
+
maxToolCalls: params.maxToolCalls,
|
|
822
|
+
});
|
|
540
823
|
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
541
824
|
return result(agent, `Started follow-up for ${agent.id}.`);
|
|
542
825
|
},
|
|
@@ -550,10 +833,17 @@ export function registerStatefulSubagents(
|
|
|
550
833
|
promptSnippet: "List or control retained detached subagents",
|
|
551
834
|
parameters: ManageParamsSchema,
|
|
552
835
|
...createStatefulToolRenderer("manage"),
|
|
553
|
-
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
836
|
+
async execute(_id, params, signal): Promise<StatefulActionToolResult> {
|
|
837
|
+
const generation = runtimeGeneration;
|
|
838
|
+
const ownedRegistry = requireRegistry();
|
|
839
|
+
const ownedAgent = (agentId: string): ManagedAgent => {
|
|
840
|
+
const value = ownedRegistry.get(agentId);
|
|
841
|
+
if (!value) throw new Error(`Unknown subagent: ${agentId}`);
|
|
842
|
+
return value;
|
|
843
|
+
};
|
|
554
844
|
const operation = validateManageParams(params);
|
|
555
845
|
if (operation.action === "list") {
|
|
556
|
-
const agents =
|
|
846
|
+
const agents = ownedRegistry.list(operation.includeClosed);
|
|
557
847
|
return {
|
|
558
848
|
content: [
|
|
559
849
|
{
|
|
@@ -567,46 +857,51 @@ export function registerStatefulSubagents(
|
|
|
567
857
|
const agentId = operation.agentId;
|
|
568
858
|
if (operation.action === "interrupt") {
|
|
569
859
|
if (operation.subtree) {
|
|
570
|
-
const agents = await
|
|
860
|
+
const agents = await ownedRegistry.interruptTree(agentId);
|
|
861
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
571
862
|
return {
|
|
572
863
|
content: [{ type: "text", text: `Interrupted ${agents.length} active agent(s).` }],
|
|
573
864
|
details: {
|
|
574
|
-
agent: summarizeAgent(
|
|
865
|
+
agent: summarizeAgent(ownedAgent(agentId)),
|
|
575
866
|
agents: agents.map(summarizeAgent),
|
|
576
867
|
},
|
|
577
868
|
};
|
|
578
869
|
}
|
|
579
|
-
const agent = await
|
|
870
|
+
const agent = await ownedRegistry.interrupt(agentId);
|
|
871
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
580
872
|
return result(agent, `Interrupted ${agent.id}; it remains reusable.`);
|
|
581
873
|
}
|
|
582
|
-
const existing =
|
|
874
|
+
const existing = ownedRegistry.get(agentId);
|
|
583
875
|
if (existing?.state === "closed" && !operation.subtree) {
|
|
584
876
|
const pendingOwner = isolatedAgents.get(existing.id);
|
|
585
877
|
if (pendingOwner) await workspaceManager.cleanup(pendingOwner);
|
|
878
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
586
879
|
isolatedAgents.delete(existing.id);
|
|
587
880
|
return result(existing, `Closed ${existing.id}.`);
|
|
588
881
|
}
|
|
589
882
|
if (operation.subtree) {
|
|
590
883
|
let agents: ManagedAgent[];
|
|
591
884
|
try {
|
|
592
|
-
agents = await
|
|
885
|
+
agents = await ownedRegistry.closeTree(agentId);
|
|
593
886
|
} finally {
|
|
594
|
-
await cleanupClosedWorkspaces(
|
|
887
|
+
await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, workspaceManager);
|
|
595
888
|
}
|
|
889
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
596
890
|
return {
|
|
597
891
|
content: [{ type: "text", text: `Closed ${agents.length} agent(s).` }],
|
|
598
892
|
details: {
|
|
599
|
-
agent: summarizeAgent(
|
|
893
|
+
agent: summarizeAgent(ownedAgent(agentId)),
|
|
600
894
|
agents: agents.map(summarizeAgent),
|
|
601
895
|
},
|
|
602
896
|
};
|
|
603
897
|
}
|
|
604
898
|
let agent: ManagedAgent;
|
|
605
899
|
try {
|
|
606
|
-
agent = await
|
|
900
|
+
agent = await ownedRegistry.close(agentId);
|
|
607
901
|
} finally {
|
|
608
|
-
await cleanupClosedWorkspaces(
|
|
902
|
+
await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, workspaceManager);
|
|
609
903
|
}
|
|
904
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
610
905
|
return result(agent, `Closed ${agent.id}.`);
|
|
611
906
|
},
|
|
612
907
|
});
|
|
@@ -619,25 +914,29 @@ export function registerStatefulSubagents(
|
|
|
619
914
|
promptSnippet: "Send or read queue-only detached-subagent mailbox messages",
|
|
620
915
|
parameters: MailboxParamsSchema,
|
|
621
916
|
...createStatefulToolRenderer("mailbox"),
|
|
622
|
-
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
917
|
+
async execute(_id, params, signal): Promise<StatefulActionToolResult> {
|
|
918
|
+
const generation = runtimeGeneration;
|
|
919
|
+
const ownedRegistry = requireRegistry();
|
|
623
920
|
const operation = validateMailboxParams(params);
|
|
624
921
|
if (operation.action === "send") {
|
|
625
|
-
const message = await
|
|
922
|
+
const message = await ownedRegistry.sendMessage(
|
|
626
923
|
operation.agentId,
|
|
627
924
|
operation.message,
|
|
628
925
|
operation.senderId,
|
|
629
926
|
operation.deduplicationKey,
|
|
630
927
|
);
|
|
928
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
631
929
|
return {
|
|
632
930
|
content: [{ type: "text", text: `Queued ${message.id} for ${message.recipientId}.` }],
|
|
633
931
|
details: { message },
|
|
634
932
|
};
|
|
635
933
|
}
|
|
636
|
-
const messages = await
|
|
934
|
+
const messages = await ownedRegistry.readMessages(
|
|
637
935
|
operation.agentId,
|
|
638
936
|
operation.acknowledge,
|
|
639
937
|
operation.limit,
|
|
640
938
|
);
|
|
939
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
641
940
|
const summaries = messages.map((message) => ({
|
|
642
941
|
...message,
|
|
643
942
|
content: truncateUtf8(message.content, MAX_TOOL_MESSAGE_BYTES).text,
|
|
@@ -683,7 +982,18 @@ export function formatStatefulAgentLine(agent: ManagedAgent): string {
|
|
|
683
982
|
const unread = agent.mailbox.filter((message) => !message.readAt).length;
|
|
684
983
|
const indent = " ".repeat(agent.depth);
|
|
685
984
|
const thinking = agent.thinkingLevel ? ` thinking:${agent.thinkingLevel}` : "";
|
|
686
|
-
|
|
985
|
+
const timeout = agent.currentTimeoutMs ?? agent.timeoutMs;
|
|
986
|
+
const timeoutText = timeout ? ` timeout:${timeout}ms` : "";
|
|
987
|
+
const idleTimeout = agent.currentIdleTimeoutMs ?? agent.idleTimeoutMs;
|
|
988
|
+
const idleText = idleTimeout ? ` idle:${idleTimeout}ms` : "";
|
|
989
|
+
const maxTurns = agent.currentMaxTurns ?? agent.maxTurns;
|
|
990
|
+
const turnsText = maxTurns ? ` turns:${maxTurns}` : "";
|
|
991
|
+
const maxToolCalls = agent.currentMaxToolCalls ?? agent.maxToolCalls;
|
|
992
|
+
const toolsText = maxToolCalls ? ` tools:${maxToolCalls}` : "";
|
|
993
|
+
const transport = agent.telemetry?.transport ? ` transport:${agent.telemetry.transport}` : "";
|
|
994
|
+
const phase = agent.telemetry?.phase ? ` phase:${agent.telemetry.phase}` : "";
|
|
995
|
+
const queued = agent.telemetry?.queuePosition ? ` queue:${agent.telemetry.queuePosition}` : "";
|
|
996
|
+
return `${indent}${sanitizeStatusLine(agent.id, 128)} ${sanitizeStatusLine(agent.agent, 128)} ${agent.state} ${elapsedSeconds}s${thinking}${timeoutText}${idleText}${turnsText}${toolsText}${transport}${phase}${queued} unread:${unread} [${actions}]${task}`;
|
|
687
997
|
}
|
|
688
998
|
|
|
689
999
|
function sanitizeStatusLine(value: string, maxLength: number): string {
|
|
@@ -715,233 +1025,40 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
715
1025
|
cwd: agent.cwd,
|
|
716
1026
|
workspaceMode: agent.workspaceMode ?? "shared",
|
|
717
1027
|
thinkingLevel: agent.thinkingLevel,
|
|
1028
|
+
timeoutMs: agent.timeoutMs,
|
|
1029
|
+
currentTimeoutMs: agent.currentTimeoutMs,
|
|
1030
|
+
idleTimeoutMs: agent.idleTimeoutMs,
|
|
1031
|
+
currentIdleTimeoutMs: agent.currentIdleTimeoutMs,
|
|
1032
|
+
maxTurns: agent.maxTurns,
|
|
1033
|
+
currentMaxTurns: agent.currentMaxTurns,
|
|
1034
|
+
maxToolCalls: agent.maxToolCalls,
|
|
1035
|
+
currentMaxToolCalls: agent.currentMaxToolCalls,
|
|
718
1036
|
currentTask: agent.currentTask
|
|
719
1037
|
? truncateUtf8(agent.currentTask, MAX_TOOL_MESSAGE_BYTES).text
|
|
720
1038
|
: undefined,
|
|
721
1039
|
historyCount: agent.history.length,
|
|
722
1040
|
unreadMessages: agent.mailbox.filter((message) => !message.readAt).length,
|
|
1041
|
+
context: {
|
|
1042
|
+
turns: agent.contextTurns ?? 0,
|
|
1043
|
+
sources: agent.contextSourceIds?.length ?? 0,
|
|
1044
|
+
bytes: agent.contextBytes ?? 0,
|
|
1045
|
+
truncated: agent.contextTruncated === true,
|
|
1046
|
+
},
|
|
1047
|
+
resultFormat: agent.resultFormat ?? "text",
|
|
1048
|
+
structuredResult: agent.structuredResult,
|
|
1049
|
+
termination: agent.termination,
|
|
1050
|
+
outcome: agent.outcome,
|
|
1051
|
+
executionPlan: agent.executionPlan,
|
|
1052
|
+
capabilityGrant: agent.capabilityGrant,
|
|
1053
|
+
semanticCompatibility: agent.semanticCompatibility,
|
|
1054
|
+
semanticSnapshotDigest: agent.semanticSnapshot?.digest,
|
|
1055
|
+
telemetry: agent.telemetry,
|
|
723
1056
|
error: agent.error ? truncateUtf8(agent.error, MAX_TOOL_MESSAGE_BYTES).text : undefined,
|
|
724
1057
|
target: agent.target,
|
|
725
1058
|
policy: agent.policy,
|
|
726
1059
|
};
|
|
727
1060
|
}
|
|
728
1061
|
|
|
729
|
-
interface CompletionMetadata {
|
|
730
|
-
agentId: string;
|
|
731
|
-
agent: string;
|
|
732
|
-
state: string;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
interface CompletionMessage {
|
|
736
|
-
customType: "pi-subagent-completion";
|
|
737
|
-
content: string;
|
|
738
|
-
display: true;
|
|
739
|
-
details:
|
|
740
|
-
| CompletionMetadata
|
|
741
|
-
| {
|
|
742
|
-
completionCount: number;
|
|
743
|
-
completions: CompletionMetadata[];
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
type CompletionContext = Pick<ExtensionContext, "hasPendingMessages" | "isIdle">;
|
|
748
|
-
type CompletionPi = Pick<ExtensionAPI, "sendMessage">;
|
|
749
|
-
|
|
750
|
-
export interface CompletionDeliveryBrokerOptions {
|
|
751
|
-
onDeliveryError?: (error: unknown) => void;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Coalesces detached completions so one bounded notification batch starts at
|
|
756
|
-
* most one root synthesis turn. The broker belongs to one parent session and
|
|
757
|
-
* must be closed when that session is replaced or shut down.
|
|
758
|
-
*/
|
|
759
|
-
export class CompletionDeliveryBroker {
|
|
760
|
-
private pending: AgentTurnCompletion[] = [];
|
|
761
|
-
private flushTimer?: NodeJS.Timeout;
|
|
762
|
-
private wakeInFlight = false;
|
|
763
|
-
private closed = false;
|
|
764
|
-
|
|
765
|
-
constructor(
|
|
766
|
-
private readonly pi: CompletionPi,
|
|
767
|
-
private readonly ctx: CompletionContext,
|
|
768
|
-
private delivery: CompletionDelivery,
|
|
769
|
-
private readonly options: CompletionDeliveryBrokerOptions = {},
|
|
770
|
-
) {}
|
|
771
|
-
|
|
772
|
-
enqueue(completion: AgentTurnCompletion): void {
|
|
773
|
-
if (this.closed) return;
|
|
774
|
-
this.pending.push(completion);
|
|
775
|
-
this.scheduleFlush();
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
setDelivery(value: CompletionDelivery): void {
|
|
779
|
-
this.delivery = value;
|
|
780
|
-
this.scheduleFlush();
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
onParentTurnStart(): void {
|
|
784
|
-
this.wakeInFlight = false;
|
|
785
|
-
this.scheduleFlush();
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
onParentSettled(): void {
|
|
789
|
-
this.wakeInFlight = false;
|
|
790
|
-
this.scheduleFlush();
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
flush(): void {
|
|
794
|
-
if (this.closed || this.pending.length === 0) return;
|
|
795
|
-
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
796
|
-
this.flushTimer = undefined;
|
|
797
|
-
if (this.delivery === "auto-resume" && !this.isRootIdle()) return;
|
|
798
|
-
|
|
799
|
-
const completions = this.pending.splice(0);
|
|
800
|
-
const batches = chunkCompletions(completions);
|
|
801
|
-
let canWake = this.shouldWakeRoot();
|
|
802
|
-
for (let index = 0; index < batches.length; index++) {
|
|
803
|
-
const triggerTurn = canWake && index === batches.length - 1;
|
|
804
|
-
const message = buildCompletionMessage(batches[index]);
|
|
805
|
-
if (triggerTurn) this.wakeInFlight = true;
|
|
806
|
-
try {
|
|
807
|
-
this.pi.sendMessage(message, { deliverAs: "steer", triggerTurn });
|
|
808
|
-
} catch (primaryError) {
|
|
809
|
-
if (triggerTurn) this.wakeInFlight = false;
|
|
810
|
-
canWake = false;
|
|
811
|
-
try {
|
|
812
|
-
this.pi.sendMessage(message, { deliverAs: "nextTurn", triggerTurn: false });
|
|
813
|
-
} catch (fallbackError) {
|
|
814
|
-
this.pending = [...batches.slice(index).flat(), ...this.pending];
|
|
815
|
-
try {
|
|
816
|
-
this.options.onDeliveryError?.(
|
|
817
|
-
new AggregateError(
|
|
818
|
-
[primaryError, fallbackError],
|
|
819
|
-
"Detached subagent completion delivery failed",
|
|
820
|
-
),
|
|
821
|
-
);
|
|
822
|
-
} catch {
|
|
823
|
-
// Delivery retention must survive a failing observer.
|
|
824
|
-
}
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
close(): void {
|
|
832
|
-
this.closed = true;
|
|
833
|
-
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
834
|
-
this.flushTimer = undefined;
|
|
835
|
-
this.pending = [];
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
private scheduleFlush(): void {
|
|
839
|
-
if (this.closed || this.pending.length === 0 || this.flushTimer) return;
|
|
840
|
-
this.flushTimer = setTimeout(() => {
|
|
841
|
-
this.flushTimer = undefined;
|
|
842
|
-
this.flush();
|
|
843
|
-
}, COMPLETION_BATCH_DELAY_MS);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
private isRootIdle(): boolean {
|
|
847
|
-
try {
|
|
848
|
-
return this.ctx.isIdle();
|
|
849
|
-
} catch {
|
|
850
|
-
return false;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
private shouldWakeRoot(): boolean {
|
|
855
|
-
if (this.delivery !== "auto-resume" || this.wakeInFlight) return false;
|
|
856
|
-
try {
|
|
857
|
-
return !this.ctx.hasPendingMessages();
|
|
858
|
-
} catch {
|
|
859
|
-
return false;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function chunkCompletions(completions: AgentTurnCompletion[]): AgentTurnCompletion[][] {
|
|
865
|
-
const batches: AgentTurnCompletion[][] = [];
|
|
866
|
-
for (let index = 0; index < completions.length; index += MAX_COMPLETIONS_PER_MESSAGE) {
|
|
867
|
-
batches.push(completions.slice(index, index + MAX_COMPLETIONS_PER_MESSAGE));
|
|
868
|
-
}
|
|
869
|
-
return batches;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function buildCompletionMessage(completions: AgentTurnCompletion[]): CompletionMessage {
|
|
873
|
-
if (completions.length === 1) {
|
|
874
|
-
const completion = completions[0];
|
|
875
|
-
return {
|
|
876
|
-
customType: "pi-subagent-completion",
|
|
877
|
-
content: buildDetachedCompletionMessage(completion),
|
|
878
|
-
display: true,
|
|
879
|
-
details: completionMetadata(completion),
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
const content = truncateUtf8(
|
|
883
|
-
[
|
|
884
|
-
"Message Type: SUBAGENT_COMPLETION_BATCH",
|
|
885
|
-
`Completion Count: ${completions.length}`,
|
|
886
|
-
...completions.flatMap((completion, index) => [
|
|
887
|
-
"",
|
|
888
|
-
`--- Completion ${index + 1} of ${completions.length} ---`,
|
|
889
|
-
buildDetachedCompletionMessage(completion),
|
|
890
|
-
]),
|
|
891
|
-
].join("\n"),
|
|
892
|
-
DEFAULT_MAX_CONTEXT_BYTES,
|
|
893
|
-
).text;
|
|
894
|
-
return {
|
|
895
|
-
customType: "pi-subagent-completion",
|
|
896
|
-
content,
|
|
897
|
-
display: true,
|
|
898
|
-
details: {
|
|
899
|
-
completionCount: completions.length,
|
|
900
|
-
completions: completions.map(completionMetadata),
|
|
901
|
-
},
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
function completionMetadata(completion: AgentTurnCompletion): CompletionMetadata {
|
|
906
|
-
return {
|
|
907
|
-
agentId: completion.agent.id,
|
|
908
|
-
agent: completion.agent.agent,
|
|
909
|
-
state: completion.agent.state,
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
export function buildDetachedCompletionMessage(completion: AgentTurnCompletion): string {
|
|
914
|
-
const task = sanitizeCompletionLine(completion.task, 256) || "(unknown task)";
|
|
915
|
-
const agentName = sanitizeCompletionLine(completion.agent.agent, 128) || "(unknown agent)";
|
|
916
|
-
const output = redactPrivateText(completion.output);
|
|
917
|
-
const error = completion.error
|
|
918
|
-
? truncateUtf8(redactPrivateText(completion.error), MAX_COMPLETION_ERROR_BYTES).text
|
|
919
|
-
: "";
|
|
920
|
-
return truncateUtf8(
|
|
921
|
-
[
|
|
922
|
-
"Message Type: SUBAGENT_COMPLETION",
|
|
923
|
-
`Agent ID: ${completion.agent.id}`,
|
|
924
|
-
`Agent: ${agentName}`,
|
|
925
|
-
`Task: ${task}`,
|
|
926
|
-
`State: ${completion.agent.state}`,
|
|
927
|
-
...(error.trim() ? ["Error:", error] : []),
|
|
928
|
-
"Payload:",
|
|
929
|
-
output.trim() ? output : "(no output)",
|
|
930
|
-
].join("\n"),
|
|
931
|
-
MAX_TOOL_MESSAGE_BYTES,
|
|
932
|
-
).text;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
function sanitizeCompletionLine(value: string, maxBytes: number): string {
|
|
936
|
-
return (
|
|
937
|
-
truncateUtf8(redactPrivateText(value), maxBytes)
|
|
938
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: Strip untrusted terminal controls.
|
|
939
|
-
.text.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
940
|
-
.replace(/\s+/g, " ")
|
|
941
|
-
.trim()
|
|
942
|
-
);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
1062
|
async function cleanupClosedWorkspaces(
|
|
946
1063
|
registry: AgentRegistry,
|
|
947
1064
|
isolatedAgents: Map<string, string>,
|
|
@@ -965,22 +1082,15 @@ function result(agent: ManagedAgent, text: string) {
|
|
|
965
1082
|
};
|
|
966
1083
|
}
|
|
967
1084
|
|
|
968
|
-
export function resolveStatefulTransportKind(
|
|
969
|
-
value: "subprocess" | "in-process" | undefined,
|
|
970
|
-
): "subprocess" | "in-process" {
|
|
971
|
-
return value ?? "subprocess";
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
export function resolveCompletionDelivery(
|
|
975
|
-
value: CompletionDelivery | undefined,
|
|
976
|
-
): CompletionDelivery {
|
|
977
|
-
return value ?? "next-turn";
|
|
978
|
-
}
|
|
979
|
-
|
|
980
1085
|
function normalizeRuntimeThinkingLevel(value: string): ParentRuntimeSnapshot["thinkingLevel"] {
|
|
981
1086
|
return isThinkingLevel(value) ? value : "off";
|
|
982
1087
|
}
|
|
983
1088
|
|
|
1089
|
+
export {
|
|
1090
|
+
buildDetachedCompletionMessage,
|
|
1091
|
+
CompletionDeliveryBroker,
|
|
1092
|
+
} from "./completion-delivery.js";
|
|
1093
|
+
export { resolveCompletionDelivery, resolveStatefulTransportKind } from "./stateful-config.js";
|
|
984
1094
|
export {
|
|
985
1095
|
buildStatefulTurnPrompt,
|
|
986
1096
|
resolveStatefulTurnTimeout,
|