@tt-a1i/hive 2.1.1 → 2.1.3
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 +31 -0
- package/README.en.md +5 -460
- package/README.md +338 -187
- package/README.zh.md +389 -0
- package/dist/src/cli/hive-update.d.ts +3 -2
- package/dist/src/cli/hive-update.js +20 -60
- package/dist/src/cli/hive.js +5 -1
- package/dist/src/server/agent-manager-support.js +16 -9
- package/dist/src/server/agent-runtime-contract.d.ts +4 -0
- package/dist/src/server/agent-runtime.js +27 -0
- package/dist/src/server/agent-stdin-dispatcher.d.ts +3 -0
- package/dist/src/server/agent-stdin-dispatcher.js +3 -0
- package/dist/src/server/diagnostics-support-bundle.d.ts +3 -0
- package/dist/src/server/diagnostics-support-bundle.js +3 -0
- package/dist/src/server/hive-team-guidance.js +34 -4
- package/dist/src/server/message-log-store.d.ts +1 -0
- package/dist/src/server/message-log-store.js +13 -0
- package/dist/src/server/orchestrator-autostart.d.ts +2 -0
- package/dist/src/server/orchestrator-autostart.js +30 -11
- package/dist/src/server/post-start-input-writer.d.ts +1 -0
- package/dist/src/server/post-start-input-writer.js +78 -12
- package/dist/src/server/role-templates.d.ts +8 -1
- package/dist/src/server/role-templates.js +102 -48
- package/dist/src/server/route-types.d.ts +6 -0
- package/dist/src/server/routes-scenarios.d.ts +2 -1
- package/dist/src/server/routes-scenarios.js +11 -6
- package/dist/src/server/routes-team.js +21 -15
- package/dist/src/server/routes-workspaces.js +22 -2
- package/dist/src/server/runtime-store-contract.d.ts +2 -0
- package/dist/src/server/runtime-store-helpers.d.ts +11 -2
- package/dist/src/server/runtime-store-helpers.js +29 -5
- package/dist/src/server/runtime-store-workflows.js +20 -0
- package/dist/src/server/runtime-store.js +2 -0
- package/dist/src/server/scenario-presets.d.ts +3 -1
- package/dist/src/server/scenario-presets.js +12 -1
- package/dist/src/server/spawn-worker-defaults.d.ts +2 -0
- package/dist/src/server/spawn-worker-defaults.js +10 -5
- package/dist/src/server/team-operations.d.ts +3 -0
- package/dist/src/server/team-operations.js +97 -29
- package/dist/src/server/terminal-input-profile.d.ts +2 -0
- package/dist/src/server/update-install-plan.d.ts +20 -0
- package/dist/src/server/update-install-plan.js +151 -0
- package/dist/src/server/version-service.d.ts +5 -0
- package/dist/src/server/version-service.js +17 -13
- package/dist/src/server/workflow-agent-call-executor.d.ts +81 -0
- package/dist/src/server/workflow-agent-call-executor.js +259 -0
- package/dist/src/server/workflow-dag-layer-tracker.d.ts +14 -0
- package/dist/src/server/workflow-dag-layer-tracker.js +37 -0
- package/dist/src/server/workflow-dag.d.ts +8 -0
- package/dist/src/server/workflow-dag.js +65 -0
- package/dist/src/server/workflow-dispatch-awaiter.d.ts +7 -1
- package/dist/src/server/workflow-dispatch-awaiter.js +60 -9
- package/dist/src/server/workflow-runner.d.ts +5 -38
- package/dist/src/server/workflow-runner.js +113 -445
- package/dist/src/server/workflow-script-host-dispatcher.d.ts +14 -0
- package/dist/src/server/workflow-script-host-dispatcher.js +42 -0
- package/dist/src/server/workflow-script-loader.d.ts +2 -2
- package/dist/src/server/workflow-script-loader.js +2 -2
- package/dist/src/server/workflow-script-worker.d.ts +14 -0
- package/dist/src/server/workflow-script-worker.js +81 -0
- package/dist/src/server/workflow-vm-worker-source.d.ts +1 -0
- package/dist/src/server/workflow-vm-worker-source.js +1 -0
- package/dist/src/server/workflow-vm-worker.cjs +238 -0
- package/dist/src/server/workspace-ui-language.d.ts +7 -0
- package/dist/src/server/workspace-ui-language.js +9 -0
- package/dist/src/shared/scenario-presets.d.ts +6 -1
- package/dist/src/shared/scenario-presets.js +80 -28
- package/dist/src/shared/types.d.ts +11 -7
- package/dist/src/shared/ui-language.d.ts +4 -0
- package/dist/src/shared/ui-language.js +3 -0
- package/package.json +4 -2
- package/web/dist/assets/{AddWorkerDialog-CTOsPurT.js → AddWorkerDialog-DHwOZ75i.js} +2 -2
- package/web/dist/assets/AddWorkspaceFlow-qOI1CmqC.js +1 -0
- package/web/dist/assets/{FirstRunWizard-C24MUssx.js → FirstRunWizard-D5kGMD_V.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-7gVUjgAx.js → MarketplaceDrawer-BuCbtqFk.js} +1 -1
- package/web/dist/assets/TaskGraphDrawer-e_bg3f5u.js +1 -0
- package/web/dist/assets/{WhatsNewDialog-vnShQWjg.js → WhatsNewDialog-BB9ieq3T.js} +1 -1
- package/web/dist/assets/{WorkerModal-DcxIBPlu.js → WorkerModal-CGeWO3Dg.js} +1 -1
- package/web/dist/assets/{WorkflowsDrawer-C8csID1I.js → WorkflowsDrawer-CVvGxxdR.js} +1 -1
- package/web/dist/assets/WorkspaceMemoryDrawer-BamHuJEp.js +1 -0
- package/web/dist/assets/WorkspaceTaskDrawer-CqntbH9-.js +1 -0
- package/web/dist/assets/index-BOpmn5Ij.js +84 -0
- package/web/dist/assets/index-rPYd0MUk.css +1 -0
- package/web/dist/assets/{search-BRQbevR2.js → search-DKgM9IgX.js} +1 -1
- package/web/dist/assets/{square-terminal-Co4pmGzn.js → square-terminal-DMWazpEv.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/AddWorkspaceFlow-D_fSv-cO.js +0 -1
- package/web/dist/assets/TaskGraphDrawer-Beh95yUu.js +0 -1
- package/web/dist/assets/WorkspaceMemoryDrawer-C_Aq9-o6.js +0 -1
- package/web/dist/assets/WorkspaceTaskDrawer-CIMYzlpt.js +0 -1
- package/web/dist/assets/index-3Hc1oXlt.css +0 -1
- package/web/dist/assets/index-D9zUE0W6.js +0 -79
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { cpus } from 'node:os';
|
|
3
|
+
import { resolveWorkflowCli } from './workflow-cli-policy.js';
|
|
4
|
+
import { buildSchemaInstruction, extractJsonBlock } from './workflow-output-schema.js';
|
|
5
|
+
const DEFAULT_MAX_AGENTS_PER_RUN = 1000;
|
|
6
|
+
const DEFAULT_MAX_CONCURRENT_AGENTS = Math.min(16, Math.max(2, cpus().length - 2));
|
|
7
|
+
const BUILT_IN_WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'custom']);
|
|
8
|
+
const isBuiltInWorkerRole = (value) => BUILT_IN_WORKER_ROLES.has(value);
|
|
9
|
+
const buildModelArgs = (_cli, model) => {
|
|
10
|
+
if (!model?.trim())
|
|
11
|
+
return [];
|
|
12
|
+
/* All supported CLIs accept `--model <id>` as a positional flag. Keeping
|
|
13
|
+
the mapping centralised here so future CLI quirks can be patched without
|
|
14
|
+
touching workflow run lifecycle code. */
|
|
15
|
+
return ['--model', model];
|
|
16
|
+
};
|
|
17
|
+
export const createWorkflowAgentCallExecutor = ({ assertRunActive, awaiter, cancelOpenDispatch, cliPolicy, getCurrentPhaseTitle, hivePort, isRunStopped, maxAgentCalls, maxConcurrentAgents: configuredMaxConcurrentAgents, registerDagDispatch, resolveCliLaunchConfig, roleTemplateResolver, runId, store, workflowAgentId, workflowName, workspaceId, }) => {
|
|
18
|
+
let stepCounter = 0;
|
|
19
|
+
const spawnedWorkers = [];
|
|
20
|
+
const activeAgentCalls = new Set();
|
|
21
|
+
const activeDispatchIds = new Set();
|
|
22
|
+
const maxAgents = typeof maxAgentCalls === 'number' && maxAgentCalls > 0
|
|
23
|
+
? maxAgentCalls
|
|
24
|
+
: DEFAULT_MAX_AGENTS_PER_RUN;
|
|
25
|
+
const maxConcurrentAgents = typeof configuredMaxConcurrentAgents === 'number' && configuredMaxConcurrentAgents > 0
|
|
26
|
+
? configuredMaxConcurrentAgents
|
|
27
|
+
: DEFAULT_MAX_CONCURRENT_AGENTS;
|
|
28
|
+
let inFlight = 0;
|
|
29
|
+
const waitQueue = [];
|
|
30
|
+
const releaseSlot = () => {
|
|
31
|
+
let released = false;
|
|
32
|
+
return () => {
|
|
33
|
+
if (released)
|
|
34
|
+
return;
|
|
35
|
+
released = true;
|
|
36
|
+
inFlight--;
|
|
37
|
+
drainSlotQueue();
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const takeSlot = () => {
|
|
41
|
+
inFlight++;
|
|
42
|
+
return releaseSlot();
|
|
43
|
+
};
|
|
44
|
+
function drainSlotQueue() {
|
|
45
|
+
while (inFlight < maxConcurrentAgents) {
|
|
46
|
+
const next = waitQueue.shift();
|
|
47
|
+
if (!next)
|
|
48
|
+
return;
|
|
49
|
+
next.resolve(takeSlot());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const acquireSlot = async (dagLayerId) => {
|
|
53
|
+
if (inFlight < maxConcurrentAgents) {
|
|
54
|
+
return takeSlot();
|
|
55
|
+
}
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
waitQueue.push({ dagLayerId, reject, resolve });
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const cancelQueuedSlotWaiters = (reason, predicate) => {
|
|
61
|
+
const error = new Error(reason);
|
|
62
|
+
const remaining = [];
|
|
63
|
+
for (const waiter of waitQueue) {
|
|
64
|
+
if (predicate(waiter))
|
|
65
|
+
waiter.reject(error);
|
|
66
|
+
else
|
|
67
|
+
remaining.push(waiter);
|
|
68
|
+
}
|
|
69
|
+
waitQueue.length = 0;
|
|
70
|
+
waitQueue.push(...remaining);
|
|
71
|
+
};
|
|
72
|
+
const resolveWorkerLaunch = (opts) => {
|
|
73
|
+
const requestedType = opts.agentType ?? 'coder';
|
|
74
|
+
let role;
|
|
75
|
+
let command;
|
|
76
|
+
let templateArgs = [];
|
|
77
|
+
if (typeof requestedType === 'string' && !isBuiltInWorkerRole(requestedType)) {
|
|
78
|
+
const template = roleTemplateResolver.findByName(requestedType);
|
|
79
|
+
if (!template) {
|
|
80
|
+
throw new Error(`Workflow agentType '${requestedType}' is not a built-in role (coder/reviewer/tester/custom) and no matching role template exists in this workspace. ` +
|
|
81
|
+
`Use a built-in role or an existing dispatchable role template.`);
|
|
82
|
+
}
|
|
83
|
+
if (template.roleType === 'sentinel') {
|
|
84
|
+
throw new Error(`Workflow agentType '${requestedType}' resolves to a sentinel role, but sentinels are read-only observers and cannot receive workflow dispatches. Use coder, reviewer, tester, custom, or a dispatchable role template.`);
|
|
85
|
+
}
|
|
86
|
+
role = template.roleType === 'orchestrator' ? 'custom' : template.roleType;
|
|
87
|
+
command = resolveWorkflowCli({
|
|
88
|
+
...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
|
|
89
|
+
isCustomTemplate: true,
|
|
90
|
+
templateDefaultCommand: template.defaultCommand,
|
|
91
|
+
policy: cliPolicy,
|
|
92
|
+
});
|
|
93
|
+
templateArgs = template.defaultArgs;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
role = requestedType;
|
|
97
|
+
command = resolveWorkflowCli({
|
|
98
|
+
...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
|
|
99
|
+
isCustomTemplate: false,
|
|
100
|
+
policy: cliPolicy,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const baseLaunchConfig = resolveCliLaunchConfig(command) ?? { command, args: [] };
|
|
104
|
+
const launchArgs = [
|
|
105
|
+
...(baseLaunchConfig.args ?? []),
|
|
106
|
+
...templateArgs,
|
|
107
|
+
...buildModelArgs(baseLaunchConfig.command, opts.model),
|
|
108
|
+
];
|
|
109
|
+
return {
|
|
110
|
+
requestedType,
|
|
111
|
+
role,
|
|
112
|
+
launchConfig: { ...baseLaunchConfig, args: launchArgs },
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
const agent = async (prompt, opts = {}) => {
|
|
116
|
+
let markAgentCallDone;
|
|
117
|
+
const agentCallDone = new Promise((resolve) => {
|
|
118
|
+
markAgentCallDone = resolve;
|
|
119
|
+
});
|
|
120
|
+
activeAgentCalls.add(agentCallDone);
|
|
121
|
+
try {
|
|
122
|
+
assertRunActive();
|
|
123
|
+
if (stepCounter >= maxAgents) {
|
|
124
|
+
throw new Error(`Workflow agent cap exceeded: ${maxAgents} calls (set meta.maxAgentCalls to raise)`);
|
|
125
|
+
}
|
|
126
|
+
const myStep = ++stepCounter;
|
|
127
|
+
const dagLayerId = typeof opts.__hiveDagLayerId === 'string' && opts.__hiveDagLayerId.trim()
|
|
128
|
+
? opts.__hiveDagLayerId.trim()
|
|
129
|
+
: null;
|
|
130
|
+
const { launchConfig, requestedType, role } = resolveWorkerLaunch(opts);
|
|
131
|
+
const name = opts.label ?? `${requestedType}-${myStep}-${randomUUID()}`;
|
|
132
|
+
const releaseSlot = await acquireSlot(dagLayerId);
|
|
133
|
+
let worker;
|
|
134
|
+
let dispatchId;
|
|
135
|
+
try {
|
|
136
|
+
assertRunActive();
|
|
137
|
+
worker = store.addWorkerWithLaunch(workspaceId, { name, role, ephemeral: true, spawnedBy: 'workflow' }, launchConfig);
|
|
138
|
+
spawnedWorkers.push(worker.id);
|
|
139
|
+
assertRunActive();
|
|
140
|
+
const liveRun = await store.startAgent(workspaceId, worker.id, { hivePort });
|
|
141
|
+
await liveRun.postStartInputReady;
|
|
142
|
+
assertRunActive();
|
|
143
|
+
const dispatchPrompt = opts.outputSchema
|
|
144
|
+
? prompt + buildSchemaInstruction(opts.outputSchema)
|
|
145
|
+
: prompt;
|
|
146
|
+
const currentPhaseTitle = getCurrentPhaseTitle();
|
|
147
|
+
const dispatch = await store.dispatchTaskByWorkerName(workspaceId, name, dispatchPrompt, {
|
|
148
|
+
fromAgentId: workflowAgentId,
|
|
149
|
+
hivePort,
|
|
150
|
+
workflowRunId: runId,
|
|
151
|
+
stepIndex: myStep,
|
|
152
|
+
...(currentPhaseTitle ? { phase: currentPhaseTitle } : {}),
|
|
153
|
+
label: opts.label ?? name,
|
|
154
|
+
});
|
|
155
|
+
dispatchId = dispatch.id;
|
|
156
|
+
activeDispatchIds.add(dispatch.id);
|
|
157
|
+
registerDagDispatch(dagLayerId, dispatch.id);
|
|
158
|
+
assertRunActive();
|
|
159
|
+
const report = await awaiter.awaitReport(dispatch.id, opts.timeoutMs);
|
|
160
|
+
assertRunActive();
|
|
161
|
+
if (opts.outputSchema) {
|
|
162
|
+
return extractJsonBlock(report.text) ?? { text: report.text };
|
|
163
|
+
}
|
|
164
|
+
return report.text;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (dispatchId) {
|
|
168
|
+
try {
|
|
169
|
+
cancelOpenDispatch(dispatchId, error instanceof Error ? error.message : String(error));
|
|
170
|
+
}
|
|
171
|
+
catch (cancelError) {
|
|
172
|
+
console.error('[hive] swallowed:workflow.agent.cancelDispatch', cancelError);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
if (dispatchId)
|
|
179
|
+
activeDispatchIds.delete(dispatchId);
|
|
180
|
+
if (worker) {
|
|
181
|
+
try {
|
|
182
|
+
store.deleteWorker(workspaceId, worker.id);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
/* idempotent — worker may already be gone via cascade or boot cleanup */
|
|
186
|
+
}
|
|
187
|
+
const idx = spawnedWorkers.indexOf(worker.id);
|
|
188
|
+
if (idx !== -1)
|
|
189
|
+
spawnedWorkers.splice(idx, 1);
|
|
190
|
+
}
|
|
191
|
+
releaseSlot();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
markAgentCallDone();
|
|
196
|
+
activeAgentCalls.delete(agentCallDone);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const catchPerItem = (value) => {
|
|
200
|
+
if (isRunStopped())
|
|
201
|
+
throw value;
|
|
202
|
+
console.warn(`[workflow ${workflowName}] item failed:`, value);
|
|
203
|
+
return null;
|
|
204
|
+
};
|
|
205
|
+
const waitForActiveCalls = async (timeoutMs) => {
|
|
206
|
+
const calls = [...activeAgentCalls];
|
|
207
|
+
if (calls.length === 0) {
|
|
208
|
+
return { settled: true, activeCount: 0 };
|
|
209
|
+
}
|
|
210
|
+
if (timeoutMs === undefined) {
|
|
211
|
+
await Promise.allSettled(calls);
|
|
212
|
+
return { settled: true, activeCount: 0 };
|
|
213
|
+
}
|
|
214
|
+
let timeoutHandle;
|
|
215
|
+
const timeout = new Promise((resolve) => {
|
|
216
|
+
timeoutHandle = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
217
|
+
});
|
|
218
|
+
const result = await Promise.race([
|
|
219
|
+
Promise.allSettled(calls).then(() => 'settled'),
|
|
220
|
+
timeout,
|
|
221
|
+
]);
|
|
222
|
+
if (timeoutHandle)
|
|
223
|
+
clearTimeout(timeoutHandle);
|
|
224
|
+
return {
|
|
225
|
+
settled: result === 'settled',
|
|
226
|
+
activeCount: activeAgentCalls.size,
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
const forceCancelActiveDispatchWaiters = (reason) => {
|
|
230
|
+
for (const dispatchId of activeDispatchIds) {
|
|
231
|
+
awaiter.forceCancel(dispatchId, reason);
|
|
232
|
+
}
|
|
233
|
+
cancelQueuedSlotWaiters(reason, () => true);
|
|
234
|
+
};
|
|
235
|
+
const cancelQueuedAgentCallsForDagLayer = (layerId, reason) => {
|
|
236
|
+
const normalized = layerId.trim();
|
|
237
|
+
if (!normalized)
|
|
238
|
+
return;
|
|
239
|
+
cancelQueuedSlotWaiters(reason, (waiter) => waiter.dagLayerId === normalized);
|
|
240
|
+
};
|
|
241
|
+
const deleteSpawnedWorkers = () => {
|
|
242
|
+
for (const workerId of spawnedWorkers.splice(0)) {
|
|
243
|
+
try {
|
|
244
|
+
store.deleteWorker(workspaceId, workerId);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
/* swallow */
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
agent,
|
|
253
|
+
cancelQueuedAgentCallsForDagLayer,
|
|
254
|
+
catchPerItem,
|
|
255
|
+
deleteSpawnedWorkers,
|
|
256
|
+
forceCancelActiveDispatchWaiters,
|
|
257
|
+
waitForActiveCalls,
|
|
258
|
+
};
|
|
259
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
|
|
2
|
+
interface WorkflowDagDispatchCancelPort {
|
|
3
|
+
cancelOpenDispatchForRun: (workspaceId: string, dispatchId: string, reason: string) => boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare const createWorkflowDagLayerTracker: (input: {
|
|
6
|
+
awaiter: Pick<WorkflowDispatchAwaiter, "notifyCancel">;
|
|
7
|
+
cancelQueuedAgentCallsForDagLayer: (layerId: string, reason: string) => void;
|
|
8
|
+
dispatchPort: WorkflowDagDispatchCancelPort;
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
}) => {
|
|
11
|
+
cancelLayerAgents: (layerId: unknown, reason: unknown) => void;
|
|
12
|
+
registerDispatch: (layerId: string | null, dispatchId: string) => void;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const errorToMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
2
|
+
export const createWorkflowDagLayerTracker = (input) => {
|
|
3
|
+
const dagLayerDispatches = new Map();
|
|
4
|
+
const registerDispatch = (layerId, dispatchId) => {
|
|
5
|
+
if (!layerId)
|
|
6
|
+
return;
|
|
7
|
+
let dispatches = dagLayerDispatches.get(layerId);
|
|
8
|
+
if (!dispatches) {
|
|
9
|
+
dispatches = new Set();
|
|
10
|
+
dagLayerDispatches.set(layerId, dispatches);
|
|
11
|
+
}
|
|
12
|
+
dispatches.add(dispatchId);
|
|
13
|
+
};
|
|
14
|
+
const cancelLayerAgents = (layerId, reason) => {
|
|
15
|
+
const message = typeof reason === 'string' && reason.trim() ? reason : 'Workflow DAG node failed';
|
|
16
|
+
const normalizedLayerId = typeof layerId === 'string' ? layerId.trim() : '';
|
|
17
|
+
if (!normalizedLayerId)
|
|
18
|
+
throw new Error('Missing DAG layer id');
|
|
19
|
+
input.cancelQueuedAgentCallsForDagLayer(normalizedLayerId, message);
|
|
20
|
+
const dispatchIds = dagLayerDispatches.get(normalizedLayerId) ?? new Set();
|
|
21
|
+
const errors = [];
|
|
22
|
+
for (const dispatchId of dispatchIds) {
|
|
23
|
+
try {
|
|
24
|
+
const cancelled = input.dispatchPort.cancelOpenDispatchForRun(input.workspaceId, dispatchId, message);
|
|
25
|
+
if (cancelled)
|
|
26
|
+
input.awaiter.notifyCancel(dispatchId, message);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
errors.push(errorToMessage(error));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (errors.length > 0) {
|
|
33
|
+
throw new Error(errors.join('; '));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return { cancelLayerAgents, registerDispatch };
|
|
37
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const assertDagNodeInput = (node, index) => {
|
|
2
|
+
if (!node || typeof node !== 'object') {
|
|
3
|
+
throw new Error(`dag(): node #${index + 1} must be an object`);
|
|
4
|
+
}
|
|
5
|
+
const record = node;
|
|
6
|
+
const id = typeof record.id === 'string' ? record.id.trim() : '';
|
|
7
|
+
if (!id)
|
|
8
|
+
throw new Error(`dag(): node #${index + 1} requires a non-empty string id`);
|
|
9
|
+
if (!Array.isArray(record.deps)) {
|
|
10
|
+
throw new Error(`dag(${id}): deps must be an array`);
|
|
11
|
+
}
|
|
12
|
+
const deps = record.deps.map((dep) => String(dep).trim());
|
|
13
|
+
for (const dep of deps) {
|
|
14
|
+
if (!dep)
|
|
15
|
+
throw new Error(`dag(${id}): dependency ids must be non-empty strings`);
|
|
16
|
+
}
|
|
17
|
+
return { deps, id };
|
|
18
|
+
};
|
|
19
|
+
export const planWorkflowDag = (input) => {
|
|
20
|
+
if (!Array.isArray(input)) {
|
|
21
|
+
throw new Error('dag(): expected planner input to be an array of nodes');
|
|
22
|
+
}
|
|
23
|
+
const nodes = input.map(assertDagNodeInput);
|
|
24
|
+
const remainingDeps = new Map();
|
|
25
|
+
const dependents = new Map();
|
|
26
|
+
for (const node of nodes) {
|
|
27
|
+
if (remainingDeps.has(node.id))
|
|
28
|
+
throw new Error(`dag(): duplicate node id "${node.id}"`);
|
|
29
|
+
remainingDeps.set(node.id, node.deps.length);
|
|
30
|
+
dependents.set(node.id, []);
|
|
31
|
+
}
|
|
32
|
+
for (const node of nodes) {
|
|
33
|
+
for (const dep of node.deps) {
|
|
34
|
+
const children = dependents.get(dep);
|
|
35
|
+
if (!children)
|
|
36
|
+
throw new Error(`dag(${node.id}): unknown dependency "${dep}"`);
|
|
37
|
+
children.push(node.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const queue = nodes.filter((node) => node.deps.length === 0).map((node) => node.id);
|
|
41
|
+
const layers = [];
|
|
42
|
+
let cursor = 0;
|
|
43
|
+
let visited = 0;
|
|
44
|
+
while (cursor < queue.length) {
|
|
45
|
+
const layer = queue.slice(cursor);
|
|
46
|
+
cursor = queue.length;
|
|
47
|
+
layers.push(layer);
|
|
48
|
+
for (const id of layer) {
|
|
49
|
+
visited += 1;
|
|
50
|
+
for (const childId of dependents.get(id) ?? []) {
|
|
51
|
+
const next = (remainingDeps.get(childId) ?? 0) - 1;
|
|
52
|
+
remainingDeps.set(childId, next);
|
|
53
|
+
if (next === 0)
|
|
54
|
+
queue.push(childId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (visited !== nodes.length) {
|
|
59
|
+
const cyclic = nodes
|
|
60
|
+
.filter((node) => (remainingDeps.get(node.id) ?? 0) > 0)
|
|
61
|
+
.map((node) => node.id);
|
|
62
|
+
throw new Error(`dag(): cycle or unsatisfied dependencies among: ${cyclic.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
return { layers };
|
|
65
|
+
};
|
|
@@ -7,6 +7,12 @@ export interface WorkflowDispatchAwaiter {
|
|
|
7
7
|
awaitReport(dispatchId: string, timeoutMs?: number): Promise<ReportPayload>;
|
|
8
8
|
notifyReport(dispatchId: string, payload: ReportPayload): void;
|
|
9
9
|
notifyCancel(dispatchId: string, reason: string): void;
|
|
10
|
+
forceCancel(dispatchId: string, reason: string): void;
|
|
10
11
|
cancelAll(reason: string): void;
|
|
11
12
|
}
|
|
12
|
-
export
|
|
13
|
+
export interface WorkflowDispatchAwaiterOptions {
|
|
14
|
+
deferredEventTtlMs?: number;
|
|
15
|
+
maxDeferredEvents?: number;
|
|
16
|
+
now?: () => number;
|
|
17
|
+
}
|
|
18
|
+
export declare const createWorkflowDispatchAwaiter: (options?: WorkflowDispatchAwaiterOptions) => WorkflowDispatchAwaiter;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Default 10 minutes — workflows often run long. Callers may shorten via opts.
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
3
|
-
|
|
3
|
+
const DEFAULT_DEFERRED_EVENT_TTL_MS = DEFAULT_TIMEOUT_MS;
|
|
4
|
+
const DEFAULT_MAX_DEFERRED_EVENTS = 4096;
|
|
5
|
+
export const createWorkflowDispatchAwaiter = (options = {}) => {
|
|
4
6
|
const pending = new Map();
|
|
5
7
|
// TIER 2 #10 — deferred-cancel state. The runner creates a dispatch
|
|
6
8
|
// and THEN calls awaitReport, with at least one microtask gap between
|
|
@@ -13,6 +15,13 @@ export const createWorkflowDispatchAwaiter = () => {
|
|
|
13
15
|
// reason — defensive against a future ordering change.
|
|
14
16
|
const cancelledIds = new Map();
|
|
15
17
|
const reportedIds = new Map();
|
|
18
|
+
const now = options.now ?? (() => Date.now());
|
|
19
|
+
const deferredEventTtlMs = typeof options.deferredEventTtlMs === 'number' && options.deferredEventTtlMs > 0
|
|
20
|
+
? options.deferredEventTtlMs
|
|
21
|
+
: DEFAULT_DEFERRED_EVENT_TTL_MS;
|
|
22
|
+
const maxDeferredEvents = typeof options.maxDeferredEvents === 'number' && options.maxDeferredEvents > 0
|
|
23
|
+
? options.maxDeferredEvents
|
|
24
|
+
: DEFAULT_MAX_DEFERRED_EVENTS;
|
|
16
25
|
const take = (dispatchId) => {
|
|
17
26
|
const entry = pending.get(dispatchId);
|
|
18
27
|
if (entry) {
|
|
@@ -21,19 +30,53 @@ export const createWorkflowDispatchAwaiter = () => {
|
|
|
21
30
|
}
|
|
22
31
|
return entry;
|
|
23
32
|
};
|
|
33
|
+
const pruneDeferredEvents = () => {
|
|
34
|
+
const time = now();
|
|
35
|
+
for (const [id, entry] of cancelledIds) {
|
|
36
|
+
if (entry.expiresAt <= time)
|
|
37
|
+
cancelledIds.delete(id);
|
|
38
|
+
}
|
|
39
|
+
for (const [id, entry] of reportedIds) {
|
|
40
|
+
if (entry.expiresAt <= time)
|
|
41
|
+
reportedIds.delete(id);
|
|
42
|
+
}
|
|
43
|
+
while (cancelledIds.size + reportedIds.size > maxDeferredEvents) {
|
|
44
|
+
const reportedKey = reportedIds.keys().next().value;
|
|
45
|
+
if (typeof reportedKey === 'string') {
|
|
46
|
+
reportedIds.delete(reportedKey);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const cancelledKey = cancelledIds.keys().next().value;
|
|
50
|
+
if (typeof cancelledKey === 'string') {
|
|
51
|
+
cancelledIds.delete(cancelledKey);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const putDeferred = (map, dispatchId, value) => {
|
|
58
|
+
map.set(dispatchId, { value, expiresAt: now() + deferredEventTtlMs });
|
|
59
|
+
pruneDeferredEvents();
|
|
60
|
+
};
|
|
61
|
+
const takeDeferred = (map, dispatchId) => {
|
|
62
|
+
pruneDeferredEvents();
|
|
63
|
+
const entry = map.get(dispatchId);
|
|
64
|
+
if (!entry)
|
|
65
|
+
return undefined;
|
|
66
|
+
map.delete(dispatchId);
|
|
67
|
+
return entry.value;
|
|
68
|
+
};
|
|
24
69
|
return {
|
|
25
70
|
awaitReport(dispatchId, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
26
71
|
return new Promise((resolve, reject) => {
|
|
27
72
|
// Pre-arrived events: drain immediately, no timer / no entry.
|
|
28
|
-
const earlyReport = reportedIds
|
|
73
|
+
const earlyReport = takeDeferred(reportedIds, dispatchId);
|
|
29
74
|
if (earlyReport) {
|
|
30
|
-
reportedIds.delete(dispatchId);
|
|
31
75
|
resolve(earlyReport);
|
|
32
76
|
return;
|
|
33
77
|
}
|
|
34
|
-
const earlyCancel = cancelledIds
|
|
35
|
-
if (earlyCancel) {
|
|
36
|
-
cancelledIds.delete(dispatchId);
|
|
78
|
+
const earlyCancel = takeDeferred(cancelledIds, dispatchId);
|
|
79
|
+
if (earlyCancel !== undefined) {
|
|
37
80
|
reject(new Error(`workflow dispatch ${dispatchId} cancelled: ${earlyCancel}`));
|
|
38
81
|
return;
|
|
39
82
|
}
|
|
@@ -51,8 +94,8 @@ export const createWorkflowDispatchAwaiter = () => {
|
|
|
51
94
|
}
|
|
52
95
|
else {
|
|
53
96
|
// Pre-arrival: stash for the eventual awaitReport call. Cap
|
|
54
|
-
// stash size
|
|
55
|
-
reportedIds
|
|
97
|
+
// stash size and TTL so unknown ids cannot grow forever.
|
|
98
|
+
putDeferred(reportedIds, dispatchId, payload);
|
|
56
99
|
}
|
|
57
100
|
},
|
|
58
101
|
notifyCancel(dispatchId, reason) {
|
|
@@ -61,7 +104,15 @@ export const createWorkflowDispatchAwaiter = () => {
|
|
|
61
104
|
entry.reject(new Error(`workflow dispatch ${dispatchId} cancelled: ${reason}`));
|
|
62
105
|
}
|
|
63
106
|
else {
|
|
64
|
-
cancelledIds
|
|
107
|
+
putDeferred(cancelledIds, dispatchId, reason);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
forceCancel(dispatchId, reason) {
|
|
111
|
+
cancelledIds.delete(dispatchId);
|
|
112
|
+
reportedIds.delete(dispatchId);
|
|
113
|
+
const entry = take(dispatchId);
|
|
114
|
+
if (entry) {
|
|
115
|
+
entry.reject(new Error(`workflow dispatch ${dispatchId} cancelled: ${reason}`));
|
|
65
116
|
}
|
|
66
117
|
},
|
|
67
118
|
cancelAll(reason) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { WorkerRole } from '../shared/types.js';
|
|
2
1
|
import type { AgentLaunchConfigInput } from './agent-run-store.js';
|
|
3
|
-
import { type
|
|
2
|
+
import { type WorkflowAgentExecutorStorePort, type WorkflowRoleTemplateResolver } from './workflow-agent-call-executor.js';
|
|
3
|
+
import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
|
|
4
4
|
import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
|
|
5
5
|
import type { WorkflowRunRecord, WorkflowRunStatus } from './workflow-run-store.js';
|
|
6
6
|
export interface RunWorkflowInput {
|
|
@@ -31,41 +31,7 @@ export interface RunInlineWorkflowInput {
|
|
|
31
31
|
* into an ephemeral worker. Returns `undefined` if the name doesn't
|
|
32
32
|
* match any template (built-in or custom); the runner falls back to
|
|
33
33
|
* built-in-role semantics in that case. */
|
|
34
|
-
export
|
|
35
|
-
findByName(name: string): {
|
|
36
|
-
name: string;
|
|
37
|
-
roleType: WorkerRole | 'orchestrator';
|
|
38
|
-
defaultCommand: string;
|
|
39
|
-
defaultArgs: string[];
|
|
40
|
-
} | undefined;
|
|
41
|
-
}
|
|
42
|
-
interface RunnerStorePort {
|
|
43
|
-
addWorkerWithLaunch: (workspaceId: string, input: {
|
|
44
|
-
name: string;
|
|
45
|
-
role: WorkerRole;
|
|
46
|
-
ephemeral: true;
|
|
47
|
-
spawnedBy: 'workflow';
|
|
48
|
-
}, launchConfig: AgentLaunchConfigInput) => {
|
|
49
|
-
id: string;
|
|
50
|
-
name: string;
|
|
51
|
-
};
|
|
52
|
-
startAgent: (workspaceId: string, agentId: string, input: {
|
|
53
|
-
hivePort: string;
|
|
54
|
-
}) => Promise<{
|
|
55
|
-
postStartInputReady?: Promise<void>;
|
|
56
|
-
}>;
|
|
57
|
-
dispatchTaskByWorkerName: (workspaceId: string, workerName: string, text: string, input: {
|
|
58
|
-
fromAgentId: string;
|
|
59
|
-
hivePort: string;
|
|
60
|
-
workflowRunId: string;
|
|
61
|
-
stepIndex: number;
|
|
62
|
-
phase?: string;
|
|
63
|
-
label?: string;
|
|
64
|
-
}) => Promise<{
|
|
65
|
-
id: string;
|
|
66
|
-
}>;
|
|
67
|
-
deleteWorker: (workspaceId: string, workerId: string) => void;
|
|
68
|
-
}
|
|
34
|
+
export type RoleTemplateResolver = WorkflowRoleTemplateResolver;
|
|
69
35
|
/** TIER 2 #3 — narrator lane sink. The runner calls append(runId, message)
|
|
70
36
|
* every time the script invokes log(). Implementation lives in
|
|
71
37
|
* workflow-run-log-store; the port keeps the runner free of DB types. */
|
|
@@ -111,9 +77,10 @@ export interface WorkflowRunner {
|
|
|
111
77
|
}
|
|
112
78
|
interface ListDispatchesForStopPort {
|
|
113
79
|
listOpenDispatchIdsForRun: (runId: string) => string[];
|
|
80
|
+
cancelOpenDispatchForRun: (workspaceId: string, dispatchId: string, reason: string) => boolean;
|
|
114
81
|
}
|
|
115
82
|
export declare const createWorkflowRunner: (deps: {
|
|
116
|
-
store:
|
|
83
|
+
store: WorkflowAgentExecutorStorePort;
|
|
117
84
|
workflowRunStore: WorkflowRunStorePort;
|
|
118
85
|
awaiter: WorkflowDispatchAwaiter;
|
|
119
86
|
dispatchPort: ListDispatchesForStopPort;
|