@archmax-ai/harness 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +167 -165
  2. package/dist/agent.d.ts +2 -2
  3. package/dist/agent.js +2 -2
  4. package/dist/assembly/compose.js +5 -3
  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 -261
  8. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +244 -764
  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 +614 -1977
  13. package/dist/core/attachments.d.ts +2 -0
  14. package/dist/core/attachments.js +2 -1
  15. package/dist/core/attachments.js.map +1 -1
  16. package/dist/core/deepagents.d.ts +1 -2
  17. package/dist/core/deepagents.js +1 -2
  18. package/dist/core/deepagents.js.map +1 -1
  19. package/dist/core/file-operations.d.ts +6 -0
  20. package/dist/core/file-operations.js +29 -0
  21. package/dist/core/file-operations.js.map +1 -1
  22. package/dist/index.d.ts +5 -1
  23. package/dist/index.js +4 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/kernel/kernel.js +2 -2
  26. package/dist/kernel/kernel.js.map +1 -1
  27. package/dist/machine/lint-spec.js +6 -5
  28. package/dist/machine/lint-spec.js.map +1 -1
  29. package/dist/machine/machine.d.ts +8 -10
  30. package/dist/machine/machine.js +11 -12
  31. package/dist/machine/machine.js.map +1 -1
  32. package/dist/machine/signature-pattern.d.ts +24 -0
  33. package/dist/machine/signature-pattern.js +67 -0
  34. package/dist/machine/signature-pattern.js.map +1 -0
  35. package/dist/machine/signature.d.ts +137 -35
  36. package/dist/machine/signature.js +476 -25
  37. package/dist/machine/signature.js.map +1 -1
  38. package/dist/machine/spec-schema.d.ts +203 -5
  39. package/dist/machine/spec-schema.js +209 -51
  40. package/dist/machine/spec-schema.js.map +1 -1
  41. package/dist/machine/tool-names.d.ts +24 -34
  42. package/dist/machine/tool-names.js +43 -63
  43. package/dist/machine/tool-names.js.map +1 -1
  44. package/dist/machine/types.d.ts +11 -10
  45. package/dist/machine/types.js.map +1 -1
  46. package/dist/public/spec.d.ts +10 -6
  47. package/dist/public/spec.js +9 -5
  48. package/dist/public/spec.js.map +1 -1
  49. package/dist/validate/validate.js +3 -3
  50. package/dist/validate/validate.js.map +1 -1
  51. package/dist/workflow/automated.js +6 -1
  52. package/dist/workflow/automated.js.map +1 -1
  53. package/dist/workflow/control-tools.js +3 -3
  54. package/dist/workflow/control-tools.js.map +1 -1
  55. package/dist/workflow/file-values.d.ts +4 -1
  56. package/dist/workflow/file-values.js +2 -0
  57. package/dist/workflow/file-values.js.map +1 -1
  58. package/dist/workflow/grader-state.d.ts +2 -2
  59. package/dist/workflow/grader-state.js +5 -15
  60. package/dist/workflow/grader-state.js.map +1 -1
  61. package/dist/workflow/middleware.d.ts +2 -0
  62. package/dist/workflow/middleware.js +3 -26
  63. package/dist/workflow/middleware.js.map +1 -1
  64. package/dist/workflow/on-error.js +0 -1
  65. package/dist/workflow/on-error.js.map +1 -1
  66. package/dist/workflow/parks.js +0 -1
  67. package/dist/workflow/parks.js.map +1 -1
  68. package/dist/workflow/render-prompt.d.ts +4 -5
  69. package/dist/workflow/render-prompt.js +31 -29
  70. package/dist/workflow/render-prompt.js.map +1 -1
  71. package/dist/workflow/signature-checks.d.ts +8 -3
  72. package/dist/workflow/signature-checks.js +17 -4
  73. package/dist/workflow/signature-checks.js.map +1 -1
  74. package/dist/workflow/state.d.ts +0 -9
  75. package/dist/workflow/state.js +0 -3
  76. package/dist/workflow/state.js.map +1 -1
  77. package/dist/workflow/sub-workflow.js +4 -1
  78. package/dist/workflow/sub-workflow.js.map +1 -1
  79. package/dist/workflow/turn-boundary.js +0 -1
  80. package/dist/workflow/turn-boundary.js.map +1 -1
  81. package/package.json +2 -2
  82. package/skills/archmax-harness/README.md +13 -15
  83. package/skills/archmax-harness/SKILL.md +174 -261
  84. package/skills/archmax-harness/references/backend-integration.md +244 -764
  85. package/skills/archmax-harness/references/cases.md +178 -53
  86. package/skills/archmax-harness/references/code-states-and-scripts.md +211 -100
  87. package/skills/archmax-harness/references/diagnostics.md +57 -78
  88. package/skills/archmax-harness/references/workflow-schema.md +614 -1977
  89. package/dist/authoring-skill/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
  90. package/dist/authoring-skill/archmax-harness/references/wiring.md +0 -119
  91. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +0 -399
  92. package/skills/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
  93. package/skills/archmax-harness/references/wiring.md +0 -119
  94. package/skills/archmax-harness/references/workflow-yaml.md +0 -399
