harnery 0.21.0 → 0.22.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 +1 -1
- package/dist/core/work/index.d.ts +1 -1
- package/dist/core/work/index.d.ts.map +1 -1
- package/dist/core/work/read.d.ts +1 -1
- package/dist/core/work/read.d.ts.map +1 -1
- package/dist/core/work/runner.d.ts +1 -1
- package/dist/core/work/runner.d.ts.map +1 -1
- package/dist/core/work/runner.js +54 -1
- package/dist/core/work/state.d.ts +3 -0
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +19 -5
- package/dist/core/workflow/attempt-context.d.ts +5 -0
- package/dist/core/workflow/attempt-context.d.ts.map +1 -0
- package/dist/core/workflow/attempt-context.js +189 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +20 -2
- package/dist/core/workflow/index.d.ts +3 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/proof.d.ts +2 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +7 -1
- package/dist/core/workflow/run-state.d.ts +2 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +5 -0
- package/dist/core/workflow/types.d.ts +27 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +1 -0
- package/package.json +1 -1
- package/src/core/work/index.ts +1 -0
- package/src/core/work/read.ts +1 -0
- package/src/core/work/runner.ts +69 -3
- package/src/core/work/state.ts +36 -5
- package/src/core/workflow/attempt-context.ts +215 -0
- package/src/core/workflow/engine.ts +29 -1
- package/src/core/workflow/index.ts +10 -0
- package/src/core/workflow/proof.ts +9 -1
- package/src/core/workflow/run-state.ts +11 -1
- package/src/core/workflow/types.ts +35 -0
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
summarizePolicyRequest,
|
|
37
37
|
} from "../policy/index.ts";
|
|
38
38
|
import { assertWorkflowRunId, createWorkflowApproval } from "./approvals.ts";
|
|
39
|
+
import { freezeWorkflowAttemptContext } from "./attempt-context.ts";
|
|
39
40
|
import { type BillingProbe, probeBilling } from "./billing.ts";
|
|
40
41
|
import {
|
|
41
42
|
buildWorkflowProof,
|
|
@@ -60,6 +61,7 @@ import type {
|
|
|
60
61
|
SpawnResult,
|
|
61
62
|
StageSchema,
|
|
62
63
|
WorkflowAgentProof,
|
|
64
|
+
WorkflowAttemptContext,
|
|
63
65
|
WorkflowContext,
|
|
64
66
|
WorkflowEvidenceRecord,
|
|
65
67
|
WorkflowModule,
|
|
@@ -115,8 +117,15 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
115
117
|
if (opts.resumeRunId && opts.workContext !== undefined) {
|
|
116
118
|
throw new Error("parked workflow resume uses the frozen manifest work context");
|
|
117
119
|
}
|
|
120
|
+
if (opts.resumeRunId && opts.attemptContext !== undefined) {
|
|
121
|
+
throw new Error("parked workflow resume uses the frozen manifest attempt context");
|
|
122
|
+
}
|
|
118
123
|
const requestedWorkContext =
|
|
119
124
|
opts.workContext === undefined ? undefined : freezeWorkflowWorkContext(opts.workContext);
|
|
125
|
+
const requestedAttemptContext =
|
|
126
|
+
opts.attemptContext === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: freezeWorkflowAttemptContext(opts.attemptContext);
|
|
120
129
|
if (requestedWorkContext && !opts.workItemId) {
|
|
121
130
|
throw new Error("workflow work context requires a work item id");
|
|
122
131
|
}
|
|
@@ -125,6 +134,9 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
125
134
|
`workflow work context ${requestedWorkContext.id} does not match work item ${opts.workItemId}`,
|
|
126
135
|
);
|
|
127
136
|
}
|
|
137
|
+
if (requestedAttemptContext && !requestedWorkContext) {
|
|
138
|
+
throw new Error("workflow attempt context requires work context");
|
|
139
|
+
}
|
|
128
140
|
if (opts.resumeRunId && opts.resumeFrom) {
|
|
129
141
|
throw new Error("resumeRunId and resumeFrom are mutually exclusive");
|
|
130
142
|
}
|
|
@@ -141,6 +153,9 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
141
153
|
const workContext = resumeState?.manifest.work_context
|
|
142
154
|
? freezeWorkflowWorkContext(resumeState.manifest.work_context)
|
|
143
155
|
: requestedWorkContext;
|
|
156
|
+
const attemptContext = resumeState?.manifest.attempt_context
|
|
157
|
+
? freezeWorkflowAttemptContext(resumeState.manifest.attempt_context)
|
|
158
|
+
: requestedAttemptContext;
|
|
144
159
|
const absScript = isAbsolute(scriptPath) ? scriptPath : resolve(process.cwd(), scriptPath);
|
|
145
160
|
if (resumeState) {
|
|
146
161
|
if (resolve(resumeState.manifest.script.path) !== resolve(absScript)) {
|
|
@@ -157,7 +172,14 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
157
172
|
// the run between the optimistic read and lease acquisition.
|
|
158
173
|
assertWorkflowRunResumable(opts.coordRoot, resumeState.manifest.run_id);
|
|
159
174
|
}
|
|
160
|
-
return await executeWorkflow(
|
|
175
|
+
return await executeWorkflow(
|
|
176
|
+
scriptPath,
|
|
177
|
+
absScript,
|
|
178
|
+
opts,
|
|
179
|
+
resumeState,
|
|
180
|
+
workContext,
|
|
181
|
+
attemptContext,
|
|
182
|
+
);
|
|
161
183
|
} finally {
|
|
162
184
|
releaseResumeLease?.();
|
|
163
185
|
}
|
|
@@ -169,6 +191,7 @@ async function executeWorkflow(
|
|
|
169
191
|
opts: EngineOpts,
|
|
170
192
|
resumeState: ReturnType<typeof assertWorkflowRunResumable> | undefined,
|
|
171
193
|
workContext: Readonly<WorkflowWorkContext> | undefined,
|
|
194
|
+
attemptContext: Readonly<WorkflowAttemptContext> | undefined,
|
|
172
195
|
): Promise<RunReport> {
|
|
173
196
|
const mod = (await import(pathToFileURL(absScript).href)) as WorkflowModule;
|
|
174
197
|
if (typeof mod.default !== "function") {
|
|
@@ -218,6 +241,7 @@ async function executeWorkflow(
|
|
|
218
241
|
run_id: runId,
|
|
219
242
|
work_item_id: opts.workItemId,
|
|
220
243
|
work_context: workContext,
|
|
244
|
+
attempt_context: attemptContext,
|
|
221
245
|
name,
|
|
222
246
|
started_at: startedAt,
|
|
223
247
|
script: { path: absScript, sha256: workflowScriptDigest(absScript) },
|
|
@@ -784,6 +808,7 @@ async function executeWorkflow(
|
|
|
784
808
|
|
|
785
809
|
const ctx: WorkflowContext = {
|
|
786
810
|
work: workContext,
|
|
811
|
+
attempt: attemptContext,
|
|
787
812
|
agent,
|
|
788
813
|
parallel,
|
|
789
814
|
stage,
|
|
@@ -803,6 +828,7 @@ async function executeWorkflow(
|
|
|
803
828
|
name,
|
|
804
829
|
work_item_id: opts.workItemId ?? null,
|
|
805
830
|
work_context: workContext ?? null,
|
|
831
|
+
attempt_context: attemptContext ?? null,
|
|
806
832
|
script: absScript,
|
|
807
833
|
objective: meta.objective ?? null,
|
|
808
834
|
acceptance: meta.acceptance,
|
|
@@ -830,6 +856,7 @@ async function executeWorkflow(
|
|
|
830
856
|
runId,
|
|
831
857
|
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
832
858
|
workContext,
|
|
859
|
+
attemptContext,
|
|
833
860
|
meta,
|
|
834
861
|
status: "succeeded",
|
|
835
862
|
startedAt,
|
|
@@ -897,6 +924,7 @@ async function executeWorkflow(
|
|
|
897
924
|
runId,
|
|
898
925
|
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
899
926
|
workContext,
|
|
927
|
+
attemptContext,
|
|
900
928
|
meta,
|
|
901
929
|
status: "failed",
|
|
902
930
|
startedAt,
|
|
@@ -16,6 +16,11 @@ export {
|
|
|
16
16
|
WORKFLOW_APPROVAL_SCHEMA_VERSION,
|
|
17
17
|
workflowApprovalId,
|
|
18
18
|
} from "./approvals.ts";
|
|
19
|
+
export {
|
|
20
|
+
freezeWorkflowAttemptContext,
|
|
21
|
+
isCanonicalWorkflowAttemptContext,
|
|
22
|
+
normalizeWorkflowAttemptContext,
|
|
23
|
+
} from "./attempt-context.ts";
|
|
19
24
|
export { runWorkflow, WorkflowParkedError, WorkflowRunError } from "./engine.ts";
|
|
20
25
|
export {
|
|
21
26
|
buildWorkflowProof,
|
|
@@ -59,6 +64,10 @@ export type {
|
|
|
59
64
|
StageSchema,
|
|
60
65
|
WorkflowAgentProof,
|
|
61
66
|
WorkflowApprovalMode,
|
|
67
|
+
WorkflowAttemptContext,
|
|
68
|
+
WorkflowAttemptFailureCause,
|
|
69
|
+
WorkflowAttemptPriorContext,
|
|
70
|
+
WorkflowAttemptUnresolvedCriterion,
|
|
62
71
|
WorkflowContext,
|
|
63
72
|
WorkflowEvidenceInput,
|
|
64
73
|
WorkflowEvidenceRecord,
|
|
@@ -74,6 +83,7 @@ export type {
|
|
|
74
83
|
WorkflowWorkContext,
|
|
75
84
|
} from "./types.ts";
|
|
76
85
|
export {
|
|
86
|
+
WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION,
|
|
77
87
|
WORKFLOW_PROOF_SCHEMA_VERSION,
|
|
78
88
|
WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION,
|
|
79
89
|
} from "./types.ts";
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
PolicyNetworkAccess,
|
|
19
19
|
} from "../policy/index.ts";
|
|
20
20
|
import { policyDigest } from "../policy/index.ts";
|
|
21
|
+
import { isCanonicalWorkflowAttemptContext } from "./attempt-context.ts";
|
|
21
22
|
import type {
|
|
22
23
|
AcceptanceCriterion,
|
|
23
24
|
AcceptanceResult,
|
|
@@ -26,6 +27,7 @@ import type {
|
|
|
26
27
|
HarnessEvidenceCoverage,
|
|
27
28
|
ResultDigest,
|
|
28
29
|
WorkflowAgentProof,
|
|
30
|
+
WorkflowAttemptContext,
|
|
29
31
|
WorkflowEvidenceInput,
|
|
30
32
|
WorkflowEvidenceRecord,
|
|
31
33
|
WorkflowMeta,
|
|
@@ -62,6 +64,7 @@ export interface BuildWorkflowProofInput {
|
|
|
62
64
|
runId: string;
|
|
63
65
|
workItemId?: string;
|
|
64
66
|
workContext?: Readonly<WorkflowWorkContext>;
|
|
67
|
+
attemptContext?: Readonly<WorkflowAttemptContext>;
|
|
65
68
|
meta: NormalizedWorkflowMeta;
|
|
66
69
|
status: "succeeded" | "failed";
|
|
67
70
|
startedAt: string;
|
|
@@ -225,6 +228,7 @@ export function buildWorkflowProof(input: BuildWorkflowProofInput): WorkflowProo
|
|
|
225
228
|
ended_at: input.endedAt,
|
|
226
229
|
duration_ms: input.durationMs,
|
|
227
230
|
work_context: input.workContext,
|
|
231
|
+
attempt_context: input.attemptContext,
|
|
228
232
|
objective: input.meta.objective,
|
|
229
233
|
error: clippedOptional(input.error, MAX_SUMMARY_CHARS),
|
|
230
234
|
result: input.result === undefined ? undefined : digestResult(input.result),
|
|
@@ -313,7 +317,11 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
|
|
|
313
317
|
(proof.run.work_context !== undefined &&
|
|
314
318
|
(!proof.run.work_item_id ||
|
|
315
319
|
proof.run.work_context.id !== proof.run.work_item_id ||
|
|
316
|
-
!isCanonicalWorkflowWorkContext(proof.run.work_context)))
|
|
320
|
+
!isCanonicalWorkflowWorkContext(proof.run.work_context))) ||
|
|
321
|
+
(proof.run.attempt_context !== undefined &&
|
|
322
|
+
(!proof.run.work_item_id ||
|
|
323
|
+
!proof.run.work_context ||
|
|
324
|
+
!isCanonicalWorkflowAttemptContext(proof.run.attempt_context)))
|
|
317
325
|
) {
|
|
318
326
|
throw new Error(`workflow proof at ${path} has an unsupported or mismatched schema`);
|
|
319
327
|
}
|
|
@@ -22,8 +22,13 @@ import {
|
|
|
22
22
|
policyDigest,
|
|
23
23
|
} from "../policy/index.ts";
|
|
24
24
|
import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.ts";
|
|
25
|
+
import { isCanonicalWorkflowAttemptContext } from "./attempt-context.ts";
|
|
25
26
|
import { normalizeWorkflowSpecialists } from "./specialists.ts";
|
|
26
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
WorkflowAttemptContext,
|
|
29
|
+
WorkflowSpecialistProfile,
|
|
30
|
+
WorkflowWorkContext,
|
|
31
|
+
} from "./types.ts";
|
|
27
32
|
import { isCanonicalWorkflowWorkContext } from "./work-context.ts";
|
|
28
33
|
|
|
29
34
|
export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 1 as const;
|
|
@@ -36,6 +41,7 @@ export interface WorkflowRunManifest {
|
|
|
36
41
|
run_id: string;
|
|
37
42
|
work_item_id?: string;
|
|
38
43
|
work_context?: WorkflowWorkContext;
|
|
44
|
+
attempt_context?: WorkflowAttemptContext;
|
|
39
45
|
name: string;
|
|
40
46
|
started_at: string;
|
|
41
47
|
script: { path: string; sha256: string };
|
|
@@ -109,6 +115,10 @@ export function readWorkflowRunManifest(coordRoot: string, runId: string): Workf
|
|
|
109
115
|
(!manifest.work_item_id ||
|
|
110
116
|
manifest.work_context.id !== manifest.work_item_id ||
|
|
111
117
|
!isCanonicalWorkflowWorkContext(manifest.work_context))) ||
|
|
118
|
+
(manifest.attempt_context !== undefined &&
|
|
119
|
+
(!manifest.work_item_id ||
|
|
120
|
+
!manifest.work_context ||
|
|
121
|
+
!isCanonicalWorkflowAttemptContext(manifest.attempt_context))) ||
|
|
112
122
|
typeof manifest.name !== "string" ||
|
|
113
123
|
manifest.name.length === 0 ||
|
|
114
124
|
manifest.name.length > 200 ||
|
|
@@ -21,6 +21,7 @@ import type { BillingMode, BillingProber } from "./billing.ts";
|
|
|
21
21
|
|
|
22
22
|
export const WORKFLOW_PROOF_SCHEMA_VERSION = 1 as const;
|
|
23
23
|
export const WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION = 1 as const;
|
|
24
|
+
export const WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION = 1 as const;
|
|
24
25
|
|
|
25
26
|
export type EvidenceKind = "test" | "command" | "artifact" | "change" | "review" | "observation";
|
|
26
27
|
export type EvidenceStatus = "passed" | "failed" | "observed" | "unknown";
|
|
@@ -157,6 +158,7 @@ export interface WorkflowProof {
|
|
|
157
158
|
ended_at: string;
|
|
158
159
|
duration_ms: number;
|
|
159
160
|
work_context?: WorkflowWorkContext;
|
|
161
|
+
attempt_context?: WorkflowAttemptContext;
|
|
160
162
|
objective?: string;
|
|
161
163
|
error?: string;
|
|
162
164
|
result?: ResultDigest;
|
|
@@ -304,6 +306,9 @@ export interface WorkflowContext {
|
|
|
304
306
|
/** Frozen durable-work assignment for work-linked runs. Standalone and
|
|
305
307
|
* legacy-resumed workflows have no work context. */
|
|
306
308
|
work?: Readonly<WorkflowWorkContext>;
|
|
309
|
+
/** Frozen attempt identity and, on retry, a bounded synopsis of prior
|
|
310
|
+
* terminal evidence. Absent for standalone and legacy-resumed workflows. */
|
|
311
|
+
attempt?: Readonly<WorkflowAttemptContext>;
|
|
307
312
|
/** Spawn one subagent; resolves to validated JSON (schema) or reply text. */
|
|
308
313
|
agent: (prompt: string, opts?: AgentOpts) => Promise<unknown>;
|
|
309
314
|
/** Run thunks with bounded concurrency; a rejected thunk resolves to null. */
|
|
@@ -340,6 +345,33 @@ export interface WorkflowWorkContext {
|
|
|
340
345
|
readonly acceptance: readonly string[];
|
|
341
346
|
}
|
|
342
347
|
|
|
348
|
+
export type WorkflowAttemptFailureCause =
|
|
349
|
+
| "workflow_error"
|
|
350
|
+
| "acceptance_unsatisfied"
|
|
351
|
+
| "acceptance_unknown"
|
|
352
|
+
| "lost";
|
|
353
|
+
|
|
354
|
+
export interface WorkflowAttemptUnresolvedCriterion {
|
|
355
|
+
readonly id: string;
|
|
356
|
+
readonly statement: string;
|
|
357
|
+
readonly status: "unsatisfied" | "unknown";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface WorkflowAttemptPriorContext {
|
|
361
|
+
readonly run_id: string;
|
|
362
|
+
readonly causes: readonly WorkflowAttemptFailureCause[];
|
|
363
|
+
readonly error?: string;
|
|
364
|
+
readonly acceptance?: Readonly<AcceptanceSummary>;
|
|
365
|
+
readonly unresolved: readonly WorkflowAttemptUnresolvedCriterion[];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface WorkflowAttemptContext {
|
|
369
|
+
readonly schema_version: typeof WORKFLOW_ATTEMPT_CONTEXT_SCHEMA_VERSION;
|
|
370
|
+
readonly number: number;
|
|
371
|
+
readonly trigger: "initial" | "retry";
|
|
372
|
+
readonly prior?: Readonly<WorkflowAttemptPriorContext>;
|
|
373
|
+
}
|
|
374
|
+
|
|
343
375
|
export interface EngineOpts {
|
|
344
376
|
/** Repo root whose .harnery/ receives the run journal. */
|
|
345
377
|
coordRoot: string;
|
|
@@ -365,6 +397,9 @@ export interface EngineOpts {
|
|
|
365
397
|
/** Frozen assignment data supplied by a durable-work host. Requires the
|
|
366
398
|
* matching `workItemId`; parked resume always uses the manifest copy. */
|
|
367
399
|
workContext?: WorkflowWorkContext;
|
|
400
|
+
/** Frozen attempt data supplied by a durable-work host. Requires matching
|
|
401
|
+
* work item and work contexts; parked resume uses the manifest copy. */
|
|
402
|
+
attemptContext?: WorkflowAttemptContext;
|
|
368
403
|
/** Total-agent ceiling for the run (default 50): the runaway backstop. */
|
|
369
404
|
maxAgents?: number;
|
|
370
405
|
/** Concurrent-subagent cap for parallel() (default 4). */
|