harnery 0.12.2 → 0.14.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 +2 -1
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/work.d.ts +4 -0
- package/dist/commands/work.d.ts.map +1 -0
- package/dist/commands/work.js +281 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +193 -0
- package/dist/core/policy/types.d.ts +1 -1
- package/dist/core/policy/types.d.ts.map +1 -1
- package/dist/core/work/index.d.ts +3 -0
- package/dist/core/work/index.d.ts.map +1 -0
- package/dist/core/work/index.js +2 -0
- package/dist/core/work/read.d.ts +2 -0
- package/dist/core/work/read.d.ts.map +1 -0
- package/dist/core/work/read.js +1 -0
- package/dist/core/work/runner.d.ts +10 -0
- package/dist/core/work/runner.d.ts.map +1 -0
- package/dist/core/work/runner.js +63 -0
- package/dist/core/work/state.d.ts +109 -0
- package/dist/core/work/state.d.ts.map +1 -0
- package/dist/core/work/state.js +698 -0
- package/dist/core/workflow/approvals.d.ts +71 -0
- package/dist/core/workflow/approvals.d.ts.map +1 -0
- package/dist/core/workflow/approvals.js +288 -0
- package/dist/core/workflow/engine.d.ts +6 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +243 -43
- package/dist/core/workflow/index.d.ts +6 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +3 -1
- package/dist/core/workflow/proof.d.ts +1 -0
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +1 -0
- package/dist/core/workflow/run-state.d.ts +44 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -0
- package/dist/core/workflow/run-state.js +233 -0
- package/dist/core/workflow/types.d.ts +16 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/package.json +11 -1
- package/src/commander.ts +2 -0
- package/src/commands/work.ts +367 -0
- package/src/commands/workflow.ts +226 -0
- package/src/core/policy/types.ts +1 -1
- package/src/core/work/index.ts +24 -0
- package/src/core/work/read.ts +15 -0
- package/src/core/work/runner.ts +81 -0
- package/src/core/work/state.ts +900 -0
- package/src/core/workflow/approvals.ts +393 -0
- package/src/core/workflow/engine.ts +288 -44
- package/src/core/workflow/index.ts +31 -1
- package/src/core/workflow/proof.ts +2 -0
- package/src/core/workflow/run-state.ts +290 -0
- package/src/core/workflow/types.ts +16 -0
|
@@ -22,7 +22,12 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "n
|
|
|
22
22
|
import { isAbsolute, join, resolve } from "node:path";
|
|
23
23
|
import { pathToFileURL } from "node:url";
|
|
24
24
|
import { snapshotRepo } from "../context/index.ts";
|
|
25
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
ExternalMutationRequest,
|
|
27
|
+
NormalizedPolicy,
|
|
28
|
+
PolicyDecision,
|
|
29
|
+
PolicyRequest,
|
|
30
|
+
} from "../policy/index.ts";
|
|
26
31
|
import {
|
|
27
32
|
evaluatePolicy,
|
|
28
33
|
normalizePolicy,
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
30
35
|
policyDigest,
|
|
31
36
|
summarizePolicyRequest,
|
|
32
37
|
} from "../policy/index.ts";
|
|
38
|
+
import { assertWorkflowRunId, createWorkflowApproval } from "./approvals.ts";
|
|
33
39
|
import { type BillingProbe, probeBilling } from "./billing.ts";
|
|
34
40
|
import {
|
|
35
41
|
buildWorkflowProof,
|
|
@@ -38,6 +44,13 @@ import {
|
|
|
38
44
|
normalizeWorkflowMeta,
|
|
39
45
|
writeWorkflowProof,
|
|
40
46
|
} from "./proof.ts";
|
|
47
|
+
import {
|
|
48
|
+
acquireWorkflowResumeLease,
|
|
49
|
+
assertWorkflowRunResumable,
|
|
50
|
+
assertWorkflowScriptUnchanged,
|
|
51
|
+
workflowScriptDigest,
|
|
52
|
+
writeWorkflowRunManifest,
|
|
53
|
+
} from "./run-state.ts";
|
|
41
54
|
import type {
|
|
42
55
|
AgentOpts,
|
|
43
56
|
EngineOpts,
|
|
@@ -73,8 +86,69 @@ export class WorkflowRunError extends Error {
|
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
|
|
89
|
+
export class WorkflowParkedError extends Error {
|
|
90
|
+
readonly runId: string;
|
|
91
|
+
readonly approvalId: string;
|
|
92
|
+
readonly journalPath: string;
|
|
93
|
+
|
|
94
|
+
constructor(message: string, runId: string, approvalId: string, journalPath: string) {
|
|
95
|
+
super(message);
|
|
96
|
+
this.name = "WorkflowParkedError";
|
|
97
|
+
this.runId = runId;
|
|
98
|
+
this.approvalId = approvalId;
|
|
99
|
+
this.journalPath = journalPath;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise<RunReport> {
|
|
104
|
+
if (opts.runId && opts.resumeRunId) {
|
|
105
|
+
throw new Error("runId and resumeRunId are mutually exclusive");
|
|
106
|
+
}
|
|
107
|
+
if (opts.runId) assertWorkflowRunId(opts.runId);
|
|
108
|
+
if (opts.workItemId && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(opts.workItemId)) {
|
|
109
|
+
throw new Error(`invalid work item id ${JSON.stringify(opts.workItemId)}`);
|
|
110
|
+
}
|
|
111
|
+
if (opts.resumeRunId && opts.resumeFrom) {
|
|
112
|
+
throw new Error("resumeRunId and resumeFrom are mutually exclusive");
|
|
113
|
+
}
|
|
114
|
+
const resumeState = opts.resumeRunId
|
|
115
|
+
? assertWorkflowRunResumable(opts.coordRoot, opts.resumeRunId)
|
|
116
|
+
: undefined;
|
|
117
|
+
if (
|
|
118
|
+
resumeState !== undefined &&
|
|
119
|
+
opts.workItemId !== undefined &&
|
|
120
|
+
resumeState.manifest.work_item_id !== opts.workItemId
|
|
121
|
+
) {
|
|
122
|
+
throw new Error(`workflow run ${opts.resumeRunId} belongs to a different work item`);
|
|
123
|
+
}
|
|
77
124
|
const absScript = isAbsolute(scriptPath) ? scriptPath : resolve(process.cwd(), scriptPath);
|
|
125
|
+
if (resumeState) {
|
|
126
|
+
if (resolve(resumeState.manifest.script.path) !== resolve(absScript)) {
|
|
127
|
+
throw new Error(`workflow run ${opts.resumeRunId} belongs to a different script`);
|
|
128
|
+
}
|
|
129
|
+
assertWorkflowScriptUnchanged(resumeState.manifest);
|
|
130
|
+
}
|
|
131
|
+
const releaseResumeLease = opts.resumeRunId
|
|
132
|
+
? acquireWorkflowResumeLease(opts.coordRoot, opts.resumeRunId)
|
|
133
|
+
: undefined;
|
|
134
|
+
try {
|
|
135
|
+
if (resumeState) {
|
|
136
|
+
// Recheck under the exclusive lease. Another process may have completed
|
|
137
|
+
// the run between the optimistic read and lease acquisition.
|
|
138
|
+
assertWorkflowRunResumable(opts.coordRoot, resumeState.manifest.run_id);
|
|
139
|
+
}
|
|
140
|
+
return await executeWorkflow(scriptPath, absScript, opts, resumeState);
|
|
141
|
+
} finally {
|
|
142
|
+
releaseResumeLease?.();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function executeWorkflow(
|
|
147
|
+
scriptPath: string,
|
|
148
|
+
absScript: string,
|
|
149
|
+
opts: EngineOpts,
|
|
150
|
+
resumeState: ReturnType<typeof assertWorkflowRunResumable> | undefined,
|
|
151
|
+
): Promise<RunReport> {
|
|
78
152
|
const mod = (await import(pathToFileURL(absScript).href)) as WorkflowModule;
|
|
79
153
|
if (typeof mod.default !== "function") {
|
|
80
154
|
throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
|
|
@@ -83,21 +157,63 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
83
157
|
const meta = normalizeWorkflowMeta(mod.meta, fallbackName);
|
|
84
158
|
const name = meta.name;
|
|
85
159
|
|
|
86
|
-
const runId =
|
|
160
|
+
const runId =
|
|
161
|
+
opts.resumeRunId ??
|
|
162
|
+
opts.runId ??
|
|
163
|
+
`wf-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomBytes(3).toString("hex")}`;
|
|
87
164
|
const runDir = join(opts.coordRoot, ".harnery", "workflows", runId);
|
|
88
165
|
mkdirSync(runDir, { recursive: true });
|
|
89
166
|
const journalPath = join(runDir, "journal.jsonl");
|
|
90
167
|
const proofPath = join(runDir, "proof.json");
|
|
91
168
|
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
169
|
+
const frozen = resumeState?.manifest.execution;
|
|
170
|
+
const maxAgents = frozen?.max_agents ?? opts.maxAgents ?? DEFAULT_MAX_AGENTS;
|
|
171
|
+
const concurrency = frozen?.concurrency ?? opts.concurrency ?? DEFAULT_CONCURRENCY;
|
|
172
|
+
assertPositiveWorkflowBound(maxAgents, "maxAgents");
|
|
173
|
+
assertPositiveWorkflowBound(concurrency, "concurrency");
|
|
174
|
+
const cwd = frozen?.cwd ?? opts.cwd ?? opts.coordRoot;
|
|
95
175
|
const log = opts.onLog ?? ((line: string) => process.stderr.write(`${line}\n`));
|
|
96
|
-
const defaultHarness: HarnessName =
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
176
|
+
const defaultHarness: HarnessName =
|
|
177
|
+
frozen?.default_harness ?? opts.defaultHarness ?? "claude-code";
|
|
178
|
+
const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
|
|
179
|
+
const networkAccess = frozen?.network_access ?? opts.networkAccess ?? "unknown";
|
|
180
|
+
const policy =
|
|
181
|
+
frozen?.policy ?? (opts.policy ? normalizePolicy(opts.policy, { baseDir: cwd }) : undefined);
|
|
182
|
+
const approvalMode = frozen?.approval_mode ?? opts.approvalMode ?? "deny";
|
|
183
|
+
const approvalAddressee = frozen?.approval_addressee ?? opts.approvalAddressee ?? "operator";
|
|
184
|
+
const subscriptionOnly = frozen?.subscription_only ?? Boolean(opts.subscriptionOnly);
|
|
185
|
+
const allowApiBilling = frozen?.allow_api_billing ?? Boolean(opts.allowApiBilling);
|
|
186
|
+
const repoBefore = resumeState?.manifest.repository_before ?? snapshotRepo(cwd);
|
|
187
|
+
const startedAt = resumeState?.manifest.started_at ?? new Date().toISOString();
|
|
188
|
+
const t0 = resumeState ? Date.parse(startedAt) : Date.now();
|
|
189
|
+
|
|
190
|
+
if (!resumeState) {
|
|
191
|
+
writeWorkflowRunManifest({
|
|
192
|
+
coordRoot: opts.coordRoot,
|
|
193
|
+
manifest: {
|
|
194
|
+
schema_version: 1,
|
|
195
|
+
run_id: runId,
|
|
196
|
+
work_item_id: opts.workItemId,
|
|
197
|
+
name,
|
|
198
|
+
started_at: startedAt,
|
|
199
|
+
script: { path: absScript, sha256: workflowScriptDigest(absScript) },
|
|
200
|
+
repository_before: repoBefore,
|
|
201
|
+
execution: {
|
|
202
|
+
cwd,
|
|
203
|
+
default_harness: defaultHarness,
|
|
204
|
+
max_agents: maxAgents,
|
|
205
|
+
concurrency,
|
|
206
|
+
subscription_only: subscriptionOnly,
|
|
207
|
+
allow_api_billing: allowApiBilling,
|
|
208
|
+
approval_mode: approvalMode,
|
|
209
|
+
approval_addressee: approvalAddressee,
|
|
210
|
+
isolation,
|
|
211
|
+
network_access: networkAccess,
|
|
212
|
+
policy: policy ? (policy as NormalizedPolicy) : undefined,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
101
217
|
|
|
102
218
|
// Per-child fixed context overhead: children spawn in `cwd` and load its
|
|
103
219
|
// repo-instructions file into their system prompt, cache-writing it once
|
|
@@ -111,13 +227,16 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
111
227
|
}
|
|
112
228
|
|
|
113
229
|
// Resume: journaled results of a prior run, keyed by agent-call identity.
|
|
114
|
-
const
|
|
115
|
-
|
|
230
|
+
const resumeSource = opts.resumeRunId ?? opts.resumeFrom;
|
|
231
|
+
const resumeCache = resumeSource
|
|
232
|
+
? loadResumeCache(opts.coordRoot, resumeSource)
|
|
116
233
|
: new Map<string, { kind: "json" | "text"; value: unknown }>();
|
|
117
234
|
|
|
118
|
-
|
|
235
|
+
const history = opts.resumeRunId ? loadRunHistory(journalPath) : undefined;
|
|
236
|
+
|
|
237
|
+
let agentsSpawned = history?.agentsSpawned ?? 0;
|
|
119
238
|
let agentsCached = 0;
|
|
120
|
-
let costUsd = 0;
|
|
239
|
+
let costUsd = history?.costUsd ?? 0;
|
|
121
240
|
let reservedCostUsd = 0;
|
|
122
241
|
let currentStage = "";
|
|
123
242
|
let agentSeq = 0;
|
|
@@ -126,7 +245,9 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
126
245
|
const billingProbed = new Map<HarnessName, BillingProbe>();
|
|
127
246
|
const agentProofs = new Map<string, WorkflowAgentProof>();
|
|
128
247
|
const evidenceRecords: WorkflowEvidenceRecord[] = [];
|
|
129
|
-
const policyDecisions
|
|
248
|
+
const policyDecisions = new Map<string, PolicyDecision>(
|
|
249
|
+
history?.policyDecisions.map((decision) => [decision.id, decision]),
|
|
250
|
+
);
|
|
130
251
|
const acceptanceIds = new Set(meta.acceptance.map((criterion) => criterion.id));
|
|
131
252
|
|
|
132
253
|
const journal = (event: string, data: Record<string, unknown>): void => {
|
|
@@ -181,7 +302,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
181
302
|
if (!policy) {
|
|
182
303
|
throw new Error("external mutation authorization requires a host policy");
|
|
183
304
|
}
|
|
184
|
-
if (policyDecisions.
|
|
305
|
+
if (policyDecisions.size >= MAX_POLICY_DECISIONS && !policyDecisions.has(`p${policySeq + 1}`)) {
|
|
185
306
|
throw new Error(
|
|
186
307
|
`workflow policy decision cap reached (${MAX_POLICY_DECISIONS}); split the workflow or reduce protected actions`,
|
|
187
308
|
);
|
|
@@ -204,9 +325,8 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
204
325
|
let reason = evaluation.reason;
|
|
205
326
|
if (evaluation.verdict === "ask") {
|
|
206
327
|
resolvedBy = "fail_closed";
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} else {
|
|
328
|
+
let immediateResolution = false;
|
|
329
|
+
if (opts.resolvePolicyAsk) {
|
|
210
330
|
try {
|
|
211
331
|
const resolution = await withTimeout(
|
|
212
332
|
Promise.resolve(opts.resolvePolicyAsk(request, evaluation)),
|
|
@@ -218,10 +338,57 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
218
338
|
verdict = resolution.verdict;
|
|
219
339
|
resolvedBy = "host";
|
|
220
340
|
reason = boundedPolicyReason(resolution.reason ?? evaluation.reason);
|
|
341
|
+
immediateResolution = true;
|
|
221
342
|
}
|
|
222
343
|
} catch (error) {
|
|
223
344
|
reason = `${evaluation.reason}; approval failed closed: ${(error as Error).message}`;
|
|
224
345
|
}
|
|
346
|
+
} else {
|
|
347
|
+
reason = `${evaluation.reason}; no host approval resolver is configured`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!immediateResolution && approvalMode === "park") {
|
|
351
|
+
const stored = createWorkflowApproval({
|
|
352
|
+
coordRoot: opts.coordRoot,
|
|
353
|
+
runId,
|
|
354
|
+
decisionId: id,
|
|
355
|
+
addressedTo: approvalAddressee,
|
|
356
|
+
policy: { name: policy.name, sha256: policyDigest(policy) },
|
|
357
|
+
request,
|
|
358
|
+
evaluation,
|
|
359
|
+
});
|
|
360
|
+
if (stored.approval.status === "pending") {
|
|
361
|
+
if (stored.created) {
|
|
362
|
+
journal("approval.requested", {
|
|
363
|
+
approval_id: stored.approval.request.id,
|
|
364
|
+
decision_id: id,
|
|
365
|
+
addressed_to: stored.approval.request.addressed_to,
|
|
366
|
+
request_sha256: stored.approval.request.request_sha256,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
journal("run.parked", {
|
|
370
|
+
approval_id: stored.approval.request.id,
|
|
371
|
+
decision_id: id,
|
|
372
|
+
phase: request.phase,
|
|
373
|
+
action: request.action,
|
|
374
|
+
});
|
|
375
|
+
throw new WorkflowParkedError(
|
|
376
|
+
`workflow parked for approval ${stored.approval.request.id}: ${evaluation.reason}`,
|
|
377
|
+
runId,
|
|
378
|
+
stored.approval.request.id,
|
|
379
|
+
journalPath,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
verdict = stored.approval.decision!.verdict;
|
|
383
|
+
resolvedBy = "approval";
|
|
384
|
+
reason = boundedPolicyReason(stored.approval.decision!.reason ?? evaluation.reason);
|
|
385
|
+
journal("approval.consumed", {
|
|
386
|
+
approval_id: stored.approval.request.id,
|
|
387
|
+
decision_id: id,
|
|
388
|
+
verdict,
|
|
389
|
+
actor: stored.approval.decision!.actor,
|
|
390
|
+
decided_at: stored.approval.decision!.decided_at,
|
|
391
|
+
});
|
|
225
392
|
}
|
|
226
393
|
}
|
|
227
394
|
|
|
@@ -237,7 +404,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
237
404
|
rule_codes: evaluation.rules.map((rule) => rule.code),
|
|
238
405
|
request,
|
|
239
406
|
};
|
|
240
|
-
policyDecisions.
|
|
407
|
+
policyDecisions.set(decision.id, decision);
|
|
241
408
|
journal("policy.resolve", { ...decision });
|
|
242
409
|
if (decision.verdict === "deny") {
|
|
243
410
|
throw new PolicyDeniedError(
|
|
@@ -279,6 +446,10 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
279
446
|
const key = agentCallKey(currentStage, harness, agentOpts, prompt);
|
|
280
447
|
const cached = resumeCache.get(key);
|
|
281
448
|
if (cached) {
|
|
449
|
+
// Exact-run replay skips dispatch authorization because no dispatch
|
|
450
|
+
// occurs, but it must still reserve the original policy slot so a later
|
|
451
|
+
// durable ASK resolves against the same pN identity.
|
|
452
|
+
if (policy && opts.resumeRunId) policySeq++;
|
|
282
453
|
agentsCached++;
|
|
283
454
|
agentProof.status = "cached";
|
|
284
455
|
agentProof.result = digestResult(cached.value, cached.kind);
|
|
@@ -291,7 +462,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
291
462
|
kind: cached.kind,
|
|
292
463
|
});
|
|
293
464
|
log(
|
|
294
|
-
`[${name}] ${currentStage || "(no stage)"} → ${id} ${label} (cached from ${
|
|
465
|
+
`[${name}] ${currentStage || "(no stage)"} → ${id} ${label} (cached from ${resumeSource})`,
|
|
295
466
|
);
|
|
296
467
|
return cached.value;
|
|
297
468
|
}
|
|
@@ -370,12 +541,12 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
370
541
|
billingProbed.set(harness, probe);
|
|
371
542
|
journal("billing.probe", {
|
|
372
543
|
harness,
|
|
373
|
-
mode:
|
|
544
|
+
mode: subscriptionOnly ? "subscription" : probe.mode,
|
|
374
545
|
api_key_source: probe.apiKeySource,
|
|
375
546
|
login: probe.login,
|
|
376
|
-
subscription_only:
|
|
547
|
+
subscription_only: subscriptionOnly,
|
|
377
548
|
});
|
|
378
|
-
if (
|
|
549
|
+
if (subscriptionOnly) {
|
|
379
550
|
if (probe.login === "absent") {
|
|
380
551
|
throw new Error(
|
|
381
552
|
`subscription-only: no stored login detected for ${harness}; ` +
|
|
@@ -383,7 +554,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
383
554
|
);
|
|
384
555
|
}
|
|
385
556
|
log(`[billing] ${harness}: subscription-only (API-key vars scrubbed from child env)`);
|
|
386
|
-
} else if (probe.mode === "api-key-override" && !
|
|
557
|
+
} else if (probe.mode === "api-key-override" && !allowApiBilling) {
|
|
387
558
|
throw new Error(
|
|
388
559
|
`${probe.apiKeySource} is set AND a stored ${harness} login exists — the key silently ` +
|
|
389
560
|
`overrides your subscription auth, so children would bill per-token API rates. ` +
|
|
@@ -440,7 +611,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
440
611
|
maxTurns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
441
612
|
cwd,
|
|
442
613
|
runId,
|
|
443
|
-
subscriptionOnly
|
|
614
|
+
subscriptionOnly,
|
|
444
615
|
});
|
|
445
616
|
agentProof.attempts = attempt;
|
|
446
617
|
agentProof.duration_ms += last.durationMs;
|
|
@@ -462,6 +633,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
462
633
|
key,
|
|
463
634
|
attempts: attempt,
|
|
464
635
|
cost_usd: last.costUsd,
|
|
636
|
+
total_cost_usd: agentCostUsd,
|
|
465
637
|
duration_ms: last.durationMs,
|
|
466
638
|
session_id: last.sessionId,
|
|
467
639
|
result_kind: "text",
|
|
@@ -486,6 +658,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
486
658
|
key,
|
|
487
659
|
attempts: attempt,
|
|
488
660
|
cost_usd: last.costUsd,
|
|
661
|
+
total_cost_usd: agentCostUsd,
|
|
489
662
|
duration_ms: last.durationMs,
|
|
490
663
|
session_id: last.sessionId,
|
|
491
664
|
result_kind: "json",
|
|
@@ -572,21 +745,27 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
572
745
|
};
|
|
573
746
|
|
|
574
747
|
const ctx: WorkflowContext = { agent, parallel, stage, log, evidence, authorize };
|
|
575
|
-
|
|
576
|
-
const t0 = Date.now();
|
|
577
|
-
const startedAt = new Date(t0).toISOString();
|
|
578
|
-
journal("run.start", {
|
|
579
|
-
name,
|
|
580
|
-
script: absScript,
|
|
581
|
-
objective: meta.objective ?? null,
|
|
582
|
-
acceptance: meta.acceptance,
|
|
583
|
-
max_agents: maxAgents,
|
|
584
|
-
concurrency,
|
|
585
|
-
policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
|
|
586
|
-
isolation,
|
|
587
|
-
network_access: networkAccess,
|
|
588
|
-
});
|
|
589
748
|
try {
|
|
749
|
+
if (resumeState) {
|
|
750
|
+
journal("run.resume", {
|
|
751
|
+
approval_id: resumeState.approvalId,
|
|
752
|
+
script: absScript,
|
|
753
|
+
resumed_at: new Date().toISOString(),
|
|
754
|
+
});
|
|
755
|
+
} else {
|
|
756
|
+
journal("run.start", {
|
|
757
|
+
name,
|
|
758
|
+
work_item_id: opts.workItemId ?? null,
|
|
759
|
+
script: absScript,
|
|
760
|
+
objective: meta.objective ?? null,
|
|
761
|
+
acceptance: meta.acceptance,
|
|
762
|
+
max_agents: maxAgents,
|
|
763
|
+
concurrency,
|
|
764
|
+
policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
|
|
765
|
+
isolation,
|
|
766
|
+
network_access: networkAccess,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
590
769
|
const result = await mod.default(ctx);
|
|
591
770
|
const endedAt = new Date().toISOString();
|
|
592
771
|
const durationMs = Date.now() - t0;
|
|
@@ -601,6 +780,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
601
780
|
try {
|
|
602
781
|
proof = buildWorkflowProof({
|
|
603
782
|
runId,
|
|
783
|
+
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
604
784
|
meta,
|
|
605
785
|
status: "succeeded",
|
|
606
786
|
startedAt,
|
|
@@ -613,7 +793,12 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
613
793
|
evidence: evidenceRecords,
|
|
614
794
|
harnessEvidence: opts.harnessEvidence,
|
|
615
795
|
policy: policy
|
|
616
|
-
? {
|
|
796
|
+
? {
|
|
797
|
+
config: policy,
|
|
798
|
+
decisions: Array.from(policyDecisions.values()),
|
|
799
|
+
isolation,
|
|
800
|
+
networkAccess,
|
|
801
|
+
}
|
|
617
802
|
: undefined,
|
|
618
803
|
result,
|
|
619
804
|
});
|
|
@@ -628,6 +813,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
628
813
|
}
|
|
629
814
|
const report: RunReport = {
|
|
630
815
|
runId,
|
|
816
|
+
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
631
817
|
name,
|
|
632
818
|
result,
|
|
633
819
|
agentsSpawned,
|
|
@@ -640,13 +826,13 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
640
826
|
contextTokensPerChildEstimate,
|
|
641
827
|
billing: Array.from(billingProbed.values()).map((p) => ({
|
|
642
828
|
harness: p.harness,
|
|
643
|
-
mode:
|
|
829
|
+
mode: subscriptionOnly ? "subscription" : p.mode,
|
|
644
830
|
})),
|
|
645
831
|
policy: proof.policy?.summary,
|
|
646
832
|
};
|
|
647
833
|
return report;
|
|
648
834
|
} catch (err) {
|
|
649
|
-
if (err instanceof WorkflowRunError) throw err;
|
|
835
|
+
if (err instanceof WorkflowParkedError || err instanceof WorkflowRunError) throw err;
|
|
650
836
|
const endedAt = new Date().toISOString();
|
|
651
837
|
const durationMs = Date.now() - t0;
|
|
652
838
|
journal("run.end", {
|
|
@@ -660,6 +846,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
660
846
|
try {
|
|
661
847
|
const proof = buildWorkflowProof({
|
|
662
848
|
runId,
|
|
849
|
+
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
663
850
|
meta,
|
|
664
851
|
status: "failed",
|
|
665
852
|
startedAt,
|
|
@@ -672,7 +859,12 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
672
859
|
evidence: evidenceRecords,
|
|
673
860
|
harnessEvidence: opts.harnessEvidence,
|
|
674
861
|
policy: policy
|
|
675
|
-
? {
|
|
862
|
+
? {
|
|
863
|
+
config: policy,
|
|
864
|
+
decisions: Array.from(policyDecisions.values()),
|
|
865
|
+
isolation,
|
|
866
|
+
networkAccess,
|
|
867
|
+
}
|
|
676
868
|
: undefined,
|
|
677
869
|
error: (err as Error).message,
|
|
678
870
|
});
|
|
@@ -758,6 +950,52 @@ function loadResumeCache(
|
|
|
758
950
|
return cache;
|
|
759
951
|
}
|
|
760
952
|
|
|
953
|
+
function loadRunHistory(path: string): {
|
|
954
|
+
agentsSpawned: number;
|
|
955
|
+
costUsd: number;
|
|
956
|
+
policyDecisions: PolicyDecision[];
|
|
957
|
+
} {
|
|
958
|
+
const agentIds = new Set<string>();
|
|
959
|
+
const agentCosts = new Map<string, number>();
|
|
960
|
+
const decisions = new Map<string, PolicyDecision>();
|
|
961
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
962
|
+
if (!line.trim()) continue;
|
|
963
|
+
const event = JSON.parse(line) as {
|
|
964
|
+
event?: string;
|
|
965
|
+
id?: string;
|
|
966
|
+
cost_usd?: number;
|
|
967
|
+
total_cost_usd?: number;
|
|
968
|
+
verdict?: unknown;
|
|
969
|
+
resolved_by?: unknown;
|
|
970
|
+
request?: unknown;
|
|
971
|
+
};
|
|
972
|
+
if (event.event === "agent.start" && typeof event.id === "string") {
|
|
973
|
+
agentIds.add(event.id);
|
|
974
|
+
}
|
|
975
|
+
if (event.event === "agent.end" && typeof event.id === "string") {
|
|
976
|
+
const cost = event.total_cost_usd ?? event.cost_usd;
|
|
977
|
+
if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
|
|
978
|
+
agentCosts.set(event.id, cost);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (
|
|
982
|
+
event.event === "policy.resolve" &&
|
|
983
|
+
typeof event.id === "string" &&
|
|
984
|
+
(event.verdict === "allow" || event.verdict === "deny") &&
|
|
985
|
+
typeof event.resolved_by === "string" &&
|
|
986
|
+
event.request &&
|
|
987
|
+
typeof event.request === "object"
|
|
988
|
+
) {
|
|
989
|
+
decisions.set(event.id, event as PolicyDecision);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
agentsSpawned: agentIds.size,
|
|
994
|
+
costUsd: round4(Array.from(agentCosts.values()).reduce((sum, value) => sum + value, 0)),
|
|
995
|
+
policyDecisions: Array.from(decisions.values()),
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
761
999
|
function round4(n: number): number {
|
|
762
1000
|
return Math.round(n * 10_000) / 10_000;
|
|
763
1001
|
}
|
|
@@ -770,6 +1008,12 @@ function assertAgentCapacity(agentsSpawned: number, maxAgents: number): void {
|
|
|
770
1008
|
}
|
|
771
1009
|
}
|
|
772
1010
|
|
|
1011
|
+
function assertPositiveWorkflowBound(value: number, field: string): void {
|
|
1012
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1013
|
+
throw new Error(`${field} must be a positive safe integer`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
773
1017
|
function boundedPolicyReason(value: string): string {
|
|
774
1018
|
const normalized = Array.from(value, (character) => {
|
|
775
1019
|
const code = character.charCodeAt(0);
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type {
|
|
2
|
+
CreateWorkflowApprovalInput,
|
|
3
|
+
ResolveWorkflowApprovalInput,
|
|
4
|
+
WorkflowApproval,
|
|
5
|
+
WorkflowApprovalDecision,
|
|
6
|
+
WorkflowApprovalRequest,
|
|
7
|
+
WorkflowApprovalStatus,
|
|
8
|
+
} from "./approvals.ts";
|
|
9
|
+
export {
|
|
10
|
+
assertWorkflowRunId,
|
|
11
|
+
createWorkflowApproval,
|
|
12
|
+
listWorkflowApprovals,
|
|
13
|
+
readWorkflowApproval,
|
|
14
|
+
renderWorkflowApproval,
|
|
15
|
+
resolveWorkflowApproval,
|
|
16
|
+
WORKFLOW_APPROVAL_SCHEMA_VERSION,
|
|
17
|
+
workflowApprovalId,
|
|
18
|
+
} from "./approvals.ts";
|
|
19
|
+
export { runWorkflow, WorkflowParkedError, WorkflowRunError } from "./engine.ts";
|
|
2
20
|
export {
|
|
3
21
|
buildWorkflowProof,
|
|
4
22
|
createEvidenceRecord,
|
|
@@ -9,6 +27,17 @@ export {
|
|
|
9
27
|
rollupAcceptance,
|
|
10
28
|
writeWorkflowProof,
|
|
11
29
|
} from "./proof.ts";
|
|
30
|
+
export type { WorkflowRunManifest } from "./run-state.ts";
|
|
31
|
+
export {
|
|
32
|
+
acquireWorkflowResumeLease,
|
|
33
|
+
assertWorkflowRunResumable,
|
|
34
|
+
assertWorkflowScriptUnchanged,
|
|
35
|
+
readWorkflowRunManifest,
|
|
36
|
+
WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION,
|
|
37
|
+
workflowRunManifestPath,
|
|
38
|
+
workflowScriptDigest,
|
|
39
|
+
writeWorkflowRunManifest,
|
|
40
|
+
} from "./run-state.ts";
|
|
12
41
|
export type {
|
|
13
42
|
AcceptanceCriterion,
|
|
14
43
|
AcceptanceResult,
|
|
@@ -29,6 +58,7 @@ export type {
|
|
|
29
58
|
SpawnResult,
|
|
30
59
|
StageSchema,
|
|
31
60
|
WorkflowAgentProof,
|
|
61
|
+
WorkflowApprovalMode,
|
|
32
62
|
WorkflowContext,
|
|
33
63
|
WorkflowEvidenceInput,
|
|
34
64
|
WorkflowEvidenceRecord,
|
|
@@ -58,6 +58,7 @@ export interface NormalizedWorkflowMeta {
|
|
|
58
58
|
|
|
59
59
|
export interface BuildWorkflowProofInput {
|
|
60
60
|
runId: string;
|
|
61
|
+
workItemId?: string;
|
|
61
62
|
meta: NormalizedWorkflowMeta;
|
|
62
63
|
status: "succeeded" | "failed";
|
|
63
64
|
startedAt: string;
|
|
@@ -213,6 +214,7 @@ export function buildWorkflowProof(input: BuildWorkflowProofInput): WorkflowProo
|
|
|
213
214
|
schema_version: WORKFLOW_PROOF_SCHEMA_VERSION,
|
|
214
215
|
run: {
|
|
215
216
|
id: input.runId,
|
|
217
|
+
work_item_id: input.workItemId,
|
|
216
218
|
name: input.meta.name,
|
|
217
219
|
status: input.status,
|
|
218
220
|
started_at: input.startedAt,
|