@@ -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,399 +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
- returns: [result_file] # variables the run guarantees on completion (a caller's result);
63
- # short of them: a child returns what it set + a `note`; a top-level run is rejected
64
- # entry types: string integer number boolean date date-time object array file
65
- instructions: >-
66
- Record the decision in scratchpad/refund.json, then archmax_advance to
67
- check-policy; never close the case yourself. If a note sends the case
68
- back, fix what it names and advance again.
69
- tools:
70
- allow: # adds to allow_always; entry = name | { tool, args: {<arg>: [globs]} } | { tool, paths }
71
- - lookup_customer
72
- - { tool: archmax_run, paths: ["skills/refund-policy/scripts/**"] } # narrows the workflow-wide grant, within an enabled skill
73
- - { tool: write_file, args: { file_path: ["reports/${{case_id}}/*.json"] } } # variable-bound guard
74
- - archmax_workflow_enrich-order # runs a sibling workflow
75
- forbid: [archmax_wait] # denies here; beats every grant incl. allow_always
76
- skills:
77
- allow: [refund-policy] # adds to allow_always (bundle readable + scripts runnable, no path entries)
78
- forbid: [order-data] # subtracts, even from allow_always ([] never denies)
79
- requires: [case_id] # variables that must be set before this state may be left
80
- locks: [case_id] # variables only this state may write (change = come back here)
81
- model: claude-opus-5 # MODEL ID ONLY — this state's turns; beats settings.model
82
- budget: { maxTurns: 12, timeoutMs: 60000, maxParks: 5 } # bounds the state's segment
83
- on_error: closed # where a terminal failure of this segment routes
84
- metadata: { x: 40, y: 180 }
85
- transitions: # none => terminal state
86
- - to: check-policy # slug; must exist
87
- description: The decision is recorded in scratchpad/refund.json. # what the agent routes
88
- # on. Say WHEN, never where. Omit it to route on the target's
89
- # instructions instead (target must have some); REQUIRED on a
90
- # human state's transitions (the button label) and on every edge
91
- # INTO a grader or code state, like this one.
92
- type: none # approve | reject | refine | none — semantic on human states
93
-
94
- check-policy: # CODE state: the runtime runs its script on entry, NO model call
95
- type: code
96
- script: scripts/check-policy.js # REQUIRED on a code state, refused on any other type; resolves
97
- # to workflows/<slug>/scripts/check-policy.js (.js|.mjs, no `..`)
98
- summary: Re-derive the decision from the refund policy. # instructions/model/budget/evidence/
99
- # approvers here are validate ERRORS: nothing reads them
100
- skills: { allow: [order-data] } # the script's tools are THIS state's surface: grant here
101
- # what it reads or calls (another state's grant never reaches it)
102
- on_error: closed # a throw, a timeout, or finishing undecided routes here
103
- transitions: # the script takes one: tools.archmax_advance({ to, reason })
104
- - { to: review, description: The recorded decision matches the policy. }
105
- - { to: triage, description: The decision contradicts the policy; send it back with the fix. }
106
-
107
- review: # human state: parks with a decision record; a PERSON picks the edge
108
- type: human
109
- title: Review refund
110
- summary: A person approves the recorded decision or sends it back. # label only
111
- instructions: >- # required on human states: the decision to present
112
- Confirm scratchpad/refund.json matches policy.
113
- approvers: [finance@acme.com]
114
- evidence: [scratchpad/refund.json] # plus whatever archmax_advance attached via `evidence:`
115
- transitions: # required; no self-edge; one target per labeled type
116
- - { to: reply, type: approve, description: Approve; tell the customer. }
117
- - { to: triage, type: refine, description: Send back with a comment. }
118
-
119
- reply: # agent state (type omitted)
120
- instructions: >-
121
- Draft the customer's email in scratchpad/reply.md, then archmax_advance to
122
- grade-reply; if a note sends it back, revise the draft as it says.
123
- transitions:
124
- - { to: grade-reply, description: The email is drafted in scratchpad/reply.md. }
125
-
126
- grade-reply: # GRADER state: an isolated grader judges WORK HANDED ON (a draft,
127
- type: grader # a report), never a live answer: a turn's reply is every text
128
- # the agent said, a draft sent back included
129
- instructions: |- # REQUIRED: its criteria, appended to a compiled-in grader preamble
130
- Judge only the tone of scratchpad/reply.md.
131
- - Friendly and professional: advance to `send-reply`.
132
- - Curt or cold: advance back to `reply`, saying exactly what to change.
133
- - If archmax_get_history's trail already holds two steps with kind
134
- "grader" and to "reply", raise `reply-tone` instead of sending it back.
135
- model: gpt-4.1-mini # optional; resolved under the `grader` model role
136
- budget: { maxTurns: 4, timeoutMs: 60000 } # ITS OWN model calls / run time; no maxParks
137
- on_error: closed # fails closed: an undecided grader routes here
138
- transitions: # REQUIRED, at least one; no evidence/approvers on a grader
139
- - { to: send-reply, description: The draft is friendly and professional. }
140
- - { to: reply, description: The tone is off; send it back naming what to change. }
141
-
142
- send-reply: # TERMINAL code state: the session completes when the script
143
- type: code # returns — "approve, then end" with no further model call
144
- script: scripts/send-reply.js
145
- tools: { allow: [send_email] } # the host tool its script calls, granted HERE
146
-
147
- closed: # bare key = terminal (agent) state with no declarations
148
- ```
149
-
150
- ## Machine rules
151
-
152
- - **Movement.** The agent moves only by `archmax_advance({ to, reason, evidence? })`
153
- — one per message, edge and `requires` validated, then the target's tools
154
- unlock (or, for a grader or code target, the target runs at once). A state
155
- with no transitions is terminal; the run ends when the agent finishes there.
156
- Human decisions, `on_error`, and grader and code states' own `archmax_advance`
157
- are the other movers. `archmax_reset({ reason })`
158
- returns to the entry state (forbid it via `forbid_always` when a run must
159
- never restart). No fan-out: sequence states; concurrency lives in
160
- sub-workflow calls.
161
- - **Entry.** At least one state declares `triggers`; one state per trigger id;
162
- `manual` is the entry the CLI, a host firing and a caller all use. A
163
- re-invoked finished session is a **new turn in the state it ended in**, not
164
- at the entry (one that completed in a terminal code state opens at its
165
- trigger's entry) — give a state re-entry transitions if a conversation must
166
- move on, or park instead of finishing.
167
- - **Variables.** One flat checkpointed `name → value` store, never declared.
168
- Host seeds (`--variables`, `createAgent({ variables })`, a delivery) are
169
- **locked**; the agent uses `archmax_get_variables`/`archmax_set_variables`
170
- (`lock: true` to freeze, until `archmax_reset` releases the agent's locks).
171
- A value that must be decided in one place yet stay correctable belongs to a
172
- state: `locks: [order_id]` lets only that state write it, so a change routes
173
- back there. Reserved: `trigger` (current trigger id, always
174
- locked) and `title` (the run's one-line task label, agent-owned; a
175
- sub-workflow child is not asked for one). A
176
- `${{name}}` / `${{a.b.-1.c}}` reference works in `tools.allow` globs
177
- (guard) and inside any text tool argument the model writes (exact
178
- substitution, no retyping) — but not in scripts, which read
179
- `args.variables`. Name a guarded variable in an earlier state's `requires`
180
- or seed it; an unresolvable guard reference fails the run, an unresolvable
181
- argument reference is refused and retried. An `archmax_set_variables` typo
182
- creates a new variable; `requires` is what surfaces it.
183
- - **Models.** `settings.model` names the id every state runs on and a state's
184
- `model` names its own — an **id only**, over the endpoint the assembly is
185
- already configured with (a block with `temperature` is a load error;
186
- sampling is env/host configuration). Precedence: state → workflow →
187
- `ARCHMAX_MODEL`. Author it where the work differs in kind: small models for
188
- classification, extraction and other mechanical states; leave drafting,
189
- judgement and any state with a big tool surface or several transitions on
190
- the default, since a small model that routes badly costs more in retries
191
- than it saves. Repeating the workflow's id on a state is a validate warning.
192
- A grader state's `model` is resolved under the `grader` model role
193
- (`modelFactory("grader", env, id)`), a sub-workflow uses its own spec's, and a
194
- host that passes an explicit `model` instance to `createAgent` outranks
195
- every declared id (it must pass `modelFactory` to honour them).
196
- - **Parking.** A state that needs an event or a time calls
197
- `archmax_wait({ reason, until? })` (`until`: `30m`/`2h`/`1d` or ISO instant →
198
- `resumeAt` for the host's scheduler; the runtime holds no timer). Any
199
- delivered trigger resumes **that same state** with the payload as locked
200
- variables and an `[event]` runtime note; the state then picks its own edge.
201
- Bound a polling loop with `budget.maxParks` + `on_error`. `archmax_wait` is
202
- in every state, terminal ones included. Use a **human state** when a person
203
- picks the edge, `archmax_wait` when an event or time resumes the work.
204
- - **Failing.** When the work cannot be completed, the agent calls
205
- `archmax_raise({ code, reason })` (alone in its message; any state): the
206
- session ends at once `failed`, with the agent's free-form `code` and
207
- `reason` as the outcome's `exit` — no `on_error` route, no `returns`
208
- check. A session that never raises is a success, so it is for failure
209
- only. Forbid it (`forbid_always`, or a state's `forbid`) where the
210
- agent must not end the run itself; a child that raises fails its caller's
211
- call with kind `raised`.
212
- - **Human states** park with a record built from `instructions`, `evidence`
213
- and transition `type`s. The handoff spends one reply-only model call so the
214
- park carries a message — do **not** instruct the agent to announce the
215
- handoff (two messages). While parked, `archmax reply` is answered on a
216
- reply-only turn (every tool call refused) and re-parks unchanged; only
217
- `decide` moves the run. A
218
- `refine` edge delivers the reviewer's comment as correction.
219
- - **Grader and code states** (automated states). A **code** state runs
220
- `script: scripts/<x>.js` in the sandbox; a **grader** state runs an isolated
221
- grader whose criteria are the state's `instructions`. Both read the session
222
- with `archmax_get_history` (`{ messages, trail }`: the whole transcript, a
223
- tool result inline in its tool message's `text`, and the trail; a script
224
- calls `JSON.parse(await tools.archmax_get_history())`). Both run on
225
- entry — however the session got there — with **no call of the agent's model**
226
- (a chain of code states makes none at all), and leave like every state:
227
- `archmax_advance` along one of their transitions, whose `reason` reaches the
228
- next state as a `[code]`/`[grader]` runtime note and a `kind: code|grader`
229
- trail step, or `archmax_raise`. A **terminal** code state completes the
230
- session when its script returns. Throwing, timing out, a spent grader budget,
231
- or a non-terminal state that finishes without either call **fails closed** →
232
- `on_error`. Each is offered exactly its own state's surface (its grants and
233
- one control set for both types: `archmax_advance`, `archmax_raise`, the
234
- variable tools, `archmax_get_history`), governed like the agent's calls — never
235
- `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait`,
236
- `archmax_reset`, even when `allow_always` lists one (a state's own
237
- `tools.allow` naming one is an error): an automated state cannot park.
238
- There is no iteration budget: a state that gives up after N rounds counts
239
- them in the trail. Per type, `validate` errors on: a code state's
240
- `instructions`/`model`/`budget`/`evidence`/`approvers`; a grader without
241
- `instructions` or without a transition, or with `evidence`/`approvers`/
242
- `budget.maxParks`. There are **no `before:`/`after:` hooks and no root
243
- `extensions:`** — a leftover key is a load error naming it. Script contract:
244
- [`code-states-and-scripts.md`](code-states-and-scripts.md).
245
- - **Automated-state patterns.** A deterministic check **in front of** a state:
246
- make the trigger's entry (or the edge's target) a code state that advances
247
- to the work or to a state that declines it. A check **behind** a state: point
248
- the state's transition at a code state that advances onward, or back with
249
- the fix as its `reason` — the agent reads it and advances again. **Approve,
250
- then end**: approve into a terminal code state, which costs no model call.
251
- Judgment (tone, completeness): a grader behind the graded state with an edge
252
- back; its `instructions` say when to raise ("after two send-backs" = two
253
- trail steps `{ kind: grader, to: <graded state> }`). Grade work the agent
254
- hands on (a report, a draft), not a live answer: a turn's `reply` is every
255
- text the agent said in it, so a draft sent back has reached the person.
256
- - **Budgets and `on_error`.** `budget.maxTurns`/`timeoutMs`/`maxParks` bound
257
- a state's segment (on a grader: its own model calls and run time; a code
258
- state takes no `budget`, `settings.timeoutMs` bounds its script). There is
259
- no root `budget` (declaring one is a load error): a whole turn is bounded by
260
- LangGraph's `recursionLimit` (400 unless the host's invoke config sets one),
261
- and a turn that hits it — a cycle between states, say — ends the session
262
- `failed` (`recursion-limit`). A terminal failure (exhausted budget, exhausted parks, a
263
- grader or code state that fails) routes to `on_error` instead of failing the
264
- run; recoverable rejections (invalid edge, unmet `requires`) keep the agent
265
- in place. Declare `on_error` on every automated state and on bounded loops.
266
- A **tool that throws** is neither: the call is answered with an error-status tool message carrying
267
- the error, the agent stays in the state and can retry or switch tools, and
268
- `on_error` never sees it (a park or a cancelled run still propagates).
269
- - **Sub-workflows are tools.** Allow `archmax_workflow_<slug>` (slug verbatim)
270
- and the sibling `workflows/<slug>/` is callable: its `manual` trigger's
271
- `requires` are the parameters (typed where the entries are typed; a
272
- mistyped argument is refused `invalid-param`, and `"${{count}}"` alone
273
- passes the value with its own type), its `returns` the result
274
- (`{ message, returns }`; a child finishing with some unset completes, and
275
- `returns` holds every declared one, the unset ones `null`, plus a `note`
276
- naming them; a mistyped typed return fails `invalid-return`). Returns are
277
- owed only at the finish: no state needs them to be left, a later write
278
- replaces an unlocked earlier one, and a seed of one lands unlocked; lock one
279
- only once it is final. Each failed call is the
280
- calling state's failure, recorded with its arguments and routed through
281
- `on_error` when the agent finishes unless a later successful call of the
282
- same workflow from the same state recovers it first. One success recovers
283
- one failed call: the one whose arguments it repeats (compared after
284
- `${{…}}` resolves, key order ignored), else the oldest. A sibling in the
285
- same batch does not count; another workflow's success recovers nothing;
286
- the route names every outstanding failure with its arguments.
287
- The trigger's `description` leads the tool description. Nothing captures the result — the caller records
288
- what it needs with `archmax_set_variables`, made mandatory by `requires`.
289
- The child is a separate session with a fresh transcript, sees only its
290
- arguments (as locked variables), and shares no prose. One call = one
291
- sub-run; fan out with parallel tool calls or `Promise.all` in a script
292
- (bounds: depth 3, 4 concurrent). A child's human state parks the child;
293
- a script's delegation cannot park and fails closed.
294
- - **Choosing a mechanism.** Deterministic work the agent triggers → an
295
- `archmax_run` script. Deterministic work the graph runs at a fixed point (a
296
- screen, a policy check, a record) → a code state. Delegated work that is itself
297
- a process or reused across workflows → a sub-workflow. Judging work already
298
- done → a grader state. Neither automated kind is callable by the agent (`task`
299
- is disclosed nowhere and not grantable).
300
- - **Prompt economy.** The static prompt + tool schemas are re-sent every
301
- model call. The static part carries the workflow's `title` + `instructions`
302
- and **no state of the graph**; a state's `instructions` and its **own**
303
- outgoing edges are sent only while it is active. So: workflow-wide
304
- conventions go in the root `instructions` (paid once, cached); everything
305
- else goes in the state that needs it, and splitting a broad state into
306
- several narrow ones costs nothing in the prefix. `title` and `summary` are
307
- free — neither reaches a model call. `WORKFLOW.md` carries only what the spec
308
- cannot say; reader diagrams go inside an HTML comment (stripped from the
309
- prompt).
310
-
311
- ## What the agent sees of the graph
312
-
313
- Only the state it is in. Every model call is handed that state's outgoing edges
314
- and nothing else of the machine — no other state's slug, `title`, `summary`,
315
- script, criteria or transitions, and no count of the states:
316
-
317
- ```
318
- Transitions — choose one with `archmax_advance` when this state's work is done:
319
- - to `check-policy` (code) — The decision is recorded in scratchpad/refund.json.
320
- - to `review` (human decision node) — The decision is recorded and needs a person's approval.
321
- - to `grade-reply` (grader) — The email is drafted in scratchpad/reply.md.
322
- - to `closed` (terminal) — Nothing to refund; the case is answered.
323
- ```
324
-
325
- A `(grader)` or `(code)` target runs on its own and may send the run straight
326
- back with a note saying what to change.
327
-
328
- Two rules follow, and they are the ones authors get wrong:
329
-
330
- 1. **Write the `description` for the source state, not the target.** The agent
331
- reading it knows nothing about where it is going. Say *when* the edge applies:
332
- `The refund is over $50, or the customer has asked twice.` Not `Go to
333
- refund-review.` — that tells a reader who already knows the graph something
334
- they cannot use, and tells this reader nothing.
335
- 2. **Routing conditions cannot live downstream.** A target's `summary` is never
336
- disclosed to the state routing into it, nor its `instructions` across a
337
- described edge. If the condition is "when the order is over 30 days old", it
338
- belongs on the edge.
339
-
340
- An **undescribed** edge (no `description`, or a blank one) routes on its target's
341
- `instructions` instead, quoted under the edge's line as what the agent will do
342
- once there: leave one undescribed only when those instructions already say when
343
- it applies. It is a **load error** where the target has no instructions, on a
344
- human state's transitions (the description is the button label), and into a
345
- grader or code state (a grader's instructions are criteria the work is never
346
- shown; a code state has none). A code state's own edges may go undescribed: its
347
- script routes. There is no other way to see more of the graph.
348
-
349
- ## Governance
350
-
351
- Closed by default; **deny beats allow, and no narrower level widens a denial.**
352
-
353
- - **The workflow-wide grant**: `tools.allow_always`, in every state. Absent,
354
- it is the **default tools**: `ls`, `read_file`, `write_file`, `edit_file`,
355
- `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`,
356
- `archmax_eval`, `archmax_run`. Declared (`[]` included), it is exactly what it
357
- lists and **replaces** the default tools, so a new workflow lists all twelve
358
- (as in the skeleton) and removes only what it must not have. Never declared:
359
- the controls `archmax_advance`, `archmax_reset`, `archmax_wait`,
360
- `archmax_raise`, `archmax_get_variables`, `archmax_set_variables`, and a host's
361
- `essentialTools`. Any other tool needs an `allow`/`allow_always` entry. A
362
- state entry naming a granted tool **narrows** it there, and one naming a
363
- default tool the list omits grants it there. `task` is ungrantable (error).
364
- - **In a grader or code state** the surface is what the state is granted (the
365
- workflow-wide grant included), minus `write_todos`, `archmax_eval`,
366
- `archmax_run`, `archmax_wait` and `archmax_reset` even when `allow_always` lists
367
- them (naming one in the state's
368
- own `tools.allow` is an error), plus one control set for both types:
369
- `archmax_advance`, `archmax_raise`, `archmax_get_variables`,
370
- `archmax_set_variables` and `archmax_get_history` — never `archmax_wait` or
371
- `archmax_reset`.
372
- - **Entries**: bare name; `{ tool, args: { <arg>: ["glob", …] } }` (matches
373
- if any glob matches; `${{var}}` allowed). An argument the tool declares as a
374
- path (else `file_path`) is canonicalized first; any other argument (a URL, an
375
- address, an email) is matched **as written**, and a denial also catches its
376
- path form. `""` matches only an empty value. `*` never crosses `/` (`**` does).
377
- `{ tool, paths: [...] }`
378
- (guards every path argument the tool declares — all must match to grant,
379
- any matching denies). `"*"` as the tool is legal only in a
380
- forbid list. Globs match dot-segments (`secrets/**` covers `secrets/.env`).
381
- - **Path rules read declared path arguments**: `copy_file`/`move_file` take
382
- `{ source, destination, overwrite? }` (copy reads `source`, move removes it;
383
- both write `destination`), `remove_file` takes `{ file_path }`. A removal is
384
- refused wherever a write is; a call naming several paths is refused when any
385
- one is. An existing destination is refused unless `overwrite: true`.
386
- `forbid: [{ tool: "*", paths: [secrets/**] }]` blocks a copy in or out.
387
- - **Skills** are governed by **slug**, never path. Enabling a bundle makes
388
- `skills/<slug>/**` readable and its scripts runnable with no `tools.allow`
389
- entry; a disabled bundle is unreachable **and invisible** (not listed, not
390
- in the prompt). `allow` adds, `forbid` subtracts, `allow: []` denies
391
- nothing. A `tools.allow` entry may still narrow *within* an enabled bundle.
392
- A state enabling no skill can run no script at all.
393
- - **Scripts' `tools.*` calls** are governed per call against the state they
394
- run in: an `archmax_run` script is bound like the model (grant every tool it
395
- calls in the agent's state); a code state's script is bound by **its own**
396
- state's grants (`tools`, `skills`, `mounts`) — there is no runtime authority.
397
- `tools` holds exactly that state's surface: anything else is `undefined`.
398
- Neither may read `workflows/**` or write a read-only mount. A refused call
399
- rejects inside the script.