@agwab/pi-workflow 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/artifact-graph-runtime.d.ts +1 -1
- package/dist/artifact-graph-runtime.js +10 -5
- package/dist/artifact-graph-schema.js +127 -5
- package/dist/compiler.js +46 -11
- package/dist/dynamic-decision.d.ts +1 -0
- package/dist/dynamic-decision.js +7 -0
- package/dist/dynamic-generated-task-runtime.js +3 -1
- package/dist/dynamic-profiles.d.ts +1 -0
- package/dist/dynamic-profiles.js +3 -0
- package/dist/engine-run-graph.d.ts +2 -0
- package/dist/engine-run-graph.js +55 -5
- package/dist/engine.js +278 -15
- package/dist/extension.js +3 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/prompt-json.d.ts +7 -0
- package/dist/prompt-json.js +13 -0
- package/dist/roles.d.ts +1 -1
- package/dist/roles.js +5 -8
- package/dist/store.d.ts +20 -1
- package/dist/store.js +89 -29
- package/dist/strings.d.ts +11 -0
- package/dist/strings.js +24 -0
- package/dist/subagent-backend.js +557 -13
- package/dist/types.d.ts +101 -1
- package/dist/verification-ontology.d.ts +31 -0
- package/dist/verification-ontology.js +66 -0
- package/dist/workflow-artifact-tool.js +5 -6
- package/dist/workflow-artifacts.d.ts +7 -0
- package/dist/workflow-artifacts.js +55 -4
- package/dist/workflow-fetch-cache-extension.d.ts +1 -0
- package/dist/workflow-fetch-cache-extension.js +57 -9
- package/dist/workflow-metrics.d.ts +113 -0
- package/dist/workflow-metrics.js +272 -0
- package/dist/workflow-output-artifacts.js +5 -3
- package/dist/workflow-partial-output.d.ts +45 -0
- package/dist/workflow-partial-output.js +205 -0
- package/dist/workflow-progress-health.js +42 -10
- package/dist/workflow-web-source-extension.js +27 -4
- package/dist/workflow-web-source.js +26 -12
- package/docs/usage.md +76 -29
- package/node_modules/@agwab/pi-subagent/package.json +1 -1
- package/node_modules/@agwab/pi-subagent/src/index.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/panel.ts +7 -3
- package/package.json +2 -2
- package/skills/workflow-guide/SKILL.md +1 -0
- package/src/artifact-graph-runtime.ts +19 -13
- package/src/artifact-graph-schema.ts +143 -3
- package/src/cli.mjs +52 -0
- package/src/compiler.ts +49 -9
- package/src/dynamic-decision.ts +11 -0
- package/src/dynamic-generated-task-runtime.ts +3 -1
- package/src/dynamic-profiles.ts +4 -0
- package/src/engine-run-graph.ts +63 -4
- package/src/engine.ts +400 -14
- package/src/extension.ts +3 -2
- package/src/index.ts +49 -0
- package/src/prompt-json.ts +13 -0
- package/src/roles.ts +6 -9
- package/src/store.ts +123 -34
- package/src/strings.ts +38 -0
- package/src/subagent-backend.ts +727 -41
- package/src/types.ts +110 -2
- package/src/verification-ontology.ts +88 -0
- package/src/workflow-artifact-tool.ts +5 -7
- package/src/workflow-artifacts.ts +83 -3
- package/src/workflow-fetch-cache-extension.ts +78 -13
- package/src/workflow-metrics.ts +478 -0
- package/src/workflow-output-artifacts.ts +5 -3
- package/src/workflow-partial-output.ts +299 -0
- package/src/workflow-progress-health.ts +47 -15
- package/src/workflow-web-source-extension.ts +33 -4
- package/src/workflow-web-source.ts +36 -12
- package/workflows/README.md +7 -25
- package/workflows/deep-research/batched-verification.spec.json +253 -0
- package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +173 -20
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +80 -1
- package/workflows/deep-research/helpers/render-executive.mjs +32 -5
- package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
- package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -2
- package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
- package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +10 -3
- package/workflows/deep-research/spec.json +32 -12
- package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
- package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
package/src/roles.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compactStrings } from "./strings.js";
|
|
2
|
+
import type { AgentDefinition, CompiledRole, RoleSpec } from "./types.js";
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_SAFE_SECTIONS = [
|
|
4
5
|
"Core Principles",
|
|
@@ -24,14 +25,10 @@ export function compileRole(name: string, spec: RoleSpec, sourceAgent?: AgentDef
|
|
|
24
25
|
const maxChars = spec.maxChars ?? DEFAULT_MAX_ROLE_CHARS;
|
|
25
26
|
const includeSections = spec.includeSections ?? [...DEFAULT_SAFE_SECTIONS];
|
|
26
27
|
const excludedSections = [...ALWAYS_EXCLUDED_SECTIONS, ...(spec.excludeSections ?? [])];
|
|
27
|
-
const parts
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (extracted.trim() !== "") parts.push(extracted.trim());
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (spec.prompt?.trim()) parts.push(spec.prompt.trim());
|
|
28
|
+
const parts = compactStrings([
|
|
29
|
+
sourceAgent ? extractMarkdownSections(sourceAgent.body, includeSections, excludedSections) : undefined,
|
|
30
|
+
spec.prompt,
|
|
31
|
+
], { unique: false });
|
|
35
32
|
|
|
36
33
|
const fullContent = parts.join("\n\n");
|
|
37
34
|
const truncated = fullContent.length > maxChars;
|
package/src/store.ts
CHANGED
|
@@ -55,7 +55,25 @@ const runLeaseContext = new AsyncLocalStorage<{
|
|
|
55
55
|
cwd: string;
|
|
56
56
|
runId: string;
|
|
57
57
|
ownerId: string;
|
|
58
|
+
abortSignal: AbortSignal;
|
|
58
59
|
}>();
|
|
60
|
+
type RunLeaseTestHooks = {
|
|
61
|
+
heartbeatIntervalMs?: number;
|
|
62
|
+
onAfterReclaimRename?: (context: {
|
|
63
|
+
lockFile: string;
|
|
64
|
+
reclaimFile: string;
|
|
65
|
+
}) => void | Promise<void>;
|
|
66
|
+
onBeforeRestoreReclaimFile?: (context: {
|
|
67
|
+
lockFile: string;
|
|
68
|
+
reclaimFile: string;
|
|
69
|
+
}) => void | Promise<void>;
|
|
70
|
+
onBeforeReleaseLockRename?: (context: {
|
|
71
|
+
lockFile: string;
|
|
72
|
+
releaseFile: string;
|
|
73
|
+
ownerId: string;
|
|
74
|
+
}) => void | Promise<void>;
|
|
75
|
+
};
|
|
76
|
+
let runLeaseTestHooks: RunLeaseTestHooks = {};
|
|
59
77
|
const TASK_STATUSES: Array<keyof Omit<TaskSummary, "total">> = [
|
|
60
78
|
"pending",
|
|
61
79
|
"running",
|
|
@@ -148,10 +166,14 @@ export async function writeJsonAtomic(
|
|
|
148
166
|
await rename(temp, file);
|
|
149
167
|
}
|
|
150
168
|
|
|
169
|
+
export function setRunLeaseTestHooksForTests(hooks?: RunLeaseTestHooks): void {
|
|
170
|
+
runLeaseTestHooks = hooks ?? {};
|
|
171
|
+
}
|
|
172
|
+
|
|
151
173
|
export async function withRunLease<T>(
|
|
152
174
|
cwd: string,
|
|
153
175
|
runId: string,
|
|
154
|
-
action: () => Promise<T>,
|
|
176
|
+
action: (abortSignal: AbortSignal) => Promise<T>,
|
|
155
177
|
): Promise<T | undefined> {
|
|
156
178
|
const dir = workflowRunDir(cwd, runId);
|
|
157
179
|
await ensureDir(dir);
|
|
@@ -160,8 +182,14 @@ export async function withRunLease<T>(
|
|
|
160
182
|
const lock = await acquireLock(lockFile, ownerId);
|
|
161
183
|
if (!lock) return undefined;
|
|
162
184
|
|
|
185
|
+
const abortController = new AbortController();
|
|
186
|
+
const abortLease = (error: unknown): void => {
|
|
187
|
+
if (abortController.signal.aborted) return;
|
|
188
|
+
abortController.abort(asLeaseError(error));
|
|
189
|
+
};
|
|
163
190
|
const supervisorFile = join(dir, "supervisor.json");
|
|
164
191
|
const heartbeat = async (): Promise<void> => {
|
|
192
|
+
assertLeaseNotAborted(abortController.signal);
|
|
165
193
|
await assertLockOwner(lockFile, ownerId);
|
|
166
194
|
const timestamp = nowIso();
|
|
167
195
|
const now = new Date();
|
|
@@ -176,22 +204,51 @@ export async function withRunLease<T>(
|
|
|
176
204
|
};
|
|
177
205
|
|
|
178
206
|
await heartbeat();
|
|
179
|
-
const heartbeatTimer = setInterval(
|
|
180
|
-
()
|
|
181
|
-
|
|
182
|
-
},
|
|
183
|
-
Math.max(1000, Math.floor(LEASE_STALE_MS / 3)),
|
|
184
|
-
);
|
|
207
|
+
const heartbeatTimer = setInterval(() => {
|
|
208
|
+
void heartbeat().catch(abortLease);
|
|
209
|
+
}, runLeaseHeartbeatIntervalMs());
|
|
185
210
|
heartbeatTimer.unref?.();
|
|
186
211
|
|
|
187
212
|
try {
|
|
188
|
-
|
|
213
|
+
const result = await runLeaseContext.run(
|
|
214
|
+
{ cwd, runId, ownerId, abortSignal: abortController.signal },
|
|
215
|
+
() => action(abortController.signal),
|
|
216
|
+
);
|
|
217
|
+
assertLeaseNotAborted(abortController.signal);
|
|
218
|
+
return result;
|
|
189
219
|
} finally {
|
|
190
220
|
clearInterval(heartbeatTimer);
|
|
191
221
|
await releaseLock(lockFile, ownerId);
|
|
192
222
|
}
|
|
193
223
|
}
|
|
194
224
|
|
|
225
|
+
function runLeaseHeartbeatIntervalMs(): number {
|
|
226
|
+
return Math.max(
|
|
227
|
+
1,
|
|
228
|
+
Math.floor(
|
|
229
|
+
runLeaseTestHooks.heartbeatIntervalMs ??
|
|
230
|
+
Math.max(1000, Math.floor(LEASE_STALE_MS / 3)),
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function assertLeaseNotAborted(signal: AbortSignal): void {
|
|
236
|
+
if (signal.aborted) throw abortSignalError(signal);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function abortSignalError(signal: AbortSignal): Error {
|
|
240
|
+
return asLeaseError((signal as AbortSignal & { reason?: unknown }).reason);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function asLeaseError(error: unknown): Error {
|
|
244
|
+
if (error instanceof Error) return error;
|
|
245
|
+
return new Error(
|
|
246
|
+
error === undefined
|
|
247
|
+
? "Lost supervisor lease"
|
|
248
|
+
: `Lost supervisor lease: ${String(error)}`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
195
252
|
async function acquireLock(
|
|
196
253
|
lockFile: string,
|
|
197
254
|
ownerId: string,
|
|
@@ -231,6 +288,7 @@ async function reclaimStaleLock(lockFile: string): Promise<boolean> {
|
|
|
231
288
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
232
289
|
return false;
|
|
233
290
|
}
|
|
291
|
+
await runLeaseTestHooks.onAfterReclaimRename?.({ lockFile, reclaimFile });
|
|
234
292
|
|
|
235
293
|
const claimed = await readLockSnapshot(reclaimFile);
|
|
236
294
|
if (!claimed) return true;
|
|
@@ -251,13 +309,22 @@ async function restoreReclaimFile(
|
|
|
251
309
|
reclaimFile: string,
|
|
252
310
|
lockFile: string,
|
|
253
311
|
): Promise<void> {
|
|
312
|
+
await runLeaseTestHooks.onBeforeRestoreReclaimFile?.({
|
|
313
|
+
lockFile,
|
|
314
|
+
reclaimFile,
|
|
315
|
+
});
|
|
254
316
|
try {
|
|
255
317
|
await link(reclaimFile, lockFile);
|
|
256
318
|
} catch (error) {
|
|
257
|
-
if ((error as NodeJS.ErrnoException).code
|
|
258
|
-
|
|
259
|
-
|
|
319
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Could not restore reclaimed lock because another owner acquired ${lockFile}`,
|
|
322
|
+
{ cause: error },
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
throw error;
|
|
260
326
|
}
|
|
327
|
+
await unlink(reclaimFile).catch(() => undefined);
|
|
261
328
|
}
|
|
262
329
|
|
|
263
330
|
function isReclaimableLockSnapshot(snapshot: LockSnapshot): boolean {
|
|
@@ -339,8 +406,27 @@ async function acquireLockWithWait(
|
|
|
339
406
|
}
|
|
340
407
|
|
|
341
408
|
async function releaseLock(lockFile: string, ownerId: string): Promise<void> {
|
|
342
|
-
|
|
343
|
-
|
|
409
|
+
const snapshot = await readLockSnapshot(lockFile);
|
|
410
|
+
if (!snapshot || snapshot.ownerId !== ownerId) return;
|
|
411
|
+
const releaseFile = `${lockFile}.release-${process.pid}-${randomBytes(3).toString("hex")}`;
|
|
412
|
+
await runLeaseTestHooks.onBeforeReleaseLockRename?.({
|
|
413
|
+
lockFile,
|
|
414
|
+
releaseFile,
|
|
415
|
+
ownerId,
|
|
416
|
+
});
|
|
417
|
+
try {
|
|
418
|
+
await rename(lockFile, releaseFile);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
const claimed = await readLockSnapshot(releaseFile);
|
|
424
|
+
if (!claimed) return;
|
|
425
|
+
if (sameLockOwnerSnapshot(snapshot, claimed)) {
|
|
426
|
+
await unlink(releaseFile).catch(() => undefined);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
await restoreReclaimFile(releaseFile, lockFile);
|
|
344
430
|
}
|
|
345
431
|
|
|
346
432
|
async function assertLockOwner(
|
|
@@ -1095,6 +1181,7 @@ async function assertActiveRunLease(cwd: string, runId: string): Promise<void> {
|
|
|
1095
1181
|
const context = runLeaseContext.getStore();
|
|
1096
1182
|
if (!context) return;
|
|
1097
1183
|
if (context.cwd !== cwd || context.runId !== runId) return;
|
|
1184
|
+
assertLeaseNotAborted(context.abortSignal);
|
|
1098
1185
|
await assertLockOwner(
|
|
1099
1186
|
join(workflowRunDir(cwd, runId), "supervisor.lock"),
|
|
1100
1187
|
context.ownerId,
|
|
@@ -1243,6 +1330,7 @@ async function updateIndexIncremental(
|
|
|
1243
1330
|
const changedEntry = buildIndexEntry(cwd, changedRun);
|
|
1244
1331
|
const entries = existing.runs
|
|
1245
1332
|
.filter((entry) => entry.runId !== changedRun.runId)
|
|
1333
|
+
.map(stripIndexTaskRows)
|
|
1246
1334
|
.concat(changedEntry);
|
|
1247
1335
|
return {
|
|
1248
1336
|
schemaVersion: 1,
|
|
@@ -1290,6 +1378,11 @@ function selectIndexEntries(
|
|
|
1290
1378
|
);
|
|
1291
1379
|
}
|
|
1292
1380
|
|
|
1381
|
+
function stripIndexTaskRows(entry: WorkflowIndexRunEntry): WorkflowIndexRunEntry {
|
|
1382
|
+
const { tasks: _tasks, ...slim } = entry;
|
|
1383
|
+
return slim;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1293
1386
|
function buildIndexEntry(
|
|
1294
1387
|
cwd: string,
|
|
1295
1388
|
run: WorkflowRunRecord,
|
|
@@ -1308,17 +1401,6 @@ function buildIndexEntry(
|
|
|
1308
1401
|
round: run.round,
|
|
1309
1402
|
fanout: run.fanout,
|
|
1310
1403
|
runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
|
|
1311
|
-
tasks: run.tasks.map((task) => ({
|
|
1312
|
-
taskId: task.taskId,
|
|
1313
|
-
displayName: task.displayName,
|
|
1314
|
-
agent: task.agent,
|
|
1315
|
-
kind: task.kind,
|
|
1316
|
-
stageId: task.stageId,
|
|
1317
|
-
backendHandle: task.backendHandle,
|
|
1318
|
-
status: task.status,
|
|
1319
|
-
statusDetail: task.statusDetail,
|
|
1320
|
-
lastMessage: task.lastMessage,
|
|
1321
|
-
})),
|
|
1322
1404
|
};
|
|
1323
1405
|
}
|
|
1324
1406
|
|
|
@@ -1328,15 +1410,16 @@ function isIndexRecordLike(
|
|
|
1328
1410
|
return (
|
|
1329
1411
|
value?.schemaVersion === 1 &&
|
|
1330
1412
|
Array.isArray(value.runs) &&
|
|
1331
|
-
value.runs.every(
|
|
1332
|
-
(entry)
|
|
1333
|
-
|
|
1334
|
-
|
|
1413
|
+
value.runs.every((entry) => {
|
|
1414
|
+
if (!entry || typeof entry !== "object") return false;
|
|
1415
|
+
const tasks = (entry as { tasks?: unknown }).tasks;
|
|
1416
|
+
return (
|
|
1335
1417
|
typeof entry.runId === "string" &&
|
|
1336
1418
|
typeof entry.updatedAt === "string" &&
|
|
1337
1419
|
typeof entry.status === "string" &&
|
|
1338
|
-
Array.isArray(
|
|
1339
|
-
|
|
1420
|
+
(tasks === undefined || Array.isArray(tasks))
|
|
1421
|
+
);
|
|
1422
|
+
})
|
|
1340
1423
|
);
|
|
1341
1424
|
}
|
|
1342
1425
|
|
|
@@ -1411,13 +1494,19 @@ const RESUMABLE_BLOCKED_STATUS_DETAILS = new Set([
|
|
|
1411
1494
|
"dynamic_approval_timeout",
|
|
1412
1495
|
]);
|
|
1413
1496
|
|
|
1497
|
+
export function isBlockedTaskResumableForResume(
|
|
1498
|
+
task: Pick<WorkflowTaskRunRecord, "status" | "statusDetail">,
|
|
1499
|
+
): boolean {
|
|
1500
|
+
return (
|
|
1501
|
+
task.status === "blocked" &&
|
|
1502
|
+
RESUMABLE_BLOCKED_STATUS_DETAILS.has(task.statusDetail)
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1414
1506
|
export function resetTaskForResume(task: WorkflowTaskRunRecord): boolean {
|
|
1415
1507
|
if (
|
|
1416
1508
|
!RESUMABLE_TASK_STATUSES.has(task.status) &&
|
|
1417
|
-
!(
|
|
1418
|
-
task.status === "blocked" &&
|
|
1419
|
-
RESUMABLE_BLOCKED_STATUS_DETAILS.has(task.statusDetail)
|
|
1420
|
-
)
|
|
1509
|
+
!isBlockedTaskResumableForResume(task)
|
|
1421
1510
|
) {
|
|
1422
1511
|
return false;
|
|
1423
1512
|
}
|
package/src/strings.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface CompactStringsOptions {
|
|
2
|
+
/** Trim returned strings before filtering. Defaults to true. */
|
|
3
|
+
trim?: boolean;
|
|
4
|
+
/** Drop duplicate strings after optional trimming. Defaults to true. */
|
|
5
|
+
unique?: boolean;
|
|
6
|
+
/** Drop strings whose raw/trimmed form is empty. Defaults to true. */
|
|
7
|
+
dropEmpty?: boolean;
|
|
8
|
+
/** Drop strings whose trimmed form is empty even when trim=false. */
|
|
9
|
+
dropWhitespaceOnly?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function compactStrings(
|
|
13
|
+
values: readonly unknown[],
|
|
14
|
+
options: CompactStringsOptions = {},
|
|
15
|
+
): string[] {
|
|
16
|
+
const trim = options.trim ?? true;
|
|
17
|
+
const unique = options.unique ?? true;
|
|
18
|
+
const dropEmpty = options.dropEmpty ?? true;
|
|
19
|
+
const dropWhitespaceOnly = options.dropWhitespaceOnly ?? trim;
|
|
20
|
+
const seen = new Set<string>();
|
|
21
|
+
const result: string[] = [];
|
|
22
|
+
for (const value of values) {
|
|
23
|
+
if (typeof value !== "string") continue;
|
|
24
|
+
const compacted = trim ? value.trim() : value;
|
|
25
|
+
if (
|
|
26
|
+
dropEmpty &&
|
|
27
|
+
(dropWhitespaceOnly ? value.trim().length === 0 : compacted.length === 0)
|
|
28
|
+
) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (unique) {
|
|
32
|
+
if (seen.has(compacted)) continue;
|
|
33
|
+
seen.add(compacted);
|
|
34
|
+
}
|
|
35
|
+
result.push(compacted);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|