@cleocode/playbooks 2026.6.12 → 2026.6.14
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 +1 -1
- package/dist/index.js +9 -2
- package/dist/parser.js +114 -1
- package/dist/runtime.d.ts +92 -34
- package/dist/runtime.js +257 -149
- package/package.json +3 -3
- package/src/__tests__/fixtures/conditional-branch.cantbook +50 -0
- package/src/__tests__/fixtures/pi-skill-only.cantbook +26 -0
- package/src/__tests__/guarded-deterministic-dispatch.test.ts +225 -0
- package/src/__tests__/inputs-bindings.test.ts +250 -0
- package/src/__tests__/node-allowlist.test.ts +186 -0
- package/src/__tests__/node-profile.test.ts +142 -0
- package/src/__tests__/parser.test.ts +191 -0
- package/src/__tests__/pi-cantbook-e2e.test.ts +294 -0
- package/src/__tests__/rcasd-one-envelope.test.ts +239 -0
- package/src/__tests__/runtime.test.ts +502 -4
- package/src/index.ts +8 -3
- package/src/parser.ts +144 -1
- package/src/runtime.ts +363 -198
package/dist/index.d.ts
CHANGED
|
@@ -27,5 +27,5 @@ export { approveGate, type CreateApprovalGateInput, createApprovalGate, E_APPROV
|
|
|
27
27
|
export { type MigratePlaybookFileResult, migratePlaybook, migratePlaybookFile, type NodeComplianceEntry, type PlaybookComplianceReport, validatePlaybookCompliance, } from './migrate-e4.js';
|
|
28
28
|
export { type ParsePlaybookResult, PlaybookParseError, parsePlaybook, } from './parser.js';
|
|
29
29
|
export { DEFAULT_POLICY_RULES, type EvaluatePolicyResult, evaluatePolicy, type PolicyRule, } from './policy.js';
|
|
30
|
-
export { type AgentDispatcher, type AgentDispatchInput, type AgentDispatchResult, type DeterministicRunInput, type DeterministicRunner, type DeterministicRunResult, E_PLAYBOOK_RESUME_BLOCKED, E_PLAYBOOK_RUNTIME_INVALID, type ExecutePlaybookOptions, type ExecutePlaybookResult, executePlaybook, type PlaybookTerminalStatus, type ResumePlaybookOptions,
|
|
30
|
+
export { type AgentDispatcher, type AgentDispatchInput, type AgentDispatchResult, type DeterministicRunInput, type DeterministicRunner, type DeterministicRunResult, E_PLAYBOOK_RESUME_BLOCKED, E_PLAYBOOK_RUNTIME_INVALID, type ExecutePlaybookOptions, type ExecutePlaybookResult, executePlaybook, type PlaybookTerminalStatus, type ResumePlaybookOptions, resolveStepBindings, resumePlaybook, } from './runtime.js';
|
|
31
31
|
export { type CreatePlaybookApprovalInput, type CreatePlaybookRunInput, createPlaybookApproval, createPlaybookRun, deletePlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, type ListPlaybookRunsOptions, listPlaybookApprovals, listPlaybookRuns, updatePlaybookApproval, updatePlaybookRun, } from './state.js';
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,14 @@ export { PlaybookParseError, parsePlaybook, } from './parser.js';
|
|
|
31
31
|
// W4-9: HITL auto-policy evaluator
|
|
32
32
|
export { DEFAULT_POLICY_RULES, evaluatePolicy, } from './policy.js';
|
|
33
33
|
// W4-10 / T930: playbook runtime state machine + HITL resume
|
|
34
|
-
//
|
|
35
|
-
|
|
34
|
+
// T11762 ST-2: the bespoke `validateDecompositionTaskTree` /
|
|
35
|
+
// `validateIvtrEvidenceOutput` validators (and their barrel re-exports) were
|
|
36
|
+
// deleted — their rules are now the `task_tree` / `evidence` Zod schemas in the
|
|
37
|
+
// ensures-schema registry (`@cleocode/contracts` data + `@cleocode/core`
|
|
38
|
+
// accessors). No cross-package consumer imported them, so dropping the
|
|
39
|
+
// re-exports is safe.
|
|
40
|
+
export { E_PLAYBOOK_RESUME_BLOCKED, E_PLAYBOOK_RUNTIME_INVALID, executePlaybook,
|
|
41
|
+
// T1944 (M4): per-step `node.inputs` → dispatch-boundary binding resolver.
|
|
42
|
+
resolveStepBindings, resumePlaybook, } from './runtime.js';
|
|
36
43
|
// W4-8: state layer CRUD for playbook_runs + playbook_approvals
|
|
37
44
|
export { createPlaybookApproval, createPlaybookRun, deletePlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, listPlaybookApprovals, listPlaybookRuns, updatePlaybookApproval, updatePlaybookRun, } from './state.js';
|
package/dist/parser.js
CHANGED
|
@@ -230,6 +230,17 @@ function parseAgenticNode(raw, base, index) {
|
|
|
230
230
|
inputs = acc;
|
|
231
231
|
}
|
|
232
232
|
const context_files = parseStringArray(raw.context_files, `nodes[${index}].context_files`);
|
|
233
|
+
// T11759 (M4): per-stage LLM pin. Each is an optional non-empty string — the
|
|
234
|
+
// dispatcher resolves them through the E9 chokepoint at dispatch time. Absent
|
|
235
|
+
// fields leave resolution unchanged (additive).
|
|
236
|
+
const profile = parseOptionalNonEmptyString(raw.profile, `nodes[${index}].profile`);
|
|
237
|
+
const model = parseOptionalNonEmptyString(raw.model, `nodes[${index}].model`);
|
|
238
|
+
const provider = parseOptionalNonEmptyString(raw.provider, `nodes[${index}].provider`);
|
|
239
|
+
// T1947 (M4): per-step skill/tool allowlists. Each is an optional string[] —
|
|
240
|
+
// enforced at spawn time by composeSpawnPayload as an INTERSECTION against the
|
|
241
|
+
// Tier-0/1/2 baseline (restrict-only, never expand). Absent → no restriction.
|
|
242
|
+
const allowed_skills = parseStringArray(raw.allowed_skills, `nodes[${index}].allowed_skills`);
|
|
243
|
+
const allowed_tools = parseStringArray(raw.allowed_tools, `nodes[${index}].allowed_tools`);
|
|
233
244
|
return {
|
|
234
245
|
...base,
|
|
235
246
|
type: 'agentic',
|
|
@@ -238,6 +249,11 @@ function parseAgenticNode(raw, base, index) {
|
|
|
238
249
|
role,
|
|
239
250
|
inputs,
|
|
240
251
|
...(context_files !== undefined ? { context_files } : {}),
|
|
252
|
+
...(profile !== undefined ? { profile } : {}),
|
|
253
|
+
...(model !== undefined ? { model } : {}),
|
|
254
|
+
...(provider !== undefined ? { provider } : {}),
|
|
255
|
+
...(allowed_skills !== undefined ? { allowed_skills } : {}),
|
|
256
|
+
...(allowed_tools !== undefined ? { allowed_tools } : {}),
|
|
241
257
|
};
|
|
242
258
|
}
|
|
243
259
|
function parseDeterministicNode(raw, base, index) {
|
|
@@ -326,7 +342,87 @@ function parseEdge(raw, index, ids) {
|
|
|
326
342
|
const ensures = parseStringArray(raw.contract.ensures, `edges[${index}].contract.ensures`);
|
|
327
343
|
contract = { requires, ensures };
|
|
328
344
|
}
|
|
329
|
-
|
|
345
|
+
const when = parseEdgeCondition(raw.when, `edges[${index}].when`);
|
|
346
|
+
const edge = { from: raw.from, to: raw.to, contract };
|
|
347
|
+
if (when !== undefined)
|
|
348
|
+
edge.when = when;
|
|
349
|
+
return edge;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Recognised comparison operators on a {@link PlaybookEdgeCondition}. Exactly
|
|
353
|
+
* one MUST be present so a guarded edge is unambiguous (T11806).
|
|
354
|
+
*/
|
|
355
|
+
const EDGE_CONDITION_OPERATORS = ['equals', 'notEquals', 'exists', 'in', 'truthy'];
|
|
356
|
+
/**
|
|
357
|
+
* Parse the optional `when` branch guard on an edge into a validated
|
|
358
|
+
* {@link PlaybookEdgeCondition}. Returns `undefined` for an absent guard
|
|
359
|
+
* (the edge is unconditional).
|
|
360
|
+
*
|
|
361
|
+
* Validation:
|
|
362
|
+
* - `field` MUST be a non-empty string.
|
|
363
|
+
* - Exactly ONE comparison operator (`equals` | `notEquals` | `exists` | `in`
|
|
364
|
+
* | `truthy`) MUST be supplied — zero is ambiguous, more than one is
|
|
365
|
+
* contradictory, both rejected at parse time so the runtime never has to
|
|
366
|
+
* disambiguate.
|
|
367
|
+
* - `exists` / `truthy` MUST be boolean; `in` MUST be a non-empty array.
|
|
368
|
+
*
|
|
369
|
+
* @param raw - Raw YAML value for `edge.when`.
|
|
370
|
+
* @param field - Field path for diagnostics (e.g. `"edges[2].when"`).
|
|
371
|
+
* @returns The validated condition, or `undefined` when absent.
|
|
372
|
+
* @throws {PlaybookParseError} On any structural violation.
|
|
373
|
+
*
|
|
374
|
+
* @task T11806 — cantbook runtime branching
|
|
375
|
+
*/
|
|
376
|
+
function parseEdgeCondition(raw, field) {
|
|
377
|
+
if (raw === undefined)
|
|
378
|
+
return undefined;
|
|
379
|
+
if (!isRecord(raw)) {
|
|
380
|
+
throw new PlaybookParseError(`${field} must be an object`, field, raw);
|
|
381
|
+
}
|
|
382
|
+
if (typeof raw.field !== 'string' || raw.field.length === 0) {
|
|
383
|
+
throw new PlaybookParseError(`${field}.field must be a non-empty string`, `${field}.field`, raw.field);
|
|
384
|
+
}
|
|
385
|
+
const present = EDGE_CONDITION_OPERATORS.filter((op) => Object.hasOwn(raw, op));
|
|
386
|
+
if (present.length === 0) {
|
|
387
|
+
throw new PlaybookParseError(`${field} must declare exactly one of ${EDGE_CONDITION_OPERATORS.join(' | ')}`, field, raw);
|
|
388
|
+
}
|
|
389
|
+
if (present.length > 1) {
|
|
390
|
+
throw new PlaybookParseError(`${field} declares multiple operators (${present.join(', ')}); exactly one is allowed`, field, raw);
|
|
391
|
+
}
|
|
392
|
+
const condition = { field: raw.field };
|
|
393
|
+
const [op] = present;
|
|
394
|
+
switch (op) {
|
|
395
|
+
case 'equals':
|
|
396
|
+
condition.equals = raw.equals;
|
|
397
|
+
break;
|
|
398
|
+
case 'notEquals':
|
|
399
|
+
condition.notEquals = raw.notEquals;
|
|
400
|
+
break;
|
|
401
|
+
case 'in':
|
|
402
|
+
if (!Array.isArray(raw.in) || raw.in.length === 0) {
|
|
403
|
+
throw new PlaybookParseError(`${field}.in must be a non-empty array`, `${field}.in`, raw.in);
|
|
404
|
+
}
|
|
405
|
+
condition.in = [...raw.in];
|
|
406
|
+
break;
|
|
407
|
+
case 'exists':
|
|
408
|
+
if (typeof raw.exists !== 'boolean') {
|
|
409
|
+
throw new PlaybookParseError(`${field}.exists must be a boolean`, `${field}.exists`, raw.exists);
|
|
410
|
+
}
|
|
411
|
+
condition.exists = raw.exists;
|
|
412
|
+
break;
|
|
413
|
+
case 'truthy':
|
|
414
|
+
if (typeof raw.truthy !== 'boolean') {
|
|
415
|
+
throw new PlaybookParseError(`${field}.truthy must be a boolean`, `${field}.truthy`, raw.truthy);
|
|
416
|
+
}
|
|
417
|
+
condition.truthy = raw.truthy;
|
|
418
|
+
break;
|
|
419
|
+
default: {
|
|
420
|
+
// Exhaustiveness guard — EDGE_CONDITION_OPERATORS is the SSoT.
|
|
421
|
+
const exhaustive = op;
|
|
422
|
+
throw new PlaybookParseError(`${field} unknown operator ${String(exhaustive)}`, field, raw);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return condition;
|
|
330
426
|
}
|
|
331
427
|
function parseInputs(raw) {
|
|
332
428
|
if (raw === undefined)
|
|
@@ -449,6 +545,23 @@ function parseStringArray(raw, field) {
|
|
|
449
545
|
return v;
|
|
450
546
|
});
|
|
451
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Parse an optional scalar string field that, when present, MUST be a non-empty
|
|
550
|
+
* string (T11759 — the per-stage LLM pins `profile` / `model` / `provider`).
|
|
551
|
+
*
|
|
552
|
+
* @param raw - Raw YAML value.
|
|
553
|
+
* @param field - Field path for diagnostics.
|
|
554
|
+
* @returns The trimmed-as-authored string, or `undefined` when absent.
|
|
555
|
+
* @throws {PlaybookParseError} When present but not a non-empty string.
|
|
556
|
+
*/
|
|
557
|
+
function parseOptionalNonEmptyString(raw, field) {
|
|
558
|
+
if (raw === undefined)
|
|
559
|
+
return undefined;
|
|
560
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
561
|
+
throw new PlaybookParseError(`${field} must be a non-empty string`, field, raw);
|
|
562
|
+
}
|
|
563
|
+
return raw;
|
|
564
|
+
}
|
|
452
565
|
/**
|
|
453
566
|
* Detect cycles across the combined edge-set (explicit `edges[]` plus
|
|
454
567
|
* `depends[]` back-references). Uses 3-color DFS.
|
package/dist/runtime.d.ts
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* @task T1261 PSYCHE E4 — contract enforcement + context boundary
|
|
36
36
|
*/
|
|
37
37
|
import type { DatabaseSync } from 'node:sqlite';
|
|
38
|
-
import type { PlaybookDefinition } from '@cleocode/contracts';
|
|
38
|
+
import type { PlaybookAgenticNode, PlaybookDefinition } from '@cleocode/contracts';
|
|
39
39
|
/**
|
|
40
40
|
* Input payload handed to {@link AgentDispatcher.dispatch} on every
|
|
41
41
|
* `agentic` node execution. All fields are read-only from the dispatcher's
|
|
@@ -52,8 +52,69 @@ export interface AgentDispatchInput {
|
|
|
52
52
|
taskId: string;
|
|
53
53
|
/** Snapshot of the accumulated bindings at dispatch time. */
|
|
54
54
|
context: Record<string, unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* Per-step resolved bindings for this node (T1944 · M4 cantbook done-gate).
|
|
57
|
+
*
|
|
58
|
+
* Additive field. Built by {@link resolveStepBindings} from
|
|
59
|
+
* {@link PlaybookAgenticNode.inputs} — each declared input is a mustache
|
|
60
|
+
* `{{path}}` template resolved against the accumulated run `context` (via the
|
|
61
|
+
* canonical T1238 {@link DefaultVariableResolver}), then layered ON TOP of
|
|
62
|
+
* that context so a step input SHADOWS a same-named context key.
|
|
63
|
+
*
|
|
64
|
+
* The runtime always populates this when dispatching an agentic node (it is a
|
|
65
|
+
* shallow copy of `context` when the node declares no `inputs`). It is typed
|
|
66
|
+
* optional only to keep the change strictly additive for any out-of-tree
|
|
67
|
+
* dispatcher that constructs an {@link AgentDispatchInput} directly.
|
|
68
|
+
*
|
|
69
|
+
* Resolver precedence (highest → lowest):
|
|
70
|
+
* `step bindings ⊳ playbook bindings (run context) ⊳ session ⊳ project-context ⊳ env ⊳ default`.
|
|
71
|
+
*/
|
|
72
|
+
bindings?: Record<string, unknown>;
|
|
55
73
|
/** 1-based iteration counter for this specific node (retry-aware). */
|
|
56
74
|
iteration: number;
|
|
75
|
+
/**
|
|
76
|
+
* Name of the running playbook (`.cantbook`) — the first segment of the
|
|
77
|
+
* cantbook node identity `cantbook:<playbook>#<nodeId>` (T11759 · M4).
|
|
78
|
+
*
|
|
79
|
+
* Additive. The runtime always populates it for agentic nodes; the dispatcher
|
|
80
|
+
* uses it (with {@link nodeId}) to resolve a declared per-stage LLM profile
|
|
81
|
+
* through the E9 chokepoint. Optional only so an out-of-tree dispatcher that
|
|
82
|
+
* constructs an {@link AgentDispatchInput} directly remains source-compatible.
|
|
83
|
+
*/
|
|
84
|
+
playbookName?: string;
|
|
85
|
+
/**
|
|
86
|
+
* The node's declared per-stage LLM pin (T11759 · M4) — lifted verbatim from
|
|
87
|
+
* {@link PlaybookAgenticNode.profile} / `.model` / `.provider`.
|
|
88
|
+
*
|
|
89
|
+
* Additive + all-optional. When any field is set the dispatcher resolves the
|
|
90
|
+
* stage's LLM through `resolveLLMForSystem` keyed by the cantbook node
|
|
91
|
+
* identity, honoring the profile pin; when all are unset resolution is
|
|
92
|
+
* unchanged. The runtime NEVER acts on these itself — it only forwards them.
|
|
93
|
+
*/
|
|
94
|
+
profile?: string;
|
|
95
|
+
/** Inline model-id pin forwarded from {@link PlaybookAgenticNode.model} (T11759). */
|
|
96
|
+
model?: string;
|
|
97
|
+
/** Inline provider pin forwarded from {@link PlaybookAgenticNode.provider} (T11759). */
|
|
98
|
+
provider?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Per-step skill allowlist lifted verbatim from
|
|
101
|
+
* {@link PlaybookAgenticNode.allowed_skills} (T1947 · M4).
|
|
102
|
+
*
|
|
103
|
+
* Additive + optional. When present the dispatcher passes it to
|
|
104
|
+
* `composeSpawnPayload`, which intersects it with the resolved agent's skill
|
|
105
|
+
* baseline (Tier-0/1/2) — the allowlist can only RESTRICT, never expand. When
|
|
106
|
+
* absent the spawn carries its full baseline skill set. The runtime NEVER acts
|
|
107
|
+
* on this itself — it only forwards it.
|
|
108
|
+
*/
|
|
109
|
+
allowedSkills?: string[];
|
|
110
|
+
/**
|
|
111
|
+
* Per-step tool allowlist lifted verbatim from
|
|
112
|
+
* {@link PlaybookAgenticNode.allowed_tools} (T1947 · M4).
|
|
113
|
+
*
|
|
114
|
+
* Same restrict-only intersection semantics as {@link allowedSkills}, applied
|
|
115
|
+
* to the spawn's tool baseline.
|
|
116
|
+
*/
|
|
117
|
+
allowedTools?: string[];
|
|
57
118
|
}
|
|
58
119
|
/**
|
|
59
120
|
* Terminal output returned by {@link AgentDispatcher.dispatch}. The runtime
|
|
@@ -192,47 +253,44 @@ export declare const E_PLAYBOOK_RUNTIME_INVALID: "E_PLAYBOOK_RUNTIME_INVALID";
|
|
|
192
253
|
*/
|
|
193
254
|
export declare const E_PLAYBOOK_RESUME_BLOCKED: "E_PLAYBOOK_RESUME_BLOCKED";
|
|
194
255
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* Returns a human-readable violation string when the shape is invalid, or
|
|
198
|
-
* `null` when the tree is well-formed. The check is performed AFTER the node
|
|
199
|
-
* output is merged into the playbook context (post-merge ensures check).
|
|
256
|
+
* Resolve a {@link PlaybookAgenticNode}'s declared `inputs` into the per-step
|
|
257
|
+
* binding map handed to the dispatcher (T1944 · M4 cantbook done-gate).
|
|
200
258
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* sibling (intra-tree orphan check).
|
|
259
|
+
* Each `inputs` value is a mustache `{{path}}` template. Templates are resolved
|
|
260
|
+
* against the accumulated run `context` using the canonical T1238
|
|
261
|
+
* {@link DefaultVariableResolver} (no second substitution engine — DRY) and the
|
|
262
|
+
* resolved key/value pairs are then layered ON TOP of the run context. The
|
|
263
|
+
* result is the dispatch-time binding map.
|
|
207
264
|
*
|
|
208
|
-
*
|
|
209
|
-
* depends) so that agents can emit a minimal valid tree without being rejected
|
|
210
|
-
* for cosmetic omissions.
|
|
265
|
+
* ## Canonical resolver precedence (highest → lowest)
|
|
211
266
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Validate the `evidence` field emitted by an IVTR validation node.
|
|
267
|
+
* 1. **step bindings** — the resolved `node.inputs` themselves. They are merged
|
|
268
|
+
* LAST over the run context, so a step input always SHADOWS a same-named
|
|
269
|
+
* context key (AC3 / AC6).
|
|
270
|
+
* 2. **playbook bindings** — the accumulated run `context` (seeded from the
|
|
271
|
+
* playbook's declared inputs + every prior node's merged output).
|
|
272
|
+
* 3. **session / project-context / env / default** — supplied to the underlying
|
|
273
|
+
* {@link DefaultVariableResolver} via the {@link SubstitutionContext} below.
|
|
221
274
|
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
275
|
+
* The run `context` is exposed to the resolver through BOTH the `projectContext`
|
|
276
|
+
* tier (which supports dot-notation, so `{{inputs.epicId}}` and `{{context.foo}}`
|
|
277
|
+
* resolve into nested objects) and `process.env` (the `env` tier). The
|
|
278
|
+
* resolver runs in lenient mode.
|
|
225
279
|
*
|
|
226
|
-
*
|
|
227
|
-
* cannot satisfy the gate by emitting an empty object.
|
|
280
|
+
* ## Missing-key rule
|
|
228
281
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
282
|
+
* Lenient. When a `{{path}}` template references a value that resolves through
|
|
283
|
+
* NO tier, the literal placeholder text is left intact in the bound value (e.g.
|
|
284
|
+
* `{{missing}}` stays `"{{missing}}"`). This mirrors the canonical T1238
|
|
285
|
+
* lenient posture — a missing input never aborts the dispatch; the agent
|
|
286
|
+
* receives the literal placeholder and can fail loudly downstream if required.
|
|
232
287
|
*
|
|
233
|
-
* @
|
|
288
|
+
* @param node - The agentic node whose `inputs` are resolved.
|
|
289
|
+
* @param context - The accumulated run bindings at dispatch time.
|
|
290
|
+
* @returns A new binding map = `{ ...context, ...resolvedStepInputs }`. When the
|
|
291
|
+
* node declares no `inputs`, this is a shallow copy of `context`.
|
|
234
292
|
*/
|
|
235
|
-
export declare function
|
|
293
|
+
export declare function resolveStepBindings(node: PlaybookAgenticNode, context: Record<string, unknown>): Record<string, unknown>;
|
|
236
294
|
/**
|
|
237
295
|
* Execute a playbook from its entry node until a terminal state is reached
|
|
238
296
|
* (`completed`, `failed`, `exceeded_iteration_cap`, or `pending_approval`).
|