@agentproto/workflow-runtime 0.12.0 → 0.13.1

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/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { DriverHandle, ResolverContext } from '@agentproto/driver';
2
2
  import { ToolContext, ToolHandle } from '@agentproto/tool';
3
- import { ZodType } from 'zod';
4
3
  import { WorkflowHandle } from '@agentproto/workflow';
5
4
  import { FsPort, FsStat, FsLockHandle } from '@agentproto/corpus';
6
5
 
@@ -27,6 +26,13 @@ interface Bindings {
27
26
  readonly item?: unknown;
28
27
  /** Present inside a `map` body — the element's index. */
29
28
  readonly index?: number;
29
+ /** AIP-58 §4 Run workspace — present when the host wires
30
+ * {@link RunWorkflowArgs.workspace}. `$run.workspace` / `{{run.workspace}}`
31
+ * is a convenience alias for the same absolute path AIP-16 injects as
32
+ * `$input._workflowFsRoot` — steps may use either. */
33
+ readonly run?: {
34
+ readonly workspace: string;
35
+ };
30
36
  }
31
37
  /** The AIP-16 IO seam: read a value out of the run bindings. */
32
38
  type Selector<T> = (bindings: Bindings) => T;
@@ -67,6 +73,14 @@ type FanOutOutcome<T = unknown> = {
67
73
  readonly item: unknown;
68
74
  /** `err.message` if the throw was an `Error`, else `String(err)`. */
69
75
  readonly error: string;
76
+ } | {
77
+ /** Never started: the fan-out's spawn circuit breaker opened first
78
+ * (see {@link MapStep.maxConsecutiveSpawnFailures}). */
79
+ readonly status: "skipped";
80
+ readonly index: number;
81
+ readonly item: unknown;
82
+ /** `circuit-open: <first error of the failure streak>`. */
83
+ readonly reason: string;
70
84
  };
71
85
  /**
72
86
  * The bound output of a `map`/`pipeline` step run with `onError: "collect"`:
@@ -77,7 +91,17 @@ interface TolerantFanOutResult<T = unknown> {
77
91
  readonly results: readonly FanOutOutcome<T>[];
78
92
  readonly succeeded: number;
79
93
  readonly failed: number;
94
+ /** Items never started because the spawn circuit breaker opened. */
95
+ readonly skipped: number;
96
+ /** Set when the breaker opened: the first error of the spawn-failure
97
+ * streak that tripped it. */
98
+ readonly circuitOpen?: {
99
+ readonly error: string;
100
+ };
80
101
  }
102
+ /** Default for {@link MapStep.maxConsecutiveSpawnFailures} /
103
+ * {@link PipelineStep.maxConsecutiveSpawnFailures}. */
104
+ declare const DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES = 3;
81
105
  /**
82
106
  * Run a sub-step once per element of an array, optionally with bounded
83
107
  * concurrency. The element + index are exposed to the body via `bindings.item`
@@ -99,6 +123,17 @@ interface MapStep {
99
123
  * silently dropped.
100
124
  */
101
125
  onError?: "throw" | "collect";
126
+ /**
127
+ * `"collect"` only: once this many items IN A ROW fail because an agent
128
+ * step's session could not be spawned ({@link AgentSpawnError}), stop
129
+ * starting new items — the failure is systemic, not per-item. In-flight
130
+ * items finish; every item not yet started is reported skipped
131
+ * (`circuit-open: <first error>`) and the map returns normally. A settled
132
+ * item that isn't a spawn failure resets the streak. Default
133
+ * {@link DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES}; `0` disables the breaker.
134
+ * (`"throw"` already stops at the first failure of any kind.)
135
+ */
136
+ maxConsecutiveSpawnFailures?: number;
102
137
  }
103
138
  /**
104
139
  * Run N items through K sequential stage bodies with NO cross-item barrier:
@@ -120,6 +155,8 @@ interface PipelineStep {
120
155
  * `"collect"` runs every item's chain to completion and binds a
121
156
  * {@link TolerantFanOutResult} instead of a bare array. */
122
157
  onError?: "throw" | "collect";
158
+ /** Same semantics as {@link MapStep.maxConsecutiveSpawnFailures}. */
159
+ maxConsecutiveSpawnFailures?: number;
123
160
  }
124
161
  /** Run one of two branches based on a predicate over the bindings. */
