@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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { hashDynamicRequest } from "./dynamic-events.js";
|
|
5
|
+
import { readJson, writeJsonAtomic } from "./store.js";
|
|
6
|
+
|
|
7
|
+
export const WORKFLOW_PARTIAL_OUTPUT_PROTOCOL =
|
|
8
|
+
"workflow-partial-output-v1" as const;
|
|
9
|
+
export const WORKFLOW_PARTIAL_OUTPUT_LEDGER_SCHEMA =
|
|
10
|
+
"workflow-partial-output-ledger-v1" as const;
|
|
11
|
+
export const WORKFLOW_PARTIAL_OUTPUT_LEDGER_FILE = "partial-control.json";
|
|
12
|
+
|
|
13
|
+
export type WorkflowPartialOutputIssueCode =
|
|
14
|
+
| "invalid_json"
|
|
15
|
+
| "invalid_type"
|
|
16
|
+
| "invalid_schema"
|
|
17
|
+
| "invalid_path"
|
|
18
|
+
| "disallowed_path"
|
|
19
|
+
| "missing_items"
|
|
20
|
+
| "missing_item_id"
|
|
21
|
+
| "duplicate_item_id";
|
|
22
|
+
|
|
23
|
+
export interface WorkflowPartialOutputIssue {
|
|
24
|
+
code: WorkflowPartialOutputIssueCode;
|
|
25
|
+
message: string;
|
|
26
|
+
sectionIndex?: number;
|
|
27
|
+
path?: string;
|
|
28
|
+
itemId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WorkflowPartialOutputItem {
|
|
32
|
+
path: string;
|
|
33
|
+
itemId: string;
|
|
34
|
+
itemHash: string;
|
|
35
|
+
item: unknown;
|
|
36
|
+
ordinal: number;
|
|
37
|
+
sectionIndex: number;
|
|
38
|
+
sectionItemIndex: number;
|
|
39
|
+
itemRef: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WorkflowPartialOutputLedger {
|
|
43
|
+
schema: typeof WORKFLOW_PARTIAL_OUTPUT_LEDGER_SCHEMA;
|
|
44
|
+
protocol: typeof WORKFLOW_PARTIAL_OUTPUT_PROTOCOL;
|
|
45
|
+
items: WorkflowPartialOutputItem[];
|
|
46
|
+
issues: WorkflowPartialOutputIssue[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ParseWorkflowPartialOutputOptions {
|
|
50
|
+
allowedPaths?: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PartialSectionMatch {
|
|
54
|
+
content: string;
|
|
55
|
+
start: number;
|
|
56
|
+
end: number;
|
|
57
|
+
index: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PARTIAL_CONTROL_OPEN = "partial-control";
|
|
61
|
+
|
|
62
|
+
export function partialOutputLedgerPath(taskDir: string): string {
|
|
63
|
+
return join(taskDir, WORKFLOW_PARTIAL_OUTPUT_LEDGER_FILE);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function readWorkflowPartialOutputLedger(
|
|
67
|
+
taskDir: string,
|
|
68
|
+
): Promise<WorkflowPartialOutputLedger | undefined> {
|
|
69
|
+
return await readJson<WorkflowPartialOutputLedger>(
|
|
70
|
+
partialOutputLedgerPath(taskDir),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function writeWorkflowPartialOutputLedger(options: {
|
|
75
|
+
taskDir: string;
|
|
76
|
+
rawOutput: string;
|
|
77
|
+
allowedPaths?: readonly string[];
|
|
78
|
+
}): Promise<WorkflowPartialOutputLedger> {
|
|
79
|
+
const ledger = parseWorkflowPartialOutput(options.rawOutput, {
|
|
80
|
+
allowedPaths: options.allowedPaths,
|
|
81
|
+
});
|
|
82
|
+
await writeJsonAtomic(partialOutputLedgerPath(options.taskDir), ledger);
|
|
83
|
+
return ledger;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function writeWorkflowPartialOutputLedgerFromFile(options: {
|
|
87
|
+
taskDir: string;
|
|
88
|
+
outputFile: string;
|
|
89
|
+
allowedPaths?: readonly string[];
|
|
90
|
+
}): Promise<WorkflowPartialOutputLedger | undefined> {
|
|
91
|
+
const rawOutput = await readFile(options.outputFile, "utf8").catch(
|
|
92
|
+
() => undefined,
|
|
93
|
+
);
|
|
94
|
+
if (rawOutput === undefined) return undefined;
|
|
95
|
+
return await writeWorkflowPartialOutputLedger({
|
|
96
|
+
taskDir: options.taskDir,
|
|
97
|
+
rawOutput,
|
|
98
|
+
allowedPaths: options.allowedPaths,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function stripWorkflowPartialOutputSections(raw: string): string {
|
|
103
|
+
if (!raw.includes(PARTIAL_CONTROL_OPEN)) return raw;
|
|
104
|
+
return raw.replace(partialControlSectionRegExp(), "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseWorkflowPartialOutput(
|
|
108
|
+
raw: string,
|
|
109
|
+
options: ParseWorkflowPartialOutputOptions = {},
|
|
110
|
+
): WorkflowPartialOutputLedger {
|
|
111
|
+
const allowedPaths = options.allowedPaths
|
|
112
|
+
? new Set(options.allowedPaths)
|
|
113
|
+
: undefined;
|
|
114
|
+
const items: WorkflowPartialOutputItem[] = [];
|
|
115
|
+
const issues: WorkflowPartialOutputIssue[] = [];
|
|
116
|
+
const byPathAndId = new Map<string, WorkflowPartialOutputItem>();
|
|
117
|
+
|
|
118
|
+
for (const section of collectPartialControlSections(raw)) {
|
|
119
|
+
const parsed = parsePartialSectionJson(section, issues);
|
|
120
|
+
if (!parsed) continue;
|
|
121
|
+
const path = parsePartialSectionPath(parsed, section, allowedPaths, issues);
|
|
122
|
+
if (!path) continue;
|
|
123
|
+
const rawItems = parsePartialSectionItems(parsed, section, path, issues);
|
|
124
|
+
if (!rawItems) continue;
|
|
125
|
+
for (const [sectionItemIndex, item] of rawItems.entries()) {
|
|
126
|
+
const itemId = stablePartialItemId(item);
|
|
127
|
+
if (!itemId) {
|
|
128
|
+
issues.push({
|
|
129
|
+
code: "missing_item_id",
|
|
130
|
+
sectionIndex: section.index,
|
|
131
|
+
path,
|
|
132
|
+
message:
|
|
133
|
+
"partial output items must be objects with a stable non-empty string id",
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const itemHash = hashDynamicRequest(item);
|
|
138
|
+
const key = `${path}\0${itemId}`;
|
|
139
|
+
const existing = byPathAndId.get(key);
|
|
140
|
+
if (existing) {
|
|
141
|
+
if (existing.itemHash !== itemHash) {
|
|
142
|
+
issues.push({
|
|
143
|
+
code: "duplicate_item_id",
|
|
144
|
+
sectionIndex: section.index,
|
|
145
|
+
path,
|
|
146
|
+
itemId,
|
|
147
|
+
message: `partial output item ${itemId} at ${path} changed after it was published`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const ordinal = items.length;
|
|
153
|
+
const partialItem: WorkflowPartialOutputItem = {
|
|
154
|
+
path,
|
|
155
|
+
itemId,
|
|
156
|
+
itemHash,
|
|
157
|
+
item,
|
|
158
|
+
ordinal,
|
|
159
|
+
sectionIndex: section.index,
|
|
160
|
+
sectionItemIndex,
|
|
161
|
+
itemRef: `${WORKFLOW_PARTIAL_OUTPUT_LEDGER_FILE}#/items/${ordinal}`,
|
|
162
|
+
};
|
|
163
|
+
items.push(partialItem);
|
|
164
|
+
byPathAndId.set(key, partialItem);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
schema: WORKFLOW_PARTIAL_OUTPUT_LEDGER_SCHEMA,
|
|
170
|
+
protocol: WORKFLOW_PARTIAL_OUTPUT_PROTOCOL,
|
|
171
|
+
items,
|
|
172
|
+
issues,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function hasFatalPartialOutputIssue(
|
|
177
|
+
ledger: Pick<WorkflowPartialOutputLedger, "issues"> | undefined,
|
|
178
|
+
): WorkflowPartialOutputIssue | undefined {
|
|
179
|
+
return ledger?.issues.find((issue) => issue.code === "duplicate_item_id");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function collectPartialControlSections(raw: string): PartialSectionMatch[] {
|
|
183
|
+
if (!raw.includes(PARTIAL_CONTROL_OPEN)) return [];
|
|
184
|
+
const matches: PartialSectionMatch[] = [];
|
|
185
|
+
const re = partialControlSectionRegExp();
|
|
186
|
+
let match: RegExpExecArray | null;
|
|
187
|
+
while ((match = re.exec(raw)) !== null) {
|
|
188
|
+
matches.push({
|
|
189
|
+
content: match[1] ?? "",
|
|
190
|
+
start: match.index,
|
|
191
|
+
end: re.lastIndex,
|
|
192
|
+
index: matches.length,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return matches;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function partialControlSectionRegExp(): RegExp {
|
|
199
|
+
return /[ \t]*<partial-control\s*>([\s\S]*?)<\/partial-control>[ \t]*(?:\r?\n)?/gi;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parsePartialSectionJson(
|
|
203
|
+
section: PartialSectionMatch,
|
|
204
|
+
issues: WorkflowPartialOutputIssue[],
|
|
205
|
+
): Record<string, unknown> | undefined {
|
|
206
|
+
let parsed: unknown;
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(section.content.trim());
|
|
209
|
+
} catch (error) {
|
|
210
|
+
issues.push({
|
|
211
|
+
code: "invalid_json",
|
|
212
|
+
sectionIndex: section.index,
|
|
213
|
+
message: error instanceof Error ? error.message : String(error),
|
|
214
|
+
});
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
if (!isRecord(parsed)) {
|
|
218
|
+
issues.push({
|
|
219
|
+
code: "invalid_type",
|
|
220
|
+
sectionIndex: section.index,
|
|
221
|
+
message: "partial-control section must contain a JSON object",
|
|
222
|
+
});
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
if (parsed.schema !== WORKFLOW_PARTIAL_OUTPUT_PROTOCOL) {
|
|
226
|
+
issues.push({
|
|
227
|
+
code: "invalid_schema",
|
|
228
|
+
sectionIndex: section.index,
|
|
229
|
+
message: `partial-control schema must be ${WORKFLOW_PARTIAL_OUTPUT_PROTOCOL}`,
|
|
230
|
+
});
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
return parsed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parsePartialSectionPath(
|
|
237
|
+
section: Record<string, unknown>,
|
|
238
|
+
match: PartialSectionMatch,
|
|
239
|
+
allowedPaths: Set<string> | undefined,
|
|
240
|
+
issues: WorkflowPartialOutputIssue[],
|
|
241
|
+
): string | undefined {
|
|
242
|
+
const path = section.path;
|
|
243
|
+
if (typeof path !== "string" || !path.startsWith("$.")) {
|
|
244
|
+
issues.push({
|
|
245
|
+
code: "invalid_path",
|
|
246
|
+
sectionIndex: match.index,
|
|
247
|
+
message: "partial-control path must be a control JSONPath starting with $.",
|
|
248
|
+
});
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
if (allowedPaths && !allowedPaths.has(path)) {
|
|
252
|
+
issues.push({
|
|
253
|
+
code: "disallowed_path",
|
|
254
|
+
sectionIndex: match.index,
|
|
255
|
+
path,
|
|
256
|
+
message: `partial-control path ${path} is not declared for this stage`,
|
|
257
|
+
});
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
return path;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function parsePartialSectionItems(
|
|
264
|
+
section: Record<string, unknown>,
|
|
265
|
+
match: PartialSectionMatch,
|
|
266
|
+
path: string,
|
|
267
|
+
issues: WorkflowPartialOutputIssue[],
|
|
268
|
+
): unknown[] | undefined {
|
|
269
|
+
const items = section.items;
|
|
270
|
+
if (!Array.isArray(items)) {
|
|
271
|
+
issues.push({
|
|
272
|
+
code: "missing_items",
|
|
273
|
+
sectionIndex: match.index,
|
|
274
|
+
path,
|
|
275
|
+
message: "partial-control items must be an array",
|
|
276
|
+
});
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return items;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function stablePartialItemId(item: unknown): string | undefined {
|
|
283
|
+
if (!isRecord(item) || typeof item.id !== "string") return undefined;
|
|
284
|
+
const sanitized = sanitizePartialItemId(item.id);
|
|
285
|
+
return sanitized || undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sanitizePartialItemId(value: string): string {
|
|
289
|
+
return value
|
|
290
|
+
.trim()
|
|
291
|
+
.toLowerCase()
|
|
292
|
+
.replace(/[^a-z0-9_.-]+/g, "-")
|
|
293
|
+
.replace(/^-+|-+$/g, "")
|
|
294
|
+
.slice(0, 64);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
298
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
299
|
+
}
|
|
@@ -104,16 +104,16 @@ export function diagnoseWorkflowRunHealth(
|
|
|
104
104
|
options: WorkflowHealthOptions = {},
|
|
105
105
|
): WorkflowProgressHealth {
|
|
106
106
|
const nowMs = options.nowMs ?? Date.now();
|
|
107
|
-
const runningTask = currentRunningTask(run.tasks ?? []);
|
|
108
|
-
if (runningTask)
|
|
109
|
-
return diagnoseWorkflowTaskHealth(runningTask, run, { nowMs });
|
|
110
|
-
|
|
111
107
|
const problem = (run.tasks ?? []).find((task) =>
|
|
112
108
|
isProblemStatus(task.status),
|
|
113
109
|
);
|
|
114
110
|
if (problem) return problemRunHealth(problem, nowMs);
|
|
115
111
|
if (isProblemStatus(run.status)) return problemWorkflowHealth(run.status);
|
|
116
112
|
if (run.status === "completed") return completedWorkflowHealth();
|
|
113
|
+
|
|
114
|
+
const runningTask = currentRunningTask(run.tasks ?? []);
|
|
115
|
+
if (runningTask)
|
|
116
|
+
return diagnoseWorkflowTaskHealth(runningTask, run, { nowMs });
|
|
117
117
|
return waitingWorkflowHealth(run, nowMs);
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -143,19 +143,20 @@ export function classifyWorkflowTaskDuration(
|
|
|
143
143
|
.filter(Boolean)
|
|
144
144
|
.join(" ")
|
|
145
145
|
.toLowerCase();
|
|
146
|
-
if (/\b(render|helper|support|schema|partition|gate)\b/.test(text))
|
|
147
|
-
return "short";
|
|
148
|
-
if (
|
|
149
|
-
/\b(research|audit|synthesis|review|verify|verifier|normalize|plan|impact|spec)\b/.test(
|
|
150
|
-
text,
|
|
151
|
-
)
|
|
152
|
-
)
|
|
153
|
-
return "long";
|
|
154
146
|
const maxRuntimeMs = task.runtime?.maxRuntimeMs;
|
|
155
147
|
if (maxRuntimeMs !== undefined && Number.isFinite(maxRuntimeMs)) {
|
|
156
148
|
if (maxRuntimeMs <= 5 * 60_000) return "short";
|
|
157
149
|
if (maxRuntimeMs >= 60 * 60_000) return "long";
|
|
158
150
|
}
|
|
151
|
+
if (task.kind === "support") return "short";
|
|
152
|
+
if (
|
|
153
|
+
/\b(research|audit|synthesi[sz]e?r?|review(?:er|ers|ing|s)?|verif(?:y|ier|iers|ication)|normaliz(?:e|er|ing|ation)?|plan(?:ning)?|impact|spec)\b/.test(
|
|
154
|
+
text,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return "long";
|
|
158
|
+
if (/\b(render|helper|support|schema|partition|gate)\b/.test(text))
|
|
159
|
+
return "short";
|
|
159
160
|
return "medium";
|
|
160
161
|
}
|
|
161
162
|
|
|
@@ -215,6 +216,33 @@ function waitingWorkflowHealth(
|
|
|
215
216
|
nowMs: number,
|
|
216
217
|
): WorkflowProgressHealth {
|
|
217
218
|
const hasPending = run.taskSummary.pending > 0;
|
|
219
|
+
const lastActivityAgeMs = ageMs(run.updatedAt, nowMs);
|
|
220
|
+
if (hasPending && lastActivityAgeMs !== undefined) {
|
|
221
|
+
if (lastActivityAgeMs >= STUCK_BY_DURATION.medium) {
|
|
222
|
+
return {
|
|
223
|
+
state: "likely-stuck",
|
|
224
|
+
label: "scheduler stuck",
|
|
225
|
+
summary: "pending tasks have not scheduled",
|
|
226
|
+
tone: "error",
|
|
227
|
+
suggestion: "resume",
|
|
228
|
+
reason: "no task is running and run activity is stale",
|
|
229
|
+
lastActivityAt: run.updatedAt,
|
|
230
|
+
lastActivityAgeMs,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (lastActivityAgeMs >= STALL_BY_DURATION.medium) {
|
|
234
|
+
return {
|
|
235
|
+
state: "stalled",
|
|
236
|
+
label: "scheduler quiet",
|
|
237
|
+
summary: "pending tasks are waiting without recent activity",
|
|
238
|
+
tone: "warning",
|
|
239
|
+
suggestion: "inspect",
|
|
240
|
+
reason: "no task is running and run activity is stale",
|
|
241
|
+
lastActivityAt: run.updatedAt,
|
|
242
|
+
lastActivityAgeMs,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
218
246
|
return {
|
|
219
247
|
state: hasPending ? "pending" : "active",
|
|
220
248
|
label: hasPending ? "pending" : "active",
|
|
@@ -227,7 +255,7 @@ function waitingWorkflowHealth(
|
|
|
227
255
|
? "no task is currently running"
|
|
228
256
|
: "workflow is still in progress",
|
|
229
257
|
lastActivityAt: run.updatedAt,
|
|
230
|
-
lastActivityAgeMs
|
|
258
|
+
lastActivityAgeMs,
|
|
231
259
|
};
|
|
232
260
|
}
|
|
233
261
|
|
|
@@ -335,8 +363,12 @@ function runningContext(
|
|
|
335
363
|
const durationClass = classifyWorkflowTaskDuration(task);
|
|
336
364
|
const startedAtMs = parseTime(task.startedAt);
|
|
337
365
|
const heartbeatAt = parseHeartbeatAt(task.lastMessage);
|
|
366
|
+
const heartbeatAgeMs = ageMs(heartbeatAt, nowMs);
|
|
338
367
|
const activityAt = latestIso([heartbeatAt, run?.updatedAt, task.startedAt]);
|
|
339
368
|
const lastActivityAgeMs = ageMs(activityAt, nowMs);
|
|
369
|
+
const hasFreshHeartbeat =
|
|
370
|
+
heartbeatAgeMs !== undefined &&
|
|
371
|
+
heartbeatAgeMs <= STALL_BY_DURATION[durationClass];
|
|
340
372
|
return {
|
|
341
373
|
task,
|
|
342
374
|
nowMs,
|
|
@@ -346,8 +378,8 @@ function runningContext(
|
|
|
346
378
|
activityAt,
|
|
347
379
|
lastActivityAgeMs,
|
|
348
380
|
heartbeatAt,
|
|
349
|
-
heartbeatAgeMs
|
|
350
|
-
hasBackendSignal: Boolean(task.
|
|
381
|
+
heartbeatAgeMs,
|
|
382
|
+
hasBackendSignal: Boolean(task.pid || hasFreshHeartbeat),
|
|
351
383
|
staleMs: lastActivityAgeMs ?? Number.POSITIVE_INFINITY,
|
|
352
384
|
};
|
|
353
385
|
}
|
|
@@ -942,8 +942,8 @@ async function cachedFetchFailureResult(
|
|
|
942
942
|
return errorToolResult(failure.code, failure.message, failure.extra);
|
|
943
943
|
}
|
|
944
944
|
|
|
945
|
-
const FETCH_LOCK_STALE_MS = 60_000;
|
|
946
|
-
const FETCH_LOCK_WAIT_MS =
|
|
945
|
+
const FETCH_LOCK_STALE_MS = 4 * 60_000;
|
|
946
|
+
const FETCH_LOCK_WAIT_MS = 5 * 60_000;
|
|
947
947
|
|
|
948
948
|
async function withWorkflowWebFetchLock<T>(
|
|
949
949
|
config: WorkflowWebSourceCacheConfig,
|
|
@@ -970,14 +970,15 @@ async function acquireWorkflowWebFetchLock(
|
|
|
970
970
|
for (;;) {
|
|
971
971
|
if (signal?.aborted) throw new Error("aborted");
|
|
972
972
|
try {
|
|
973
|
+
const ownerId = `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
973
974
|
await mkdir(lockDir);
|
|
974
975
|
await writeFile(
|
|
975
976
|
resolve(lockDir, "owner.json"),
|
|
976
|
-
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), key }, null, 2)}\n`,
|
|
977
|
+
`${JSON.stringify({ ownerId, pid: process.pid, createdAt: new Date().toISOString(), key }, null, 2)}\n`,
|
|
977
978
|
"utf8",
|
|
978
979
|
);
|
|
979
980
|
return async () => {
|
|
980
|
-
await
|
|
981
|
+
await releaseWorkflowWebFetchLock(lockDir, ownerId);
|
|
981
982
|
};
|
|
982
983
|
} catch (error) {
|
|
983
984
|
if (!isFileExistsError(error)) throw error;
|
|
@@ -990,6 +991,19 @@ async function acquireWorkflowWebFetchLock(
|
|
|
990
991
|
}
|
|
991
992
|
}
|
|
992
993
|
|
|
994
|
+
async function releaseWorkflowWebFetchLock(
|
|
995
|
+
lockDir: string,
|
|
996
|
+
ownerId: string,
|
|
997
|
+
): Promise<void> {
|
|
998
|
+
try {
|
|
999
|
+
const current = await readFetchLockOwner(lockDir);
|
|
1000
|
+
if (current?.ownerId !== ownerId) return;
|
|
1001
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
1002
|
+
} catch {
|
|
1003
|
+
// Missing or unreadable lock will be retried by the caller.
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
993
1007
|
async function removeStaleFetchLock(lockDir: string): Promise<void> {
|
|
994
1008
|
try {
|
|
995
1009
|
const current = await stat(lockDir);
|
|
@@ -1001,6 +1015,21 @@ async function removeStaleFetchLock(lockDir: string): Promise<void> {
|
|
|
1001
1015
|
}
|
|
1002
1016
|
}
|
|
1003
1017
|
|
|
1018
|
+
async function readFetchLockOwner(
|
|
1019
|
+
lockDir: string,
|
|
1020
|
+
): Promise<{ ownerId?: string } | undefined> {
|
|
1021
|
+
try {
|
|
1022
|
+
const parsed = JSON.parse(
|
|
1023
|
+
await readFile(resolve(lockDir, "owner.json"), "utf8"),
|
|
1024
|
+
) as unknown;
|
|
1025
|
+
return isRecord(parsed) && typeof parsed.ownerId === "string"
|
|
1026
|
+
? { ownerId: parsed.ownerId }
|
|
1027
|
+
: undefined;
|
|
1028
|
+
} catch {
|
|
1029
|
+
return undefined;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1004
1033
|
async function readDurableFetchFailure(
|
|
1005
1034
|
config: WorkflowWebSourceCacheConfig,
|
|
1006
1035
|
key: string,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { isIP } from "node:net";
|
|
11
11
|
import { dirname, resolve } from "node:path";
|
|
12
12
|
|
|
13
|
+
import { compactStrings } from "./strings.js";
|
|
14
|
+
|
|
13
15
|
export const WORKFLOW_WEB_SOURCE_CACHE_SCHEMA =
|
|
14
16
|
"workflow-web-source-cache-v1" as const;
|
|
15
17
|
export const WORKFLOW_WEB_SOURCE_INDEX_SCHEMA =
|
|
@@ -150,7 +152,7 @@ export const DEFAULT_WORKFLOW_WEB_SECURITY_POLICY: WorkflowWebSecurityPolicy = {
|
|
|
150
152
|
};
|
|
151
153
|
|
|
152
154
|
const SENSITIVE_QUERY_PARAM_PATTERN =
|
|
153
|
-
/(^|[-_])(access[-_]?token|auth|code|credential|key|password|secret|session|signature|sig|token)([-_]|$)/i;
|
|
155
|
+
/(^|[-_])(access[-_]?token|auth|code|credential|key|password|secret|session|session[-_]?id|sessionid|signature|sig|sid|jwt|token)([-_]|$)/i;
|
|
154
156
|
const PRIVATE_HOST_PATTERNS = [
|
|
155
157
|
/^localhost$/i,
|
|
156
158
|
/^127\./,
|
|
@@ -331,7 +333,7 @@ export function createWorkflowWebSource(options: {
|
|
|
331
333
|
redactedUrl,
|
|
332
334
|
urlKey: sourceUrlCacheKey(options.url),
|
|
333
335
|
domain,
|
|
334
|
-
...(options.title ? { title: options.title } : {}),
|
|
336
|
+
...(options.title ? { title: redactInlineSecrets(options.title) } : {}),
|
|
335
337
|
...(options.provider ? { provider: options.provider } : {}),
|
|
336
338
|
contentHash,
|
|
337
339
|
text: options.text,
|
|
@@ -602,14 +604,14 @@ export function extractTextFromToolResult(result: unknown): string {
|
|
|
602
604
|
if (!isRecord(result)) return "";
|
|
603
605
|
const content = result.content;
|
|
604
606
|
if (!Array.isArray(content)) return "";
|
|
605
|
-
return
|
|
606
|
-
.map((entry) => {
|
|
607
|
+
return compactStrings(
|
|
608
|
+
content.map((entry) => {
|
|
607
609
|
if (!isRecord(entry)) return "";
|
|
608
610
|
const text = entry.text;
|
|
609
611
|
return typeof text === "string" ? text : "";
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
|
|
612
|
+
}),
|
|
613
|
+
{ trim: false, unique: false },
|
|
614
|
+
).join("\n\n");
|
|
613
615
|
}
|
|
614
616
|
|
|
615
617
|
export function extractTitleFromToolResult(
|
|
@@ -712,7 +714,7 @@ function sourceToIndexEntry(
|
|
|
712
714
|
redactedUrl: source.redactedUrl,
|
|
713
715
|
...(source.urlKey ? { urlKey: source.urlKey } : {}),
|
|
714
716
|
domain: source.domain,
|
|
715
|
-
...(source.title ? { title: source.title } : {}),
|
|
717
|
+
...(source.title ? { title: redactInlineSecrets(source.title) } : {}),
|
|
716
718
|
contentHash: source.contentHash,
|
|
717
719
|
textChars: source.textChars,
|
|
718
720
|
...(source.provider ? { provider: source.provider } : {}),
|
|
@@ -1066,9 +1068,18 @@ function consumeAnchoredSnippet(options: {
|
|
|
1066
1068
|
raw,
|
|
1067
1069
|
visibleLimit,
|
|
1068
1070
|
);
|
|
1071
|
+
// Redaction can expand secrets. Promote only when the redacted anchor
|
|
1072
|
+
// itself no longer fits; clipping trailing context can remain a match.
|
|
1073
|
+
const redactedThroughAnchorLength = consumed.truncated
|
|
1074
|
+
? redactInlineSecrets(
|
|
1075
|
+
options.text.slice(sourceStart, Math.min(sourceEnd, anchorEnd)),
|
|
1076
|
+
).length
|
|
1077
|
+
: 0;
|
|
1078
|
+
const anchorTruncated =
|
|
1079
|
+
status === "truncated" || redactedThroughAnchorLength > visibleLimit;
|
|
1069
1080
|
const truncated = status === "truncated" || consumed.truncated;
|
|
1070
1081
|
return {
|
|
1071
|
-
status,
|
|
1082
|
+
status: anchorTruncated ? "truncated" : status,
|
|
1072
1083
|
quote: consumed.text,
|
|
1073
1084
|
visibleChars: consumed.text.length,
|
|
1074
1085
|
sourceStart,
|
|
@@ -1105,7 +1116,15 @@ function normalizeForSearch(text: string): {
|
|
|
1105
1116
|
map.push(index);
|
|
1106
1117
|
}
|
|
1107
1118
|
}
|
|
1108
|
-
|
|
1119
|
+
while (normalized.startsWith(" ")) {
|
|
1120
|
+
normalized = normalized.slice(1);
|
|
1121
|
+
map.shift();
|
|
1122
|
+
}
|
|
1123
|
+
while (normalized.endsWith(" ")) {
|
|
1124
|
+
normalized = normalized.slice(0, -1);
|
|
1125
|
+
map.pop();
|
|
1126
|
+
}
|
|
1127
|
+
return { normalized, map };
|
|
1109
1128
|
}
|
|
1110
1129
|
|
|
1111
1130
|
function nearbySnippet(text: string, needle: string, maxChars: number): string {
|
|
@@ -1216,7 +1235,9 @@ function sourceIndexEntryFromUnknown(
|
|
|
1216
1235
|
redactedUrl: value.redactedUrl,
|
|
1217
1236
|
...(typeof value.urlKey === "string" ? { urlKey: value.urlKey } : {}),
|
|
1218
1237
|
domain: value.domain,
|
|
1219
|
-
...(typeof value.title === "string"
|
|
1238
|
+
...(typeof value.title === "string"
|
|
1239
|
+
? { title: redactInlineSecrets(value.title) }
|
|
1240
|
+
: {}),
|
|
1220
1241
|
contentHash: value.contentHash,
|
|
1221
1242
|
textChars: Number(value.textChars),
|
|
1222
1243
|
...(typeof value.provider === "string" ? { provider: value.provider } : {}),
|
|
@@ -1375,7 +1396,10 @@ function redactInlineSecrets(value: string): string {
|
|
|
1375
1396
|
function redactInlineSecretsNoUrls(value: string): string {
|
|
1376
1397
|
return value
|
|
1377
1398
|
.replace(/(authorization|cookie|set-cookie):\s*[^\n\r]+/gi, "$1: REDACTED")
|
|
1378
|
-
.replace(
|
|
1399
|
+
.replace(
|
|
1400
|
+
/(token|secret|password|api[-_]?key|jwt|sid|sessionid|session[-_]?id)=([^\s&]+)/gi,
|
|
1401
|
+
"$1=REDACTED",
|
|
1402
|
+
)
|
|
1379
1403
|
.replace(/\/Users\/[^\s:'")]+/g, "/Users/REDACTED");
|
|
1380
1404
|
}
|
|
1381
1405
|
|
package/workflows/README.md
CHANGED
|
@@ -22,7 +22,9 @@ For spec-less direct dynamic execution, use `/workflow dynamic "<task>"`; it doe
|
|
|
22
22
|
| `spec-review` | `scout` | Use when you want to check whether requirements, an API spec, or a contract are reflected in the implementation and tests. |
|
|
23
23
|
| `impact-review` | `scout` | Use before merging or releasing a change to check affected areas, risks, missing tests, and missing docs. |
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Experimental or candidate workflows should live outside the bundled `workflows/` directory until their task fit is validated. `deep-research` also ships a path-ref-only batched verification variant at `workflows/deep-research/batched-verification.spec.json`; it is intentionally not registered as an official workflow name and must be invoked by explicit path after validation.
|
|
26
|
+
|
|
27
|
+
Bundled workflows that verify source-backed claims can share the verification outcome ontology exported by the package: `verified`, `partially_supported`, `unsupported`, `conflicting`, and `verification_blocked`. Workflow helpers should keep dependency-free bundle-local shims in parity with that package export, because helper imports are bundled from the workflow spec directory. `verification_blocked` means verification could not complete because evidence, tool, source-access, or policy conditions blocked evaluation; it is never counted as verified. Deep-research adopts this ontology now. Workflows with different verdict models, such as finding disposition or ship readiness, should not be forced into it. Deep-diff-review revival is intentionally out of scope for this ontology update.
|
|
26
28
|
|
|
27
29
|
## Bundle layout
|
|
28
30
|
|
|
@@ -45,30 +47,10 @@ Bundle names resolve from the directory name (`/workflow run name ...`). If two
|
|
|
45
47
|
|
|
46
48
|
Artifact-graph workflows use `from` for data edges, `after` for order-only edges, and `type: "dag"` containers for nested sibling-scoped graphs. A downstream stage consumes a container with `from: "analysis"`, which resolves to the container's `outputFrom` child. See `docs/usage.md` for the full DAG example, artifact bundle rules, and validation rules.
|
|
47
49
|
|
|
48
|
-
## Support helpers
|
|
49
|
-
|
|
50
|
-
A support node runs local helper code inline instead of launching a subagent. Declare it with a `support` object, not a separate `type` value:
|
|
51
|
-
|
|
52
|
-
```json
|
|
53
|
-
{
|
|
54
|
-
"id": "audit-claims",
|
|
55
|
-
"from": "verify-claims",
|
|
56
|
-
"sourcePolicy": "partial",
|
|
57
|
-
"support": {
|
|
58
|
-
"uses": "./helpers/claim-evidence-gate.mjs",
|
|
59
|
-
"options": { "downgradeExactQuantitativeWithoutSource": true }
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Helper API:
|
|
50
|
+
## Support helpers and web tools
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
export default async function helper({ sources, options, context }) {
|
|
68
|
-
return { schema: "helper-output-v1", digest: "...", value: { /* control data */ } };
|
|
69
|
-
}
|
|
70
|
-
```
|
|
52
|
+
Support nodes run bundle-local `.mjs` helper code inline instead of launching a subagent (deep-research uses them to compact normalize inputs and preserve audited verdict/sourceRef ledgers). Bundled workflows prefer the normalized web-source tools (`workflow_web_search`, `workflow_web_fetch_source`, `workflow_web_source_read`) over legacy web tools.
|
|
71
53
|
|
|
72
|
-
|
|
54
|
+
Legacy `fetch_content` workflow tasks use a run-scoped cache and a configurable inline text cap to reduce worker context pressure.
|
|
73
55
|
|
|
74
|
-
|
|
56
|
+
See `docs/usage.md` for the support helper API and path-containment rules ("Support helpers") and for web tool semantics, batching, cache layout, and the `fetch_content` security policy ("Run-scoped web-source cache" and "Web tools").
|