@marcoscale98/pi-extensible-workflows 5.14.1-fork.1
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 +536 -0
- package/README.md +64 -0
- package/dist/src/agent-execution.d.ts +338 -0
- package/dist/src/agent-execution.js +1633 -0
- package/dist/src/background-widget.d.ts +74 -0
- package/dist/src/background-widget.js +1340 -0
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +188 -0
- package/dist/src/changelog.d.ts +6 -0
- package/dist/src/changelog.js +112 -0
- package/dist/src/decoders.d.ts +96 -0
- package/dist/src/decoders.js +629 -0
- package/dist/src/execution.d.ts +19 -0
- package/dist/src/execution.js +749 -0
- package/dist/src/herdr.d.ts +53 -0
- package/dist/src/herdr.js +270 -0
- package/dist/src/host-delivery.d.ts +95 -0
- package/dist/src/host-delivery.js +376 -0
- package/dist/src/host-navigator.d.ts +83 -0
- package/dist/src/host-navigator.js +1087 -0
- package/dist/src/host-phases.d.ts +89 -0
- package/dist/src/host-phases.js +320 -0
- package/dist/src/host-recovery.d.ts +141 -0
- package/dist/src/host-recovery.js +356 -0
- package/dist/src/host-runtime.d.ts +78 -0
- package/dist/src/host-runtime.js +378 -0
- package/dist/src/host-view.d.ts +116 -0
- package/dist/src/host-view.js +868 -0
- package/dist/src/host.d.ts +40 -0
- package/dist/src/host.js +1641 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.js +13725 -0
- package/dist/src/index.js.map +6 -0
- package/dist/src/io.d.ts +14 -0
- package/dist/src/io.js +82 -0
- package/dist/src/package-identity.d.ts +4 -0
- package/dist/src/package-identity.js +6 -0
- package/dist/src/paths.d.ts +14 -0
- package/dist/src/paths.js +51 -0
- package/dist/src/persistence.d.ts +5 -0
- package/dist/src/persistence.js +5 -0
- package/dist/src/pi-runtime-adapter.d.ts +27 -0
- package/dist/src/pi-runtime-adapter.js +243 -0
- package/dist/src/pi-runtime-runner.d.ts +42 -0
- package/dist/src/pi-runtime-runner.js +505 -0
- package/dist/src/registry.d.ts +48 -0
- package/dist/src/registry.js +379 -0
- package/dist/src/retention.d.ts +17 -0
- package/dist/src/retention.js +195 -0
- package/dist/src/runtime/agent-runner.d.ts +143 -0
- package/dist/src/runtime/agent-runner.js +12 -0
- package/dist/src/runtime/index.d.ts +2 -0
- package/dist/src/runtime/index.js +2 -0
- package/dist/src/runtime/namespace.d.ts +14 -0
- package/dist/src/runtime/namespace.js +1 -0
- package/dist/src/runtime/workflow-result.d.ts +2 -0
- package/dist/src/runtime/workflow-result.js +6 -0
- package/dist/src/session-handoff.d.ts +2 -0
- package/dist/src/session-handoff.js +82 -0
- package/dist/src/session-lease.d.ts +12 -0
- package/dist/src/session-lease.js +180 -0
- package/dist/src/store.d.ts +95 -0
- package/dist/src/store.js +951 -0
- package/dist/src/subagent-manager-handle.d.ts +4 -0
- package/dist/src/subagent-manager-handle.js +6 -0
- package/dist/src/tool-timing.d.ts +12 -0
- package/dist/src/tool-timing.js +33 -0
- package/dist/src/trajectory-contracts.d.ts +8 -0
- package/dist/src/trajectory-contracts.js +9 -0
- package/dist/src/trajectory-host-handle.d.ts +12 -0
- package/dist/src/trajectory-host-handle.js +6 -0
- package/dist/src/trajectory.d.ts +128 -0
- package/dist/src/trajectory.js +594 -0
- package/dist/src/types.d.ts +960 -0
- package/dist/src/types.js +60 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +225 -0
- package/dist/src/validation.d.ts +29 -0
- package/dist/src/validation.js +1100 -0
- package/dist/src/workflow-artifacts.d.ts +14 -0
- package/dist/src/workflow-artifacts.js +40 -0
- package/dist/starter/index.d.ts +1 -0
- package/dist/starter/index.js +103 -0
- package/dist/starter/index.js.map +6 -0
- package/dist/starter/review-loop.d.ts +72 -0
- package/dist/starter/review-loop.js +43 -0
- package/dist/starter/roles/developer.md +6 -0
- package/dist/starter/roles/oracle.md +18 -0
- package/dist/starter/roles/researcher.md +20 -0
- package/dist/starter/roles/reviewer.md +19 -0
- package/dist/starter/roles/scout.md +18 -0
- package/dist/subagents/index.d.ts +8 -0
- package/dist/subagents/index.js +8021 -0
- package/dist/subagents/index.js.map +6 -0
- package/dist/subagents/src/contracts.d.ts +173 -0
- package/dist/subagents/src/contracts.js +68 -0
- package/dist/subagents/src/decode.d.ts +15 -0
- package/dist/subagents/src/decode.js +182 -0
- package/dist/subagents/src/index.d.ts +12 -0
- package/dist/subagents/src/index.js +180 -0
- package/dist/subagents/src/manager.d.ts +3 -0
- package/dist/subagents/src/manager.js +1636 -0
- package/dist/subagents/src/navigator.d.ts +5 -0
- package/dist/subagents/src/navigator.js +627 -0
- package/dist/subagents/src/view.d.ts +70 -0
- package/dist/subagents/src/view.js +387 -0
- package/dist/subagents/src/worktree.d.ts +34 -0
- package/dist/subagents/src/worktree.js +68 -0
- package/dist/trajectory/assets/favicon.png +0 -0
- package/dist/trajectory/assets/index.html +772 -0
- package/dist/trajectory/assets/marked.min.js +6 -0
- package/dist/trajectory/assets/morphdom.min.js +1 -0
- package/dist/trajectory/index.d.ts +5 -0
- package/dist/trajectory/index.js +3459 -0
- package/dist/trajectory/index.js.map +6 -0
- package/dist/trajectory/src/assets/favicon.png +0 -0
- package/dist/trajectory/src/assets/index.html +772 -0
- package/dist/trajectory/src/assets/marked.min.js +6 -0
- package/dist/trajectory/src/assets/morphdom.min.js +1 -0
- package/dist/trajectory/src/export.d.ts +25 -0
- package/dist/trajectory/src/export.js +80 -0
- package/dist/trajectory/src/index.d.ts +26 -0
- package/dist/trajectory/src/index.js +732 -0
- package/dist/trajectory/src/server.d.ts +7 -0
- package/dist/trajectory/src/server.js +2534 -0
- package/dist/trajectory/src/server.js.map +6 -0
- package/examples/advanced-patterns/auto-model-router.mjs +78 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +105 -0
- package/skills/pi-extensible-workflows/SKILL.md +160 -0
- package/src/agent-execution.ts +1533 -0
- package/src/background-widget.ts +1496 -0
- package/src/budget.ts +109 -0
- package/src/changelog.ts +114 -0
- package/src/decoders.ts +567 -0
- package/src/execution.ts +652 -0
- package/src/herdr.ts +240 -0
- package/src/host-delivery.ts +346 -0
- package/src/host-navigator.ts +871 -0
- package/src/host-phases.ts +264 -0
- package/src/host-recovery.ts +333 -0
- package/src/host-runtime.ts +373 -0
- package/src/host-view.ts +828 -0
- package/src/host.ts +1502 -0
- package/src/index.ts +29 -0
- package/src/io.ts +73 -0
- package/src/package-identity.ts +7 -0
- package/src/paths.ts +49 -0
- package/src/persistence.ts +11 -0
- package/src/pi-runtime-adapter.ts +247 -0
- package/src/pi-runtime-runner.ts +436 -0
- package/src/registry.ts +321 -0
- package/src/retention.ts +178 -0
- package/src/runtime/agent-runner.ts +158 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/namespace.ts +16 -0
- package/src/runtime/workflow-result.ts +8 -0
- package/src/session-handoff.ts +78 -0
- package/src/session-lease.ts +125 -0
- package/src/store.ts +832 -0
- package/src/subagent-manager-handle.ts +8 -0
- package/src/tool-timing.ts +47 -0
- package/src/trajectory-contracts.ts +13 -0
- package/src/trajectory-host-handle.ts +16 -0
- package/src/trajectory.ts +543 -0
- package/src/types.ts +374 -0
- package/src/utils.ts +192 -0
- package/src/validation.ts +910 -0
- package/src/workflow-artifacts.ts +35 -0
- package/starter/index.ts +35 -0
- package/starter/prompts/council.md +16 -0
- package/starter/prompts/deep-research.md +16 -0
- package/starter/prompts/oracle.md +14 -0
- package/starter/prompts/parallel-review.md +22 -0
- package/starter/prompts/parallel-scout.md +16 -0
- package/starter/prompts/review-loop.md +14 -0
- package/starter/prompts/review.md +14 -0
- package/starter/prompts/scout.md +14 -0
- package/starter/review-loop.ts +73 -0
- package/starter/roles/developer.md +6 -0
- package/starter/roles/oracle.md +18 -0
- package/starter/roles/researcher.md +20 -0
- package/starter/roles/reviewer.md +19 -0
- package/starter/roles/scout.md +18 -0
- package/subagents/README.md +145 -0
- package/subagents/index.ts +8 -0
- package/subagents/src/contracts.ts +176 -0
- package/subagents/src/decode.ts +158 -0
- package/subagents/src/index.ts +194 -0
- package/subagents/src/manager.ts +1560 -0
- package/subagents/src/navigator.ts +488 -0
- package/subagents/src/view.ts +376 -0
- package/subagents/src/worktree.ts +99 -0
- package/trajectory/index.ts +5 -0
- package/trajectory/src/assets/favicon.png +0 -0
- package/trajectory/src/assets/index.html +772 -0
- package/trajectory/src/assets/marked.min.js +6 -0
- package/trajectory/src/assets/morphdom.min.js +1 -0
- package/trajectory/src/export.ts +85 -0
- package/trajectory/src/index.ts +604 -0
- package/trajectory/src/server.ts +447 -0
|
@@ -0,0 +1,1636 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, link, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { addAccounting, deepFreeze, errorCode, errorText, finiteNumber, isNodeError, isThinkingLevel, jsonValue, object, loadAgentDefinitions, loadingRegistry, localAgentTransport, resolveAgentResourcePolicy, resolveWorkflowSettings, roleNameOf, sanitizeDisplayText, sumAccounting, structuralPath, validateModelAliasAvailability, WorkflowAgentExecutor, WorkflowError, workflowProjectSettingsPath, workflowSettingsPath, SerialLane, } from "../../src/index.js";
|
|
6
|
+
import { decodeAgentDefinition, decodeWorkflowExtensions } from "../../src/decoders.js";
|
|
7
|
+
import { atomicJson, json as readJson, processAlive } from "../../src/persistence.js";
|
|
8
|
+
import { accountingValue, activityValue, legacyAccountingValue, worktreeValue } from "./decode.js";
|
|
9
|
+
import { SUBAGENT_ATTEMPT_DETAILS_LIMIT, SUBAGENT_MAX_RETRIES, SUBAGENT_SYSTEM_PROMPT_LIMIT, SUBAGENTS_TOOL_NAMES, normalizeSubagentRunRequest, } from "./contracts.js";
|
|
10
|
+
import { createRunStoreWorktreeAdapter, defaultWorktreeHome } from "./worktree.js";
|
|
11
|
+
const WORKFLOW_NAME = "subagents";
|
|
12
|
+
const STORAGE_DIRECTORY = "subagents";
|
|
13
|
+
const OWNER_FILE = "owner.json";
|
|
14
|
+
const CONFIGURATION_FILE = "configuration.json";
|
|
15
|
+
const OWNER_WRITE_GRACE_MS = 30_000;
|
|
16
|
+
const MAX_STORAGE_OWNER_ATTEMPTS = 8;
|
|
17
|
+
const MAX_TERMINAL_SUMMARIES = 128;
|
|
18
|
+
const MAX_PENDING_STEERING_MESSAGES = 16;
|
|
19
|
+
const MAX_PERSISTED_ATTEMPT_ARRAY_ITEMS = SUBAGENT_MAX_RETRIES + 1;
|
|
20
|
+
const MAX_PERSISTED_ATTEMPT_STRING_CHARS = 4096;
|
|
21
|
+
const MAX_PERSISTED_ATTEMPT_LOCATOR_CHARS = 16 * 1024;
|
|
22
|
+
const SUBAGENT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
23
|
+
class InvalidPersistedSubagentStatusError extends WorkflowError {
|
|
24
|
+
constructor() { super("INTERNAL_ERROR", "Persisted subagent status is invalid"); }
|
|
25
|
+
}
|
|
26
|
+
const EXCLUDED_TOOLS = new Set([
|
|
27
|
+
...SUBAGENTS_TOOL_NAMES,
|
|
28
|
+
"workflow",
|
|
29
|
+
"workflow_respond",
|
|
30
|
+
"workflow_stop",
|
|
31
|
+
"workflow_status",
|
|
32
|
+
"workflow_resume",
|
|
33
|
+
"workflow_retry",
|
|
34
|
+
"workflow_catalog",
|
|
35
|
+
]);
|
|
36
|
+
function modelNames(models) {
|
|
37
|
+
return new Set(models.map(({ provider, id }) => `${provider}/${id}`));
|
|
38
|
+
}
|
|
39
|
+
function rootModel(context) {
|
|
40
|
+
const model = context.extensionContext.model;
|
|
41
|
+
if (!model || typeof model.provider !== "string" || typeof model.id !== "string" || !model.provider || !model.id)
|
|
42
|
+
throw new WorkflowError("UNKNOWN_MODEL", "A current model is required to run a subagent");
|
|
43
|
+
const thinking = context.extensionContext.thinkingLevel;
|
|
44
|
+
return { provider: model.provider, model: model.id, ...(thinking === undefined ? {} : { thinking }) };
|
|
45
|
+
}
|
|
46
|
+
function standaloneExtensionSettingsPath(cwd, agentDir, trustedProject, external) {
|
|
47
|
+
return trustedProject && external.projectExtensionSettings !== undefined ? workflowProjectSettingsPath(cwd) : workflowSettingsPath(agentDir);
|
|
48
|
+
}
|
|
49
|
+
function validateStandaloneExtensionSources(context, external, registry, agentDir) {
|
|
50
|
+
const cwd = context.extensionContext.cwd;
|
|
51
|
+
const globalSettingsPath = workflowSettingsPath(agentDir);
|
|
52
|
+
const trustedProject = context.extensionContext.isProjectTrusted();
|
|
53
|
+
if (external.globalExtensionSettings !== undefined)
|
|
54
|
+
registry.validateExtensionSettings(external.globalExtensionSettings, { source: "global", cwd, projectTrusted: trustedProject, settingsPath: globalSettingsPath });
|
|
55
|
+
if (trustedProject && external.projectExtensionSettings !== undefined)
|
|
56
|
+
registry.validateExtensionSettings(external.projectExtensionSettings, { source: "project", cwd, projectTrusted: true, settingsPath: workflowProjectSettingsPath(cwd) });
|
|
57
|
+
}
|
|
58
|
+
function executionRoot(context, dependencies, signal, runId, worktree, external) {
|
|
59
|
+
const extensionContext = context.extensionContext;
|
|
60
|
+
const model = rootModel(context);
|
|
61
|
+
const agentDir = dependencies.agentDir ?? getAgentDir();
|
|
62
|
+
const currentTrustedProject = extensionContext.isProjectTrusted();
|
|
63
|
+
if (external.projectTrusted && !currentTrustedProject)
|
|
64
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Cannot retry standalone configuration from an untrusted project");
|
|
65
|
+
const trustedProject = currentTrustedProject;
|
|
66
|
+
const settingsPath = workflowSettingsPath(agentDir);
|
|
67
|
+
const settings = resolveWorkflowSettings(extensionContext.cwd, trustedProject, settingsPath);
|
|
68
|
+
const resourcePolicy = resolveAgentResourcePolicy(extensionContext.cwd, trustedProject, settingsPath);
|
|
69
|
+
const knownModels = modelNames(extensionContext.modelRegistry.getAll());
|
|
70
|
+
const availableModels = modelNames(extensionContext.modelRegistry.getAvailable());
|
|
71
|
+
const rootModelName = `${model.provider}/${model.model}`;
|
|
72
|
+
knownModels.add(rootModelName);
|
|
73
|
+
availableModels.add(rootModelName);
|
|
74
|
+
const staticAliases = settings.effective.modelAliases ?? {};
|
|
75
|
+
const activeTools = dependencies.getActiveTools?.() ?? [];
|
|
76
|
+
const tools = new Set(activeTools.filter((tool) => !EXCLUDED_TOOLS.has(tool)));
|
|
77
|
+
const sessionId = extensionContext.sessionManager.getSessionId();
|
|
78
|
+
const run = {
|
|
79
|
+
cwd: extensionContext.cwd,
|
|
80
|
+
sessionId,
|
|
81
|
+
runId,
|
|
82
|
+
workflow: { name: WORKFLOW_NAME },
|
|
83
|
+
args: null,
|
|
84
|
+
signal,
|
|
85
|
+
};
|
|
86
|
+
const registry = loadingRegistry();
|
|
87
|
+
validateStandaloneExtensionSources(context, external, registry, agentDir);
|
|
88
|
+
const extensionSettings = trustedProject && external.projectExtensionSettings !== undefined ? external.projectExtensionSettings : external.globalExtensionSettings;
|
|
89
|
+
const extensionSettingsPath = standaloneExtensionSettingsPath(extensionContext.cwd, agentDir, trustedProject, external);
|
|
90
|
+
return {
|
|
91
|
+
cwd: extensionContext.cwd,
|
|
92
|
+
model,
|
|
93
|
+
tools,
|
|
94
|
+
resourceSelectors: resourcePolicy.effective,
|
|
95
|
+
...(worktree === undefined ? {} : { runStore: worktree.runStore }),
|
|
96
|
+
agentDir,
|
|
97
|
+
extensionSettings,
|
|
98
|
+
availableModels,
|
|
99
|
+
knownModels,
|
|
100
|
+
...(Object.keys(staticAliases).length ? { modelAliases: staticAliases } : {}),
|
|
101
|
+
settingsPath: settings.sources.modelAliases,
|
|
102
|
+
extensionSettingsPath,
|
|
103
|
+
agentDefinitions: external.agentDefinitions,
|
|
104
|
+
agentSetupHooks: registry.agentSetupHooks(),
|
|
105
|
+
validateExtensionSettings: registry.validateExtensionSettings,
|
|
106
|
+
agentResourcePolicy: () => structuredClone(resourcePolicy),
|
|
107
|
+
runContext: run,
|
|
108
|
+
...(dependencies.onResourceWarning ? { onResourceWarning: dependencies.onResourceWarning } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function addDynamicAliases(context, signal, root) {
|
|
112
|
+
const registry = loadingRegistry();
|
|
113
|
+
if (registry.modelAliases().length === 0)
|
|
114
|
+
return root;
|
|
115
|
+
const staticAliases = root.modelAliases ?? {};
|
|
116
|
+
const resourcePolicy = await root.agentResourcePolicy?.();
|
|
117
|
+
const trustedProject = resourcePolicy?.projectTrusted ?? context.extensionContext.isProjectTrusted();
|
|
118
|
+
const dynamicAliases = await registry.resolveModelAliases({ cwd: context.extensionContext.cwd, projectTrusted: trustedProject, rootModel: root.model, knownModels: root.knownModels ?? new Set(), availableModels: root.availableModels ?? new Set(), signal }, new Set(Object.keys(staticAliases)));
|
|
119
|
+
validateModelAliasAvailability(dynamicAliases, Object.keys(dynamicAliases), root.availableModels ?? new Set(), root.knownModels ?? new Set(), root.settingsPath);
|
|
120
|
+
return { ...root, modelAliases: { ...dynamicAliases, ...staticAliases } };
|
|
121
|
+
}
|
|
122
|
+
function executionOptions(request, onAttempt, onProgress) {
|
|
123
|
+
const role = request.role;
|
|
124
|
+
const label = request.label ?? roleNameOf(role) ?? "subagent";
|
|
125
|
+
return {
|
|
126
|
+
label,
|
|
127
|
+
workflowName: WORKFLOW_NAME,
|
|
128
|
+
...(request.model === undefined ? {} : { model: request.model }),
|
|
129
|
+
...(request.tools === undefined ? {} : { tools: request.tools }),
|
|
130
|
+
...(request.skills === undefined ? {} : { skills: request.skills }),
|
|
131
|
+
...(request.extensions === undefined ? {} : { extensions: request.extensions }),
|
|
132
|
+
...(request.contextFiles === undefined ? {} : { contextFiles: request.contextFiles }),
|
|
133
|
+
...(role === undefined ? {} : { role }),
|
|
134
|
+
...(request.worktree === undefined ? {} : { worktreeOwner: structuralPath("worktree", "named", request.worktree) }),
|
|
135
|
+
...(request.outputSchema === undefined ? {} : { schema: request.outputSchema }),
|
|
136
|
+
...(request.retries === undefined ? {} : { retries: request.retries }),
|
|
137
|
+
...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
|
|
138
|
+
onAttempt,
|
|
139
|
+
onProgress,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function storageDirectory(dependencies) {
|
|
143
|
+
return dependencies.storageDir ?? join(dependencies.agentDir ?? getAgentDir(), STORAGE_DIRECTORY);
|
|
144
|
+
}
|
|
145
|
+
function runDirectory(root, id) { return join(root, id); }
|
|
146
|
+
function requestPath(directory) { return join(directory, "request.json"); }
|
|
147
|
+
function configurationPath(directory) { return join(directory, CONFIGURATION_FILE); }
|
|
148
|
+
function statusPath(directory) { return join(directory, "status.json"); }
|
|
149
|
+
function resultPath(directory) { return join(directory, "result.json"); }
|
|
150
|
+
function failurePath(directory) { return join(directory, "failure.json"); }
|
|
151
|
+
async function secureDirectory(directory) {
|
|
152
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
153
|
+
await chmod(directory, 0o700);
|
|
154
|
+
}
|
|
155
|
+
async function currentProcessStart() {
|
|
156
|
+
if (process.platform === "linux") {
|
|
157
|
+
try {
|
|
158
|
+
return (await stat(`/proc/${String(process.pid)}`)).ctimeMs;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (!isNodeError(error, "ENOENT"))
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return Math.max(0, Date.now() - Math.floor(process.uptime() * 1000));
|
|
166
|
+
}
|
|
167
|
+
function decodeOwnerMarker(value) {
|
|
168
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
169
|
+
return undefined;
|
|
170
|
+
const record = value;
|
|
171
|
+
const pid = record.pid;
|
|
172
|
+
const processStart = record.processStart;
|
|
173
|
+
const sessionId = record.sessionId;
|
|
174
|
+
const token = record.token;
|
|
175
|
+
const acquiredAt = record.acquiredAt;
|
|
176
|
+
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid < 0 || !finiteNumber(processStart) || processStart < 0 || typeof sessionId !== "string" || !sessionId.trim() || typeof token !== "string" || !token.trim() || typeof acquiredAt !== "number" || !Number.isSafeInteger(acquiredAt) || acquiredAt < 0)
|
|
177
|
+
return undefined;
|
|
178
|
+
return { pid, processStart, sessionId, token, acquiredAt };
|
|
179
|
+
}
|
|
180
|
+
async function createOwnerMarker(liveness) {
|
|
181
|
+
const pid = liveness?.pid ?? process.pid;
|
|
182
|
+
const processStart = liveness?.processStart ?? await currentProcessStart();
|
|
183
|
+
const sessionId = liveness?.sessionId ?? `${String(pid)}:${String(processStart)}`;
|
|
184
|
+
const token = liveness?.token ?? randomUUID();
|
|
185
|
+
if (!Number.isSafeInteger(pid) || pid < 0 || !finiteNumber(processStart) || processStart < 0 || typeof sessionId !== "string" || !sessionId.trim() || typeof token !== "string" || !token.trim())
|
|
186
|
+
throw new WorkflowError("INTERNAL_ERROR", "Invalid subagent storage owner identity");
|
|
187
|
+
return { pid, processStart, sessionId, token, acquiredAt: Date.now() };
|
|
188
|
+
}
|
|
189
|
+
async function ownerIsLive(owner, liveness) {
|
|
190
|
+
if (liveness?.isLive) {
|
|
191
|
+
try {
|
|
192
|
+
return await liveness.isLive(owner);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (liveness?.pid !== undefined && liveness.processStart !== undefined && owner.pid === liveness.pid && owner.processStart === liveness.processStart)
|
|
199
|
+
return true;
|
|
200
|
+
return processAlive(owner.pid, owner.processStart);
|
|
201
|
+
}
|
|
202
|
+
async function restoreOwnerMarker(path, stale) {
|
|
203
|
+
try {
|
|
204
|
+
await link(stale, path);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (!isNodeError(error, "EEXIST") && !isNodeError(error, "ENOENT"))
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
await rm(stale, { force: true });
|
|
211
|
+
}
|
|
212
|
+
async function writeOwnerMarker(path, marker) {
|
|
213
|
+
let handle;
|
|
214
|
+
let created = false;
|
|
215
|
+
try {
|
|
216
|
+
handle = await open(path, "wx", 0o600);
|
|
217
|
+
created = true;
|
|
218
|
+
await handle.writeFile(`${JSON.stringify(marker)}\n`, "utf8");
|
|
219
|
+
await handle.close();
|
|
220
|
+
handle = undefined;
|
|
221
|
+
await chmod(path, 0o600);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
await handle?.close().catch(() => undefined);
|
|
225
|
+
if (created)
|
|
226
|
+
await rm(path, { force: true });
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function releaseOwnerMarker(path, token) {
|
|
231
|
+
let marker;
|
|
232
|
+
try {
|
|
233
|
+
marker = decodeOwnerMarker(JSON.parse(await readFile(path, "utf8")));
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (isNodeError(error, "ENOENT"))
|
|
237
|
+
return;
|
|
238
|
+
// A malformed or replaced marker cannot prove this manager still owns the storage.
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (marker?.token === token)
|
|
242
|
+
await rm(path, { force: true });
|
|
243
|
+
}
|
|
244
|
+
async function acquireStorageOwner(root, owner, liveness) {
|
|
245
|
+
await secureDirectory(root);
|
|
246
|
+
const path = join(root, OWNER_FILE);
|
|
247
|
+
for (let attempt = 0; attempt < MAX_STORAGE_OWNER_ATTEMPTS; attempt += 1) {
|
|
248
|
+
const marker = { ...owner, acquiredAt: Date.now() };
|
|
249
|
+
try {
|
|
250
|
+
await writeOwnerMarker(path, marker);
|
|
251
|
+
return { token: marker.token, release: () => releaseOwnerMarker(path, marker.token) };
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (!isNodeError(error, "EEXIST"))
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
let existingText;
|
|
258
|
+
try {
|
|
259
|
+
existingText = await readFile(path, "utf8");
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (isNodeError(error, "ENOENT"))
|
|
263
|
+
continue;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
let existing;
|
|
267
|
+
try {
|
|
268
|
+
existing = decodeOwnerMarker(JSON.parse(existingText));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
existing = undefined;
|
|
272
|
+
}
|
|
273
|
+
if (existing) {
|
|
274
|
+
if (await ownerIsLive(existing, liveness))
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
let age;
|
|
279
|
+
try {
|
|
280
|
+
age = Math.max(0, Date.now() - (await stat(path)).mtimeMs);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (isNodeError(error, "ENOENT"))
|
|
284
|
+
continue;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
if (age < OWNER_WRITE_GRACE_MS)
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
const stale = `${path}.${randomUUID()}.stale`;
|
|
291
|
+
try {
|
|
292
|
+
await rename(path, stale);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (isNodeError(error, "ENOENT"))
|
|
296
|
+
continue;
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
let movedText;
|
|
300
|
+
try {
|
|
301
|
+
movedText = await readFile(stale, "utf8");
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (isNodeError(error, "ENOENT"))
|
|
305
|
+
continue;
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
const moved = (() => { try {
|
|
309
|
+
return decodeOwnerMarker(JSON.parse(movedText));
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return undefined;
|
|
313
|
+
} })();
|
|
314
|
+
const same = existing ? moved?.token === existing.token : movedText === existingText;
|
|
315
|
+
if (!same) {
|
|
316
|
+
await restoreOwnerMarker(path, stale);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
await rm(stale, { force: true });
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
function validSubagentId(id) { return SUBAGENT_ID_PATTERN.test(id); }
|
|
324
|
+
function checkedRequest(request) {
|
|
325
|
+
return normalizeSubagentRunRequest(request);
|
|
326
|
+
}
|
|
327
|
+
function decodePersistedStandaloneExternalConfiguration(value) {
|
|
328
|
+
if (!object(value) || !jsonValue(value) || typeof value.projectTrusted !== "boolean")
|
|
329
|
+
return undefined;
|
|
330
|
+
const globalExtensionSettings = value.globalExtensionSettings === undefined ? undefined : decodeWorkflowExtensions(value.globalExtensionSettings);
|
|
331
|
+
const projectExtensionSettings = value.projectExtensionSettings === undefined ? undefined : decodeWorkflowExtensions(value.projectExtensionSettings);
|
|
332
|
+
if (value.globalExtensionSettings !== undefined && globalExtensionSettings === undefined || value.projectExtensionSettings !== undefined && projectExtensionSettings === undefined)
|
|
333
|
+
return undefined;
|
|
334
|
+
if (!object(value.agentDefinitions))
|
|
335
|
+
return undefined;
|
|
336
|
+
const agentDefinitions = Object.create(null);
|
|
337
|
+
for (const [name, definition] of Object.entries(value.agentDefinitions)) {
|
|
338
|
+
const decoded = decodeAgentDefinition(definition);
|
|
339
|
+
if (decoded === undefined)
|
|
340
|
+
return undefined;
|
|
341
|
+
agentDefinitions[name] = decoded;
|
|
342
|
+
}
|
|
343
|
+
return deepFreeze({ projectTrusted: value.projectTrusted, ...(globalExtensionSettings === undefined ? {} : { globalExtensionSettings }), ...(projectExtensionSettings === undefined ? {} : { projectExtensionSettings }), agentDefinitions });
|
|
344
|
+
}
|
|
345
|
+
function standaloneExternalConfiguration(context, dependencies) {
|
|
346
|
+
const cwd = context.extensionContext.cwd;
|
|
347
|
+
const trustedProject = context.extensionContext.isProjectTrusted();
|
|
348
|
+
const agentDir = dependencies.agentDir ?? getAgentDir();
|
|
349
|
+
const resolution = resolveWorkflowSettings(cwd, trustedProject, workflowSettingsPath(agentDir));
|
|
350
|
+
const globalExtensionSettings = resolution.global.extensionSettings;
|
|
351
|
+
const projectExtensionSettings = resolution.project.extensionSettings;
|
|
352
|
+
return deepFreeze({
|
|
353
|
+
projectTrusted: trustedProject,
|
|
354
|
+
...(globalExtensionSettings === undefined ? {} : { globalExtensionSettings: structuredClone(globalExtensionSettings) }),
|
|
355
|
+
...(projectExtensionSettings === undefined ? {} : { projectExtensionSettings: structuredClone(projectExtensionSettings) }),
|
|
356
|
+
agentDefinitions: structuredClone(loadAgentDefinitions(cwd, agentDir, trustedProject)),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function checkedId(request) {
|
|
360
|
+
if (typeof request.id !== "string" || !validSubagentId(request.id))
|
|
361
|
+
throw new WorkflowError("RUN_NOT_FOUND", `Unknown subagent ${request.id}`);
|
|
362
|
+
return request.id;
|
|
363
|
+
}
|
|
364
|
+
function failureFrom(error) {
|
|
365
|
+
return { code: errorCode(error) ?? "AGENT_FAILED", message: errorText(error) };
|
|
366
|
+
}
|
|
367
|
+
function internalStorageError(error, operation) {
|
|
368
|
+
return new WorkflowError("INTERNAL_ERROR", `${operation}: ${errorText(error)}`);
|
|
369
|
+
}
|
|
370
|
+
function boundedAttemptText(value) { return value.length > MAX_PERSISTED_ATTEMPT_STRING_CHARS ? value.slice(0, MAX_PERSISTED_ATTEMPT_STRING_CHARS) : value; }
|
|
371
|
+
function boundedAttemptStrings(values) { return values.slice(0, MAX_PERSISTED_ATTEMPT_ARRAY_ITEMS).map(boundedAttemptText); }
|
|
372
|
+
function boundedAttemptLocator(value) {
|
|
373
|
+
if (value === undefined)
|
|
374
|
+
return undefined;
|
|
375
|
+
let serialized;
|
|
376
|
+
try {
|
|
377
|
+
serialized = JSON.stringify(value);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
return serialized.length > MAX_PERSISTED_ATTEMPT_LOCATOR_CHARS ? undefined : structuredClone(value);
|
|
383
|
+
}
|
|
384
|
+
function boundedAttemptSetup(setup) {
|
|
385
|
+
const resources = setup.resourceSelectors;
|
|
386
|
+
return {
|
|
387
|
+
hookNames: boundedAttemptStrings(setup.hookNames),
|
|
388
|
+
model: { provider: boundedAttemptText(setup.model.provider), model: boundedAttemptText(setup.model.model), ...(setup.model.thinking === undefined ? {} : { thinking: setup.model.thinking }) },
|
|
389
|
+
tools: boundedAttemptStrings(setup.tools),
|
|
390
|
+
cwd: boundedAttemptText(setup.cwd),
|
|
391
|
+
...(resources === undefined ? {} : { resourceSelectors: { selectors: { skills: boundedAttemptStrings(resources.selectors.skills), extensions: boundedAttemptStrings(resources.selectors.extensions), tools: boundedAttemptStrings(resources.selectors.tools ?? []) }, skills: boundedAttemptStrings(resources.skills), extensions: boundedAttemptStrings(resources.extensions), tools: boundedAttemptStrings(resources.tools), unmatchedSkills: boundedAttemptStrings(resources.unmatchedSkills), unmatchedExtensions: boundedAttemptStrings(resources.unmatchedExtensions), unmatchedTools: boundedAttemptStrings(resources.unmatchedTools) } }),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function portableProgress(progress, includeActivity = false) {
|
|
395
|
+
return {
|
|
396
|
+
accounting: structuredClone(progress.accounting),
|
|
397
|
+
toolCalls: structuredClone(progress.toolCalls),
|
|
398
|
+
...(progress.state === undefined ? {} : { state: { model: { ...progress.state.model }, ...(progress.state.thinking === undefined ? {} : { thinking: progress.state.thinking }), tools: [...progress.state.tools] } }),
|
|
399
|
+
...(includeActivity && progress.activity !== undefined ? { activity: { ...progress.activity, text: sanitizeDisplayText(progress.activity.text) } } : {}),
|
|
400
|
+
...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function statusFields(run) {
|
|
404
|
+
return run.progress === undefined ? {} : { progress: portableProgress(run.progress) };
|
|
405
|
+
}
|
|
406
|
+
function withoutOwner(status) {
|
|
407
|
+
const next = { ...status };
|
|
408
|
+
delete next.owner;
|
|
409
|
+
return next;
|
|
410
|
+
}
|
|
411
|
+
function persistedStatus(run) {
|
|
412
|
+
const status = {
|
|
413
|
+
id: run.id,
|
|
414
|
+
sessionId: run.sessionId,
|
|
415
|
+
state: run.state,
|
|
416
|
+
startedAt: run.startedAt,
|
|
417
|
+
owner: { ...run.owner },
|
|
418
|
+
...(run.finishedAt === undefined ? {} : { finishedAt: run.finishedAt }),
|
|
419
|
+
...(run.worktree === undefined ? {} : { worktree: { path: run.worktree.path, branch: run.worktree.branch } }),
|
|
420
|
+
...(run.worktreeContext === undefined ? {} : { worktreeContext: { ...run.worktreeContext } }),
|
|
421
|
+
...(run.error === undefined ? {} : { error: run.error }),
|
|
422
|
+
...(run.attempts === undefined ? {} : { attempts: run.attempts }),
|
|
423
|
+
...(run.attemptDetails === undefined ? {} : { attemptDetails: structuredClone(run.attemptDetails) }),
|
|
424
|
+
...statusFields(run),
|
|
425
|
+
};
|
|
426
|
+
return run.state === "running" ? status : withoutOwner(status);
|
|
427
|
+
}
|
|
428
|
+
function decodeFailure(value) {
|
|
429
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
430
|
+
return undefined;
|
|
431
|
+
const record = value;
|
|
432
|
+
const code = errorCode(value);
|
|
433
|
+
return code && typeof record.message === "string" ? { code, message: record.message } : undefined;
|
|
434
|
+
}
|
|
435
|
+
function recordValue(value) {
|
|
436
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
437
|
+
}
|
|
438
|
+
function attemptNumberValue(value) {
|
|
439
|
+
const attempt = recordValue(value)?.attempt;
|
|
440
|
+
return typeof attempt === "number" && Number.isSafeInteger(attempt) && attempt >= 1 ? attempt : undefined;
|
|
441
|
+
}
|
|
442
|
+
function finalizedAttemptValue(value) {
|
|
443
|
+
const record = recordValue(value);
|
|
444
|
+
return record !== undefined && (Object.prototype.hasOwnProperty.call(record, "result") || Object.prototype.hasOwnProperty.call(record, "error"));
|
|
445
|
+
}
|
|
446
|
+
function toolCallsValue(value) {
|
|
447
|
+
if (!Array.isArray(value))
|
|
448
|
+
return undefined;
|
|
449
|
+
const calls = [];
|
|
450
|
+
for (const item of value) {
|
|
451
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
452
|
+
return undefined;
|
|
453
|
+
const record = item;
|
|
454
|
+
if (typeof record.id !== "string" || typeof record.name !== "string" || (record.state !== "running" && record.state !== "completed" && record.state !== "failed"))
|
|
455
|
+
return undefined;
|
|
456
|
+
calls.push({ id: record.id, name: record.name, state: record.state });
|
|
457
|
+
}
|
|
458
|
+
return calls;
|
|
459
|
+
}
|
|
460
|
+
function stringArrayValue(value) {
|
|
461
|
+
if (!Array.isArray(value) || value.length > 256)
|
|
462
|
+
return undefined;
|
|
463
|
+
const strings = value.filter((item) => typeof item === "string");
|
|
464
|
+
return strings.length === value.length ? strings : undefined;
|
|
465
|
+
}
|
|
466
|
+
function modelValue(value) {
|
|
467
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
468
|
+
return undefined;
|
|
469
|
+
const record = value;
|
|
470
|
+
const thinking = record.thinking;
|
|
471
|
+
if (typeof record.provider !== "string" || !record.provider.trim() || typeof record.model !== "string" || !record.model.trim() || thinking !== undefined && !isThinkingLevel(thinking))
|
|
472
|
+
return undefined;
|
|
473
|
+
return { provider: record.provider, model: record.model, ...(thinking === undefined ? {} : { thinking }) };
|
|
474
|
+
}
|
|
475
|
+
function sessionReferenceValue(value) {
|
|
476
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
477
|
+
return undefined;
|
|
478
|
+
const record = value;
|
|
479
|
+
const transport = record.transport;
|
|
480
|
+
const sessionId = record.sessionId;
|
|
481
|
+
const rawLocator = record.locator;
|
|
482
|
+
if (typeof transport !== "string" || !transport.trim() || typeof sessionId !== "string" || !sessionId.trim())
|
|
483
|
+
return undefined;
|
|
484
|
+
if (rawLocator !== undefined && !jsonValue(rawLocator))
|
|
485
|
+
return undefined;
|
|
486
|
+
const locator = rawLocator === undefined ? undefined : boundedAttemptLocator(rawLocator);
|
|
487
|
+
return { transport: boundedAttemptText(transport), sessionId: boundedAttemptText(sessionId), ...(locator === undefined ? {} : { locator }) };
|
|
488
|
+
}
|
|
489
|
+
function resourceSummaryValue(value) {
|
|
490
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
491
|
+
return undefined;
|
|
492
|
+
const record = value;
|
|
493
|
+
const selectorRecord = record.selectors;
|
|
494
|
+
if (typeof selectorRecord !== "object" || selectorRecord === null || Array.isArray(selectorRecord))
|
|
495
|
+
return undefined;
|
|
496
|
+
const skills = stringArrayValue(record.skills);
|
|
497
|
+
const extensions = stringArrayValue(record.extensions);
|
|
498
|
+
const tools = stringArrayValue(record.tools);
|
|
499
|
+
const selectorSkills = stringArrayValue(selectorRecord.skills);
|
|
500
|
+
const selectorExtensions = stringArrayValue(selectorRecord.extensions);
|
|
501
|
+
const selectorTools = stringArrayValue(selectorRecord.tools);
|
|
502
|
+
const unmatchedSkills = stringArrayValue(record.unmatchedSkills);
|
|
503
|
+
const unmatchedExtensions = stringArrayValue(record.unmatchedExtensions);
|
|
504
|
+
const unmatchedTools = stringArrayValue(record.unmatchedTools);
|
|
505
|
+
if (!skills || !extensions || !tools || !selectorSkills || !selectorExtensions || !selectorTools || !unmatchedSkills || !unmatchedExtensions || !unmatchedTools)
|
|
506
|
+
return undefined;
|
|
507
|
+
return { selectors: { skills: selectorSkills, extensions: selectorExtensions, tools: selectorTools }, skills, extensions, tools, unmatchedSkills, unmatchedExtensions, unmatchedTools };
|
|
508
|
+
}
|
|
509
|
+
function setupSummaryValue(value) {
|
|
510
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
511
|
+
return undefined;
|
|
512
|
+
const record = value;
|
|
513
|
+
const hookNames = stringArrayValue(record.hookNames);
|
|
514
|
+
const tools = stringArrayValue(record.tools);
|
|
515
|
+
const model = modelValue(record.model);
|
|
516
|
+
const resources = record.resourceSelectors === undefined ? undefined : resourceSummaryValue(record.resourceSelectors);
|
|
517
|
+
if (!hookNames || !tools || !model || typeof record.cwd !== "string" || !record.cwd.trim() || record.resourceSelectors !== undefined && !resources)
|
|
518
|
+
return undefined;
|
|
519
|
+
return { hookNames, model, tools, cwd: record.cwd, ...(resources === undefined ? {} : { resourceSelectors: resources }) };
|
|
520
|
+
}
|
|
521
|
+
function attemptSummaryValue(value) {
|
|
522
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
523
|
+
return undefined;
|
|
524
|
+
const record = value;
|
|
525
|
+
const attempt = record.attempt;
|
|
526
|
+
const transport = record.transport;
|
|
527
|
+
const session = record.session === undefined ? undefined : sessionReferenceValue(record.session);
|
|
528
|
+
const setup = setupSummaryValue(record.setup);
|
|
529
|
+
const accounting = accountingValue(record.accounting);
|
|
530
|
+
const error = record.error === undefined ? undefined : decodeFailure(record.error);
|
|
531
|
+
if (typeof attempt !== "number" || !Number.isSafeInteger(attempt) || attempt < 1 || typeof transport !== "string" || !transport.trim() || !setup || !accounting || record.session !== undefined && !session || record.error !== undefined && !error)
|
|
532
|
+
return undefined;
|
|
533
|
+
return {
|
|
534
|
+
attempt,
|
|
535
|
+
transport: boundedAttemptText(transport),
|
|
536
|
+
...(session === undefined ? {} : { session }),
|
|
537
|
+
...(error === undefined ? {} : { error: { code: boundedAttemptText(error.code), message: boundedAttemptText(error.message) } }),
|
|
538
|
+
accounting: { ...accounting },
|
|
539
|
+
setup: boundedAttemptSetup(setup),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function isSessionLifecycle(value) {
|
|
543
|
+
const record = recordValue(value);
|
|
544
|
+
return Boolean(record && typeof record.abort === "function" && typeof record.dispose === "function");
|
|
545
|
+
}
|
|
546
|
+
function isLiveSession(value) {
|
|
547
|
+
const record = recordValue(value);
|
|
548
|
+
return Boolean(isSessionLifecycle(value) && record && sessionReferenceValue(record.reference) && typeof record.getState === "function" && typeof record.getSessionStats === "function" && typeof record.getLastAssistant === "function" && typeof record.subscribe === "function" && typeof record.prompt === "function" && typeof record.steer === "function");
|
|
549
|
+
}
|
|
550
|
+
function isPreparedSession(value) {
|
|
551
|
+
const record = recordValue(value);
|
|
552
|
+
const model = modelValue(record?.model);
|
|
553
|
+
const tools = stringArrayValue(record?.tools);
|
|
554
|
+
if (!record || !model || !tools || typeof record.cwd !== "string" || !record.cwd.trim() || typeof record.sessionLabel !== "string")
|
|
555
|
+
return false;
|
|
556
|
+
const textFields = ["initialPrompt", "agentDir", "systemPromptPath", "systemPromptAppend", "piRuntimeError"];
|
|
557
|
+
if (textFields.some((field) => record[field] !== undefined && typeof record[field] !== "string"))
|
|
558
|
+
return false;
|
|
559
|
+
if (record.systemPrompt !== undefined && (typeof record.systemPrompt !== "string" || record.systemPrompt.length > SUBAGENT_SYSTEM_PROMPT_LIMIT))
|
|
560
|
+
return false;
|
|
561
|
+
const runtime = recordValue(record.piRuntime);
|
|
562
|
+
if (record.piRuntime !== undefined && (!runtime || typeof runtime.executable !== "string" || !runtime.executable.trim() || runtime.entrypoint !== undefined && (typeof runtime.entrypoint !== "string" || !runtime.entrypoint.trim())))
|
|
563
|
+
return false;
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
function isLiveSessionHandoff(value) {
|
|
567
|
+
const record = recordValue(value);
|
|
568
|
+
return Boolean(record && (record.state === "local-running" || record.state === "handoff-pending" || record.state === "herdr-running" || record.state === "returning-local" || record.state === "completed") && typeof record.transferred === "boolean" && typeof record.observe === "function" && typeof record.request === "function" && typeof record.waitForTakeover === "function" && typeof record.takeover === "function" && typeof record.waitForResume === "function" && typeof record.release === "function");
|
|
569
|
+
}
|
|
570
|
+
function executionResultValue(value) {
|
|
571
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
572
|
+
return undefined;
|
|
573
|
+
const record = value;
|
|
574
|
+
const result = record.value;
|
|
575
|
+
const attempts = record.attempts;
|
|
576
|
+
if (!jsonValue(result) || !Array.isArray(attempts) || attempts.length > MAX_PERSISTED_ATTEMPT_ARRAY_ITEMS)
|
|
577
|
+
return undefined;
|
|
578
|
+
return { value: result, attempts };
|
|
579
|
+
}
|
|
580
|
+
function sessionStateValue(value) {
|
|
581
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
582
|
+
return undefined;
|
|
583
|
+
const record = value;
|
|
584
|
+
const model = modelValue(record.model);
|
|
585
|
+
const rawTools = record.tools;
|
|
586
|
+
if (!model || !Array.isArray(rawTools))
|
|
587
|
+
return undefined;
|
|
588
|
+
const tools = rawTools.filter((tool) => typeof tool === "string");
|
|
589
|
+
if (tools.length !== rawTools.length)
|
|
590
|
+
return undefined;
|
|
591
|
+
if (record.thinking !== undefined && typeof record.thinking !== "string")
|
|
592
|
+
return undefined;
|
|
593
|
+
const thinking = typeof record.thinking === "string" ? record.thinking : undefined;
|
|
594
|
+
return { model: { ...model, ...(thinking === undefined ? {} : { thinking }) }, ...(thinking === undefined ? {} : { thinking }), tools };
|
|
595
|
+
}
|
|
596
|
+
function progressValue(value) {
|
|
597
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
598
|
+
return undefined;
|
|
599
|
+
const record = value;
|
|
600
|
+
const accounting = accountingValue(record.accounting);
|
|
601
|
+
const toolCalls = toolCallsValue(record.toolCalls);
|
|
602
|
+
const state = record.state === undefined ? undefined : sessionStateValue(record.state);
|
|
603
|
+
if (!accounting || !toolCalls || (record.state !== undefined && state === undefined))
|
|
604
|
+
return undefined;
|
|
605
|
+
const activity = record.activity === undefined ? undefined : activityValue(record.activity);
|
|
606
|
+
if (record.activity !== undefined && activity === undefined)
|
|
607
|
+
return undefined;
|
|
608
|
+
if (record.lastEventAt !== undefined && (!Number.isSafeInteger(record.lastEventAt) || record.lastEventAt < 0))
|
|
609
|
+
return undefined;
|
|
610
|
+
return { accounting, toolCalls, ...(state === undefined ? {} : { state }), ...(activity === undefined ? {} : { activity }), ...(record.lastEventAt === undefined ? {} : { lastEventAt: record.lastEventAt }) };
|
|
611
|
+
}
|
|
612
|
+
function legacyProgressValue(record) {
|
|
613
|
+
const accounting = legacyAccountingValue(record);
|
|
614
|
+
const toolCalls = record.toolCalls === undefined ? [] : toolCallsValue(record.toolCalls);
|
|
615
|
+
if (!accounting || !toolCalls)
|
|
616
|
+
return undefined;
|
|
617
|
+
const activity = record.activity === undefined ? undefined : activityValue(record.activity);
|
|
618
|
+
if (record.activity !== undefined && activity === undefined)
|
|
619
|
+
return undefined;
|
|
620
|
+
if (record.lastEventAt !== undefined && (!Number.isSafeInteger(record.lastEventAt) || record.lastEventAt < 0))
|
|
621
|
+
return undefined;
|
|
622
|
+
return { accounting, toolCalls, ...(activity === undefined ? {} : { activity }), ...(record.lastEventAt === undefined ? {} : { lastEventAt: record.lastEventAt }) };
|
|
623
|
+
}
|
|
624
|
+
function worktreeContextValue(value) {
|
|
625
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
626
|
+
return undefined;
|
|
627
|
+
const record = value;
|
|
628
|
+
const command = record.worktreePostCreateCommand === undefined ? undefined : Array.isArray(record.worktreePostCreateCommand) && record.worktreePostCreateCommand.length > 0 && record.worktreePostCreateCommand.every((argument) => typeof argument === "string" && Boolean(argument.trim())) ? record.worktreePostCreateCommand : undefined;
|
|
629
|
+
const source = record.worktreePostCreateCommandSource === undefined ? undefined : typeof record.worktreePostCreateCommandSource === "string" && record.worktreePostCreateCommandSource.trim() ? record.worktreePostCreateCommandSource : undefined;
|
|
630
|
+
const context = typeof record.cwd === "string" && typeof record.sessionId === "string" && typeof record.runId === "string" && typeof record.name === "string" && typeof record.owner === "string" ? { cwd: record.cwd, sessionId: record.sessionId, runId: record.runId, name: record.name, owner: record.owner, ...(command === undefined ? {} : { worktreePostCreateCommand: command }), ...(source === undefined ? {} : { worktreePostCreateCommandSource: source }) } : undefined;
|
|
631
|
+
if (!context || !context.cwd.trim() || !context.sessionId.trim() || !context.runId.trim() || !context.name.trim() || !context.owner.trim() || record.worktreePostCreateCommand !== undefined && command === undefined || record.worktreePostCreateCommandSource !== undefined && source === undefined)
|
|
632
|
+
return undefined;
|
|
633
|
+
return context;
|
|
634
|
+
}
|
|
635
|
+
function decodeStatus(value, id, includeAttemptMetadata = true) {
|
|
636
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
637
|
+
return undefined;
|
|
638
|
+
const record = value;
|
|
639
|
+
if (record.id !== id || !validSubagentId(id) || (record.state !== "running" && record.state !== "completed" && record.state !== "failed" && record.state !== "stopped") || typeof record.startedAt !== "number" || !Number.isSafeInteger(record.startedAt) || record.startedAt < 0)
|
|
640
|
+
return undefined;
|
|
641
|
+
const persistedSessionId = record.sessionId;
|
|
642
|
+
if (persistedSessionId !== undefined && (typeof persistedSessionId !== "string" || !persistedSessionId.trim()))
|
|
643
|
+
return undefined;
|
|
644
|
+
const owner = record.owner === undefined ? undefined : decodeOwnerMarker(record.owner);
|
|
645
|
+
if (record.owner !== undefined && owner === undefined)
|
|
646
|
+
return undefined;
|
|
647
|
+
const error = record.error === undefined ? undefined : decodeFailure(record.error);
|
|
648
|
+
if (record.error !== undefined && error === undefined)
|
|
649
|
+
return undefined;
|
|
650
|
+
if (record.finishedAt !== undefined && (typeof record.finishedAt !== "number" || !Number.isSafeInteger(record.finishedAt) || record.finishedAt < record.startedAt))
|
|
651
|
+
return undefined;
|
|
652
|
+
const worktree = record.worktree === undefined ? undefined : worktreeValue(record.worktree);
|
|
653
|
+
const worktreeContext = record.worktreeContext === undefined ? undefined : worktreeContextValue(record.worktreeContext);
|
|
654
|
+
if (record.worktree !== undefined && worktree === undefined || record.worktreeContext !== undefined && worktreeContext === undefined)
|
|
655
|
+
return undefined;
|
|
656
|
+
if (worktreeContext && (worktreeContext.runId !== id || worktreeContext.owner !== structuralPath("worktree", "named", worktreeContext.name)))
|
|
657
|
+
return undefined;
|
|
658
|
+
const sessionId = persistedSessionId ?? worktreeContext?.sessionId;
|
|
659
|
+
const progress = record.progress === undefined ? legacyProgressValue(record) : progressValue(record.progress);
|
|
660
|
+
if (record.progress !== undefined && progress === undefined)
|
|
661
|
+
return undefined;
|
|
662
|
+
const legacyAccounting = record.accounting === undefined && record.usage === undefined ? undefined : legacyAccountingValue(record);
|
|
663
|
+
const legacyActivity = record.activity === undefined ? undefined : activityValue(record.activity);
|
|
664
|
+
const legacyToolCalls = record.toolCalls === undefined ? undefined : toolCallsValue(record.toolCalls);
|
|
665
|
+
if (record.accounting !== undefined && !legacyAccounting || record.usage !== undefined && !legacyAccounting || record.activity !== undefined && !legacyActivity || record.toolCalls !== undefined && !legacyToolCalls || record.lastEventAt !== undefined && (!Number.isSafeInteger(record.lastEventAt) || record.lastEventAt < 0))
|
|
666
|
+
return undefined;
|
|
667
|
+
if (includeAttemptMetadata && record.systemPrompt !== undefined && (typeof record.systemPrompt !== "string" || record.systemPrompt.length > SUBAGENT_SYSTEM_PROMPT_LIMIT))
|
|
668
|
+
return undefined;
|
|
669
|
+
const attempts = record.attempts;
|
|
670
|
+
if (attempts !== undefined && (typeof attempts !== "number" || !Number.isSafeInteger(attempts) || attempts < 1))
|
|
671
|
+
return undefined;
|
|
672
|
+
const attemptDetails = includeAttemptMetadata && record.attemptDetails !== undefined ? Array.isArray(record.attemptDetails) && record.attemptDetails.length <= SUBAGENT_ATTEMPT_DETAILS_LIMIT ? record.attemptDetails.map(attemptSummaryValue) : undefined : undefined;
|
|
673
|
+
if (includeAttemptMetadata && record.attemptDetails !== undefined && (!attemptDetails || attemptDetails.some((attempt) => attempt === undefined)))
|
|
674
|
+
return undefined;
|
|
675
|
+
if (includeAttemptMetadata && attempts !== undefined && attemptDetails?.some((attempt) => attempt !== undefined && attempt.attempt > attempts))
|
|
676
|
+
return undefined;
|
|
677
|
+
return { id, ...(sessionId === undefined ? {} : { sessionId }), state: record.state, startedAt: record.startedAt, ...(attempts === undefined ? {} : { attempts }), ...(attemptDetails === undefined ? {} : { attemptDetails: attemptDetails.filter((attempt) => attempt !== undefined) }), ...(record.finishedAt === undefined ? {} : { finishedAt: record.finishedAt }), ...(owner === undefined ? {} : { owner }), ...(worktree === undefined ? {} : { worktree }), ...(worktreeContext === undefined ? {} : { worktreeContext }), ...(error === undefined ? {} : { error }), ...(progress === undefined ? {} : { progress }) };
|
|
678
|
+
}
|
|
679
|
+
function publicStatus(status, includeAttemptMetadata = false, includeActivity = false) {
|
|
680
|
+
return {
|
|
681
|
+
id: status.id,
|
|
682
|
+
...(status.sessionId === undefined ? {} : { sessionId: status.sessionId }),
|
|
683
|
+
...(status.attempts === undefined ? {} : { attempts: status.attempts }),
|
|
684
|
+
...(includeAttemptMetadata && status.attemptDetails !== undefined ? { attemptDetails: structuredClone(status.attemptDetails) } : {}),
|
|
685
|
+
state: status.state,
|
|
686
|
+
...(status.startedAt === undefined ? {} : { startedAt: status.startedAt }),
|
|
687
|
+
...(status.finishedAt === undefined ? {} : { finishedAt: status.finishedAt }),
|
|
688
|
+
...(status.worktree === undefined ? {} : { worktree: { ...status.worktree } }),
|
|
689
|
+
...(status.error === undefined ? {} : { error: { ...status.error } }),
|
|
690
|
+
...(status.progress === undefined ? {} : { progress: portableProgress(status.progress, includeActivity) }),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function liveStatus(run) {
|
|
694
|
+
const persisted = persistedStatus(run);
|
|
695
|
+
return run.progress === undefined ? persisted : { ...persisted, progress: portableProgress(run.progress, true) };
|
|
696
|
+
}
|
|
697
|
+
function emitUpdate(run) {
|
|
698
|
+
const live = liveStatus(run);
|
|
699
|
+
try {
|
|
700
|
+
run.update?.(publicStatus(live, false, true));
|
|
701
|
+
}
|
|
702
|
+
catch { /* Rendering must not affect execution. */ }
|
|
703
|
+
try {
|
|
704
|
+
run.observe?.(publicStatus(live, true, true));
|
|
705
|
+
}
|
|
706
|
+
catch { /* A widget failure is display-only. */ }
|
|
707
|
+
}
|
|
708
|
+
async function createRunStorage(root, id, request, status, external) {
|
|
709
|
+
await secureDirectory(root);
|
|
710
|
+
const directory = runDirectory(root, id);
|
|
711
|
+
let created = false;
|
|
712
|
+
try {
|
|
713
|
+
await mkdir(directory, { mode: 0o700 });
|
|
714
|
+
created = true;
|
|
715
|
+
await chmod(directory, 0o700);
|
|
716
|
+
await atomicJson(requestPath(directory), request);
|
|
717
|
+
await atomicJson(configurationPath(directory), external);
|
|
718
|
+
await atomicJson(statusPath(directory), status);
|
|
719
|
+
return directory;
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
if (created)
|
|
723
|
+
await rm(directory, { recursive: true, force: true });
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async function loadPersistedStatus(root, id, includeAttemptMetadata = true) {
|
|
728
|
+
try {
|
|
729
|
+
const value = await readJson(statusPath(runDirectory(root, id)));
|
|
730
|
+
const status = decodeStatus(value, id, includeAttemptMetadata);
|
|
731
|
+
if (!status)
|
|
732
|
+
throw new InvalidPersistedSubagentStatusError();
|
|
733
|
+
return status;
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
if (isNodeError(error, "ENOENT"))
|
|
737
|
+
throw new WorkflowError("RUN_NOT_FOUND", `Unknown subagent ${id}`);
|
|
738
|
+
if (error instanceof InvalidPersistedSubagentStatusError)
|
|
739
|
+
throw error;
|
|
740
|
+
if (error instanceof SyntaxError)
|
|
741
|
+
throw new InvalidPersistedSubagentStatusError();
|
|
742
|
+
if (error instanceof WorkflowError)
|
|
743
|
+
throw error;
|
|
744
|
+
throw internalStorageError(error, `Unable to read subagent ${id} status`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function loadPersistedRequest(root, id) {
|
|
748
|
+
try {
|
|
749
|
+
return checkedRequest(await readJson(requestPath(runDirectory(root, id))));
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
if (error instanceof WorkflowError)
|
|
753
|
+
throw error;
|
|
754
|
+
throw internalStorageError(error, `Unable to read subagent ${id} request`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async function loadPersistedFailure(root, id) {
|
|
758
|
+
try {
|
|
759
|
+
const failure = decodeFailure(await readJson(failurePath(runDirectory(root, id))));
|
|
760
|
+
if (!failure)
|
|
761
|
+
throw new WorkflowError("INTERNAL_ERROR", "Persisted subagent failure is invalid");
|
|
762
|
+
return failure;
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
if (error instanceof WorkflowError)
|
|
766
|
+
throw error;
|
|
767
|
+
throw internalStorageError(error, `Unable to read subagent ${id} failure`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async function loadPersistedResult(root, id) {
|
|
771
|
+
try {
|
|
772
|
+
const value = await readJson(resultPath(runDirectory(root, id)));
|
|
773
|
+
if (!jsonValue(value))
|
|
774
|
+
throw new WorkflowError("INTERNAL_ERROR", "Persisted subagent result is not a JSON value");
|
|
775
|
+
return value;
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
if (error instanceof WorkflowError)
|
|
779
|
+
throw error;
|
|
780
|
+
throw internalStorageError(error, `Unable to read subagent ${id} result`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
async function loadOptionalJson(path) {
|
|
784
|
+
try {
|
|
785
|
+
return await readJson(path);
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
if (isNodeError(error, "ENOENT"))
|
|
789
|
+
return undefined;
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
async function loadPersistedStandaloneExternalConfiguration(root, id) {
|
|
794
|
+
try {
|
|
795
|
+
const value = await loadOptionalJson(configurationPath(runDirectory(root, id)));
|
|
796
|
+
if (value === undefined)
|
|
797
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Subagent ${id} has no frozen external configuration; this historic run cannot be retried deterministically`);
|
|
798
|
+
const configuration = decodePersistedStandaloneExternalConfiguration(value);
|
|
799
|
+
if (configuration === undefined)
|
|
800
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Subagent ${id} has invalid frozen external configuration`);
|
|
801
|
+
return configuration;
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
if (error instanceof WorkflowError)
|
|
805
|
+
throw error;
|
|
806
|
+
if (error instanceof SyntaxError)
|
|
807
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Subagent ${id} has invalid frozen external configuration`);
|
|
808
|
+
throw internalStorageError(error, `Unable to read subagent ${id} external configuration`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function orphanedFailure() {
|
|
812
|
+
return { code: "INTERNAL_ERROR", message: "Subagent run was interrupted before completion" };
|
|
813
|
+
}
|
|
814
|
+
function reconciledStatus(status, state, finishedAt, error) {
|
|
815
|
+
const next = {
|
|
816
|
+
...status,
|
|
817
|
+
state,
|
|
818
|
+
finishedAt,
|
|
819
|
+
...(error === undefined ? {} : { error }),
|
|
820
|
+
};
|
|
821
|
+
return withoutOwner(next);
|
|
822
|
+
}
|
|
823
|
+
function withoutWorktree(status) {
|
|
824
|
+
const next = { ...status };
|
|
825
|
+
delete next.worktree;
|
|
826
|
+
delete next.worktreeContext;
|
|
827
|
+
return next;
|
|
828
|
+
}
|
|
829
|
+
async function reconcilePersistedResult(root, id, status) {
|
|
830
|
+
if (status.state !== "running")
|
|
831
|
+
return status;
|
|
832
|
+
const directory = runDirectory(root, id);
|
|
833
|
+
const result = await loadOptionalJson(resultPath(directory));
|
|
834
|
+
if (result === undefined)
|
|
835
|
+
return status;
|
|
836
|
+
if (!jsonValue(result))
|
|
837
|
+
throw new WorkflowError("INTERNAL_ERROR", "Persisted subagent result is not a JSON value");
|
|
838
|
+
const completed = reconciledStatus(status, "completed", Math.max(Date.now(), status.startedAt));
|
|
839
|
+
await atomicJson(statusPath(directory), completed);
|
|
840
|
+
return completed;
|
|
841
|
+
}
|
|
842
|
+
async function reconcilePersistedRun(root, id, status) {
|
|
843
|
+
const afterResult = await reconcilePersistedResult(root, id, status);
|
|
844
|
+
if (afterResult.state !== "running")
|
|
845
|
+
return afterResult;
|
|
846
|
+
const directory = runDirectory(root, id);
|
|
847
|
+
const failureValue = await loadOptionalJson(failurePath(directory));
|
|
848
|
+
const failure = failureValue === undefined ? orphanedFailure() : decodeFailure(failureValue);
|
|
849
|
+
if (!failure)
|
|
850
|
+
throw new WorkflowError("INTERNAL_ERROR", "Persisted subagent failure is invalid");
|
|
851
|
+
const failed = reconciledStatus(afterResult, "failed", Math.max(Date.now(), afterResult.startedAt), failure);
|
|
852
|
+
await atomicJson(failurePath(directory), failure);
|
|
853
|
+
await atomicJson(statusPath(directory), failed);
|
|
854
|
+
return failed;
|
|
855
|
+
}
|
|
856
|
+
async function persistReconciliationFailure(root, id, status, error) {
|
|
857
|
+
if (status.state !== "running")
|
|
858
|
+
return;
|
|
859
|
+
let persisted;
|
|
860
|
+
try {
|
|
861
|
+
persisted = await loadPersistedStatus(root, id);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (persisted.state !== "running")
|
|
867
|
+
return;
|
|
868
|
+
const failure = failureFrom(error);
|
|
869
|
+
const failed = reconciledStatus(persisted, "failed", Math.max(Date.now(), persisted.startedAt), failure);
|
|
870
|
+
try {
|
|
871
|
+
await atomicJson(statusPath(runDirectory(root, id)), failed);
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
// A run without a writable status file stays isolated from healthy records.
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
await atomicJson(failurePath(runDirectory(root, id)), failure);
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
// The terminal status retains the recovery failure when its detail file cannot be written.
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async function reconcilePersistedRuns(root, liveness, cleanupWorktree) {
|
|
885
|
+
await secureDirectory(root);
|
|
886
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
887
|
+
const errors = new Map();
|
|
888
|
+
for (const entry of entries) {
|
|
889
|
+
if (!entry.isDirectory() || !validSubagentId(entry.name))
|
|
890
|
+
continue;
|
|
891
|
+
let status;
|
|
892
|
+
try {
|
|
893
|
+
status = await loadPersistedStatus(root, entry.name, false);
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
if (error instanceof WorkflowError && (error.code === "RUN_NOT_FOUND" || error instanceof InvalidPersistedSubagentStatusError))
|
|
897
|
+
continue;
|
|
898
|
+
errors.set(entry.name, error instanceof WorkflowError ? error : internalStorageError(error, `Unable to reconcile subagent ${entry.name} status`));
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (status.state !== "running" && status.worktreeContext === undefined && status.worktree === undefined)
|
|
902
|
+
continue;
|
|
903
|
+
if (status.state === "running" && status.owner !== undefined && await ownerIsLive(status.owner, liveness))
|
|
904
|
+
continue;
|
|
905
|
+
try {
|
|
906
|
+
if (status.state === "running")
|
|
907
|
+
status = await reconcilePersistedResult(root, entry.name, status);
|
|
908
|
+
let cleaned = false;
|
|
909
|
+
if (status.worktreeContext !== undefined && cleanupWorktree !== undefined) {
|
|
910
|
+
cleaned = await cleanupWorktree(root, entry.name, status);
|
|
911
|
+
}
|
|
912
|
+
if (status.state === "running")
|
|
913
|
+
status = await reconcilePersistedRun(root, entry.name, status);
|
|
914
|
+
if (cleaned || (status.worktreeContext === undefined && status.worktree !== undefined)) {
|
|
915
|
+
status = withoutOwner(withoutWorktree(status));
|
|
916
|
+
await atomicJson(statusPath(runDirectory(root, entry.name)), status);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
await persistReconciliationFailure(root, entry.name, status, error);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return errors;
|
|
924
|
+
}
|
|
925
|
+
function enqueueWrite(run, operation) {
|
|
926
|
+
return run.writes.run(operation);
|
|
927
|
+
}
|
|
928
|
+
function terminalSummary(run) {
|
|
929
|
+
return persistedStatus(run);
|
|
930
|
+
}
|
|
931
|
+
function unavailable(operation) {
|
|
932
|
+
return { ok: false, error: { code: "SUBAGENTS_NOT_CONFIGURED", message: `Subagent manager does not implement ${operation} yet.` } };
|
|
933
|
+
}
|
|
934
|
+
class PersistentSubagentManager {
|
|
935
|
+
dependencies;
|
|
936
|
+
activeRuns = new Map();
|
|
937
|
+
activeRunCount = 0;
|
|
938
|
+
terminalSummaries = new Map();
|
|
939
|
+
reconciliationErrors = new Map();
|
|
940
|
+
notificationPromises = new Set();
|
|
941
|
+
initialization;
|
|
942
|
+
worktreeAdapter;
|
|
943
|
+
initializationError;
|
|
944
|
+
storageOwner;
|
|
945
|
+
runOwner;
|
|
946
|
+
disposed = false;
|
|
947
|
+
disposePromise;
|
|
948
|
+
constructor(dependencies) {
|
|
949
|
+
this.dependencies = dependencies;
|
|
950
|
+
this.worktreeAdapter = dependencies.worktreeAdapter ?? createRunStoreWorktreeAdapter(defaultWorktreeHome(storageDirectory(dependencies)));
|
|
951
|
+
this.initialization = this.initialize();
|
|
952
|
+
}
|
|
953
|
+
async initialize() {
|
|
954
|
+
try {
|
|
955
|
+
const owner = await createOwnerMarker(this.dependencies.liveness);
|
|
956
|
+
this.runOwner = owner;
|
|
957
|
+
this.storageOwner = await acquireStorageOwner(storageDirectory(this.dependencies), owner, this.dependencies.liveness);
|
|
958
|
+
if (!this.storageOwner)
|
|
959
|
+
return;
|
|
960
|
+
const cleanup = this.worktreeAdapter.cleanup?.bind(this.worktreeAdapter);
|
|
961
|
+
const errors = await reconcilePersistedRuns(storageDirectory(this.dependencies), this.dependencies.liveness, async (_root, _id, status) => {
|
|
962
|
+
const worktreeContext = status.worktreeContext;
|
|
963
|
+
if (!cleanup || !worktreeContext)
|
|
964
|
+
return false;
|
|
965
|
+
await cleanup(worktreeContext);
|
|
966
|
+
return true;
|
|
967
|
+
});
|
|
968
|
+
for (const [id, error] of errors)
|
|
969
|
+
this.reconciliationErrors.set(id, error);
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
this.initializationError = error instanceof WorkflowError ? error : internalStorageError(error, "Unable to reconcile subagent storage");
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async ensureInitialized() {
|
|
976
|
+
await this.initialization;
|
|
977
|
+
if (this.initializationError)
|
|
978
|
+
throw this.initializationError;
|
|
979
|
+
}
|
|
980
|
+
async run(request, context) {
|
|
981
|
+
const run = await this.start(checkedRequest(request), context);
|
|
982
|
+
emitUpdate(run);
|
|
983
|
+
if (run.request.mode !== "foreground")
|
|
984
|
+
return { id: run.id, state: "running" };
|
|
985
|
+
return run.terminal;
|
|
986
|
+
}
|
|
987
|
+
async start(snapshot, context, frozenExternal) {
|
|
988
|
+
await this.ensureInitialized();
|
|
989
|
+
if (this.disposed)
|
|
990
|
+
throw new WorkflowError("CANCELLED", "Subagent manager is disposed");
|
|
991
|
+
if (context.signal?.aborted)
|
|
992
|
+
throw new WorkflowError("CANCELLED", "Subagent cancelled");
|
|
993
|
+
const external = frozenExternal ?? standaloneExternalConfiguration(context, this.dependencies);
|
|
994
|
+
if (external.projectTrusted && !context.extensionContext.isProjectTrusted())
|
|
995
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Cannot retry standalone configuration from an untrusted project");
|
|
996
|
+
const id = randomUUID();
|
|
997
|
+
const startedAt = Date.now();
|
|
998
|
+
const sessionId = context.extensionContext.sessionManager.getSessionId();
|
|
999
|
+
if (!sessionId.trim())
|
|
1000
|
+
throw new WorkflowError("INTERNAL_ERROR", "Current Pi session identity is unavailable");
|
|
1001
|
+
const owner = this.runOwner;
|
|
1002
|
+
if (!owner)
|
|
1003
|
+
throw new WorkflowError("INTERNAL_ERROR", "Subagent storage owner identity is unavailable");
|
|
1004
|
+
const settings = resolveWorkflowSettings(context.extensionContext.cwd, context.extensionContext.isProjectTrusted(), workflowSettingsPath(this.dependencies.agentDir ?? getAgentDir()));
|
|
1005
|
+
const concurrency = settings.effective.concurrency;
|
|
1006
|
+
if (this.activeRunCount >= concurrency)
|
|
1007
|
+
throw new WorkflowError("AGENT_FAILED", `Subagent concurrency limit reached (${String(this.activeRunCount)}/${String(concurrency)} active runs); no queue is maintained. Retry after an active run settles.`);
|
|
1008
|
+
this.activeRunCount += 1;
|
|
1009
|
+
const controller = new AbortController();
|
|
1010
|
+
const initialStatus = { id, sessionId, state: "running", startedAt, owner: { ...owner } };
|
|
1011
|
+
let directory;
|
|
1012
|
+
try {
|
|
1013
|
+
directory = await createRunStorage(storageDirectory(this.dependencies), id, snapshot, initialStatus, external);
|
|
1014
|
+
}
|
|
1015
|
+
catch (error) {
|
|
1016
|
+
this.activeRunCount -= 1;
|
|
1017
|
+
if (error instanceof WorkflowError)
|
|
1018
|
+
throw error;
|
|
1019
|
+
throw internalStorageError(error, `Unable to start subagent ${id}`);
|
|
1020
|
+
}
|
|
1021
|
+
const current = {};
|
|
1022
|
+
let resolveTerminal;
|
|
1023
|
+
const terminal = new Promise((resolve) => { resolveTerminal = resolve; });
|
|
1024
|
+
const executorOwnership = { default: true };
|
|
1025
|
+
const execution = Promise.resolve().then(async () => {
|
|
1026
|
+
const live = current.run;
|
|
1027
|
+
if (!live || live.disposed || controller.signal.aborted)
|
|
1028
|
+
throw new WorkflowError("CANCELLED", "Subagent cancelled");
|
|
1029
|
+
const worktreeContext = snapshot.worktree === undefined ? undefined : {
|
|
1030
|
+
cwd: context.extensionContext.cwd,
|
|
1031
|
+
sessionId,
|
|
1032
|
+
runId: id,
|
|
1033
|
+
name: snapshot.worktree,
|
|
1034
|
+
owner: structuralPath("worktree", "named", snapshot.worktree),
|
|
1035
|
+
...(settings.effective.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: settings.effective.worktreePostCreateCommand }),
|
|
1036
|
+
...(settings.sources.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommandSource: settings.sources.worktreePostCreateCommand }),
|
|
1037
|
+
};
|
|
1038
|
+
if (worktreeContext !== undefined) {
|
|
1039
|
+
live.worktreeContext = worktreeContext;
|
|
1040
|
+
await enqueueWrite(live, () => atomicJson(statusPath(live.directory), persistedStatus(live)));
|
|
1041
|
+
}
|
|
1042
|
+
const worktree = worktreeContext === undefined ? undefined : await this.worktreeAdapter.create(worktreeContext);
|
|
1043
|
+
if (worktree) {
|
|
1044
|
+
live.worktree = worktree;
|
|
1045
|
+
await enqueueWrite(live, () => atomicJson(statusPath(live.directory), persistedStatus(live)));
|
|
1046
|
+
if (!this.canSteer(live)) {
|
|
1047
|
+
await this.cleanupWorktree(live);
|
|
1048
|
+
throw new WorkflowError("CANCELLED", "Subagent cancelled");
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const baseRoot = executionRoot(context, this.dependencies, controller.signal, id, worktree, external);
|
|
1052
|
+
const root = loadingRegistry().modelAliases().length === 0 ? baseRoot : await addDynamicAliases(context, controller.signal, baseRoot);
|
|
1053
|
+
const transport = this.dependencies.transport ?? localAgentTransport;
|
|
1054
|
+
const injectedExecutor = this.dependencies.createExecutor?.(root, transport);
|
|
1055
|
+
executorOwnership.default = injectedExecutor === undefined;
|
|
1056
|
+
if (current.run)
|
|
1057
|
+
current.run.executorOwnsSession = executorOwnership.default;
|
|
1058
|
+
const setSteer = (handler) => {
|
|
1059
|
+
const run = current.run;
|
|
1060
|
+
if (run)
|
|
1061
|
+
this.registerSteerHandler(run, handler);
|
|
1062
|
+
};
|
|
1063
|
+
const options = executionOptions(snapshot, async (attempt) => {
|
|
1064
|
+
const run = current.run;
|
|
1065
|
+
if (!run)
|
|
1066
|
+
return;
|
|
1067
|
+
const record = recordValue(attempt);
|
|
1068
|
+
const summary = attemptSummaryValue(attempt);
|
|
1069
|
+
const liveSession = isSessionLifecycle(record?.liveSession) ? record.liveSession : undefined;
|
|
1070
|
+
const prepared = isPreparedSession(record?.prepared) ? record.prepared : undefined;
|
|
1071
|
+
const handoff = isLiveSessionHandoff(record?.handoff) ? record.handoff : undefined;
|
|
1072
|
+
if (run.disposed) {
|
|
1073
|
+
if (liveSession && !run.executorOwnsSession)
|
|
1074
|
+
void this.cleanupSession(run, true, true, liveSession).catch(() => undefined);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (liveSession) {
|
|
1078
|
+
const previousSession = run.session;
|
|
1079
|
+
run.activeSessions.add(liveSession);
|
|
1080
|
+
run.session = liveSession;
|
|
1081
|
+
run.prepared = prepared;
|
|
1082
|
+
run.handoff = handoff;
|
|
1083
|
+
if (previousSession && previousSession !== liveSession) {
|
|
1084
|
+
if (run.executorOwnsSession)
|
|
1085
|
+
run.activeSessions.delete(previousSession);
|
|
1086
|
+
else
|
|
1087
|
+
void this.cleanupSession(run, true, false, previousSession).catch(() => undefined);
|
|
1088
|
+
}
|
|
1089
|
+
if (run.controller.signal.aborted)
|
|
1090
|
+
void this.cleanupSession(run, !run.executorOwnsSession, true, liveSession).catch(() => undefined);
|
|
1091
|
+
}
|
|
1092
|
+
else if (summary && record?.session === undefined) {
|
|
1093
|
+
run.session = undefined;
|
|
1094
|
+
run.prepared = undefined;
|
|
1095
|
+
run.handoff = undefined;
|
|
1096
|
+
}
|
|
1097
|
+
this.recordAttempt(run, attempt, finalizedAttemptValue(attempt));
|
|
1098
|
+
try {
|
|
1099
|
+
await enqueueWrite(run, () => atomicJson(statusPath(run.directory), persistedStatus(run)));
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
// Attempt metadata remains available in memory when status storage is unavailable.
|
|
1103
|
+
}
|
|
1104
|
+
if (run.state !== "running")
|
|
1105
|
+
return;
|
|
1106
|
+
emitUpdate(run);
|
|
1107
|
+
}, (progress) => this.recordProgress(current.run, progress));
|
|
1108
|
+
if (injectedExecutor)
|
|
1109
|
+
return injectedExecutor.execute(snapshot.prompt, options, controller.signal, setSteer);
|
|
1110
|
+
return new WorkflowAgentExecutor(root, transport).execute(snapshot.prompt, options, controller.signal, [], setSteer);
|
|
1111
|
+
});
|
|
1112
|
+
const onStatus = this.dependencies.onStatus;
|
|
1113
|
+
const run = { id, sessionId, request: snapshot, directory, startedAt, owner: { ...owner }, controller, promise: execution, terminal, resolveTerminal, update: snapshot.mode === "foreground" ? context.onUpdate : undefined, observe: onStatus === undefined ? undefined : (status) => { onStatus(status, snapshot); }, state: "running", error: undefined, value: undefined, cancelled: false, session: undefined, sessionCleanups: new WeakMap(), activeSessions: new Set(), prepared: undefined, handoff: undefined, progress: undefined, activeAttempt: undefined, finalizedAttemptAccounting: new Map(), attemptDetails: undefined, attempts: undefined, worktree: undefined, worktreeContext: undefined, worktreeCleanup: undefined, steerHandler: undefined, pendingSteers: [], steerFlush: undefined, externalAbort: undefined, externalSignal: undefined, executorOwnsSession: executorOwnership.default, disposed: false, concurrencyReleased: false, notificationSent: false, terminalResolved: false, writes: new SerialLane() };
|
|
1114
|
+
current.run = run;
|
|
1115
|
+
this.activeRuns.set(id, run);
|
|
1116
|
+
const externalSignal = context.signal;
|
|
1117
|
+
if (externalSignal) {
|
|
1118
|
+
const abort = () => { this.abortRun(run); };
|
|
1119
|
+
run.externalAbort = abort;
|
|
1120
|
+
run.externalSignal = externalSignal;
|
|
1121
|
+
externalSignal.addEventListener("abort", abort, { once: true });
|
|
1122
|
+
if (externalSignal.aborted)
|
|
1123
|
+
this.abortRun(run);
|
|
1124
|
+
}
|
|
1125
|
+
this.observe(run);
|
|
1126
|
+
return run;
|
|
1127
|
+
}
|
|
1128
|
+
getAttemptActionData(id) {
|
|
1129
|
+
const run = this.activeRuns.get(id);
|
|
1130
|
+
const attempt = run?.attemptDetails?.at(-1);
|
|
1131
|
+
if (!run || run.disposed || !attempt)
|
|
1132
|
+
return undefined;
|
|
1133
|
+
const liveSession = isLiveSession(run.session) ? run.session : undefined;
|
|
1134
|
+
const live = run.state === "running" && liveSession && attempt.session && liveSession.reference.transport === attempt.session.transport && liveSession.reference.sessionId === attempt.session.sessionId ? liveSession : undefined;
|
|
1135
|
+
return {
|
|
1136
|
+
attempt: structuredClone(attempt),
|
|
1137
|
+
...(attempt.session === undefined ? {} : { session: structuredClone(attempt.session) }),
|
|
1138
|
+
...(live === undefined ? {} : { liveSession: live, ...(run.prepared === undefined ? {} : { prepared: run.prepared }), ...(run.handoff === undefined ? {} : { handoff: run.handoff }) }),
|
|
1139
|
+
signal: run.controller.signal,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
async inspect(request, context) {
|
|
1143
|
+
await this.ensureInitialized();
|
|
1144
|
+
if (request.id !== undefined) {
|
|
1145
|
+
const id = checkedId({ id: request.id });
|
|
1146
|
+
const active = this.activeRuns.get(id);
|
|
1147
|
+
const status = active ? persistedStatus(active) : this.terminalSummaries.get(id) ?? await loadPersistedStatus(storageDirectory(this.dependencies), id);
|
|
1148
|
+
const inspection = publicStatus(status, context.includeAttemptMetadata === true);
|
|
1149
|
+
if (status.state === "completed")
|
|
1150
|
+
return { ...inspection, value: await loadPersistedResult(storageDirectory(this.dependencies), id) };
|
|
1151
|
+
if (status.state === "failed") {
|
|
1152
|
+
const failure = status.error ?? await loadPersistedFailure(storageDirectory(this.dependencies), id);
|
|
1153
|
+
return { ...inspection, error: { ...failure } };
|
|
1154
|
+
}
|
|
1155
|
+
return inspection;
|
|
1156
|
+
}
|
|
1157
|
+
return this.inspectList();
|
|
1158
|
+
}
|
|
1159
|
+
async inspectList() {
|
|
1160
|
+
const root = storageDirectory(this.dependencies);
|
|
1161
|
+
await secureDirectory(root);
|
|
1162
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
1163
|
+
const statuses = [];
|
|
1164
|
+
for (const entry of entries) {
|
|
1165
|
+
if (!entry.isDirectory() || !validSubagentId(entry.name))
|
|
1166
|
+
continue;
|
|
1167
|
+
// Keep list/reconciliation available; direct lookup still re-reads and reports the error.
|
|
1168
|
+
if (this.reconciliationErrors.has(entry.name))
|
|
1169
|
+
continue;
|
|
1170
|
+
try {
|
|
1171
|
+
const active = this.activeRuns.get(entry.name);
|
|
1172
|
+
const status = publicStatus(active ? persistedStatus(active) : this.terminalSummaries.get(entry.name) ?? await loadPersistedStatus(root, entry.name, false));
|
|
1173
|
+
if (status.startedAt === undefined)
|
|
1174
|
+
continue;
|
|
1175
|
+
statuses.push({ status, startedAt: status.startedAt });
|
|
1176
|
+
}
|
|
1177
|
+
catch (error) {
|
|
1178
|
+
if (error instanceof WorkflowError && (error.code === "RUN_NOT_FOUND" || error instanceof InvalidPersistedSubagentStatusError))
|
|
1179
|
+
continue;
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
statuses.sort((left, right) => left.startedAt - right.startedAt || (left.status.id < right.status.id ? -1 : left.status.id > right.status.id ? 1 : 0));
|
|
1184
|
+
return statuses.map(({ status }) => status);
|
|
1185
|
+
}
|
|
1186
|
+
async steer(request) {
|
|
1187
|
+
const id = checkedId(request);
|
|
1188
|
+
if (typeof request.message !== "string")
|
|
1189
|
+
throw new WorkflowError("INVALID_METADATA", "Invalid subagents_steer parameters");
|
|
1190
|
+
await this.ensureInitialized();
|
|
1191
|
+
const run = this.activeRuns.get(id);
|
|
1192
|
+
if (!run || !this.canSteer(run))
|
|
1193
|
+
throw new WorkflowError("AGENT_FAILED", `Subagent ${id} is not running`);
|
|
1194
|
+
if (run.pendingSteers.length + (run.steerFlush === undefined ? 0 : 1) >= MAX_PENDING_STEERING_MESSAGES)
|
|
1195
|
+
throw new WorkflowError("AGENT_FAILED", `Steering queue is full for subagent ${id}`);
|
|
1196
|
+
run.pendingSteers.push(request.message);
|
|
1197
|
+
this.flushSteers(run);
|
|
1198
|
+
return { id, accepted: true };
|
|
1199
|
+
}
|
|
1200
|
+
async stop(request) {
|
|
1201
|
+
const id = checkedId(request);
|
|
1202
|
+
await this.ensureInitialized();
|
|
1203
|
+
const run = this.activeRuns.get(id);
|
|
1204
|
+
if (!run)
|
|
1205
|
+
return publicStatus(await loadPersistedStatus(storageDirectory(this.dependencies), id));
|
|
1206
|
+
if (run.state !== "running")
|
|
1207
|
+
return publicStatus(persistedStatus(run));
|
|
1208
|
+
await this.stopRun(run, false);
|
|
1209
|
+
return publicStatus(persistedStatus(run));
|
|
1210
|
+
}
|
|
1211
|
+
async dispose() {
|
|
1212
|
+
if (this.disposePromise)
|
|
1213
|
+
return this.disposePromise;
|
|
1214
|
+
this.disposePromise = (async () => {
|
|
1215
|
+
await this.initialization;
|
|
1216
|
+
this.disposed = true;
|
|
1217
|
+
const runs = [...this.activeRuns.values()];
|
|
1218
|
+
await Promise.allSettled(runs.map((run) => this.stopRun(run, true)));
|
|
1219
|
+
await Promise.allSettled([...this.notificationPromises]);
|
|
1220
|
+
const owner = this.storageOwner;
|
|
1221
|
+
this.storageOwner = undefined;
|
|
1222
|
+
await owner?.release();
|
|
1223
|
+
})();
|
|
1224
|
+
return this.disposePromise;
|
|
1225
|
+
}
|
|
1226
|
+
async retry(request, context) {
|
|
1227
|
+
const id = checkedId(request);
|
|
1228
|
+
await this.ensureInitialized();
|
|
1229
|
+
const active = this.activeRuns.get(id);
|
|
1230
|
+
const status = active ? persistedStatus(active) : this.terminalSummaries.get(id) ?? await loadPersistedStatus(storageDirectory(this.dependencies), id);
|
|
1231
|
+
if (status.state !== "failed" && status.state !== "stopped")
|
|
1232
|
+
throw new WorkflowError("AGENT_FAILED", `Subagent ${id} is not retryable`);
|
|
1233
|
+
const requestSnapshot = await loadPersistedRequest(storageDirectory(this.dependencies), id);
|
|
1234
|
+
const frozenExternal = await loadPersistedStandaloneExternalConfiguration(storageDirectory(this.dependencies), id);
|
|
1235
|
+
const retryRequest = context.waitForForeground === false && requestSnapshot.mode === "foreground" ? { ...requestSnapshot, mode: "background" } : requestSnapshot;
|
|
1236
|
+
const run = await this.start(retryRequest, context, frozenExternal);
|
|
1237
|
+
emitUpdate(run);
|
|
1238
|
+
if (run.request.mode !== "foreground")
|
|
1239
|
+
return { id: run.id, state: "running" };
|
|
1240
|
+
return run.terminal;
|
|
1241
|
+
}
|
|
1242
|
+
releaseConcurrency(run) {
|
|
1243
|
+
if (run.concurrencyReleased)
|
|
1244
|
+
return;
|
|
1245
|
+
run.concurrencyReleased = true;
|
|
1246
|
+
this.activeRunCount -= 1;
|
|
1247
|
+
}
|
|
1248
|
+
canSteer(run) {
|
|
1249
|
+
return !run.disposed && run.state === "running" && !run.controller.signal.aborted;
|
|
1250
|
+
}
|
|
1251
|
+
terminalOrDisposed(run) { return run.disposed || run.state === "stopped"; }
|
|
1252
|
+
registerSteerHandler(run, handler) {
|
|
1253
|
+
if (!this.canSteer(run)) {
|
|
1254
|
+
run.pendingSteers.length = 0;
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
run.steerHandler = handler;
|
|
1258
|
+
this.flushSteers(run);
|
|
1259
|
+
}
|
|
1260
|
+
flushSteers(run) {
|
|
1261
|
+
if (!run.steerHandler || run.steerFlush || run.disposed)
|
|
1262
|
+
return;
|
|
1263
|
+
const flush = (async () => {
|
|
1264
|
+
while (run.pendingSteers.length > 0) {
|
|
1265
|
+
if (!this.canSteer(run)) {
|
|
1266
|
+
run.pendingSteers.length = 0;
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const message = run.pendingSteers.shift();
|
|
1270
|
+
const handler = run.steerHandler;
|
|
1271
|
+
if (message === undefined || !handler) {
|
|
1272
|
+
if (message !== undefined)
|
|
1273
|
+
run.pendingSteers.unshift(message);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
const result = handler(message);
|
|
1278
|
+
if (result && typeof result === "object" && "then" in result && typeof result.then === "function")
|
|
1279
|
+
await result;
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
run.steerHandler = undefined;
|
|
1283
|
+
run.pendingSteers.length = 0;
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
})();
|
|
1288
|
+
run.steerFlush = flush;
|
|
1289
|
+
void flush.then(() => {
|
|
1290
|
+
if (run.steerFlush === flush)
|
|
1291
|
+
run.steerFlush = undefined;
|
|
1292
|
+
if (this.canSteer(run) && run.steerHandler && run.pendingSteers.length > 0)
|
|
1293
|
+
this.flushSteers(run);
|
|
1294
|
+
}, () => {
|
|
1295
|
+
if (run.steerFlush === flush)
|
|
1296
|
+
run.steerFlush = undefined;
|
|
1297
|
+
run.pendingSteers.length = 0;
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
abortRun(run) {
|
|
1301
|
+
if (run.disposed || run.state !== "running")
|
|
1302
|
+
return;
|
|
1303
|
+
if (run.request.mode === "foreground")
|
|
1304
|
+
run.cancelled = true;
|
|
1305
|
+
run.controller.abort();
|
|
1306
|
+
this.clearSteering(run);
|
|
1307
|
+
void this.cleanupSessions(run, false).catch(() => undefined);
|
|
1308
|
+
if (run.request.mode === "foreground")
|
|
1309
|
+
void this.settleFailure(run, new WorkflowError("CANCELLED", "Subagent cancelled")).catch(() => undefined);
|
|
1310
|
+
}
|
|
1311
|
+
clearSteering(run) {
|
|
1312
|
+
run.steerHandler = undefined;
|
|
1313
|
+
run.pendingSteers.length = 0;
|
|
1314
|
+
}
|
|
1315
|
+
async recordProgress(run, progress) {
|
|
1316
|
+
if (!run || !this.canSteer(run))
|
|
1317
|
+
return;
|
|
1318
|
+
if (run.activeAttempt !== undefined && run.finalizedAttemptAccounting.has(run.activeAttempt))
|
|
1319
|
+
return;
|
|
1320
|
+
const snapshot = portableProgress(progress, true);
|
|
1321
|
+
const accounting = addAccounting(sumAccounting(run.finalizedAttemptAccounting.values()), progress.accounting);
|
|
1322
|
+
// Agent progress is a current-attempt snapshot. Do not retain a field-wise maximum: it can combine values from different snapshots.
|
|
1323
|
+
run.progress = { ...snapshot, accounting };
|
|
1324
|
+
emitUpdate(run);
|
|
1325
|
+
if (!progress.persist)
|
|
1326
|
+
return;
|
|
1327
|
+
await enqueueWrite(run, () => atomicJson(statusPath(run.directory), persistedStatus(run)));
|
|
1328
|
+
}
|
|
1329
|
+
recordAttempt(run, value, finalized = false) {
|
|
1330
|
+
const record = recordValue(value);
|
|
1331
|
+
const attempt = attemptNumberValue(value);
|
|
1332
|
+
if (attempt !== undefined)
|
|
1333
|
+
run.activeAttempt = attempt;
|
|
1334
|
+
const summary = attemptSummaryValue(value);
|
|
1335
|
+
if (summary) {
|
|
1336
|
+
run.attempts = Math.max(run.attempts ?? 0, summary.attempt);
|
|
1337
|
+
run.attemptDetails = [...(run.attemptDetails ?? []), summary].slice(-SUBAGENT_ATTEMPT_DETAILS_LIMIT);
|
|
1338
|
+
}
|
|
1339
|
+
if (!finalized)
|
|
1340
|
+
return;
|
|
1341
|
+
const accounting = summary?.accounting ?? accountingValue(record?.accounting);
|
|
1342
|
+
if (attempt === undefined || accounting === undefined)
|
|
1343
|
+
return;
|
|
1344
|
+
if (run.finalizedAttemptAccounting.size >= MAX_PERSISTED_ATTEMPT_ARRAY_ITEMS && !run.finalizedAttemptAccounting.has(attempt))
|
|
1345
|
+
return;
|
|
1346
|
+
run.finalizedAttemptAccounting.set(attempt, accounting);
|
|
1347
|
+
const cumulative = sumAccounting(run.finalizedAttemptAccounting.values());
|
|
1348
|
+
run.progress = run.progress === undefined ? { accounting: cumulative, toolCalls: [] } : { ...run.progress, accounting: cumulative };
|
|
1349
|
+
}
|
|
1350
|
+
async stopRun(run, disposeSession) {
|
|
1351
|
+
if (run.state === "running") {
|
|
1352
|
+
run.state = "stopped";
|
|
1353
|
+
this.releaseConcurrency(run);
|
|
1354
|
+
run.finishedAt = Date.now();
|
|
1355
|
+
emitUpdate(run);
|
|
1356
|
+
const sessionCleanup = this.cleanupSessions(run, disposeSession);
|
|
1357
|
+
run.controller.abort();
|
|
1358
|
+
this.clearSteering(run);
|
|
1359
|
+
this.removeExternalAbort(run);
|
|
1360
|
+
let statusError;
|
|
1361
|
+
try {
|
|
1362
|
+
await enqueueWrite(run, () => atomicJson(statusPath(run.directory), persistedStatus(run)));
|
|
1363
|
+
}
|
|
1364
|
+
catch (error) {
|
|
1365
|
+
statusError = error;
|
|
1366
|
+
}
|
|
1367
|
+
const cleanupErrors = await sessionCleanup;
|
|
1368
|
+
if (disposeSession)
|
|
1369
|
+
this.clearAttemptLiveReferences(run);
|
|
1370
|
+
else {
|
|
1371
|
+
run.prepared = undefined;
|
|
1372
|
+
run.handoff = undefined;
|
|
1373
|
+
}
|
|
1374
|
+
let worktreeError;
|
|
1375
|
+
let worktreeCleaned = false;
|
|
1376
|
+
try {
|
|
1377
|
+
worktreeCleaned = await this.cleanupWorktree(run);
|
|
1378
|
+
}
|
|
1379
|
+
catch (error) {
|
|
1380
|
+
worktreeError = error;
|
|
1381
|
+
}
|
|
1382
|
+
if (worktreeCleaned) {
|
|
1383
|
+
try {
|
|
1384
|
+
await enqueueWrite(run, () => atomicJson(statusPath(run.directory), persistedStatus(run)));
|
|
1385
|
+
}
|
|
1386
|
+
catch (error) {
|
|
1387
|
+
statusError ??= error;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
emitUpdate(run);
|
|
1391
|
+
this.resolveTerminal(run);
|
|
1392
|
+
if (disposeSession) {
|
|
1393
|
+
run.disposed = true;
|
|
1394
|
+
this.removeLiveRun(run);
|
|
1395
|
+
}
|
|
1396
|
+
if (statusError)
|
|
1397
|
+
throw internalStorageError(statusError, `Unable to stop subagent ${run.id}`);
|
|
1398
|
+
if (cleanupErrors.length > 0)
|
|
1399
|
+
throw new WorkflowError("INTERNAL_ERROR", `Unable to stop subagent ${run.id}: ${errorText(cleanupErrors[0])}`);
|
|
1400
|
+
if (worktreeError)
|
|
1401
|
+
throw new WorkflowError("WORKTREE_FAILED", `Unable to clean up subagent ${run.id} worktree: ${errorText(worktreeError)}`);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (disposeSession) {
|
|
1405
|
+
run.disposed = true;
|
|
1406
|
+
this.clearSteering(run);
|
|
1407
|
+
await this.cleanupSessions(run, true);
|
|
1408
|
+
const worktreeCleaned = await this.cleanupWorktree(run);
|
|
1409
|
+
if (worktreeCleaned)
|
|
1410
|
+
await enqueueWrite(run, () => atomicJson(statusPath(run.directory), persistedStatus(run)));
|
|
1411
|
+
this.resolveTerminal(run);
|
|
1412
|
+
this.removeLiveRun(run);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
async cleanupSessions(run, dispose, abort = true) {
|
|
1416
|
+
const errors = [];
|
|
1417
|
+
for (const session of [...run.activeSessions])
|
|
1418
|
+
errors.push(...await this.cleanupSession(run, dispose, abort, session));
|
|
1419
|
+
return errors;
|
|
1420
|
+
}
|
|
1421
|
+
async waitForSessionAborts(run) {
|
|
1422
|
+
const pending = [];
|
|
1423
|
+
for (const session of run.activeSessions) {
|
|
1424
|
+
const abort = run.sessionCleanups.get(session)?.abort;
|
|
1425
|
+
if (abort)
|
|
1426
|
+
pending.push(abort);
|
|
1427
|
+
}
|
|
1428
|
+
await Promise.allSettled(pending);
|
|
1429
|
+
}
|
|
1430
|
+
async cleanupSession(run, dispose, abort = true, session = run.session) {
|
|
1431
|
+
if (!session)
|
|
1432
|
+
return [];
|
|
1433
|
+
run.activeSessions.add(session);
|
|
1434
|
+
let cleanup = run.sessionCleanups.get(session);
|
|
1435
|
+
if (!cleanup) {
|
|
1436
|
+
cleanup = {};
|
|
1437
|
+
run.sessionCleanups.set(session, cleanup);
|
|
1438
|
+
}
|
|
1439
|
+
const errors = [];
|
|
1440
|
+
if (abort) {
|
|
1441
|
+
if (!cleanup.abort)
|
|
1442
|
+
cleanup.abort = Promise.resolve().then(() => session.abort());
|
|
1443
|
+
await cleanup.abort.then(() => undefined, (error) => { errors.push(error); });
|
|
1444
|
+
}
|
|
1445
|
+
if (dispose) {
|
|
1446
|
+
if (!abort && cleanup.abort)
|
|
1447
|
+
await cleanup.abort.then(() => undefined, () => undefined);
|
|
1448
|
+
if (!cleanup.dispose)
|
|
1449
|
+
cleanup.dispose = Promise.resolve().then(() => session.dispose());
|
|
1450
|
+
await cleanup.dispose.then(() => undefined, (error) => { errors.push(error); });
|
|
1451
|
+
run.activeSessions.delete(session);
|
|
1452
|
+
if (run.session === session)
|
|
1453
|
+
run.session = undefined;
|
|
1454
|
+
}
|
|
1455
|
+
return errors;
|
|
1456
|
+
}
|
|
1457
|
+
clearAttemptLiveReferences(run) {
|
|
1458
|
+
run.session = undefined;
|
|
1459
|
+
run.activeSessions.clear();
|
|
1460
|
+
run.prepared = undefined;
|
|
1461
|
+
run.handoff = undefined;
|
|
1462
|
+
}
|
|
1463
|
+
async cleanupWorktree(run) {
|
|
1464
|
+
const worktree = run.worktree;
|
|
1465
|
+
let cleanup = run.worktreeCleanup;
|
|
1466
|
+
if (cleanup === undefined) {
|
|
1467
|
+
if (!worktree || run.worktreeContext === undefined)
|
|
1468
|
+
return false;
|
|
1469
|
+
cleanup = Promise.resolve().then(() => worktree.cleanup());
|
|
1470
|
+
run.worktreeCleanup = cleanup;
|
|
1471
|
+
}
|
|
1472
|
+
await cleanup;
|
|
1473
|
+
run.worktree = undefined;
|
|
1474
|
+
run.worktreeContext = undefined;
|
|
1475
|
+
return true;
|
|
1476
|
+
}
|
|
1477
|
+
removeExternalAbort(run) {
|
|
1478
|
+
const listener = run.externalAbort;
|
|
1479
|
+
const signal = run.externalSignal;
|
|
1480
|
+
if (listener && signal)
|
|
1481
|
+
signal.removeEventListener("abort", listener);
|
|
1482
|
+
run.externalAbort = undefined;
|
|
1483
|
+
run.externalSignal = undefined;
|
|
1484
|
+
}
|
|
1485
|
+
observe(run) {
|
|
1486
|
+
void run.promise.then((result) => this.settleSuccess(run, result), (error) => this.settleFailure(run, error)).catch((error) => this.settleFailure(run, error)).catch(() => undefined);
|
|
1487
|
+
}
|
|
1488
|
+
async settleSuccess(run, result) {
|
|
1489
|
+
if (run.disposed || run.state !== "running") {
|
|
1490
|
+
if (!run.disposed && run.state === "stopped")
|
|
1491
|
+
await this.finishTerminal(run);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
try {
|
|
1495
|
+
const executionResult = executionResultValue(result);
|
|
1496
|
+
if (!executionResult)
|
|
1497
|
+
throw new WorkflowError("INTERNAL_ERROR", "Subagent executor returned an invalid result");
|
|
1498
|
+
for (const attempt of executionResult.attempts)
|
|
1499
|
+
this.recordAttempt(run, attempt, true);
|
|
1500
|
+
const value = structuredClone(executionResult.value);
|
|
1501
|
+
await enqueueWrite(run, () => atomicJson(resultPath(run.directory), value));
|
|
1502
|
+
run.value = value;
|
|
1503
|
+
if (this.terminalOrDisposed(run)) {
|
|
1504
|
+
await this.finishTerminal(run);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
run.state = "completed";
|
|
1508
|
+
this.releaseConcurrency(run);
|
|
1509
|
+
run.finishedAt = Date.now();
|
|
1510
|
+
await this.finishTerminal(run);
|
|
1511
|
+
}
|
|
1512
|
+
catch (error) {
|
|
1513
|
+
await this.settleFailure(run, error);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async settleFailure(run, error) {
|
|
1517
|
+
if (run.disposed || run.state !== "running") {
|
|
1518
|
+
if (!run.disposed && run.state === "stopped")
|
|
1519
|
+
await this.finishTerminal(run);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const candidate = recordValue(error)?.attempts;
|
|
1523
|
+
if (Array.isArray(candidate)) {
|
|
1524
|
+
for (const attempt of candidate.slice(0, MAX_PERSISTED_ATTEMPT_ARRAY_ITEMS)) {
|
|
1525
|
+
const record = recordValue(attempt);
|
|
1526
|
+
if (record)
|
|
1527
|
+
this.recordAttempt(run, attempt, true);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
run.state = "failed";
|
|
1531
|
+
this.releaseConcurrency(run);
|
|
1532
|
+
run.error = run.cancelled ? { code: "CANCELLED", message: "Subagent cancelled" } : failureFrom(error);
|
|
1533
|
+
run.finishedAt = Date.now();
|
|
1534
|
+
try {
|
|
1535
|
+
await enqueueWrite(run, () => atomicJson(failurePath(run.directory), run.error));
|
|
1536
|
+
}
|
|
1537
|
+
catch (persistenceError) {
|
|
1538
|
+
run.error = { code: "INTERNAL_ERROR", message: errorText(persistenceError) };
|
|
1539
|
+
}
|
|
1540
|
+
await this.finishTerminal(run);
|
|
1541
|
+
}
|
|
1542
|
+
resolveTerminal(run) {
|
|
1543
|
+
if (run.terminalResolved)
|
|
1544
|
+
return;
|
|
1545
|
+
run.terminalResolved = true;
|
|
1546
|
+
if (run.state === "completed" && run.value !== undefined) {
|
|
1547
|
+
run.resolveTerminal({ id: run.id, state: "completed", value: structuredClone(run.value) });
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (run.state === "failed") {
|
|
1551
|
+
run.resolveTerminal({ id: run.id, state: "failed", error: { ...(run.error ?? { code: "AGENT_FAILED", message: "Subagent failed" }) } });
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
run.resolveTerminal({ id: run.id, state: "stopped" });
|
|
1555
|
+
}
|
|
1556
|
+
async finishTerminal(run) {
|
|
1557
|
+
if (run.disposed) {
|
|
1558
|
+
this.resolveTerminal(run);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
run.finishedAt ??= Date.now();
|
|
1562
|
+
emitUpdate(run);
|
|
1563
|
+
this.clearSteering(run);
|
|
1564
|
+
this.removeExternalAbort(run);
|
|
1565
|
+
if (!run.executorOwnsSession)
|
|
1566
|
+
await this.cleanupSessions(run, true, false);
|
|
1567
|
+
else if (run.request.mode === "foreground")
|
|
1568
|
+
await this.waitForSessionAborts(run);
|
|
1569
|
+
this.clearAttemptLiveReferences(run);
|
|
1570
|
+
try {
|
|
1571
|
+
await this.cleanupWorktree(run);
|
|
1572
|
+
}
|
|
1573
|
+
catch {
|
|
1574
|
+
// Keep the terminal result and recovery context when cleanup must be retried later.
|
|
1575
|
+
}
|
|
1576
|
+
emitUpdate(run);
|
|
1577
|
+
try {
|
|
1578
|
+
await enqueueWrite(run, () => atomicJson(statusPath(run.directory), persistedStatus(run)));
|
|
1579
|
+
}
|
|
1580
|
+
catch {
|
|
1581
|
+
// The terminal state remains available in memory when storage becomes unavailable.
|
|
1582
|
+
}
|
|
1583
|
+
this.resolveTerminal(run);
|
|
1584
|
+
this.removeLiveRun(run);
|
|
1585
|
+
if (run.state === "completed" || run.state === "failed")
|
|
1586
|
+
this.notify(run);
|
|
1587
|
+
}
|
|
1588
|
+
notify(run) {
|
|
1589
|
+
if (run.request.mode === "foreground")
|
|
1590
|
+
return;
|
|
1591
|
+
const notify = this.dependencies.notify;
|
|
1592
|
+
if (!notify || run.notificationSent || run.disposed)
|
|
1593
|
+
return;
|
|
1594
|
+
run.notificationSent = true;
|
|
1595
|
+
const role = roleNameOf(run.request.role) ?? "none";
|
|
1596
|
+
const label = run.request.label?.trim() || (role === "none" ? "subagent" : role);
|
|
1597
|
+
const notification = { id: run.id, label, role, state: run.state, ...(run.error === undefined ? {} : { error: run.error }) };
|
|
1598
|
+
const pending = Promise.resolve().then(() => notify(notification));
|
|
1599
|
+
this.notificationPromises.add(pending);
|
|
1600
|
+
void pending.then(() => { this.notificationPromises.delete(pending); }, () => { this.notificationPromises.delete(pending); });
|
|
1601
|
+
}
|
|
1602
|
+
removeLiveRun(run) {
|
|
1603
|
+
if (this.activeRuns.get(run.id) !== run)
|
|
1604
|
+
return;
|
|
1605
|
+
this.activeRuns.delete(run.id);
|
|
1606
|
+
this.terminalSummaries.delete(run.id);
|
|
1607
|
+
this.terminalSummaries.set(run.id, terminalSummary(run));
|
|
1608
|
+
while (this.terminalSummaries.size > MAX_TERMINAL_SUMMARIES) {
|
|
1609
|
+
const oldest = this.terminalSummaries.keys().next().value;
|
|
1610
|
+
if (oldest === undefined)
|
|
1611
|
+
break;
|
|
1612
|
+
this.terminalSummaries.delete(oldest);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
export function createUnavailableSubagentManager() {
|
|
1617
|
+
return {
|
|
1618
|
+
run: async () => unavailable("run"),
|
|
1619
|
+
inspect: async () => unavailable("inspect"),
|
|
1620
|
+
steer: async () => unavailable("steer"),
|
|
1621
|
+
stop: async () => unavailable("stop"),
|
|
1622
|
+
retry: async () => unavailable("retry"),
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
export function createSubagentManager(dependencies = {}) {
|
|
1626
|
+
const manager = new PersistentSubagentManager(dependencies);
|
|
1627
|
+
return {
|
|
1628
|
+
run: (request, context) => manager.run(request, context),
|
|
1629
|
+
inspect: (request, context) => manager.inspect(request, context),
|
|
1630
|
+
getAttemptActionData: (id) => manager.getAttemptActionData?.(id),
|
|
1631
|
+
steer: (request, context) => manager.steer(request, context),
|
|
1632
|
+
stop: (request, context) => manager.stop(request, context),
|
|
1633
|
+
retry: (request, context) => manager.retry(request, context),
|
|
1634
|
+
dispose: async () => { await manager.dispose?.(); },
|
|
1635
|
+
};
|
|
1636
|
+
}
|