125
162
  interface BranchStep {
@@ -128,6 +165,10 @@ interface BranchStep {
128
165
  cond: Selector<boolean>;
129
166
  then: readonly RunStep[];
130
167
  otherwise?: readonly RunStep[];
168
+ /** The authored step this node was compiled from, when it differs from
169
+ * `id` — a multi-arm manifest `kind: branch` compiles to a chain of
170
+ * nodes (`<id>`, `<id>__branch1`, …); skip reports name `<id>`. */
171
+ sourceId?: string;
131
172
  }
132
173
  /** Repeat a body while a predicate holds, up to a hard iteration ceiling. */
133
174
  interface LoopStep {
@@ -268,6 +309,37 @@ type AgentSandboxRef = string | {
268
309
  provider: string;
269
310
  [k: string]: unknown;
270
311
  };
312
+ /**
313
+ * The minimal structural contract {@link AgentStep.outputSchema} must
314
+ * satisfy — exactly the `safeParse` shape `execAgentStep` consumes (never
315
+ * `.parse`, `._def`, or any other zod-specific member). A real zod
316
+ * `ZodType` satisfies this automatically (structural typing — zod's own
317
+ * `SafeParseReturnType` is a superset of this shape), so a TS-authored step
318
+ * can still pass a zod schema directly. `compileAgentStep` additionally
319
+ * builds one of these from a WORKFLOW.md-authored JSON Schema object (ajv
320
+ * `validateAgainstJsonSchema`-backed) for the declarative manifest path,
321
+ * where `outputSchema` is plain JSON Schema, not a zod instance.
322
+ */
323
+ interface OutputSchemaLike {
324
+ safeParse(value: unknown): {
325
+ success: true;
326
+ data: unknown;
327
+ } | {
328
+ success: false;
329
+ error: {
330
+ issues: readonly {
331
+ path: readonly PropertyKey[];
332
+ message: string;
333
+ }[];
334
+ };
335
+ };
336
+ /** Set by `compileAgentStep`'s declarative (WORKFLOW.md) path to the raw
337
+ * JSON Schema object it wrapped — lets F27's prompt-affordance render the
338
+ * EXACT schema text instead of re-deriving it from the `safeParse`
339
+ * closure. Absent on a TS-authored zod schema (rendered via
340
+ * `z.toJSONSchema` instead — see `describeOutputSchemaForPrompt`). */
341
+ jsonSchema?: unknown;
342
+ }
271
343
  /**
272
344
  * Spawn or reuse an agent session and send it a prompt, waiting for the
273
345
  * turn to complete. The host injects an {@link AgentSessionHost} — this
@@ -278,7 +350,12 @@ interface AgentStep {
278
350
  id: string;
279
351
  /** Adapter slug to spawn a NEW session. Omit to reuse via sessionRef. */
280
352
  adapter?: Selector<string> | string;
281
- /** Reuse an earlier AgentStep's spawned session, by that step's id. */
353
+ /** Reuse an earlier AgentStep's spawned session, by that step's id. Inside
354
+ * a `map`/`pipeline` body, a `{{index}}` placeholder resolves to the
355
+ * current item index — `"review[{{index}}]"` names the session THIS item's
356
+ * `review` step spawned (the host indexes every fan-out spawn under its
357
+ * `stepKey`, `<stepId>[<index>]`), where a bare `"review"` would resolve to
358
+ * whichever item spawned last. */
282
359
  sessionRef?: string;
283
360
  /** Model id override for this spawn — same semantics as `agent_start.model`
284
361
  * (a literal string or a per-run selector resolving to one; `undefined` ⇒
@@ -308,8 +385,11 @@ interface AgentStep {
308
385
  } | {
309
386
  awaiting: "fail";
310
387
  };
311
- /** Validate the session's final message against this schema; re-prompt on mismatch. */
312
- outputSchema?: ZodType<unknown>;
388
+ /** Validate the session's final message against this schema; re-prompt on
389
+ * mismatch. A zod `ZodType` (TS-authored steps) or anything else
390
+ * satisfying {@link OutputSchemaLike} (a WORKFLOW.md-authored JSON
391
+ * Schema object compiles into one of these — see `compileAgentStep`). */
392
+ outputSchema?: OutputSchemaLike;
313
393
  /** Re-prompt-and-retry attempts on schema mismatch before failing. Default 2. */
314
394
  maxRetries?: number;
315
395
  /** Cache this step's output under the run's cacheKey; the resolved prompt +
@@ -324,6 +404,63 @@ interface AgentStep {
324
404
  options?: Record<string, boolean | number | string>;
325
405
  /** Harness pinning for this step's spawn. See {@link AgentHarness}. */
326
406
  harness?: AgentHarness;
407
+ /** The resolved agent manifest's declared `tools` (AGENT.md `tools:`), set
408
+ * by {@link CompileWorkflowOptions.agentRefs} resolution. Forwarded to the
409
+ * host's `spawn` so it can mount its own tool gateway scoped to this list
410
+ * (names the gateway doesn't serve — harness-native tools — match
411
+ * nothing). Unset ⇒ the agent declared no tools list. Only meaningful
412
+ * with `adapter`; ignored on a `sessionRef` reuse. */
413
+ agentTools?: readonly string[];
414
+ }
415
+ /**
416
+ * AIP-58 §4 Run workspace — declare a file under the run workspace
417
+ * (`$run.workspace` / `_workflowFsRoot`) as a run artifact: hashed, sized,
418
+ * and copied into the host's `artifactsDir` (`<runsRoot>/<runId>/artifacts/`).
419
+ * Bound output (and the value passed to {@link RunWorkflowArgs.onArtifact})
420
+ * is the resulting {@link ArtifactEntry}.
421
+ *
422
+ * Cache-aware when the run has `cache`/`cacheKey` wired (independent of any
423
+ * `cacheable` flag — there is none on this step; declaring the same key/path
424
+ * again under the same cacheKey is always cheap to re-verify): a hit COPIES
425
+ * the previously-cached file forward from its original `artifactsDir` into
426
+ * THIS run's own — never shares a directory across runs (AIP-58 §4 "two runs
427
+ * MUST NEVER share a workspace") — so a step declaring an artifact from a
428
+ * cache-hit-replayed upstream step still works. See `run-workflow.ts`'s
429
+ * `case "artifact"` for the exact mechanics.
430
+ */
431
+ interface ArtifactStep {
432
+ kind: "artifact";
433
+ id: string;
434
+ /** Artifact key — identifies this artifact for `workflow_artifact_get`/
435
+ * `workflow_publish`. Its on-disk filename under `artifactsDir` is
436
+ * `path`'s own basename (sanitized), not the key — see {@link
437
+ * ArtifactEntry.path} — unless another key's file shares the same
438
+ * basename, in which case the key disambiguates it (F42). */
439
+ key: Selector<string> | string;
440
+ /** Path to the source file. Relative to `$run.workspace`; MUST resolve
441
+ * inside it (an absolute path or a `..`-escaping relative one throws). */
442
+ path: Selector<string> | string;
443
+ contentType?: Selector<string> | string;
444
+ }
445
+ /**
446
+ * AIP-58 §4/§1 `ArtifactEntry` — one run-scoped copy of a declared output
447
+ * file. `path` is `"artifacts/<basename>"` (F42: the declared source file's
448
+ * OWN basename, sanitized — e.g. `outputsFiles.pdf: {path: transcript.pdf}`
449
+ * ⇒ `artifacts/transcript.pdf` — keeping the extension, unlike the bare key),
450
+ * relative to the RUN WORKSPACE ROOT (`<runsRoot>/<runId>/`, the parent of
451
+ * `$run.workspace` itself) — never the original in-workspace location the
452
+ * file was read from. Two keys whose files share a basename get the SECOND
453
+ * one's name prefixed with its own sanitized key instead (deterministic,
454
+ * never a silent overwrite) — always read `path` back rather than assuming
455
+ * `artifacts/<key>` or `artifacts/<basename(path)>`.
456
+ */
457
+ interface ArtifactEntry {
458
+ key: string;
459
+ path: string;
460
+ sha256: string;
461
+ size: number;
462
+ stepId: string;
463
+ contentType?: string;
327
464
  }
328
465
  /**
329
466
  * `kind: "gate"` — run a shell command through the host's subprocess runner
@@ -407,6 +544,13 @@ interface AgentRefResolution {
407
544
  adapter: string;
408
545
  /** Adapter option id → value merged onto the compiled step's `options`. */
409
546
  options?: Record<string, boolean | number | string>;
547
+ /** AGENT.md's declared `model` — the compiled step's DEFAULT when the
548
+ * step itself sets none (a step-level `model:` still wins). Forwarded
549
+ * through the same `harness.model` channel a step-level `model` uses. */
550
+ model?: string;
551
+ /** AGENT.md's declared `tools` (string ids only) — becomes the compiled
552
+ * step's {@link AgentStep.agentTools}. */
553
+ tools?: readonly string[];
410
554
  }
