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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/core/work/index.d.ts +1 -1
  3. package/dist/core/work/index.d.ts.map +1 -1
  4. package/dist/core/work/read.d.ts +1 -1
  5. package/dist/core/work/read.d.ts.map +1 -1
  6. package/dist/core/work/runner.d.ts +1 -1
  7. package/dist/core/work/runner.d.ts.map +1 -1
  8. package/dist/core/work/runner.js +54 -1
  9. package/dist/core/work/state.d.ts +3 -0
  10. package/dist/core/work/state.d.ts.map +1 -1
  11. package/dist/core/work/state.js +19 -5
  12. package/dist/core/workflow/attempt-context.d.ts +5 -0
  13. package/dist/core/workflow/attempt-context.d.ts.map +1 -0
  14. package/dist/core/workflow/attempt-context.js +189 -0
  15. package/dist/core/workflow/engine.d.ts.map +1 -1
  16. package/dist/core/workflow/engine.js +20 -2
  17. package/dist/core/workflow/index.d.ts +3 -2
  18. package/dist/core/workflow/index.d.ts.map +1 -1
  19. package/dist/core/workflow/index.js +2 -1
  20. package/dist/core/workflow/proof.d.ts +2 -1
  21. package/dist/core/workflow/proof.d.ts.map +1 -1
  22. package/dist/core/workflow/proof.js +7 -1
  23. package/dist/core/workflow/run-state.d.ts +2 -1
  24. package/dist/core/workflow/run-state.d.ts.map +1 -1
  25. package/dist/core/workflow/run-state.js +5 -0
  26. package/dist/core/workflow/types.d.ts +27 -0
  27. package/dist/core/workflow/types.d.ts.map +1 -1
  28. package/dist/core/workflow/types.js +1 -0
  29. package/package.json +1 -1
  30. package/src/core/work/index.ts +1 -0
  31. package/src/core/work/read.ts +1 -0
  32. package/src/core/work/runner.ts +69 -3
  33. package/src/core/work/state.ts +36 -5
  34. package/src/core/workflow/attempt-context.ts +215 -0
  35. package/src/core/workflow/engine.ts +29 -1
  36. package/src/core/workflow/index.ts +10 -0
  37. package/src/core/workflow/proof.ts +9 -1
  38. package/src/core/workflow/run-state.ts +11 -1
  39. 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(scriptPath, absScript, opts, resumeState, workContext);
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 { WorkflowSpecialistProfile, WorkflowWorkContext } from "./types.ts";
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). */