@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.
Files changed (90) hide show
  1. package/README.md +3 -1
  2. package/dist/artifact-graph-runtime.d.ts +1 -1
  3. package/dist/artifact-graph-runtime.js +10 -5
  4. package/dist/artifact-graph-schema.js +127 -5
  5. package/dist/compiler.js +46 -11
  6. package/dist/dynamic-decision.d.ts +1 -0
  7. package/dist/dynamic-decision.js +7 -0
  8. package/dist/dynamic-generated-task-runtime.js +3 -1
  9. package/dist/dynamic-profiles.d.ts +1 -0
  10. package/dist/dynamic-profiles.js +3 -0
  11. package/dist/engine-run-graph.d.ts +2 -0
  12. package/dist/engine-run-graph.js +55 -5
  13. package/dist/engine.js +278 -15
  14. package/dist/extension.js +3 -2
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.js +4 -0
  17. package/dist/prompt-json.d.ts +7 -0
  18. package/dist/prompt-json.js +13 -0
  19. package/dist/roles.d.ts +1 -1
  20. package/dist/roles.js +5 -8
  21. package/dist/store.d.ts +20 -1
  22. package/dist/store.js +89 -29
  23. package/dist/strings.d.ts +11 -0
  24. package/dist/strings.js +24 -0
  25. package/dist/subagent-backend.js +557 -13
  26. package/dist/types.d.ts +101 -1
  27. package/dist/verification-ontology.d.ts +31 -0
  28. package/dist/verification-ontology.js +66 -0
  29. package/dist/workflow-artifact-tool.js +5 -6
  30. package/dist/workflow-artifacts.d.ts +7 -0
  31. package/dist/workflow-artifacts.js +55 -4
  32. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  33. package/dist/workflow-fetch-cache-extension.js +57 -9
  34. package/dist/workflow-metrics.d.ts +113 -0
  35. package/dist/workflow-metrics.js +272 -0
  36. package/dist/workflow-output-artifacts.js +5 -3
  37. package/dist/workflow-partial-output.d.ts +45 -0
  38. package/dist/workflow-partial-output.js +205 -0
  39. package/dist/workflow-progress-health.js +42 -10
  40. package/dist/workflow-web-source-extension.js +27 -4
  41. package/dist/workflow-web-source.js +26 -12
  42. package/docs/usage.md +76 -29
  43. package/node_modules/@agwab/pi-subagent/package.json +1 -1
  44. package/node_modules/@agwab/pi-subagent/src/index.ts +53 -5
  45. package/node_modules/@agwab/pi-subagent/src/panel.ts +7 -3
  46. package/package.json +2 -2
  47. package/skills/workflow-guide/SKILL.md +1 -0
  48. package/src/artifact-graph-runtime.ts +19 -13
  49. package/src/artifact-graph-schema.ts +143 -3
  50. package/src/cli.mjs +52 -0
  51. package/src/compiler.ts +49 -9
  52. package/src/dynamic-decision.ts +11 -0
  53. package/src/dynamic-generated-task-runtime.ts +3 -1
  54. package/src/dynamic-profiles.ts +4 -0
  55. package/src/engine-run-graph.ts +63 -4
  56. package/src/engine.ts +400 -14
  57. package/src/extension.ts +3 -2
  58. package/src/index.ts +49 -0
  59. package/src/prompt-json.ts +13 -0
  60. package/src/roles.ts +6 -9
  61. package/src/store.ts +123 -34
  62. package/src/strings.ts +38 -0
  63. package/src/subagent-backend.ts +727 -41
  64. package/src/types.ts +110 -2
  65. package/src/verification-ontology.ts +88 -0
  66. package/src/workflow-artifact-tool.ts +5 -7
  67. package/src/workflow-artifacts.ts +83 -3
  68. package/src/workflow-fetch-cache-extension.ts +78 -13
  69. package/src/workflow-metrics.ts +478 -0
  70. package/src/workflow-output-artifacts.ts +5 -3
  71. package/src/workflow-partial-output.ts +299 -0
  72. package/src/workflow-progress-health.ts +47 -15
  73. package/src/workflow-web-source-extension.ts +33 -4
  74. package/src/workflow-web-source.ts +36 -12
  75. package/workflows/README.md +7 -25
  76. package/workflows/deep-research/batched-verification.spec.json +253 -0
  77. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  78. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +173 -20
  79. package/workflows/deep-research/helpers/normalize-input-packet.mjs +80 -1
  80. package/workflows/deep-research/helpers/render-executive.mjs +32 -5
  81. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  82. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  83. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -2
  84. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  85. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  86. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  87. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +10 -3
  88. package/workflows/deep-research/spec.json +32 -12
  89. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  90. 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 { AgentDefinition, CompiledRole, RoleSpec } from "./types.js";
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: string[] = [];
28
-
29
- if (sourceAgent) {
30
- const extracted = extractMarkdownSections(sourceAgent.body, includeSections, excludedSections);
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
- void heartbeat().catch(() => undefined);
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
- return await runLeaseContext.run({ cwd, runId, ownerId }, action);
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 !== "EEXIST") throw error;
258
- } finally {
259
- await unlink(reclaimFile).catch(() => undefined);
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
- if (await ownsLock(lockFile, ownerId))
343
- await unlink(lockFile).catch(() => undefined);
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
- entry &&
1334
- typeof entry === "object" &&
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(entry.tasks),
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
+ }