411
555
  /**
412
556
  * The element generics on `ToolStep` are erased to `any` so heterogeneous
@@ -417,13 +561,41 @@ interface AgentRefResolution {
417
561
  * issue #21534); tightening the union to `ToolStep<unknown, …>` rejects a
418
562
  * concrete `ToolStep<MarketSearchInput, …>` on the zod `ZodType<T>` variance.
419
563
  */
420
- type RunStep = ToolStep<any, any, any> | TransformStep | MapStep | PipelineStep | BranchStep | LoopStep | ParallelStep | ApprovalStep | SuspendStep | GroupStep | SubworkflowStep | AgentStep | GateStep;
564
+ type RunStep = ToolStep<any, any, any> | TransformStep | MapStep | PipelineStep | BranchStep | LoopStep | ParallelStep | ApprovalStep | SuspendStep | GroupStep | SubworkflowStep | AgentStep | GateStep | ArtifactStep;
565
+ /** One `outputsFiles.<key>` declaration carried onto a compiled
566
+ * {@link RuntimeWorkflow} — the AIP-16 file contract, amended with
567
+ * `required` (AIP-58 §4). `path` MAY use the `<runId>`/`<workflowId>`/
568
+ * `<isoDate>` interpolation tokens AIP-16 names (`<toolId>` is not
569
+ * resolvable at the workflow level and is left literal). */
570
+ interface OutputsFileContract {
571
+ path: string;
572
+ /** `true`: missing when the run's steps finish ⇒ `failed { code:
573
+ * "missing-artifact" }` (AIP-58 §4/§10). Absent (the default) or `false`
574
+ * ⇒ advisory only — a `console.warn`, the run still succeeds (mirrors
575
+ * `StepRecord.hint`'s "advisory, never load-bearing" posture; matches the
576
+ * AIP-58 V3 vector's own note: "with required absent or false, the same
577
+ * scenario would be a warning"). */
578
+ required?: boolean;
579
+ contentType?: string;
580
+ }
421
581
  interface RuntimeWorkflow {
422
582
  id: string;
423
583
  description?: string;
424
584
  steps: readonly RunStep[];
585
+ /**
586
+ * Cleanup steps that ALWAYS run once `steps` ends — succeeded, failed, or
587
+ * cancelled (they run without the abort signal). They see the same
588
+ * bindings (a step that never ran is simply absent). A failing `finally`
589
+ * step fails an otherwise-successful run; after a failed or cancelled run
590
+ * the original outcome wins and the cleanup error is only reported.
591
+ */
592
+ finally?: readonly RunStep[];
425
593
  /** Pick the run's final output (default: the last top-level step's output). */
426
594
  output?: Selector<unknown>;
595
+ /** AIP-16 `outputsFiles` (as amended by AIP-58 §4) — checked ONCE, after
596
+ * every top-level step finishes successfully. See {@link OutputsFileContract}
597
+ * and `run-workflow.ts`'s `checkOutputsFiles`. */
598
+ outputsFiles?: Readonly<Record<string, OutputsFileContract>>;
427
599
  }
