@archmax-ai/harness 1.1.0 → 2.0.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.
Files changed (71) hide show
  1. package/README.md +167 -170
  2. package/dist/agent.d.ts +2 -2
  3. package/dist/agent.js +2 -2
  4. package/dist/assembly/compose.js +2 -2
  5. package/dist/assembly/compose.js.map +1 -1
  6. package/dist/authoring-skill/archmax-harness/README.md +13 -15
  7. package/dist/authoring-skill/archmax-harness/SKILL.md +174 -264
  8. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +244 -776
  9. package/dist/authoring-skill/archmax-harness/references/cases.md +178 -53
  10. package/dist/authoring-skill/archmax-harness/references/code-states-and-scripts.md +211 -100
  11. package/dist/authoring-skill/archmax-harness/references/diagnostics.md +57 -78
  12. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +613 -2006
  13. package/dist/core/deepagents.d.ts +1 -2
  14. package/dist/core/deepagents.js +1 -2
  15. package/dist/core/deepagents.js.map +1 -1
  16. package/dist/kernel/kernel.d.ts +3 -2
  17. package/dist/kernel/kernel.js +8 -4
  18. package/dist/kernel/kernel.js.map +1 -1
  19. package/dist/machine/lint-spec.js +6 -16
  20. package/dist/machine/lint-spec.js.map +1 -1
  21. package/dist/machine/machine.d.ts +8 -10
  22. package/dist/machine/machine.js +19 -13
  23. package/dist/machine/machine.js.map +1 -1
  24. package/dist/machine/spec-schema.d.ts +1 -4
  25. package/dist/machine/spec-schema.js +84 -47
  26. package/dist/machine/spec-schema.js.map +1 -1
  27. package/dist/machine/tool-names.d.ts +24 -34
  28. package/dist/machine/tool-names.js +43 -63
  29. package/dist/machine/tool-names.js.map +1 -1
  30. package/dist/machine/types.d.ts +11 -10
  31. package/dist/machine/types.js.map +1 -1
  32. package/dist/public/spec.d.ts +5 -5
  33. package/dist/public/spec.js +5 -5
  34. package/dist/public/spec.js.map +1 -1
  35. package/dist/validate/validate.js +3 -3
  36. package/dist/validate/validate.js.map +1 -1
  37. package/dist/workflow/automated.js +6 -1
  38. package/dist/workflow/automated.js.map +1 -1
  39. package/dist/workflow/control-tools.js +0 -2
  40. package/dist/workflow/control-tools.js.map +1 -1
  41. package/dist/workflow/grader-state.d.ts +2 -2
  42. package/dist/workflow/grader-state.js +5 -15
  43. package/dist/workflow/grader-state.js.map +1 -1
  44. package/dist/workflow/middleware.js +2 -26
  45. package/dist/workflow/middleware.js.map +1 -1
  46. package/dist/workflow/on-error.js +0 -1
  47. package/dist/workflow/on-error.js.map +1 -1
  48. package/dist/workflow/parks.js +0 -1
  49. package/dist/workflow/parks.js.map +1 -1
  50. package/dist/workflow/render-prompt.d.ts +4 -5
  51. package/dist/workflow/render-prompt.js +24 -27
  52. package/dist/workflow/render-prompt.js.map +1 -1
  53. package/dist/workflow/state.d.ts +0 -9
  54. package/dist/workflow/state.js +0 -3
  55. package/dist/workflow/state.js.map +1 -1
  56. package/dist/workflow/turn-boundary.js +0 -1
  57. package/dist/workflow/turn-boundary.js.map +1 -1
  58. package/package.json +1 -2
  59. package/skills/archmax-harness/README.md +13 -15
  60. package/skills/archmax-harness/SKILL.md +174 -264
  61. package/skills/archmax-harness/references/backend-integration.md +244 -776
  62. package/skills/archmax-harness/references/cases.md +178 -53
  63. package/skills/archmax-harness/references/code-states-and-scripts.md +211 -100
  64. package/skills/archmax-harness/references/diagnostics.md +57 -78
  65. package/skills/archmax-harness/references/workflow-schema.md +613 -2006
  66. package/dist/authoring-skill/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
  67. package/dist/authoring-skill/archmax-harness/references/wiring.md +0 -119
  68. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +0 -402
  69. package/skills/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
  70. package/skills/archmax-harness/references/wiring.md +0 -119
  71. package/skills/archmax-harness/references/workflow-yaml.md +0 -402
