@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/dist/store.js CHANGED
@@ -13,6 +13,7 @@ const DEFAULT_INDEX_UPDATE_DEBOUNCE_MS = 500;
13
13
  let indexUpdateDebounceMs = DEFAULT_INDEX_UPDATE_DEBOUNCE_MS;
14
14
  const pendingIndexUpdates = new Map();
15
15
  const runLeaseContext = new AsyncLocalStorage();
16
+ let runLeaseTestHooks = {};
16
17
  const TASK_STATUSES = [
17
18
  "pending",
18
19
  "running",
@@ -80,6 +81,9 @@ export async function writeJsonAtomic(file, value) {
80
81
  await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
81
82
  await rename(temp, file);
82
83
  }
84
+ export function setRunLeaseTestHooksForTests(hooks) {
85
+ runLeaseTestHooks = hooks ?? {};
86
+ }
83
87
  export async function withRunLease(cwd, runId, action) {
84
88
  const dir = workflowRunDir(cwd, runId);
85
89
  await ensureDir(dir);
@@ -88,8 +92,15 @@ export async function withRunLease(cwd, runId, action) {
88
92
  const lock = await acquireLock(lockFile, ownerId);
89
93
  if (!lock)
90
94
  return undefined;
95
+ const abortController = new AbortController();
96
+ const abortLease = (error) => {
97
+ if (abortController.signal.aborted)
98
+ return;
99
+ abortController.abort(asLeaseError(error));
100
+ };
91
101
  const supervisorFile = join(dir, "supervisor.json");
92
102
  const heartbeat = async () => {
103
+ assertLeaseNotAborted(abortController.signal);
93
104
  await assertLockOwner(lockFile, ownerId);
94
105
  const timestamp = nowIso();
95
106
  const now = new Date();
@@ -104,17 +115,37 @@ export async function withRunLease(cwd, runId, action) {
104
115
  };
105
116
  await heartbeat();
106
117
  const heartbeatTimer = setInterval(() => {
107
- void heartbeat().catch(() => undefined);
108
- }, Math.max(1000, Math.floor(LEASE_STALE_MS / 3)));
118
+ void heartbeat().catch(abortLease);
119
+ }, runLeaseHeartbeatIntervalMs());
109
120
  heartbeatTimer.unref?.();
110
121
  try {
111
- return await runLeaseContext.run({ cwd, runId, ownerId }, action);
122
+ const result = await runLeaseContext.run({ cwd, runId, ownerId, abortSignal: abortController.signal }, () => action(abortController.signal));
123
+ assertLeaseNotAborted(abortController.signal);
124
+ return result;
112
125
  }
113
126
  finally {
114
127
  clearInterval(heartbeatTimer);
115
128
  await releaseLock(lockFile, ownerId);
116
129
  }
117
130
  }
131
+ function runLeaseHeartbeatIntervalMs() {
132
+ return Math.max(1, Math.floor(runLeaseTestHooks.heartbeatIntervalMs ??
133
+ Math.max(1000, Math.floor(LEASE_STALE_MS / 3))));
134
+ }
135
+ function assertLeaseNotAborted(signal) {
136
+ if (signal.aborted)
137
+ throw abortSignalError(signal);
138
+ }
139
+ function abortSignalError(signal) {
140
+ return asLeaseError(signal.reason);
141
+ }
142
+ function asLeaseError(error) {
143
+ if (error instanceof Error)
144
+ return error;
145
+ return new Error(error === undefined
146
+ ? "Lost supervisor lease"
147
+ : `Lost supervisor lease: ${String(error)}`);
148
+ }
118
149
  async function acquireLock(lockFile, ownerId) {
119
150
  const tryCreate = async () => {
120
151
  try {
@@ -154,6 +185,7 @@ async function reclaimStaleLock(lockFile) {
154
185
  return true;
155
186
  return false;
156
187
  }
188
+ await runLeaseTestHooks.onAfterReclaimRename?.({ lockFile, reclaimFile });
157
189
  const claimed = await readLockSnapshot(reclaimFile);
158
190
  if (!claimed)
159
191
  return true;
@@ -169,16 +201,20 @@ async function reclaimStaleLock(lockFile) {
169
201
  return true;
170
202
  }
171
203
  async function restoreReclaimFile(reclaimFile, lockFile) {
204
+ await runLeaseTestHooks.onBeforeRestoreReclaimFile?.({
205
+ lockFile,
206
+ reclaimFile,
207
+ });
172
208
  try {
173
209
  await link(reclaimFile, lockFile);
174
210
  }
175
211
  catch (error) {
176
- if (error.code !== "EEXIST")
177
- throw error;
178
- }
179
- finally {
180
- await unlink(reclaimFile).catch(() => undefined);
212
+ if (error.code === "EEXIST") {
213
+ throw new Error(`Could not restore reclaimed lock because another owner acquired ${lockFile}`, { cause: error });
214
+ }
215
+ throw error;
181
216
  }
217
+ await unlink(reclaimFile).catch(() => undefined);
182
218
  }
183
219
  function isReclaimableLockSnapshot(snapshot) {
184
220
  const now = Date.now();
@@ -238,8 +274,31 @@ async function acquireLockWithWait(lockFile, ownerId) {
238
274
  }
239
275
  }
240
276
  async function releaseLock(lockFile, ownerId) {
241
- if (await ownsLock(lockFile, ownerId))
242
- await unlink(lockFile).catch(() => undefined);
277
+ const snapshot = await readLockSnapshot(lockFile);
278
+ if (!snapshot || snapshot.ownerId !== ownerId)
279
+ return;
280
+ const releaseFile = `${lockFile}.release-${process.pid}-${randomBytes(3).toString("hex")}`;
281
+ await runLeaseTestHooks.onBeforeReleaseLockRename?.({
282
+ lockFile,
283
+ releaseFile,
284
+ ownerId,
285
+ });
286
+ try {
287
+ await rename(lockFile, releaseFile);
288
+ }
289
+ catch (error) {
290
+ if (error.code === "ENOENT")
291
+ return;
292
+ throw error;
293
+ }
294
+ const claimed = await readLockSnapshot(releaseFile);
295
+ if (!claimed)
296
+ return;
297
+ if (sameLockOwnerSnapshot(snapshot, claimed)) {
298
+ await unlink(releaseFile).catch(() => undefined);
299
+ return;
300
+ }
301
+ await restoreReclaimFile(releaseFile, lockFile);
243
302
  }
244
303
  async function assertLockOwner(lockFile, ownerId) {
245
304
  if (!(await ownsLock(lockFile, ownerId)))
@@ -833,6 +892,7 @@ async function assertActiveRunLease(cwd, runId) {
833
892
  return;
834
893
  if (context.cwd !== cwd || context.runId !== runId)
835
894
  return;
895
+ assertLeaseNotAborted(context.abortSignal);
836
896
  await assertLockOwner(join(workflowRunDir(cwd, runId), "supervisor.lock"), context.ownerId);
837
897
  }
838
898
  export async function findRunRecordPath(cwd, runIdOrPrefix) {
@@ -943,6 +1003,7 @@ async function updateIndexIncremental(cwd, changedRunId) {
943
1003
  const changedEntry = buildIndexEntry(cwd, changedRun);
944
1004
  const entries = existing.runs
945
1005
  .filter((entry) => entry.runId !== changedRun.runId)
1006
+ .map(stripIndexTaskRows)
946
1007
  .concat(changedEntry);
947
1008
  return {
948
1009
  schemaVersion: 1,
@@ -978,6 +1039,10 @@ function selectIndexEntries(entries) {
978
1039
  .slice(0, TERMINAL_INDEX_LIMIT);
979
1040
  return [...active, ...terminal].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
980
1041
  }
1042
+ function stripIndexTaskRows(entry) {
1043
+ const { tasks: _tasks, ...slim } = entry;
1044
+ return slim;
1045
+ }
981
1046
  function buildIndexEntry(cwd, run) {
982
1047
  return {
983
1048
  runId: run.runId,
@@ -993,28 +1058,20 @@ function buildIndexEntry(cwd, run) {
993
1058
  round: run.round,
994
1059
  fanout: run.fanout,
995
1060
  runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
996
- tasks: run.tasks.map((task) => ({
997
- taskId: task.taskId,
998
- displayName: task.displayName,
999
- agent: task.agent,
1000
- kind: task.kind,
1001
- stageId: task.stageId,
1002
- backendHandle: task.backendHandle,
1003
- status: task.status,
1004
- statusDetail: task.statusDetail,
1005
- lastMessage: task.lastMessage,
1006
- })),
1007
1061
  };
1008
1062
  }
1009
1063
  function isIndexRecordLike(value) {
1010
1064
  return (value?.schemaVersion === 1 &&
1011
1065
  Array.isArray(value.runs) &&
1012
- value.runs.every((entry) => entry &&
1013
- typeof entry === "object" &&
1014
- typeof entry.runId === "string" &&
1015
- typeof entry.updatedAt === "string" &&
1016
- typeof entry.status === "string" &&
1017
- Array.isArray(entry.tasks)));
1066
+ value.runs.every((entry) => {
1067
+ if (!entry || typeof entry !== "object")
1068
+ return false;
1069
+ const tasks = entry.tasks;
1070
+ return (typeof entry.runId === "string" &&
1071
+ typeof entry.updatedAt === "string" &&
1072
+ typeof entry.status === "string" &&
1073
+ (tasks === undefined || Array.isArray(tasks)));
1074
+ }));
1018
1075
  }
1019
1076
  export function deriveRunStatus(run) {
1020
1077
  const next = { ...run, tasks: run.tasks };
@@ -1072,10 +1129,13 @@ const RESUMABLE_BLOCKED_STATUS_DETAILS = new Set([
1072
1129
  "dynamic_ui_unavailable",
1073
1130
  "dynamic_approval_timeout",
1074
1131
  ]);
1132
+ export function isBlockedTaskResumableForResume(task) {
1133
+ return (task.status === "blocked" &&
1134
+ RESUMABLE_BLOCKED_STATUS_DETAILS.has(task.statusDetail));
1135
+ }
1075
1136
  export function resetTaskForResume(task) {
1076
1137
  if (!RESUMABLE_TASK_STATUSES.has(task.status) &&
1077
- !(task.status === "blocked" &&
1078
- RESUMABLE_BLOCKED_STATUS_DETAILS.has(task.statusDetail))) {
1138
+ !isBlockedTaskResumableForResume(task)) {
1079
1139
  return false;
1080
1140
  }
1081
1141
  recordTaskResumeEvent(task);
@@ -0,0 +1,11 @@
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
+ export declare function compactStrings(values: readonly unknown[], options?: CompactStringsOptions): string[];
@@ -0,0 +1,24 @@
1
+ export function compactStrings(values, options = {}) {
2
+ const trim = options.trim ?? true;
3
+ const unique = options.unique ?? true;
4
+ const dropEmpty = options.dropEmpty ?? true;
5
+ const dropWhitespaceOnly = options.dropWhitespaceOnly ?? trim;
6
+ const seen = new Set();
7
+ const result = [];
8
+ for (const value of values) {
9
+ if (typeof value !== "string")
10
+ continue;
11
+ const compacted = trim ? value.trim() : value;
12
+ if (dropEmpty &&
13
+ (dropWhitespaceOnly ? value.trim().length === 0 : compacted.length === 0)) {
14
+ continue;
15
+ }
16
+ if (unique) {
17
+ if (seen.has(compacted))
18
+ continue;
19
+ seen.add(compacted);
20
+ }
21
+ result.push(compacted);
22
+ }
23
+ return result;
24
+ }