428
600
  /** A human/host decision on one approval request. `who` records WHO decided
429
601
  * ("human", "timeout", "cancelled", …); `note` is optional free text. */
@@ -445,6 +617,16 @@ interface ResumeRequest {
445
617
  stepId: string;
446
618
  on: readonly string[];
447
619
  }
620
+ /**
621
+ * AIP-58 §3(a) explicit signal, recorded by the host when the step's own
622
+ * session calls `run.requestInput` before its turn ends. Passed to
623
+ * {@link RunWorkflowArgs.onInputRequired} to durably suspend the step.
624
+ */
625
+ interface InputRequiredRequest {
626
+ stepId: string;
627
+ prompt: string;
628
+ schema?: Record<string, unknown>;
629
+ }
448
630
  interface AgentSessionHost {
449
631
  /** Spawn a new agent session and return its id. A `sandbox` ref asks the
450
632
  * host to run the session inside that sandbox (provider slug or inline
@@ -459,13 +641,40 @@ interface AgentSessionHost {
459
641
  options?: Record<string, boolean | number | string>;
460
642
  /** Harness pinning for this spawn (see {@link AgentStep.harness}). */
461
643
  harness?: AgentHarness;
644
+ /** The agent manifest's declared tools (see {@link AgentStep.agentTools}). */
645
+ agentTools?: readonly string[];
646
+ /** Run-unique key of the spawning step when it runs inside a
647
+ * `map`/`pipeline` item: `stepId[<index>]`, the same key the run's step
648
+ * hooks report. Absent outside a fan-out (the key is then `stepId`). */
649
+ stepKey?: string;
462
650
  }): Promise<string>;
651
+ /**
652
+ * The run is done with a session it spawned: end it (if still live) and
653
+ * archive it. Called once per spawned session — when its `map`/`pipeline`
654
+ * item settles, else when the run itself ends (ok or error) — never
655
+ * earlier, so a later step's `sessionRef` can still reuse it. The session's
656
+ * id stays on the step's output. Best-effort: a throw is swallowed.
657
+ */
658
+ releaseSession?(sessionId: string): Promise<void>;
463
659
  /** Send a prompt to an existing session and wait for its turn to end. */
464
660
  sendPromptAndWait(sessionId: string, prompt: string): Promise<void>;
465
661
  /** Look up a session by the step id that spawned it (for sessionRef reuse). */
466
662
  resolveByLabel(stepId: string): string | undefined;
467
663
  /** Handle an awaiting-input policy for a session. */
468
664
  onAwaitingInput?(sessionId: string, policy: AgentStep["policy"]): Promise<void>;
665
+ /**
666
+ * AIP-58 §3(a) explicit signal: consume (and clear) a pending
667
+ * `run.requestInput` recorded for this session — checked by
668
+ * {@link AgentStep} execution right after a turn ends, before the
669
+ * outputSchema retry loop (and again inside it, after every reprompt).
670
+ * `undefined` when no request is pending. Optional: a host that omits
671
+ * this never suspends a step on this signal — the outcome rule's other
672
+ * branches (missing-output / vacuous success) still apply.
673
+ */
674
+ takeInputRequest?(sessionId: string): {
675
+ prompt: string;
676
+ schema?: Record<string, unknown>;
677
+ } | undefined;
469
678
  /** Return the session's final assistant message text (for outputSchema validation). */
470
679
  readFinalMessage?(sessionId: string): Promise<string>;
471
680
  /** Current cumulative cost (USD) of a session, for run-level budgeting. */