@@ -1,119 +0,0 @@
1
- # Wiring the runtime into a backend — the short form
2
-
3
- Load this when embedding `@archmax-ai/harness` in a service. Every option shape, the
4
- event catalogue and recipes: [`backend-integration.md`](backend-integration.md).
5
-
6
-
7
- ```ts
8
- import { createAgent, WORKFLOW_STATUSES, createUsageTracker } from "@archmax-ai/harness";
9
-
10
- const usage = createUsageTracker();
11
- const agent = await createAgent({
12
- workflow: "order-lookup",
13
- workspace: { rootDir, /* mounts?, sessionStore? */ },
14
- onEvent: (e) => { usage.handler(e); publish(e); }, // omit for the console subscriber
15
- // model | modelFactory (roles agent, judge, grader), tools, systemPrompt,
16
- // middleware, backend, checkpointer, trigger, variables, promptCache, pricing,
17
- // clock …
18
- });
19
- const sessionId = `run-${runId}`;
20
- const result = await agent.invoke(
21
- { messages: [{ role: "user", content: prompt }] },
22
- { configurable: { thread_id: sessionId } }, // recursionLimit: 400 unless you pass one
23
- );
24
- if (result.status === WORKFLOW_STATUSES.awaitingDecision) {
25
- // result.pendingDecision.transitions → show to a person, then:
26
- // await agent.workflow.decide(sessionId, { target, comment });
27
- }
28
- usage.totals(sessionId); // tokens (+ costUsd only when pricing is configured)
29
- agent.dispose(sessionId);
30
- ```
31
-
32
- - **Install**: `npm i @archmax-ai/harness`; pre-publish, `npm pack` and install the
33
- tarball (a real copy, not a symlink). This skill ships in the package at
34
- `BUNDLED_AUTHORING_SKILL_DIR` (`<dir>/archmax-harness/SKILL.md`).
35
- - **Mounts**: `workspace.mounts` is a route table — key → backend, or
36
- `{ backend, readOnly, governed }`. Mark a mount `governed: true` to hand the
37
- spec's `mounts` block the decision of which **states** reach it (closed by
38
- default, like a skill bundle) and whether each may write there; a mount not so
39
- declared is visible in every state, so an existing table behaves unchanged.
40
- `readOnly` is a ceiling the spec cannot lift.
41
-
42
- ```ts
43
- workspace: {
44
- rootDir,
45
- mounts: {
46
- ...defaultMounts(rootDir), // skills/, .platform/, AGENTS.md
47
- "/reference/": { backend: mountSubtree(store, "reference"), governed: true },
48
- "/shared/": { backend: mountSubtree(store, "shared"), readOnly: false, governed: true },
49
- },
50
- },
51
- ```
52
-
53
- Then in `workflow.yaml`: `mounts: { allow_always: [reference] }`, a state's
54
- `mounts: { allow: [{ mount: shared, access: read }] }`, and
55
- `mounts.forbid_always` for what no state (or child session) may reach. Pass the
56
- same table to `validateWorkflow({ mounts })` so `archmax validate` checks the
57
- spec's names against the wiring.
58
- - **Storage**: default filesystem store at `<root>/sessions`; else
59
- `createFilesystemSessionStore({ dir })`, `createBackendSessionStore({ backend, prefix? })`,
60
- `createMemorySessionStore()`. A custom `backend` without `sessionStore`
61
- throws `SessionStoreRequiredError`. Delete sessions via `agent.sessions.delete(id)`.
62
- - **Agent**: Deep Agents' `invoke`/`stream`/`streamEvents` + checkpoint
63
- surface; `sessions.list/get/delete`; `emitRunArtifacts`; `dispose`.
64
- Governance under `agent.workflow`: `machine`, `resolveTrigger`,
65
- `send(sessionId, input)` → one `Outcome` (`decide`/`reply`/`deliver` are
66
- conveniences), `resolveSession`.
67
- - **Multi-turn**: re-invoke the same `thread_id` with only the new message;
68
- never concatenate a conversation into one string. A spec edit is adopted,
69
- not refused: the next turn runs under the current `workflow.yaml` (the
70
- recorded `specHash` says which version each turn ran under), and a state
71
- the edit removed reopens at the entry state.
72
- - **Human-in-the-loop**: `status === "awaiting_decision"` →
73
- `pendingDecision.transitions` → `decide`. `reply(sessionId, text)`
74
- answers a message without moving the run. An `archmax_wait` park is
75
- `awaiting_input` → `deliver(sessionId, { trigger, variables, message? })` — the
76
- person's message travels with the firing; a park with `resumeAt` is the host
77
- scheduler's cue. A delegated child's park is named on `Outcome.delegation`.
78
- - **How a session ended**: `Outcome.kind` is `completed`, `parked`, `failed` or
79
- `rejected`. `failed` means the agent called `archmax_raise({ code, reason })`
80
- because the work could not be done: `Outcome.exit` is
81
- `{ success: false, code, reason }` (a completed turn: `{ success: true }`),
82
- `SessionSummary.exit` carries the same while the session is `failed`, and a
83
- later message is an ordinary new turn. `rejected` is governance ending it
84
- (`Outcome.rejected`, no `exit`). A host that read "not rejected" as success
85
- must check `failed` too. A turn that throws also ends the session `failed`,
86
- with code `turn-error`, `turn-cancelled` or `recursion-limit` — LangGraph's
87
- `recursionLimit` (400 unless the invoke config passes one) is the one bound on
88
- a whole turn. `Outcome.reply` is every assistant text of the turn, in order.
89
- - **Events**: `onEvent` receives the one typed stream — `state-enter/leave`,
90
- `advance`, `code-start/output/result` and `grader-start/result` (a code or
91
- grader state's run; `status` `ok`/`error`, `reason` on a failure),
92
- `parked/decided`, `raised` (the session ended failed: `state`, `code`,
93
- `reason`, `callId`), `tool-called/result/blocked` (every governed call,
94
- `archmax_advance` included, grouped by `callId`; a code state's carry
95
- `origin: "script"`), `agent-text-delta`/`agent-text` (streaming by default,
96
- `messageId` groups chunks; a grader's tokens never stream), `model-usage`
97
- (provider counts per call, a grader's under its state), `title-set`, warnings.
98
- There is no run-end event; `invoke` returning is the end. Metrics are a
99
- fold over this stream; only cost is checkpointed (`sessions.get(id).usage`).
100
- - **Artifacts** per session under `sessions/<id>/artifacts/`: `graph.json`/`.mmd`,
101
- `trajectory.json`, `trail.json`, `variables.json`, `metadata.json` (incl.
102
- usage); the agent's files under `scratchpad/`.
103
- - **Cases against your agent**: `runTests({ workflow, rootDir, createTarget?, sessionIdForCase? })`
104
- from `@archmax-ai/harness/testing`; wire `createToolMockMiddleware()` into your
105
- target or a case with `mocks:` is refused before running.
106
- - **Model/caching**: `ARCHMAX_API_BASE_URL`, `ARCHMAX_API_KEY`, `ARCHMAX_MODEL`
107
- (+ `ARCHMAX_TEMPERATURE`, `ARCHMAX_MAX_TOKENS`, `ARCHMAX_STREAMING`) for any
108
- OpenAI-compatible endpoint, or pass `model`. A `modelFactory(role, env, id)`
109
- serves each role its own model: `agent`, `judge` (case `grade:`), `grader`
110
- (a grader state, asked for the state's `model` id). Prompt prefix is cache-marked
111
- by default (`promptCache: { enabled, ttl: "5m" | "1h" }`); watch
112
- `cacheReadTokens`. Pricing via `pricing: { "<model-substring>": { input, output, cacheRead, cacheWrite } }`
113
- or `ARCHMAX_PRICE_*`; without it report tokens, never a guessed cost. A keyed
114
- table works behind a proxy that echoes no model back — the id falls back to the
115
- one the runtime was configured to run. Cached tokens are charged once, at the
116
- cache rate, not at the input rate as well.
117
-
118
- Typical split: worker = `createAgent` + `invoke` + `decide`/`deliver`; API =
119
- artifacts + event subscription; frontend = graph + trajectory + live stream.
@@ -1,402 +0,0 @@
1
- # `workflow.yaml` — keys, machine rules, governance
2
-
3
- Load this when writing or editing a `workflow.yaml`. Field-level detail and
4
- precedence edge cases: [`workflow-schema.md`](workflow-schema.md).
5
-
6
- ## Every key
7
-
8
- The schema is strict: an unknown key anywhere is a load error naming it. This
9
- skeleton is the complete vocabulary; omit what you do not need.
10
-
11
- ```yaml
12
- title: Triage example # label only; identity is the directory slug
13
- instructions: |- # standing direction for the whole run (warning if absent)
14
- Records live in skills/order-data/assets/orders.json; write artifacts under scratchpad/.
15
-
16
- Answer only from records you have read; never fabricate.
17
- runtime: { engine: archmax-harness, version: "3" } # 3 is the only contract; omitted = 3
18
- disabled: false # true = out of service: no new turns; parked runs still drain
19
- settings: # sandbox limits: timeoutMs, memoryLimitBytes, maxPtcCalls,
20
- timeoutMs: 15000 # maxResultChars, prompt_cache: { enabled, ttl }
21
- model: gpt-5-mini # MODEL ID ONLY — every state runs on it; overrides ARCHMAX_MODEL
22
- tests: # case suite config (no tests.config.js exists)
23
- maxConcurrency: 1 # only 1 accepted
24
- caseTimeoutMs: 120000 # per-case wall clock, distinct from settings.timeoutMs
25
- judge: {} # required for grade:; {} = env default model, or { model, modelOptions }
26
- metadata: { canvas: {} } # host data, never read; accepted at root and on a state — leave as found
27
-
28
- tools: # tool governance, workflow level
29
- allow_always: # granted in EVERY state; a state's allow adds to or narrows it.
30
- - ls # The DEFAULT TOOLS: declared, this list REPLACES them (absent,
31
- - read_file # all twelve are granted), so a new workflow lists all twelve and
32
- - write_file # removes only what it must not have. Workflow-wide host tools
33
- - edit_file # go below them; state-specific ones under a state's tools.allow.
34
- - glob
35
- - grep
36
- - copy_file
37
- - move_file
38
- - remove_file
39
- - write_todos
40
- - archmax_eval
41
- - archmax_run
42
- forbid_always: [archmax_reset] # denied in EVERY state; beats every grant; "*" = every tool
43
- skills: # skill governance, workflow level — SLUGS, never paths
44
- allow_always: [order-data] # enabled in every state; [] = every capability is state-specific
45
- forbid_always: [] # out of reach everywhere
46
-
47
- states:
48
- triage: # key = slug = identity (^[a-z0-9]+(-[a-z0-9]+)*$)
49
- type: agent # agent (default) | human | grader | code (grader/code = automated)
50
- title: Triage request # label for people/hosts; reaches no model call
51
- summary: Record the refund decision for review. # ditto: an authoring/host label, NOT in the prompt
52
- triggers: # a trigger is declared on the state it ENTERS; key = id
53
- manual: # CLI / caller / host entry; the prompt is its input
54
- email_reply: # any other id = host event; the payload arrives as locked variables
55
- session: conversation_id # dotted path over variables: which conversation a firing joins
56
- message: false # or a dotted path to the message text; false = firings carry none
57
- connection: mail # host access connection; other keys preserved with a warning
58
- description: Record one refund decision. # for CALLERS (delegating model, MCP client); never the agent
59
- requires: # variables a firing must supply (a caller's required params):
60
- - order_id # a bare name is untyped (any value, null included)
61
- - { name: due, type: date, description: The day it is due. } # typed: held at the boundary
62
- - { name: contact, type: string, format: email } # a rule beside the type: held wherever the type is
63
- returns: [result_file] # variables the run guarantees on completion (a caller's result);
64
- # short of them: a child returns what it set + a `note`; a top-level run is rejected
65
- # entry types: string integer number boolean date date-time object array file
66
- # rules: format pattern minLength maxLength enum content (string); minimum maximum
67
- # (integer number date date-time); minItems maxItems (array); accept maxSize (file)
68
- instructions: >-
69
- Record the decision in scratchpad/refund.json, then archmax_advance to
70
- check-policy; never close the case yourself. If a note sends the case
71
- back, fix what it names and advance again.
72
- tools:
73
- allow: # adds to allow_always; entry = name | { tool, args: {<arg>: [globs]} } | { tool, paths }
74
- - lookup_customer
75
- - { tool: archmax_run, paths: ["skills/refund-policy/scripts/**"] } # narrows the workflow-wide grant, within an enabled skill
76
- - { tool: write_file, args: { file_path: ["reports/${{case_id}}/*.json"] } } # variable-bound guard
77
- - archmax_workflow_enrich-order # runs a sibling workflow
78
- forbid: [archmax_wait] # denies here; beats every grant incl. allow_always
79
- skills:
80
- allow: [refund-policy] # adds to allow_always (bundle readable + scripts runnable, no path entries)
81
- forbid: [order-data] # subtracts, even from allow_always ([] never denies)
82
- requires: [case_id] # variables that must be set before this state may be left
83
- locks: [case_id] # variables only this state may write (change = come back here)
84
- model: claude-opus-5 # MODEL ID ONLY — this state's turns; beats settings.model
85
- budget: { maxTurns: 12, timeoutMs: 60000, maxParks: 5 } # bounds the state's segment
86
- on_error: closed # where a terminal failure of this segment routes
87
- metadata: { x: 40, y: 180 }
88
- transitions: # none => terminal state
89
- - to: check-policy # slug; must exist
90
- description: The decision is recorded in scratchpad/refund.json. # what the agent routes
91
- # on. Say WHEN, never where. Omit it to route on the target's
92
- # instructions instead (target must have some); REQUIRED on a
93
- # human state's transitions (the button label) and on every edge
94
- # INTO a grader or code state, like this one.
95
- type: none # approve | reject | refine | none — semantic on human states
96
-
97
- check-policy: # CODE state: the runtime runs its script on entry, NO model call
98
- type: code
99
- script: scripts/check-policy.js # REQUIRED on a code state, refused on any other type; resolves
100
- # to workflows/<slug>/scripts/check-policy.js (.js|.mjs, no `..`)
101
- summary: Re-derive the decision from the refund policy. # instructions/model/budget/evidence/
102
- # approvers here are validate ERRORS: nothing reads them
103
- skills: { allow: [order-data] } # the script's tools are THIS state's surface: grant here
104
- # what it reads or calls (another state's grant never reaches it)
105
- on_error: closed # a throw, a timeout, or finishing undecided routes here
106
- transitions: # the script takes one: tools.archmax_advance({ to, reason })
107
- - { to: review, description: The recorded decision matches the policy. }
108
- - { to: triage, description: The decision contradicts the policy; send it back with the fix. }
109
-
110
- review: # human state: parks with a decision record; a PERSON picks the edge
111
- type: human
112
- title: Review refund
113
- summary: A person approves the recorded decision or sends it back. # label only
114
- instructions: >- # required on human states: the decision to present
115
- Confirm scratchpad/refund.json matches policy.
116
- approvers: [finance@acme.com]
117
- evidence: [scratchpad/refund.json] # plus whatever archmax_advance attached via `evidence:`
118
- transitions: # required; no self-edge; one target per labeled type
119
- - { to: reply, type: approve, description: Approve; tell the customer. }
120
- - { to: triage, type: refine, description: Send back with a comment. }
121
-
122
- reply: # agent state (type omitted)
123
- instructions: >-
124
- Draft the customer's email in scratchpad/reply.md, then archmax_advance to
125
- grade-reply; if a note sends it back, revise the draft as it says.
126
- transitions:
127
- - { to: grade-reply, description: The email is drafted in scratchpad/reply.md. }
128
-
129
- grade-reply: # GRADER state: an isolated grader judges WORK HANDED ON (a draft,
130
- type: grader # a report), never a live answer: a turn's reply is every text
131
- # the agent said, a draft sent back included
132
- instructions: |- # REQUIRED: its criteria, appended to a compiled-in grader preamble
133
- Judge only the tone of scratchpad/reply.md.
134
- - Friendly and professional: advance to `send-reply`.
135
- - Curt or cold: advance back to `reply`, saying exactly what to change.
136
- - If archmax_get_history's trail already holds two steps with kind
137
- "grader" and to "reply", raise `reply-tone` instead of sending it back.
138
- model: gpt-4.1-mini # optional; resolved under the `grader` model role
139
- budget: { maxTurns: 4, timeoutMs: 60000 } # ITS OWN model calls / run time; no maxParks
140
- on_error: closed # fails closed: an undecided grader routes here
141
- transitions: # REQUIRED, at least one; no evidence/approvers on a grader
142
- - { to: send-reply, description: The draft is friendly and professional. }
143
- - { to: reply, description: The tone is off; send it back naming what to change. }
144
-
145
- send-reply: # TERMINAL code state: the session completes when the script
146
- type: code # returns — "approve, then end" with no further model call
147
- script: scripts/send-reply.js
148
- tools: { allow: [send_email] } # the host tool its script calls, granted HERE
149
-
150
- closed: # bare key = terminal (agent) state with no declarations
151
- ```
152
-
153
- ## Machine rules
154
-
155
- - **Movement.** The agent moves only by `archmax_advance({ to, reason, evidence? })`
156
- — one per message, edge and `requires` validated, then the target's tools
157
- unlock (or, for a grader or code target, the target runs at once). A state
158
- with no transitions is terminal; the run ends when the agent finishes there.
159
- Human decisions, `on_error`, and grader and code states' own `archmax_advance`
160
- are the other movers. `archmax_reset({ reason })`
161
- returns to the entry state (forbid it via `forbid_always` when a run must
162
- never restart). No fan-out: sequence states; concurrency lives in
163
- sub-workflow calls.
164
- - **Entry.** At least one state declares `triggers`; one state per trigger id;
165
- `manual` is the entry the CLI, a host firing and a caller all use. A
166
- re-invoked finished session is a **new turn in the state it ended in**, not
167
- at the entry (one that completed in a terminal code state opens at its
168
- trigger's entry) — give a state re-entry transitions if a conversation must
169
- move on, or park instead of finishing.
170
- - **Variables.** One flat checkpointed `name → value` store, never declared.
171
- Host seeds (`--variables`, `createAgent({ variables })`, a delivery) are
172
- **locked**; the agent uses `archmax_get_variables`/`archmax_set_variables`
173
- (`lock: true` to freeze, until `archmax_reset` releases the agent's locks).
174
- A value that must be decided in one place yet stay correctable belongs to a
175
- state: `locks: [order_id]` lets only that state write it, so a change routes
176
- back there. Reserved: `trigger` (current trigger id, always
177
- locked) and `title` (the run's one-line task label, agent-owned; a
178
- sub-workflow child is not asked for one). A
179
- `${{name}}` / `${{a.b.-1.c}}` reference works in `tools.allow` globs
180
- (guard) and inside any text tool argument the model writes (exact
181
- substitution, no retyping) — but not in scripts, which read
182
- `args.variables`. Name a guarded variable in an earlier state's `requires`
183
- or seed it; an unresolvable guard reference fails the run, an unresolvable
184
- argument reference is refused and retried. An `archmax_set_variables` typo
185
- creates a new variable; `requires` is what surfaces it.
186
- - **Models.** `settings.model` names the id every state runs on and a state's
187
- `model` names its own — an **id only**, over the endpoint the assembly is
188
- already configured with (a block with `temperature` is a load error;
189
- sampling is env/host configuration). Precedence: state → workflow →
190
- `ARCHMAX_MODEL`. Author it where the work differs in kind: small models for
191
- classification, extraction and other mechanical states; leave drafting,
192
- judgement and any state with a big tool surface or several transitions on
193
- the default, since a small model that routes badly costs more in retries
194
- than it saves. Repeating the workflow's id on a state is a validate warning.
195
- A grader state's `model` is resolved under the `grader` model role
196
- (`modelFactory("grader", env, id)`), a sub-workflow uses its own spec's, and a
197
- host that passes an explicit `model` instance to `createAgent` outranks
198
- every declared id (it must pass `modelFactory` to honour them).
199
- - **Parking.** A state that needs an event or a time calls
200
- `archmax_wait({ reason, until? })` (`until`: `30m`/`2h`/`1d` or ISO instant →
201
- `resumeAt` for the host's scheduler; the runtime holds no timer). Any
202
- delivered trigger resumes **that same state** with the payload as locked
203
- variables and an `[event]` runtime note; the state then picks its own edge.
204
- Bound a polling loop with `budget.maxParks` + `on_error`. `archmax_wait` is
205
- in every state, terminal ones included. Use a **human state** when a person
206
- picks the edge, `archmax_wait` when an event or time resumes the work.
207
- - **Failing.** When the work cannot be completed, the agent calls
208
- `archmax_raise({ code, reason })` (alone in its message; any state): the
209
- session ends at once `failed`, with the agent's free-form `code` and
210
- `reason` as the outcome's `exit` — no `on_error` route, no `returns`
211
- check. A session that never raises is a success, so it is for failure
212
- only. Forbid it (`forbid_always`, or a state's `forbid`) where the
213
- agent must not end the run itself; a child that raises fails its caller's
214
- call with kind `raised`.
215
- - **Human states** park with a record built from `instructions`, `evidence`
216
- and transition `type`s. The handoff spends one reply-only model call so the
217
- park carries a message — do **not** instruct the agent to announce the
218
- handoff (two messages). While parked, `archmax reply` is answered on a
219
- reply-only turn (every tool call refused) and re-parks unchanged; only
220
- `decide` moves the run. A
221
- `refine` edge delivers the reviewer's comment as correction.
222
- - **Grader and code states** (automated states). A **code** state runs
223
- `script: scripts/<x>.js` in the sandbox; a **grader** state runs an isolated
224
- grader whose criteria are the state's `instructions`. Both read the session
225
- with `archmax_get_history` (`{ messages, trail }`: the whole transcript, a
226
- tool result inline in its tool message's `text`, and the trail; a script
227
- calls `JSON.parse(await tools.archmax_get_history())`). Both run on
228
- entry — however the session got there — with **no call of the agent's model**
229
- (a chain of code states makes none at all), and leave like every state:
230
- `archmax_advance` along one of their transitions, whose `reason` reaches the
231
- next state as a `[code]`/`[grader]` runtime note and a `kind: code|grader`
232
- trail step, or `archmax_raise`. A **terminal** code state completes the
233
- session when its script returns. Throwing, timing out, a spent grader budget,
234
- or a non-terminal state that finishes without either call **fails closed** →
235
- `on_error`. Each is offered exactly its own state's surface (its grants and
236
- one control set for both types: `archmax_advance`, `archmax_raise`, the
237
- variable tools, `archmax_get_history`), governed like the agent's calls — never
238
- `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait`,
239
- `archmax_reset`, even when `allow_always` lists one (a state's own
240
- `tools.allow` naming one is an error): an automated state cannot park.
241
- There is no iteration budget: a state that gives up after N rounds counts
242
- them in the trail. Per type, `validate` errors on: a code state's
243
- `instructions`/`model`/`budget`/`evidence`/`approvers`; a grader without
244
- `instructions` or without a transition, or with `evidence`/`approvers`/
245
- `budget.maxParks`. There are **no `before:`/`after:` hooks and no root
246
- `extensions:`** — a leftover key is a load error naming it. Script contract:
247
- [`code-states-and-scripts.md`](code-states-and-scripts.md).
248
- - **Automated-state patterns.** A deterministic check **in front of** a state:
249
- make the trigger's entry (or the edge's target) a code state that advances
250
- to the work or to a state that declines it. A check **behind** a state: point
251
- the state's transition at a code state that advances onward, or back with
252
- the fix as its `reason` — the agent reads it and advances again. **Approve,
253
- then end**: approve into a terminal code state, which costs no model call.
254
- Judgment (tone, completeness): a grader behind the graded state with an edge
255
- back; its `instructions` say when to raise ("after two send-backs" = two
256
- trail steps `{ kind: grader, to: <graded state> }`). Grade work the agent
257
- hands on (a report, a draft), not a live answer: a turn's `reply` is every
258
- text the agent said in it, so a draft sent back has reached the person.
259
- - **Budgets and `on_error`.** `budget.maxTurns`/`timeoutMs`/`maxParks` bound
260
- a state's segment (on a grader: its own model calls and run time; a code
261
- state takes no `budget`, `settings.timeoutMs` bounds its script). There is
262
- no root `budget` (declaring one is a load error): a whole turn is bounded by
263
- LangGraph's `recursionLimit` (400 unless the host's invoke config sets one),
264
- and a turn that hits it — a cycle between states, say — ends the session
265
- `failed` (`recursion-limit`). A terminal failure (exhausted budget, exhausted parks, a
266
- grader or code state that fails) routes to `on_error` instead of failing the
267
- run; recoverable rejections (invalid edge, unmet `requires`) keep the agent
268
- in place. Declare `on_error` on every automated state and on bounded loops.
269
- A **tool that throws** is neither: the call is answered with an error-status tool message carrying
270
- the error, the agent stays in the state and can retry or switch tools, and
271
- `on_error` never sees it (a park or a cancelled run still propagates).
272
- - **Sub-workflows are tools.** Allow `archmax_workflow_<slug>` (slug verbatim)
273
- and the sibling `workflows/<slug>/` is callable: its `manual` trigger's
274
- `requires` are the parameters (typed where the entries are typed; a
275
- mistyped argument is refused `invalid-param`, and `"${{count}}"` alone
276
- passes the value with its own type), its `returns` the result
277
- (`{ message, returns }`; a child finishing with some unset completes, and
278
- `returns` holds every declared one, the unset ones `null`, plus a `note`
279
- naming them; a mistyped typed return fails `invalid-return`). Returns are
280
- owed only at the finish: no state needs them to be left, a later write
281
- replaces an unlocked earlier one, and a seed of one lands unlocked; lock one
282
- only once it is final. Each failed call is the
283
- calling state's failure, recorded with its arguments and routed through
284
- `on_error` when the agent finishes unless a later successful call of the
285
- same workflow from the same state recovers it first. One success recovers
286
- one failed call: the one whose arguments it repeats (compared after
287
- `${{…}}` resolves, key order ignored), else the oldest. A sibling in the
288
- same batch does not count; another workflow's success recovers nothing;
289
- the route names every outstanding failure with its arguments.
290
- The trigger's `description` leads the tool description. Nothing captures the result — the caller records
291
- what it needs with `archmax_set_variables`, made mandatory by `requires`.
292
- The child is a separate session with a fresh transcript, sees only its
293
- arguments (as locked variables), and shares no prose. One call = one
294
- sub-run; fan out with parallel tool calls or `Promise.all` in a script
295
- (bounds: depth 3, 4 concurrent). A child's human state parks the child;
296
- a script's delegation cannot park and fails closed.
297
- - **Choosing a mechanism.** Deterministic work the agent triggers → an
298
- `archmax_run` script. Deterministic work the graph runs at a fixed point (a
299
- screen, a policy check, a record) → a code state. Delegated work that is itself
300
- a process or reused across workflows → a sub-workflow. Judging work already
301
- done → a grader state. Neither automated kind is callable by the agent (`task`
302
- is disclosed nowhere and not grantable).
303
- - **Prompt economy.** The static prompt + tool schemas are re-sent every
304
- model call. The static part carries the workflow's `title` + `instructions`
305
- and **no state of the graph**; a state's `instructions` and its **own**
306
- outgoing edges are sent only while it is active. So: workflow-wide
307
- conventions go in the root `instructions` (paid once, cached); everything
308
- else goes in the state that needs it, and splitting a broad state into
309
- several narrow ones costs nothing in the prefix. `title` and `summary` are
310
- free — neither reaches a model call. `WORKFLOW.md` carries only what the spec
311
- cannot say; reader diagrams go inside an HTML comment (stripped from the
312
- prompt).
313
-
314
- ## What the agent sees of the graph
315
-
316
- Only the state it is in. Every model call is handed that state's outgoing edges
317
- and nothing else of the machine — no other state's slug, `title`, `summary`,
318
- script, criteria or transitions, and no count of the states:
319
-
320
- ```
321
- Transitions — choose one with `archmax_advance` when this state's work is done:
322
- - to `check-policy` (code) — The decision is recorded in scratchpad/refund.json.
323
- - to `review` (human decision node) — The decision is recorded and needs a person's approval.
324
- - to `grade-reply` (grader) — The email is drafted in scratchpad/reply.md.
325
- - to `closed` (terminal) — Nothing to refund; the case is answered.
326
- ```
327
-
328
- A `(grader)` or `(code)` target runs on its own and may send the run straight
329
- back with a note saying what to change.
330
-
331
- Two rules follow, and they are the ones authors get wrong:
332
-
333
- 1. **Write the `description` for the source state, not the target.** The agent
334
- reading it knows nothing about where it is going. Say *when* the edge applies:
335
- `The refund is over $50, or the customer has asked twice.` Not `Go to
336
- refund-review.` — that tells a reader who already knows the graph something
337
- they cannot use, and tells this reader nothing.
338
- 2. **Routing conditions cannot live downstream.** A target's `summary` is never
339
- disclosed to the state routing into it, nor its `instructions` across a
340
- described edge. If the condition is "when the order is over 30 days old", it
341
- belongs on the edge.
342
-
343
- An **undescribed** edge (no `description`, or a blank one) routes on its target's
344
- `instructions` instead, quoted under the edge's line as what the agent will do
345
- once there: leave one undescribed only when those instructions already say when
346
- it applies. It is a **load error** where the target has no instructions, on a
347
- human state's transitions (the description is the button label), and into a
348
- grader or code state (a grader's instructions are criteria the work is never
349
- shown; a code state has none). A code state's own edges may go undescribed: its
350
- script routes. There is no other way to see more of the graph.
351
-
352
- ## Governance
353
-
354
- Closed by default; **deny beats allow, and no narrower level widens a denial.**
355
-
356
- - **The workflow-wide grant**: `tools.allow_always`, in every state. Absent,
357
- it is the **default tools**: `ls`, `read_file`, `write_file`, `edit_file`,
358
- `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`,
359
- `archmax_eval`, `archmax_run`. Declared (`[]` included), it is exactly what it
360
- lists and **replaces** the default tools, so a new workflow lists all twelve
361
- (as in the skeleton) and removes only what it must not have. Never declared:
362
- the controls `archmax_advance`, `archmax_reset`, `archmax_wait`,
363
- `archmax_raise`, `archmax_get_variables`, `archmax_set_variables`, and a host's
364
- `essentialTools`. Any other tool needs an `allow`/`allow_always` entry. A
365
- state entry naming a granted tool **narrows** it there, and one naming a
366
- default tool the list omits grants it there. `task` is ungrantable (error).
367
- - **In a grader or code state** the surface is what the state is granted (the
368
- workflow-wide grant included), minus `write_todos`, `archmax_eval`,
369
- `archmax_run`, `archmax_wait` and `archmax_reset` even when `allow_always` lists
370
- them (naming one in the state's
371
- own `tools.allow` is an error), plus one control set for both types:
372
- `archmax_advance`, `archmax_raise`, `archmax_get_variables`,
373
- `archmax_set_variables` and `archmax_get_history` — never `archmax_wait` or
374
- `archmax_reset`.
375
- - **Entries**: bare name; `{ tool, args: { <arg>: ["glob", …] } }` (matches
376
- if any glob matches; `${{var}}` allowed). An argument the tool declares as a
377
- path (else `file_path`) is canonicalized first; any other argument (a URL, an
378
- address, an email) is matched **as written**, and a denial also catches its
379
- path form. `""` matches only an empty value. `*` never crosses `/` (`**` does).
380
- `{ tool, paths: [...] }`
381
- (guards every path argument the tool declares — all must match to grant,
382
- any matching denies). `"*"` as the tool is legal only in a
383
- forbid list. Globs match dot-segments (`secrets/**` covers `secrets/.env`).
384
- - **Path rules read declared path arguments**: `copy_file`/`move_file` take
385
- `{ source, destination, overwrite? }` (copy reads `source`, move removes it;
386
- both write `destination`), `remove_file` takes `{ file_path }`. A removal is
387
- refused wherever a write is; a call naming several paths is refused when any
388
- one is. An existing destination is refused unless `overwrite: true`.
389
- `forbid: [{ tool: "*", paths: [secrets/**] }]` blocks a copy in or out.
390
- - **Skills** are governed by **slug**, never path. Enabling a bundle makes
391
- `skills/<slug>/**` readable and its scripts runnable with no `tools.allow`
392
- entry; a disabled bundle is unreachable **and invisible** (not listed, not
393
- in the prompt). `allow` adds, `forbid` subtracts, `allow: []` denies
394
- nothing. A `tools.allow` entry may still narrow *within* an enabled bundle.
395
- A state enabling no skill can run no script at all.
396
- - **Scripts' `tools.*` calls** are governed per call against the state they
397
- run in: an `archmax_run` script is bound like the model (grant every tool it
398
- calls in the agent's state); a code state's script is bound by **its own**
399
- state's grants (`tools`, `skills`, `mounts`) — there is no runtime authority.
400
- `tools` holds exactly that state's surface: anything else is `undefined`.
401
- Neither may read `workflows/**` or write a read-only mount. A refused call
402
- rejects inside the script.