pi-subagents 0.59.0 → 0.61.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 +69 -0
- package/docs/agents.md +2 -2
- package/docs/configuration.md +9 -5
- package/docs/extension-api.md +14 -7
- package/docs/models.md +1 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +15 -3
- package/docs/workflows.md +15 -14
- package/install.mjs +2 -1
- package/package.json +1 -1
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +89 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +30 -234
- package/skills/pi-subagents/references/execution-controls.md +79 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +35 -28
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +256 -88
- package/src/agents/agents.ts +527 -221
- package/src/api/background-work.ts +7 -2
- package/src/api/external-runs.ts +67 -4
- package/src/api/preflight.ts +13 -8
- package/src/api/shared-types.ts +3 -0
- package/src/extension/index.ts +7 -4
- package/src/extension/public-execution.ts +48 -4
- package/src/extension/rpc.ts +62 -4
- package/src/extension/schemas.ts +12 -7
- package/src/extension/tool-description.ts +16 -16
- package/src/runs/background/async-execution.ts +53 -37
- package/src/runs/background/async-job-tracker.ts +65 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-status.ts +104 -11
- package/src/runs/background/auto-drain.ts +1 -1
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +1 -1
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/run-status.ts +15 -4
- package/src/runs/background/subagent-runner.ts +6 -3
- package/src/runs/background/subagent-wait.ts +30 -23
- package/src/runs/background/wait-completions.ts +4 -1
- package/src/runs/background/wait-tool.ts +24 -18
- package/src/runs/foreground/execution.ts +72 -4
- package/src/runs/foreground/subagent-executor.ts +276 -59
- package/src/runs/shared/acceptance.ts +43 -18
- package/src/runs/shared/async-status-projection.ts +138 -4
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/host-step-status.ts +1 -0
- package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/src/runs/shared/model-fallback.ts +61 -17
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +47 -1
- package/src/runs/shared/single-output.ts +45 -18
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/workflow-graph.ts +16 -0
- package/src/shared/types.ts +74 -4
- package/src/shared/workflow-child-permit.ts +91 -0
- package/src/slash/prompt-template-bridge.ts +37 -1
- package/src/slash/slash-commands.ts +18 -26
- package/src/tui/fleet-status.ts +11 -3
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +673 -145
- package/src/watchdog/change-signature.ts +40 -1
- package/src/workflows/host-command.ts +6 -1
- package/src/workflows/scripted-workflow.ts +206 -6
- package/src/workflows/workflow-child-summary.ts +1 -1
- package/src/workflows/workflow-receipt.ts +41 -4
- package/src/workflows/workflow-resources.ts +150 -0
|
@@ -117,6 +117,7 @@ const FAST_MODE_ALLOWED_MODELS = new Set([
|
|
|
117
117
|
"openai-codex/gpt-5.6-luna",
|
|
118
118
|
"openai-codex/gpt-5.6-sol",
|
|
119
119
|
]);
|
|
120
|
+
const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
120
121
|
export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
|
|
121
122
|
export const SUBAGENT_ORCHESTRATOR_TARGET_ENV =
|
|
122
123
|
"PI_SUBAGENT_ORCHESTRATOR_TARGET";
|
|
@@ -139,6 +140,7 @@ export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
|
|
|
139
140
|
export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV =
|
|
140
141
|
"PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
|
|
141
142
|
export const SUBAGENT_PARENT_SESSION_ENV = "PI_SUBAGENT_PARENT_SESSION";
|
|
143
|
+
export const SUBAGENT_FORK_CACHE_KEY_ENV = "PI_SUBAGENT_FORK_CACHE_KEY";
|
|
142
144
|
export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
|
|
143
145
|
export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
|
|
144
146
|
export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
|
|
@@ -146,8 +148,16 @@ export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
|
|
|
146
148
|
export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
|
|
147
149
|
export const SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_GLOBAL_CONTEXT";
|
|
148
150
|
|
|
151
|
+
export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
|
|
152
|
+
const parent = parentSessionId?.trim();
|
|
153
|
+
if (!parent) return undefined;
|
|
154
|
+
const digest = createHash("sha256").update(parent).digest("hex").slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH - "pi-fork:".length);
|
|
155
|
+
return `pi-fork:${digest}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
export interface BuildPiArgsInput {
|
|
150
159
|
parentSessionId?: string;
|
|
160
|
+
forkCacheKey?: string;
|
|
151
161
|
baseArgs: string[];
|
|
152
162
|
task: string;
|
|
153
163
|
sessionEnabled: boolean;
|
|
@@ -507,8 +517,13 @@ export function resolvePiLaunchToolPlan(
|
|
|
507
517
|
throw new Error(formatUnresolvedMcpDirectToolSelectors(mcpResolution.unresolvedSelectors));
|
|
508
518
|
}
|
|
509
519
|
const resolvedMcpSelections = mcpResolution.selections;
|
|
520
|
+
const resolvedMcpNames = new Set(resolvedMcpSelections.map((selection) => selection.name));
|
|
521
|
+
const legacyMcpNameCounts = countLegacyUnderscoreMcpToolNames(resolvedMcpSelections);
|
|
510
522
|
const effectiveMcpSelections = resolvedMcpSelections.filter(
|
|
511
|
-
(selection) =>
|
|
523
|
+
(selection) =>
|
|
524
|
+
!allowedToolSet ||
|
|
525
|
+
allowedToolSet.has(selection.name) ||
|
|
526
|
+
isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
|
|
512
527
|
);
|
|
513
528
|
const effectiveMcpTools = effectiveMcpSelections.map(
|
|
514
529
|
(selection) => selection.name,
|
|
@@ -663,6 +678,36 @@ export function resolvePiLaunchToolPlan(
|
|
|
663
678
|
};
|
|
664
679
|
}
|
|
665
680
|
|
|
681
|
+
// Capability ceilings persisted before #1685 may still name hyphenated MCP server prefixes with underscores.
|
|
682
|
+
function countLegacyUnderscoreMcpToolNames(selections: readonly ResolvedMcpDirectToolSelection[]): Map<string, number> {
|
|
683
|
+
const counts = new Map<string, number>();
|
|
684
|
+
for (const selection of selections) {
|
|
685
|
+
const legacyName = legacyUnderscoreMcpToolName(selection);
|
|
686
|
+
if (legacyName !== selection.name) counts.set(legacyName, (counts.get(legacyName) ?? 0) + 1);
|
|
687
|
+
}
|
|
688
|
+
return counts;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function isLegacyUnderscoreMcpToolAllowed(
|
|
692
|
+
selection: ResolvedMcpDirectToolSelection,
|
|
693
|
+
allowedToolSet: ReadonlySet<string>,
|
|
694
|
+
resolvedMcpNames: ReadonlySet<string>,
|
|
695
|
+
legacyMcpNameCounts: ReadonlyMap<string, number>,
|
|
696
|
+
): boolean {
|
|
697
|
+
const legacyName = legacyUnderscoreMcpToolName(selection);
|
|
698
|
+
return legacyMcpNameCounts.get(legacyName) === 1 && !resolvedMcpNames.has(legacyName) && allowedToolSet.has(legacyName);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function legacyUnderscoreMcpToolName(selection: ResolvedMcpDirectToolSelection): string {
|
|
702
|
+
const slash = selection.selector.indexOf("/");
|
|
703
|
+
if (slash < 1) return selection.name;
|
|
704
|
+
const toolName = selection.selector.slice(slash + 1);
|
|
705
|
+
const suffix = `_${toolName}`;
|
|
706
|
+
if (!selection.name.endsWith(suffix)) return selection.name;
|
|
707
|
+
const prefix = selection.name.slice(0, -suffix.length);
|
|
708
|
+
return `${prefix.replace(/-/g, "_")}${suffix}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
666
711
|
/** Escape XML-significant characters in a string for safe attribute interpolation. */
|
|
667
712
|
function escapeXmlAttr(value: string): string {
|
|
668
713
|
return value
|
|
@@ -974,6 +1019,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
974
1019
|
|
|
975
1020
|
env[SUBAGENT_PARENT_SESSION_ENV] =
|
|
976
1021
|
input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
|
|
1022
|
+
env[SUBAGENT_FORK_CACHE_KEY_ENV] = input.forkCacheKey?.trim() || undefined;
|
|
977
1023
|
|
|
978
1024
|
return {
|
|
979
1025
|
args,
|
|
@@ -8,6 +8,17 @@ export interface SingleOutputSnapshot {
|
|
|
8
8
|
exists: boolean;
|
|
9
9
|
mtimeMs?: number;
|
|
10
10
|
size?: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function missingFileStatError(error: unknown): boolean {
|
|
15
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
16
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function snapshotFromStatError(error: unknown): SingleOutputSnapshot {
|
|
20
|
+
if (missingFileStatError(error)) return { exists: false };
|
|
21
|
+
return { exists: false, error: error instanceof Error ? error.message : String(error) };
|
|
11
22
|
}
|
|
12
23
|
|
|
13
24
|
/**
|
|
@@ -183,12 +194,36 @@ export function captureSingleOutputSnapshot(outputPath: string | undefined): Sin
|
|
|
183
194
|
try {
|
|
184
195
|
const stat = fs.statSync(outputPath);
|
|
185
196
|
return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
186
|
-
} catch {
|
|
187
|
-
|
|
188
|
-
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return snapshotFromStatError(error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function inspectSingleOutputChange(
|
|
203
|
+
outputPath: string,
|
|
204
|
+
beforeRun: SingleOutputSnapshot | undefined,
|
|
205
|
+
): { changed: boolean; error?: string } {
|
|
206
|
+
if (beforeRun?.error) return { changed: false, error: beforeRun.error };
|
|
207
|
+
try {
|
|
208
|
+
const stat = fs.statSync(outputPath);
|
|
209
|
+
return {
|
|
210
|
+
changed: !beforeRun?.exists || stat.mtimeMs !== beforeRun.mtimeMs || stat.size !== beforeRun.size,
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (missingFileStatError(error)) return { changed: false };
|
|
214
|
+
return { changed: false, error: error instanceof Error ? error.message : String(error) };
|
|
189
215
|
}
|
|
190
216
|
}
|
|
191
217
|
|
|
218
|
+
export function hasSingleOutputChangedSinceSnapshot(
|
|
219
|
+
outputPath: string | undefined,
|
|
220
|
+
beforeRun: SingleOutputSnapshot | undefined,
|
|
221
|
+
): boolean | undefined {
|
|
222
|
+
if (!outputPath) return undefined;
|
|
223
|
+
const inspected = inspectSingleOutputChange(outputPath, beforeRun);
|
|
224
|
+
return inspected.error ? undefined : inspected.changed;
|
|
225
|
+
}
|
|
226
|
+
|
|
192
227
|
function persistSingleOutput(
|
|
193
228
|
outputPath: string | undefined,
|
|
194
229
|
fullOutput: string,
|
|
@@ -216,23 +251,15 @@ export function resolveSingleOutput(
|
|
|
216
251
|
const claimError = outputClaimError(outputPath, expectedClaimPath);
|
|
217
252
|
if (claimError) return { fullOutput: fallbackOutput, saveError: claimError, fatalError: true };
|
|
218
253
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} catch (error) {
|
|
226
|
-
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
227
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
228
|
-
return {
|
|
229
|
-
fullOutput: fallbackOutput,
|
|
230
|
-
saveError: `Failed to inspect output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
254
|
+
const changedSinceStart = inspectSingleOutputChange(outputPath, beforeRun);
|
|
255
|
+
if (changedSinceStart.error) {
|
|
256
|
+
return {
|
|
257
|
+
fullOutput: fallbackOutput,
|
|
258
|
+
saveError: `Failed to inspect output file: ${changedSinceStart.error}`,
|
|
259
|
+
};
|
|
233
260
|
}
|
|
234
261
|
|
|
235
|
-
if (changedSinceStart) {
|
|
262
|
+
if (changedSinceStart.changed) {
|
|
236
263
|
try {
|
|
237
264
|
return { fullOutput: fs.readFileSync(outputPath, "utf-8"), savedPath: outputPath };
|
|
238
265
|
} catch (error) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { BeforeProviderRequestEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { registerNativeSupervisorClient } from "../../intercom/native-supervisor-channel.ts";
|
|
6
6
|
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
7
7
|
import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, PERMISSION_POLICY_ENV } from "./permissions.ts";
|
|
8
8
|
import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
|
|
9
|
-
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
9
|
+
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_FORK_CACHE_KEY_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
10
10
|
import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
11
11
|
import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
12
12
|
import {
|
|
@@ -295,6 +295,22 @@ const COMPOSITE_TOOL_ID_APIS = new Set([
|
|
|
295
295
|
"openai-completions",
|
|
296
296
|
"openai-responses",
|
|
297
297
|
]);
|
|
298
|
+
const PROMPT_CACHE_KEY_APIS = new Set([
|
|
299
|
+
"azure-openai-responses",
|
|
300
|
+
"openai-codex-responses",
|
|
301
|
+
"openai-completions",
|
|
302
|
+
"openai-responses",
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
export function rewriteForkCacheProviderRequest(event: BeforeProviderRequestEvent, ctx?: Pick<ExtensionContext, "model">): unknown {
|
|
306
|
+
const forkCacheKey = process.env[SUBAGENT_FORK_CACHE_KEY_ENV]?.trim();
|
|
307
|
+
if (!forkCacheKey || !PROMPT_CACHE_KEY_APIS.has(ctx?.model?.api ?? "")) return undefined;
|
|
308
|
+
if (!event || typeof event !== "object") return undefined;
|
|
309
|
+
const payload = event.payload;
|
|
310
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
311
|
+
if (typeof (payload as { prompt_cache_key?: unknown }).prompt_cache_key !== "string") return undefined;
|
|
312
|
+
return { ...payload, prompt_cache_key: forkCacheKey };
|
|
313
|
+
}
|
|
298
314
|
|
|
299
315
|
function portableToolId(id: string): string {
|
|
300
316
|
if (PORTABLE_TOOL_ID_PATTERN.test(id) && id.length <= MAX_PORTABLE_TOOL_ID_LENGTH) return id;
|
|
@@ -748,6 +764,8 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
748
764
|
});
|
|
749
765
|
}
|
|
750
766
|
|
|
767
|
+
onRuntimeEvent("before_provider_request", (event: unknown, ctx?: ExtensionContext) => rewriteForkCacheProviderRequest(event as BeforeProviderRequestEvent, ctx));
|
|
768
|
+
|
|
751
769
|
onRuntimeEvent("context", (event: unknown, ctx?: ExtensionContext) => {
|
|
752
770
|
if (!event || typeof event !== "object" || !("messages" in event) || !Array.isArray(event.messages)) return undefined;
|
|
753
771
|
const messages = stripParentOnlySubagentMessages(event.messages, {
|
|
@@ -16,7 +16,7 @@ export const DEFAULT_FAST_TOOL_TIMEOUT_TOOLS = new Set([
|
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
18
|
/** Tools whose normal job can be to wait for a person or another run. */
|
|
19
|
-
export const TOOL_TIMEOUT_EXEMPT_TOOLS = new Set(["contact_supervisor", "intercom", "subagent_wait"]);
|
|
19
|
+
export const TOOL_TIMEOUT_EXEMPT_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "subagent_wait"]);
|
|
20
20
|
|
|
21
21
|
// Backward-compatible export name for existing callers/tests.
|
|
22
22
|
export const TOOL_TIMEOUT_ALLOWLIST = TOOL_TIMEOUT_EXEMPT_TOOLS;
|
|
@@ -13,6 +13,22 @@ export interface WorkflowGraphBuildInput {
|
|
|
13
13
|
dynamicGroupStatuses?: Record<number, { status: WorkflowNodeStatus; error?: string; acceptance?: SingleResult["acceptance"] }>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** Return displayable workflow stages while hiding structural parallel groups and host monitors. */
|
|
17
|
+
export function workflowGraphStageNodes(graph: WorkflowGraphSnapshot | undefined): WorkflowGraphNode[] {
|
|
18
|
+
const nodes = graph?.nodes;
|
|
19
|
+
if (!nodes?.length) return [];
|
|
20
|
+
const stages: WorkflowGraphNode[] = [];
|
|
21
|
+
const visit = (node: WorkflowGraphNode): void => {
|
|
22
|
+
if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
|
|
23
|
+
for (const child of node.children ?? []) visit(child);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (node.kind !== "host-step") stages.push(node);
|
|
27
|
+
};
|
|
28
|
+
for (const node of nodes) visit(node);
|
|
29
|
+
return stages;
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undefined {
|
|
17
33
|
switch (status) {
|
|
18
34
|
case "complete":
|
package/src/shared/types.ts
CHANGED
|
@@ -143,6 +143,20 @@ export interface WorkflowRecoveryAction {
|
|
|
143
143
|
taskRequired: true;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Bounded, host-generated identity for a resolved pi-subagents workflow resource.
|
|
148
|
+
* Permission/policy extensions can use it to distinguish resolved content from
|
|
149
|
+
* raw workflow scripts. This audit projection never grants execution authority.
|
|
150
|
+
*/
|
|
151
|
+
export interface WorkflowResourceProvenanceV1 {
|
|
152
|
+
kind: "workflow";
|
|
153
|
+
name: string;
|
|
154
|
+
version: number;
|
|
155
|
+
invocation: "named";
|
|
156
|
+
expansion: "resolved";
|
|
157
|
+
id: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
146
160
|
/**
|
|
147
161
|
* Bounded, launch-declared workflow lane metadata. This is display and
|
|
148
162
|
* triage information only; capability ceilings, authorization, and cleanup
|
|
@@ -189,6 +203,7 @@ export type WorkflowReceiptEntry = WorkflowReceiptEntryResumability & {
|
|
|
189
203
|
requestedContext?: "fresh" | "fork";
|
|
190
204
|
resolvedContext?: "fresh" | "fork" | "mixed";
|
|
191
205
|
outputReference?: string;
|
|
206
|
+
acceptanceRecovery?: AcceptanceRecoveryMetadata;
|
|
192
207
|
externalAdapter?: ExternalCliReceiptMetadata;
|
|
193
208
|
continuation: { runIds: string[] };
|
|
194
209
|
};
|
|
@@ -199,6 +214,7 @@ export interface WorkflowReceipt {
|
|
|
199
214
|
state: WorkflowReceiptState;
|
|
200
215
|
createdAt: number;
|
|
201
216
|
entries: Record<string, WorkflowReceiptEntry>;
|
|
217
|
+
resource?: WorkflowResourceProvenanceV1;
|
|
202
218
|
hostSteps?: HostStepNodeV1[];
|
|
203
219
|
workflowChildren?: WorkflowChildSummaryV1;
|
|
204
220
|
workflowResolution?: WorkflowTerminalResolution;
|
|
@@ -330,7 +346,7 @@ export interface CompletionBatchConfig {
|
|
|
330
346
|
|
|
331
347
|
export interface WaitToolConfigObject {
|
|
332
348
|
enabled?: boolean;
|
|
333
|
-
/** Default blocking window for
|
|
349
|
+
/** Default blocking window for bg_wait calls that omit timeoutMs. */
|
|
334
350
|
defaultTimeoutMs?: number;
|
|
335
351
|
}
|
|
336
352
|
|
|
@@ -559,6 +575,10 @@ export interface TimeoutRecoverySummary {
|
|
|
559
575
|
termination: "timed-out" | "stopped";
|
|
560
576
|
changedFiles: string[];
|
|
561
577
|
truncated?: boolean;
|
|
578
|
+
/** True only when a timed-out child left tracked changes without its requested report. */
|
|
579
|
+
recoveryNeeded?: boolean;
|
|
580
|
+
reason?: "timed-out-with-dirty-worktree";
|
|
581
|
+
reportStatus?: "missing" | "written" | "not-requested" | "unknown";
|
|
562
582
|
currentTool?: string;
|
|
563
583
|
currentToolArgs?: string;
|
|
564
584
|
currentPath?: string;
|
|
@@ -569,6 +589,9 @@ export interface TimeoutRecoverySummary {
|
|
|
569
589
|
message: string;
|
|
570
590
|
}
|
|
571
591
|
|
|
592
|
+
/** Safe parent-facing subset of a timeout recovery summary. */
|
|
593
|
+
export type TimeoutRecoveryProjection = Pick<TimeoutRecoverySummary, "termination" | "changedFiles" | "truncated" | "recoveryNeeded" | "reason" | "reportStatus">;
|
|
594
|
+
|
|
572
595
|
export const SUBAGENT_LIFECYCLE_ARTIFACT_VERSION = 3;
|
|
573
596
|
export type SubagentLifecycleArtifactVersion = typeof SUBAGENT_LIFECYCLE_ARTIFACT_VERSION;
|
|
574
597
|
|
|
@@ -751,6 +774,7 @@ export interface SteeringRecoveryDescriptor {
|
|
|
751
774
|
model?: string;
|
|
752
775
|
modelProvider?: string;
|
|
753
776
|
modelOverrideFromParent?: boolean;
|
|
777
|
+
modelOrigin?: "explicit" | "inherited" | "configured";
|
|
754
778
|
fallbackModels?: string[];
|
|
755
779
|
fast?: boolean;
|
|
756
780
|
thinking?: string;
|
|
@@ -1079,6 +1103,18 @@ export type AcceptanceLedgerStatus =
|
|
|
1079
1103
|
| "reviewed"
|
|
1080
1104
|
| "accepted";
|
|
1081
1105
|
|
|
1106
|
+
/**
|
|
1107
|
+
* Durable child evidence that can be handed to a read-only review after the
|
|
1108
|
+
* acceptance envelope itself was rejected. This never upgrades acceptance;
|
|
1109
|
+
* the enclosing ledger remains rejected and the child remains unsuccessful.
|
|
1110
|
+
*/
|
|
1111
|
+
export interface AcceptanceRecoveryMetadata {
|
|
1112
|
+
status: "available-for-review";
|
|
1113
|
+
reason: "acceptance-metadata-rejected";
|
|
1114
|
+
reportPath: string;
|
|
1115
|
+
reportHash: string;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1082
1118
|
export interface AcceptanceLedger {
|
|
1083
1119
|
status: AcceptanceLedgerStatus;
|
|
1084
1120
|
evidenceStatus: AcceptanceEvidenceStatus;
|
|
@@ -1088,6 +1124,7 @@ export interface AcceptanceLedger {
|
|
|
1088
1124
|
criteria: ResolvedAcceptanceGate[];
|
|
1089
1125
|
childReport?: AcceptanceReport;
|
|
1090
1126
|
childReportParseError?: string;
|
|
1127
|
+
recovery?: AcceptanceRecoveryMetadata;
|
|
1091
1128
|
runtimeChecks: AcceptanceRuntimeCheck[];
|
|
1092
1129
|
verifyRuns: AcceptanceVerifyResult[];
|
|
1093
1130
|
reviewResult?: AcceptanceReviewResult;
|
|
@@ -1272,10 +1309,11 @@ export interface WaitCompletionChild {
|
|
|
1272
1309
|
model?: string;
|
|
1273
1310
|
contextOverflow?: boolean;
|
|
1274
1311
|
artifactPaths?: Partial<ArtifactPaths>;
|
|
1312
|
+
timeoutRecovery?: TimeoutRecoveryProjection;
|
|
1275
1313
|
}
|
|
1276
1314
|
|
|
1277
1315
|
/**
|
|
1278
|
-
* Terminal completion observed for a run a
|
|
1316
|
+
* Terminal completion observed for a run a bg_wait call covered. Carries run
|
|
1279
1317
|
* identity and the artifact trail; output text stays in the tool result content.
|
|
1280
1318
|
*/
|
|
1281
1319
|
export interface WaitCompletion {
|
|
@@ -1290,6 +1328,27 @@ export interface WaitCompletion {
|
|
|
1290
1328
|
workflowChildren?: WorkflowChildSummaryV1;
|
|
1291
1329
|
}
|
|
1292
1330
|
|
|
1331
|
+
export interface AgentCapabilitiesSnapshot {
|
|
1332
|
+
agents: AgentCapabilityRow[];
|
|
1333
|
+
restrictedCount: number;
|
|
1334
|
+
capabilityCeilingSources?: string[];
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
export interface AgentCapabilityRow {
|
|
1338
|
+
name: string;
|
|
1339
|
+
description: string;
|
|
1340
|
+
source: "builtin" | "package" | "user" | "project" | "runtime";
|
|
1341
|
+
executable: boolean;
|
|
1342
|
+
restrictionSources?: string[];
|
|
1343
|
+
aliases?: string[];
|
|
1344
|
+
runner: { type: "pi" } | { type: "external-cli"; adapter?: string; capabilities: ExternalCliCapabilities } | { type: "external-job"; provider: string; available?: boolean; capabilities: ExternalJobRunnerStatus["capabilities"] };
|
|
1345
|
+
tools: { ambient: boolean; names: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
|
|
1346
|
+
model?: { value?: string; fallbackModels?: string[]; thinking?: string | false };
|
|
1347
|
+
execution?: { defaultAsync?: boolean; timeoutMs?: number };
|
|
1348
|
+
output?: { path?: string; mode?: OutputMode };
|
|
1349
|
+
extensions?: { names?: string[]; subagentOnly?: string[]; skills?: string[] };
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1293
1352
|
export interface Details {
|
|
1294
1353
|
mode: SubagentResultMode | "management";
|
|
1295
1354
|
runId?: string;
|
|
@@ -1300,7 +1359,7 @@ export interface Details {
|
|
|
1300
1359
|
results: SingleResult[];
|
|
1301
1360
|
workflowChildren?: WorkflowChildSummaryV1;
|
|
1302
1361
|
/**
|
|
1303
|
-
* Terminal completion payloads for runs this
|
|
1362
|
+
* Terminal completion payloads for runs this bg_wait call observed
|
|
1304
1363
|
* finishing. Async completions travel as result files that are consumed and
|
|
1305
1364
|
* deleted after text delivery, so without this field their run and artifact
|
|
1306
1365
|
* identity never reaches tool_result details.
|
|
@@ -1352,6 +1411,7 @@ export interface Details {
|
|
|
1352
1411
|
runFanoutBudget?: RunFanoutBudgetSnapshot;
|
|
1353
1412
|
runFanoutRejection?: RunFanoutRejection;
|
|
1354
1413
|
activeAsyncCapacity?: ActiveAsyncCapacitySnapshot;
|
|
1414
|
+
agentCapabilities?: AgentCapabilitiesSnapshot;
|
|
1355
1415
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
1356
1416
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
1357
1417
|
parallelHandoff?: ParallelHandoffReference;
|
|
@@ -1371,6 +1431,7 @@ export interface Details {
|
|
|
1371
1431
|
mission?: MissionRecord;
|
|
1372
1432
|
workflow?: {
|
|
1373
1433
|
value?: unknown;
|
|
1434
|
+
resource?: WorkflowResourceProvenanceV1;
|
|
1374
1435
|
preflightWarnings?: string[];
|
|
1375
1436
|
trace: Array<{
|
|
1376
1437
|
operation: "run" | "status" | "steer" | "host";
|
|
@@ -1852,9 +1913,10 @@ export interface AsyncStatus {
|
|
|
1852
1913
|
parallelHandoff?: ParallelHandoffReference;
|
|
1853
1914
|
}
|
|
1854
1915
|
|
|
1855
|
-
export type AsyncJobStep = NonNullable<AsyncStatus["steps"]>[number] & {
|
|
1916
|
+
export type AsyncJobStep = Omit<NonNullable<AsyncStatus["steps"]>[number], "timeoutRecovery"> & {
|
|
1856
1917
|
index?: number;
|
|
1857
1918
|
description?: string;
|
|
1919
|
+
timeoutRecovery?: TimeoutRecoveryProjection;
|
|
1858
1920
|
};
|
|
1859
1921
|
|
|
1860
1922
|
export interface AsyncJobState {
|
|
@@ -1889,6 +1951,8 @@ export interface AsyncJobState {
|
|
|
1889
1951
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
1890
1952
|
/** Bounded host-owned CI/gate nodes loaded from the workflow status graph. */
|
|
1891
1953
|
hostSteps?: HostStepNodeV1[];
|
|
1954
|
+
/** Full bounded workflow plan, including not-yet-materialized stages. */
|
|
1955
|
+
workflowGraph?: WorkflowGraphSnapshot;
|
|
1892
1956
|
steps?: AsyncJobStep[];
|
|
1893
1957
|
preflight?: WorkflowPreflightV1;
|
|
1894
1958
|
stepsTotal?: number;
|
|
@@ -2084,6 +2148,8 @@ export interface ActiveAsyncCapacitySnapshot {
|
|
|
2084
2148
|
export interface SubagentState {
|
|
2085
2149
|
baseCwd: string;
|
|
2086
2150
|
currentSessionId: string | null;
|
|
2151
|
+
/** Session for which active status projections were restored successfully. */
|
|
2152
|
+
statusProjectionSessionId?: string | null;
|
|
2087
2153
|
/** Reload-stable identity for this parent Pi process/window. */
|
|
2088
2154
|
completionOwnerId?: string;
|
|
2089
2155
|
/** Runtime-owned artifact resolution inputs used by Fleet transcript targeting. */
|
|
@@ -2250,6 +2316,8 @@ export interface RunSyncOptions {
|
|
|
2250
2316
|
allowIntercomDetach?: boolean;
|
|
2251
2317
|
intercomEvents?: IntercomEventBus;
|
|
2252
2318
|
onUpdate?: (r: import("@earendil-works/pi-agent-core").AgentToolResult<Details>) => void;
|
|
2319
|
+
/** Internal structured-delegation transport optimization: skip unchanged live snapshots. */
|
|
2320
|
+
suppressUnchangedDelegationUpdates?: boolean;
|
|
2253
2321
|
onControlEvent?: (event: ControlEvent) => void;
|
|
2254
2322
|
/** Exposes a non-terminating detach callback while the child is active. */
|
|
2255
2323
|
onDetachReady?: (detach: (reason?: string) => boolean) => void;
|
|
@@ -2285,6 +2353,8 @@ export interface RunSyncOptions {
|
|
|
2285
2353
|
fast?: boolean;
|
|
2286
2354
|
/** The override came from the running parent session, not configuration. */
|
|
2287
2355
|
modelOverrideFromParent?: boolean;
|
|
2356
|
+
/** How the launch model was selected: explicit per-call, configured agent primary, or inherited parent. */
|
|
2357
|
+
modelOrigin?: "explicit" | "inherited" | "configured";
|
|
2288
2358
|
/** LLM intent arbiter for the completion mutation guard (rescues read-only review runs). */
|
|
2289
2359
|
llmIntentArbiter?: import("../runs/shared/llm-intent-arbiter.ts").TaskMutationArbiter;
|
|
2290
2360
|
/** Override the agent's default thinking level for this run */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stableJsonDigest } from "./launch-contract.ts";
|
|
2
|
+
import type { WorkflowResourceProvenanceV1 } from "./types.ts";
|
|
2
3
|
|
|
3
4
|
export interface WorkflowChildPermitInput {
|
|
4
5
|
issuerPackage: string;
|
|
@@ -114,3 +115,93 @@ export function workflowChildPermitConsumed(permit: WorkflowChildPermit): boolea
|
|
|
114
115
|
const state = records.get(permit as object)?.state;
|
|
115
116
|
return state === "claimed" || state === "consumed";
|
|
116
117
|
}
|
|
118
|
+
|
|
119
|
+
export interface WorkflowResourceHostAuthority {
|
|
120
|
+
keys: readonly string[];
|
|
121
|
+
commands: readonly string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface WorkflowResourceAuthority {
|
|
125
|
+
host?: WorkflowResourceHostAuthority;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface WorkflowResourcePermitInput {
|
|
129
|
+
resourceName: string;
|
|
130
|
+
resourceVersion: number;
|
|
131
|
+
resourceId: string;
|
|
132
|
+
scriptDigest: string;
|
|
133
|
+
authority: WorkflowResourceAuthority;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface WorkflowResourcePermit {
|
|
137
|
+
readonly __workflowResourcePermit: unique symbol;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface WorkflowResourcePermitRecord {
|
|
141
|
+
resourceName: string;
|
|
142
|
+
resourceVersion: number;
|
|
143
|
+
resourceId: string;
|
|
144
|
+
scriptDigest: string;
|
|
145
|
+
authority: WorkflowResourceAuthority;
|
|
146
|
+
provenance: WorkflowResourceProvenanceV1;
|
|
147
|
+
state: "available" | "consumed";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const resourceRecords = new WeakMap<object, WorkflowResourcePermitRecord>();
|
|
151
|
+
|
|
152
|
+
function cloneWorkflowResourceAuthority(authority: WorkflowResourceAuthority): WorkflowResourceAuthority {
|
|
153
|
+
if (!authority || typeof authority !== "object" || Array.isArray(authority)) throw new Error("Workflow resource authority must be an object.");
|
|
154
|
+
if (authority.host === undefined) return Object.freeze({});
|
|
155
|
+
if (!authority.host || typeof authority.host !== "object" || Array.isArray(authority.host)) throw new Error("Workflow resource host authority must be an object.");
|
|
156
|
+
const { keys, commands } = authority.host;
|
|
157
|
+
if (!Array.isArray(keys) || keys.some((key) => typeof key !== "string" || !key.trim())) throw new Error("Workflow resource host authority keys must be non-empty strings.");
|
|
158
|
+
if (!Array.isArray(commands) || commands.some((command) => typeof command !== "string" || !command.trim())) throw new Error("Workflow resource host authority commands must be non-empty strings.");
|
|
159
|
+
return Object.freeze({ host: Object.freeze({ keys: Object.freeze([...keys]), commands: Object.freeze([...commands]) }) });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Package-internal permit for a workflow resource resolved by the extension. */
|
|
163
|
+
export function createWorkflowResourcePermit(input: WorkflowResourcePermitInput): WorkflowResourcePermit {
|
|
164
|
+
const resourceName = required(input.resourceName, "resourceName");
|
|
165
|
+
const resourceId = required(input.resourceId, "resourceId");
|
|
166
|
+
const scriptDigest = required(input.scriptDigest, "scriptDigest");
|
|
167
|
+
if (!Number.isInteger(input.resourceVersion) || input.resourceVersion < 1) throw new Error("resourceVersion must be a positive integer.");
|
|
168
|
+
const authority = cloneWorkflowResourceAuthority(input.authority);
|
|
169
|
+
const permit = Object.freeze(Object.create(null)) as WorkflowResourcePermit;
|
|
170
|
+
resourceRecords.set(permit as object, {
|
|
171
|
+
resourceName,
|
|
172
|
+
resourceVersion: input.resourceVersion,
|
|
173
|
+
resourceId,
|
|
174
|
+
scriptDigest,
|
|
175
|
+
authority,
|
|
176
|
+
provenance: Object.freeze({
|
|
177
|
+
kind: "workflow",
|
|
178
|
+
name: resourceName,
|
|
179
|
+
version: input.resourceVersion,
|
|
180
|
+
invocation: "named",
|
|
181
|
+
expansion: "resolved",
|
|
182
|
+
id: resourceId,
|
|
183
|
+
}),
|
|
184
|
+
state: "available",
|
|
185
|
+
});
|
|
186
|
+
return permit;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function consumeWorkflowResourcePermit(permit: WorkflowResourcePermit, script: string): { provenance: WorkflowResourceProvenanceV1; authority: WorkflowResourceAuthority } | string {
|
|
190
|
+
const record = resourceRecords.get(permit as object);
|
|
191
|
+
if (!record) return "Workflow resource permit is invalid.";
|
|
192
|
+
if (record.state !== "available") return "Workflow resource permit is already consumed.";
|
|
193
|
+
if (stableJsonDigest(script) !== record.scriptDigest) return "Workflow resource permit does not match the resolved workflow script.";
|
|
194
|
+
record.state = "consumed";
|
|
195
|
+
return { provenance: record.provenance, authority: record.authority };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Validate a host call against the authority attached to a consumed resource. */
|
|
199
|
+
export function authorizeWorkflowResourceHost(permit: WorkflowResourcePermit, key: string, command: string): string | undefined {
|
|
200
|
+
const record = resourceRecords.get(permit as object);
|
|
201
|
+
if (!record || record.state !== "consumed") return "Workflow resource authority is unavailable.";
|
|
202
|
+
const host = record.authority.host;
|
|
203
|
+
if (!host) return "runs.host is not allowed for this workflow resource.";
|
|
204
|
+
if (!host.keys.includes(key)) return `runs.host('${key}') is not allowed for workflow resource '${record.resourceName}'.`;
|
|
205
|
+
if (!host.commands.includes(command.trim())) return `The command for runs.host('${key}') is not allowed for workflow resource '${record.resourceName}'.`;
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type SubagentDelegationInvalidResponse,
|
|
8
8
|
type SubagentDelegationRequest,
|
|
9
9
|
type SubagentDelegationResponse,
|
|
10
|
+
type SubagentDelegationUpdate,
|
|
10
11
|
} from "../api/delegation.ts";
|
|
11
12
|
import { parseSubagentDelegationRequest } from "./delegation-request.ts";
|
|
12
13
|
import {
|
|
@@ -66,6 +67,37 @@ function validId(value: unknown): value is string {
|
|
|
66
67
|
return typeof value === "string" && value.trim().length > 0 && value.length <= 256 && !/[\r\n]/.test(value);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function sameStringArray(left: string[] | undefined, right: string[] | undefined): boolean {
|
|
71
|
+
if (left === right) return true;
|
|
72
|
+
if (!left || !right || left.length !== right.length) return false;
|
|
73
|
+
return left.every((value, index) => value === right[index]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sameRecentTools(
|
|
77
|
+
left: Array<{ tool: string; args: string }> | undefined,
|
|
78
|
+
right: Array<{ tool: string; args: string }> | undefined,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (left === right) return true;
|
|
81
|
+
if (!left || !right || left.length !== right.length) return false;
|
|
82
|
+
return left.every((tool, index) => tool.tool === right[index]?.tool && tool.args === right[index]?.args);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Duration is a heartbeat clock, not delegation-visible progress; terminal usage remains authoritative. */
|
|
86
|
+
function sameStructuredDelegationUpdateProgress(left: SubagentDelegationUpdate, right: SubagentDelegationUpdate): boolean {
|
|
87
|
+
return left.requestId === right.requestId
|
|
88
|
+
&& left.ownerRunId === right.ownerRunId
|
|
89
|
+
&& left.nodeId === right.nodeId
|
|
90
|
+
&& left.runId === right.runId
|
|
91
|
+
&& left.currentTool === right.currentTool
|
|
92
|
+
&& left.currentToolArgs === right.currentToolArgs
|
|
93
|
+
&& left.recentOutput === right.recentOutput
|
|
94
|
+
&& sameStringArray(left.recentOutputLines, right.recentOutputLines)
|
|
95
|
+
&& sameRecentTools(left.recentTools, right.recentTools)
|
|
96
|
+
&& left.model === right.model
|
|
97
|
+
&& left.toolCount === right.toolCount
|
|
98
|
+
&& left.tokens === right.tokens;
|
|
99
|
+
}
|
|
100
|
+
|
|
69
101
|
export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: string }>(
|
|
70
102
|
options: PromptTemplateBridgeOptions<Ctx>,
|
|
71
103
|
): {
|
|
@@ -298,6 +330,7 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
298
330
|
const executeRequest = structuredRequest && options.executeStructured
|
|
299
331
|
? options.executeStructured
|
|
300
332
|
: options.execute;
|
|
333
|
+
let lastStructuredUpdate: SubagentDelegationUpdate | undefined;
|
|
301
334
|
const result = await executeRequest(
|
|
302
335
|
requestId,
|
|
303
336
|
params,
|
|
@@ -307,7 +340,10 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
307
340
|
if (key ? !ownsAttempt(key, controller) : !ownsLegacyRequest(requestId, controller)) return;
|
|
308
341
|
if (structuredRequest) {
|
|
309
342
|
const payload = toSubagentDelegationUpdate(structuredRequest, update);
|
|
310
|
-
if (payload
|
|
343
|
+
if (payload && (!lastStructuredUpdate || !sameStructuredDelegationUpdateProgress(lastStructuredUpdate, payload))) {
|
|
344
|
+
lastStructuredUpdate = payload;
|
|
345
|
+
options.events.emit(SUBAGENT_DELEGATION_UPDATE_EVENT, payload);
|
|
346
|
+
}
|
|
311
347
|
return;
|
|
312
348
|
}
|
|
313
349
|
const payload = toDelegationUpdate(requestId, update);
|