@@ -485,6 +694,26 @@ interface AgentSessionHost {
485
694
  interface StepCacheEntry {
486
695
  output: unknown;
487
696
  resolvedInputHash: string;
697
+ /** Set only by a `kind: "artifact"` step (see {@link ArtifactStep}): the
698
+ * absolute `artifactsDir` this entry's file was copied into when first
699
+ * cached. A hit in a LATER run (a different `artifactsDir`, since AIP-58
700
+ * §4 forbids two runs sharing a workspace) copies the file forward from
701
+ * here into the new run's own `artifactsDir` instead of re-declaring it. */
702
+ artifactsDirAtCache?: string;
703
+ /** Set only for a cacheable `tool`/`agent` step (see `hashResolvedInputs`/
704
+ * `buildCacheEntry` in `run-workflow.ts`) whenever the host wires a run
705
+ * workspace: the absolute `$run.workspace` / `_workflowFsRoot` path this
706
+ * entry was cached under. A hit in a LATER run (a different workspace —
707
+ * AIP-58 §4 forbids two runs sharing one) rewrites `output`'s path
708
+ * strings from here onto the new run's own, and relocates
709
+ * {@link workspaceFiles} the same way. */
710
+ workspaceAtCache?: string;
711
+ /** Workspace-relative files/directories (under {@link workspaceAtCache})
712
+ * this entry's `output` pointed at and that existed on disk when the
713
+ * entry was written — copied forward into a later cache-hit run's own
714
+ * workspace so a downstream step reading one of these paths finds the
715
+ * bytes there too, not just in the original (by-then-gone) run's. */
716
+ workspaceFiles?: readonly string[];
488
717
  }
489
718
  /** Opt-in journal for cacheable steps. Host-injected; file-backed in the runtime. */
490
719
  interface StepCache {
@@ -493,6 +722,31 @@ interface StepCache {
493
722
  /** Write/overwrite a step's journal entry. */
494
723
  set(stepCacheKey: string, entry: StepCacheEntry): Promise<void>;
495
724
  }
725
+ /** Extra context passed to `onStepStart`/`onStepComplete`. */
726
+ interface StepHookInfo {
727
+ /** The step's output was replayed from the {@link StepCache} journal —
728
+ * it was not executed (no spawn, no tool dispatch) this run. */
729
+ cached?: boolean;
730
+ }
731
+ /** Why `onStepSkipped` fired. */
732
+ interface StepSkippedInfo {
733
+ /** `"branch-not-taken"`: the step sits in an untaken `branch` arm.
734
+ * `"circuit-open"`: a `map`/`pipeline` item never started because the
735
+ * fan-out's spawn circuit breaker opened
736
+ * ({@link MapStep.maxConsecutiveSpawnFailures}). */
737
+ reason: "branch-not-taken" | "circuit-open";
738
+ /** Id of the authored step whose decision skipped the step — the `branch`
739
+ * step, or (circuit-open) the `map`/`pipeline` step. */
740
+ branchId: string;
741
+ /** `"circuit-open"` only: the first error of the spawn-failure streak
742
+ * that tripped the breaker. */
743
+ message?: string;
744
+ }
745
+ /** What `onStepFailed` reports. */
746
+ interface StepFailedInfo {
747
+ /** `err.message` if the throw was an `Error`, else `String(err)`. */
748
+ error: string;
749
+ }
496
750
  interface RunWorkflowArgs {
497
751
  workflow: RuntimeWorkflow;
498
752
  input?: unknown;
@@ -503,12 +757,41 @@ interface RunWorkflowArgs {
503
757
  approve?: (req: ApprovalRequest) => boolean | ApprovalDecision | Promise<boolean | ApprovalDecision>;
504
758
  /** Supply a {@link SuspendStep}'s resume payload. Default: throw + suspend. */
505
759
  resume?: (req: ResumeRequest) => unknown | Promise<unknown>;
760
+ /**
761
+ * AIP-58 §3(a)/§5 outcome rule: suspend an {@link AgentStep} that
762
+ * signalled `run.requestInput` (see
763
+ * {@link AgentSessionHost.takeInputRequest}), resolving with the resume
764
+ * payload once an external event supplies one. The runtime sends that
765
+ * payload (JSON) as the step's next prompt to the SAME session and
766
+ * re-applies the outcome rule — the step may suspend again, fail
767
+ * `missing-output`, or succeed. Default (undefined) ⇒
768
+ * {@link AgentInputRequiredError} throws instead, the same
769
+ * no-hook-supplied shape {@link WorkflowSuspendedError} uses for
770
+ * {@link SuspendStep}.
771
+ */
772
+ onInputRequired?: (req: InputRequiredRequest) => unknown | Promise<unknown>;
506
773
  /** Host-injected agent session runtime. Undefined ⇒ {@link AgentStep} throws. */
507
774
  agents?: AgentSessionHost;
508
775
  /** Working directory for spawned agent sessions. */
509
776
  cwd?: string;
510
777
  /** Workspace slug for spawned agent sessions. */
511
778
  workspaceSlug?: string;
779
+ /** AIP-58 §4 Run workspace — absolute path to this run's own scratch
780
+ * directory (AIP-16's `_workflowFsRoot`), exposed to steps as
781
+ * `$run.workspace` / `{{run.workspace}}`. Undefined ⇒ the `run` binding
782
+ * and `kind: "artifact"` steps are unavailable. */
783
+ workspace?: string;
784
+ /** AIP-58 §4 — absolute path to this run's `artifacts/` directory. Required
785
+ * alongside `workspace` for a `kind: "artifact"` step (or a declared
786
+ * `RuntimeWorkflow.outputsFiles`) to run. */
787
+ artifactsDir?: string;
788
+ /** This run's id — used only to interpolate the `<runId>` token in a
789
+ * declared `outputsFiles.<key>.path` (AIP-16). Purely informational
790
+ * otherwise. */
791
+ runId?: string;
792
+ /** Called once per {@link ArtifactEntry} recorded — by a `kind: "artifact"`
793
+ * step, or by the end-of-run `outputsFiles` check — cache hit or fresh. */
794
+ onArtifact?: (entry: ArtifactEntry) => void;
512
795
  /** Run-level cost ceiling (USD). Once the summed cost of spawned sessions
513
796
  * reaches this, the next AgentStep spawn fails with `budget_exceeded`. */
514
797
  maxTotalCostUsd?: number;
@@ -517,10 +800,24 @@ interface RunWorkflowArgs {
517
800
  /** Namespacing label for this run's cache lookups (the workflow_start cacheKey).
518
801
  * Both `cache` and `cacheKey` must be set for any caching to happen. */
519
802
  cacheKey?: string;
520
- /** Called when a step begins execution (before spawn/prompt). */
521
- onStepStart?: (stepId: string) => void;
522
- /** Called when a step completes execution, with its output. */
523
- onStepComplete?: (stepId: string, output: unknown) => void;
803
+ /** Called when a step begins execution (before spawn/prompt). A cacheable
804
+ * step replayed from the journal still fires this, with `info.cached`. */
805
+ onStepStart?: (stepId: string, info?: StepHookInfo) => void;
806
+ /** Called when a step completes execution, with its output — `info.cached`
807
+ * when the output was replayed from the journal instead of executed. */
808
+ onStepComplete?: (stepId: string, output: unknown, info?: StepHookInfo) => void;
809
+ /** Called for every step in a `branch` arm that was NOT taken, once the
810
+ * branch decides — the step will not run this time. Only statically-known
811
+ * steps are reported (a `map`/`pipeline`/`subworkflow` step under the arm
812
+ * reports its own id, not its body's); a step id that also sits in the
813
+ * taken path is never reported. */
814
+ onStepSkipped?: (stepId: string, info: StepSkippedInfo) => void;
815
+ /** Called when a step inside a tolerant (`onError: "collect"`) `map`/
816
+ * `pipeline` item throws — the item is recorded as rejected and the run
817
+ * goes on, so this is the only signal the failing step gets. `stepId` is
818
+ * the innermost step that threw, indexed like `onStepStart`'s. A throw
819
+ * that fails the run is NOT reported here (the run's own failure is). */
820
+ onStepFailed?: (stepId: string, info: StepFailedInfo) => void;
524
821
  /** Host-injectable subprocess runner for `kind: "gate"` steps. Undefined ⇒
525
822
  * the runtime's own `node:child_process`-backed default. */
526
823
  runGateCommand?: GateCommandRunner;
@@ -548,6 +845,59 @@ declare class WorkflowSuspendedError extends Error {
548
845
  readonly on: readonly string[];
549
846
  constructor(stepId: string, on: readonly string[]);
550
847
  }
848
+ /**
849
+ * AIP-58 §3(a) — thrown when an {@link AgentStep}'s session signals
850
+ * `run.requestInput` but no host `onInputRequired` hook is provided (the
851
+ * same "no resume hook supplied" shape {@link WorkflowSuspendedError} uses
852
+ * for {@link SuspendStep}). A host that wires `onInputRequired` never sees
853
+ * this thrown — it durably suspends the step instead.
854
+ */
855
+ declare class AgentInputRequiredError extends Error {
856
+ readonly stepId: string;
857
+ readonly prompt: string;
858
+ readonly schema?: Record<string, unknown> | undefined;
859
+ constructor(stepId: string, prompt: string, schema?: Record<string, unknown> | undefined);
860
+ }
861
+ /**
862
+ * AIP-58 §3 Outcome rule — thrown when an {@link AgentStep} declares an
863
+ * `outputSchema` and its turn ends (after exhausting retries) without ever
864
+ * producing output that validates against it, and no explicit
865
+ * input-required signal (§3(a)/(b)) was observed either. `hint` is set when
866
+ * the final message matches the "trailing question mark" heuristic — it is
867
+ * ONLY a triage aid; it never changes the outcome (still `missing-output`).
868
+ */
869
+ declare class StepOutcomeError extends Error {
870
+ readonly stepId: string;
871
+ readonly code: "missing-output";
872
+ readonly hint?: "possible-input-request" | undefined;
873
+ constructor(stepId: string, code: "missing-output", message: string, hint?: "possible-input-request" | undefined);
874
+ }
875
+ /**
876
+ * AIP-58 §4/§10 — thrown when a declared `outputsFiles.<key>` (`required`
877
+ * absent or `true`) does not exist under the run workspace once every
878
+ * top-level step has finished. `stepId` is the last top-level step that ran
879
+ * (the manifest names no step for a workflow-level contract, so the last one
880
+ * to finish is the best available attribution).
881
+ */
882
+ declare class MissingArtifactError extends Error {
883
+ readonly key: string;
884
+ readonly stepId: string | undefined;
885
+ readonly code: "missing-artifact";
886
+ constructor(key: string, stepId: string | undefined);
887
+ }
888
+ /**
889
+ * Thrown when an {@link AgentStep}'s session could not be spawned at all
890
+ * (`AgentSessionHost.spawn` rejected) — distinct from a session that spawned
891
+ * and then failed its turn. A tolerant fan-out counts these toward its spawn
892
+ * circuit breaker ({@link MapStep.maxConsecutiveSpawnFailures}): a spawn that
893
+ * fails for one item usually fails for every item (missing cwd, adapter
894
+ * gone, process limits), so burning through the rest is pure noise.
895
+ */
896
+ declare class AgentSpawnError extends Error {
897
+ readonly stepId: string;
898
+ readonly cause: unknown;
899
+ constructor(stepId: string, cause: unknown);
900
+ }
551
901
  declare function runWorkflow(args: RunWorkflowArgs): Promise<WorkflowRunResult>;
552
902
 
553
903
  /**
@@ -574,9 +924,12 @@ declare function runWorkflow(args: RunWorkflowArgs): Promise<WorkflowRunResult>;
574
924
  * Scope: the **linear / structured subset** of AIP-15 — steps run in document
575
925
  * order; `map`/`loop`/`parallel` nest their child step lists. Non-linear `next`
576
926
  * gotos are rejected with a clear diagnostic. `kind:"branch"` compiles in its
577
- * **forward-only** form: every `branches[].next`/`default` must name a later
578
- * sibling in the SAME step list (not backward, not into a nested map/loop/
579
- * parallel body) — see `compileBranchChain` below. Compiling a full goto graph
927
+ * **forward-only** form: every `branches[].next`/`default`/`join` must name a
928
+ * later sibling in the SAME step list (not backward, not into a nested map/
929
+ * loop/parallel body). Arms are EXCLUSIVE by default — exactly one arm body
930
+ * runs, then execution continues at the join (see `compileExclusiveBranch`);
931
+ * `fallthrough: true` opts into the legacy "target + everything after it"
932
+ * semantics (see `compileBranchChain`). Compiling a full goto graph
580
933
  * (backward jumps, cross-scope targets) is a separable follow-up; hand-author
581
934
  * a `loop` step for retry-style control flow instead.
582
935
  */
@@ -660,6 +1013,90 @@ declare function compileWorkflow(handle: WorkflowHandle, opts: CompileWorkflowOp
660
1013
  */
661
1014
  declare function compileWorkflowManifest(source: string, opts: CompileWorkflowOptions): RuntimeWorkflow;
662
1015
 
1016
+ /**
1017
+ * AIP-58 §3 Outcome rule — "invalid or missing required input is checked
1018
+ * before any step runs". AIP-16 declares a workflow's `inputs` block as
1019
+ * JSON Schema, but most existing WORKFLOW.md authors write a shorthand flat
1020
+ * map instead (`inputs: { url: { type, description, default } }` — see
1021
+ * `youtube-transcriber`'s `transcribe/WORKFLOW.md`). This module normalizes
1022
+ * that shorthand into real JSON Schema, then validates a run's input
1023
+ * against it with the same ajv machinery `@agentproto/tool` already uses for
1024
+ * TOOL contracts (`define-tool.ts`'s `validateJsonSchema`).
1025
+ */
1026
+ /**
1027
+ * Normalize a WORKFLOW.md `inputs` field to a JSON Schema object.
1028
+ *
1029
+ * Already-canonical JSON Schema (`{ type: "object", properties, required?
1030
+ * }`) passes through unchanged — its own `required[]`, if any, is honored
1031
+ * as-is (this is the shape AIP-58's V1 vector authors directly).
1032
+ *
1033
+ * The shorthand flat map (`{ <name>: { type, description?, default?,
1034
+ * required? } }`) is lifted: each key becomes a `properties` entry (minus
1035
+ * its shorthand-only `required` marker); a property gets added to the
1036
+ * schema's `required[]` ONLY when its shorthand spec sets `required: true`
1037
+ * AND declares no `default` — a `default` always makes a field optional to
1038
+ * omit, regardless of `required`. A shorthand property with no `required`
1039
+ * marker at all stays optional, so normalizing an existing manifest that
1040
+ * never used the marker (e.g. `youtube-transcriber`'s `transcribe/
1041
+ * WORKFLOW.md`) never newly fails validation for it.
1042
+ */
1043
+ declare function normalizeWorkflowInputsSchema(inputs: unknown): Record<string, unknown>;
1044
+ type WorkflowInputValidation = {
1045
+ valid: true;
1046
+ schema: Record<string, unknown>;
1047
+ } | {
1048
+ valid: false;
1049
+ schema: Record<string, unknown>;
1050
+ /** AIP-58 §10 error code — this validator only ever produces one. */
1051
+ code: "invalid-input";
1052
+ /** Missing/invalid field names (deduped), for a caller that wants to
1053
+ * point at exactly what's wrong without re-parsing `message`. */
1054
+ fields: readonly string[];
1055
+ /** Human-readable message naming the missing/invalid fields. */
1056
+ message: string;
1057
+ };
1058
+ /**
1059
+ * AIP-58 §9 `run.requestInput`/`run.resume`: `true` when `schema` is a
1060
+ * usable JSON Schema (ajv can compile it) — a plain object is necessary but
1061
+ * not sufficient (e.g. `{ type: "not-a-type" }` compiles-fails). Used to
1062
+ * reject a malformed `schema` argument before it's ever recorded as a
1063
+ * step's `StepRecord.suspend.schema`.
1064
+ */
1065
+ declare function isCompilableJsonSchema(schema: unknown): schema is Record<string, unknown>;
1066
+ /** One structural (zod-`ZodIssue`-shaped) validation failure — the common
1067
+ * currency between ajv's `ErrorObject[]` and zod's `ZodError.issues`, see
1068
+ * {@link OutputSchemaLikeIssue}. */
1069
+ interface SchemaValidationIssue {
1070
+ path: readonly (string | number)[];
1071
+ message: string;
1072
+ }
1073
+ /**
1074
+ * Generic JSON Schema validation, used both by AIP-58 §3/§9 (validate a
1075
+ * `run.resume` payload against the suspended step's `StepRecord.suspend
1076
+ * .schema` BEFORE the resume transition happens — an invalid payload MUST
1077
+ * leave the run suspended, never transition it) and by `compileAgentStep`
1078
+ * (adapt a WORKFLOW.md-authored JSON Schema `outputSchema` into the
1079
+ * {@link OutputSchemaLike} shape `execAgentStep` consumes). `issues` mirrors
1080
+ * zod's `ZodError.issues` shape so both call sites format errors the same
1081
+ * way regardless of which schema language declared the contract.
1082
+ */
1083
+ declare function validateAgainstJsonSchema(schema: Record<string, unknown>, value: unknown): {
1084
+ valid: true;
1085
+ } | {
1086
+ valid: false;
1087
+ message: string;
1088
+ issues: readonly SchemaValidationIssue[];
1089
+ };
1090
+ /**
1091
+ * Validate a run's `input` against a WORKFLOW.md's declared `inputs`
1092
+ * (shorthand or canonical JSON Schema — see
1093
+ * {@link normalizeWorkflowInputsSchema}). The caller is responsible for
1094
+ * calling this BEFORE dispatching any step and BEFORE spawning any session
1095
+ * on an invalid result — this function only judges the input, it doesn't
1096
+ * gate execution itself.
1097
+ */
1098
+ declare function validateWorkflowInput(inputsField: unknown, input: unknown): WorkflowInputValidation;
1099
+
663
1100
  /**
664
1101
  * Shared `AgentStep` construction — the one place that applies AgentStep's
665
1102
  * defaults (`policy` → `{ awaiting: "fail" }`, a literal `prompt` wrapped as
@@ -674,6 +1111,8 @@ interface AgentStepFields {
674
1111
  * steps resolve `$steps.*` refs into one before calling this). */
675
1112
  prompt: string | Selector<string>;
676
1113
  adapter?: string;
1114
+ /** See {@link AgentStep.cwd}. */
1115
+ cwd?: Selector<string>;
677
1116
  model?: Selector<string> | string;
678
1117
  sessionRef?: string;
679
1118
  sandbox?: AgentSandboxRef;
@@ -683,6 +1122,7 @@ interface AgentStepFields {
683
1122
  maxRetries?: number;
684
1123
  options?: Record<string, boolean | number | string>;
685
1124
  harness?: AgentHarness;
1125
+ agentTools?: readonly string[];
686
1126
  }
687
1127
  /** Build a runtime {@link AgentStep} from field values, applying the same
688
1128
  * defaults everywhere: `policy` defaults to `{ awaiting: "fail" }`. */
@@ -740,4 +1180,4 @@ declare class NodeFsPort implements FsPort {
740
1180
  lock(): Promise<FsLockHandle>;
741
1181
  }
742
1182
 
743
- export { type AgentHarness, type AgentRefResolution, type AgentSandboxRef, type AgentSessionHost, type AgentStep, type AgentStepFields, type ApprovalDecision, type ApprovalRequest, type ApprovalStep, type Bindings, type BranchStep, type CompileWorkflowOptions, type FanOutOutcome, type GateCommandResult, type GateCommandRunner, type GateReportEvent, type GateStep, type GroupStep, type HarnessKnowledgeSelector, type KnowledgeAppliedRecord, type LoopStep, type MapStep, type MaterializedKnowledge, NodeFsPort, type ParallelStep, type PipelineStep, type ResumeRequest, type RunStep, type RunWorkflowArgs, type RuntimeWorkflow, type Selector, type StepCache, type StepCacheEntry, type SubworkflowStep, type SuspendStep, type TolerantFanOutResult, type ToolStep, type TransformStep, WorkflowCompileError, type WorkflowRunResult, WorkflowSuspendedError, buildAgentStep, compileWorkflow, compileWorkflowManifest, evalPredicate, interpolateTemplate, materializeKnowledge, resolveKnowledgeSelectors, resolveRef, resolveRefPrefixed, resolveValue, runWorkflow };
1183
+ export { type AgentHarness, AgentInputRequiredError, type AgentRefResolution, type AgentSandboxRef, type AgentSessionHost, AgentSpawnError, type AgentStep, type AgentStepFields, type ApprovalDecision, type ApprovalRequest, type ApprovalStep, type ArtifactEntry, type ArtifactStep, type Bindings, type BranchStep, type CompileWorkflowOptions, DEFAULT_MAX_CONSECUTIVE_SPAWN_FAILURES, type FanOutOutcome, type GateCommandResult, type GateCommandRunner, type GateReportEvent, type GateStep, type GroupStep, type HarnessKnowledgeSelector, type InputRequiredRequest, type KnowledgeAppliedRecord, type LoopStep, type MapStep, type MaterializedKnowledge, MissingArtifactError, NodeFsPort, type OutputSchemaLike, type OutputsFileContract, type ParallelStep, type PipelineStep, type ResumeRequest, type RunStep, type RunWorkflowArgs, type RuntimeWorkflow, type SchemaValidationIssue, type Selector, type StepCache, type StepCacheEntry, type StepFailedInfo, type StepHookInfo, StepOutcomeError, type StepSkippedInfo, type SubworkflowStep, type SuspendStep, type TolerantFanOutResult, type ToolStep, type TransformStep, WorkflowCompileError, type WorkflowInputValidation, type WorkflowRunResult, WorkflowSuspendedError, buildAgentStep, compileWorkflow, compileWorkflowManifest, evalPredicate, interpolateTemplate, isCompilableJsonSchema, materializeKnowledge, normalizeWorkflowInputsSchema, resolveKnowledgeSelectors, resolveRef, resolveRefPrefixed, resolveValue, runWorkflow, validateAgainstJsonSchema, validateWorkflowInput };