@archmax-ai/harness 1.1.0 → 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.
- package/README.md +167 -170
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +2 -2
- package/dist/assembly/compose.js +2 -2
- package/dist/assembly/compose.js.map +1 -1
- package/dist/authoring-skill/archmax-harness/README.md +13 -15
- package/dist/authoring-skill/archmax-harness/SKILL.md +174 -264
- package/dist/authoring-skill/archmax-harness/references/backend-integration.md +244 -776
- package/dist/authoring-skill/archmax-harness/references/cases.md +178 -53
- package/dist/authoring-skill/archmax-harness/references/code-states-and-scripts.md +211 -100
- package/dist/authoring-skill/archmax-harness/references/diagnostics.md +57 -78
- package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +613 -2006
- package/dist/core/deepagents.d.ts +1 -2
- package/dist/core/deepagents.js +1 -2
- package/dist/core/deepagents.js.map +1 -1
- package/dist/kernel/kernel.js +2 -2
- package/dist/kernel/kernel.js.map +1 -1
- package/dist/machine/lint-spec.js +6 -5
- package/dist/machine/lint-spec.js.map +1 -1
- package/dist/machine/machine.d.ts +8 -10
- package/dist/machine/machine.js +11 -12
- package/dist/machine/machine.js.map +1 -1
- package/dist/machine/spec-schema.d.ts +1 -4
- package/dist/machine/spec-schema.js +66 -47
- package/dist/machine/spec-schema.js.map +1 -1
- package/dist/machine/tool-names.d.ts +24 -34
- package/dist/machine/tool-names.js +43 -63
- package/dist/machine/tool-names.js.map +1 -1
- package/dist/machine/types.d.ts +11 -10
- package/dist/machine/types.js.map +1 -1
- package/dist/public/spec.d.ts +5 -5
- package/dist/public/spec.js +5 -5
- package/dist/public/spec.js.map +1 -1
- package/dist/validate/validate.js +3 -3
- package/dist/validate/validate.js.map +1 -1
- package/dist/workflow/automated.js +6 -1
- package/dist/workflow/automated.js.map +1 -1
- package/dist/workflow/control-tools.js +0 -2
- package/dist/workflow/control-tools.js.map +1 -1
- package/dist/workflow/grader-state.d.ts +2 -2
- package/dist/workflow/grader-state.js +5 -15
- package/dist/workflow/grader-state.js.map +1 -1
- package/dist/workflow/middleware.js +2 -26
- package/dist/workflow/middleware.js.map +1 -1
- package/dist/workflow/on-error.js +0 -1
- package/dist/workflow/on-error.js.map +1 -1
- package/dist/workflow/parks.js +0 -1
- package/dist/workflow/parks.js.map +1 -1
- package/dist/workflow/render-prompt.d.ts +4 -5
- package/dist/workflow/render-prompt.js +24 -27
- package/dist/workflow/render-prompt.js.map +1 -1
- package/dist/workflow/state.d.ts +0 -9
- package/dist/workflow/state.js +0 -3
- package/dist/workflow/state.js.map +1 -1
- package/dist/workflow/turn-boundary.js +0 -1
- package/dist/workflow/turn-boundary.js.map +1 -1
- package/package.json +1 -2
- package/skills/archmax-harness/README.md +13 -15
- package/skills/archmax-harness/SKILL.md +174 -264
- package/skills/archmax-harness/references/backend-integration.md +244 -776
- package/skills/archmax-harness/references/cases.md +178 -53
- package/skills/archmax-harness/references/code-states-and-scripts.md +211 -100
- package/skills/archmax-harness/references/diagnostics.md +57 -78
- package/skills/archmax-harness/references/workflow-schema.md +613 -2006
- package/dist/authoring-skill/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
- package/dist/authoring-skill/archmax-harness/references/wiring.md +0 -119
- package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +0 -402
- package/skills/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
- package/skills/archmax-harness/references/wiring.md +0 -119
- 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.
|