@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.
- package/README.md +167 -165
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +2 -2
- package/dist/assembly/compose.js +5 -3
- 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 -261
- package/dist/authoring-skill/archmax-harness/references/backend-integration.md +244 -764
- 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 +614 -1977
- package/dist/core/attachments.d.ts +2 -0
- package/dist/core/attachments.js +2 -1
- package/dist/core/attachments.js.map +1 -1
- 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/core/file-operations.d.ts +6 -0
- package/dist/core/file-operations.js +29 -0
- package/dist/core/file-operations.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/index.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/signature-pattern.d.ts +24 -0
- package/dist/machine/signature-pattern.js +67 -0
- package/dist/machine/signature-pattern.js.map +1 -0
- package/dist/machine/signature.d.ts +137 -35
- package/dist/machine/signature.js +476 -25
- package/dist/machine/signature.js.map +1 -1
- package/dist/machine/spec-schema.d.ts +203 -5
- package/dist/machine/spec-schema.js +209 -51
- 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 +10 -6
- package/dist/public/spec.js +9 -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 +3 -3
- package/dist/workflow/control-tools.js.map +1 -1
- package/dist/workflow/file-values.d.ts +4 -1
- package/dist/workflow/file-values.js +2 -0
- package/dist/workflow/file-values.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.d.ts +2 -0
- package/dist/workflow/middleware.js +3 -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 +31 -29
- package/dist/workflow/render-prompt.js.map +1 -1
- package/dist/workflow/signature-checks.d.ts +8 -3
- package/dist/workflow/signature-checks.js +17 -4
- package/dist/workflow/signature-checks.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/sub-workflow.js +4 -1
- package/dist/workflow/sub-workflow.js.map +1 -1
- package/dist/workflow/turn-boundary.js +0 -1
- package/dist/workflow/turn-boundary.js.map +1 -1
- package/package.json +2 -2
- package/skills/archmax-harness/README.md +13 -15
- package/skills/archmax-harness/SKILL.md +174 -261
- package/skills/archmax-harness/references/backend-integration.md +244 -764
- 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 +614 -1977
- 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 -399
- 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 -399
|
@@ -1,2040 +1,677 @@
|
|
|
1
|
-
# workflow.yaml
|
|
1
|
+
# `workflow.yaml` reference
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Load this when you write or edit a `workflow.yaml`. It holds every key, the rules the
|
|
4
|
+
machine enforces, and governance. How grader and code states run, and how to write
|
|
5
|
+
scripts, is in [`code-states-and-scripts.md`](code-states-and-scripts.md). Cases are in
|
|
6
|
+
[`cases.md`](cases.md).
|
|
7
|
+
|
|
8
|
+
The schema is strict: an unknown key anywhere is a load error that names the key. The
|
|
9
|
+
one free slot is `metadata`. A trigger declaration also keeps a host's own keys, with a
|
|
10
|
+
warning.
|
|
6
11
|
|
|
7
12
|
## Contents
|
|
8
13
|
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
- [Transitions](#transitions)
|
|
14
|
+
- [The skeleton](#the-skeleton)
|
|
15
|
+
- [Files](#files)
|
|
16
|
+
- [Root keys](#root-keys)
|
|
17
|
+
- [State keys](#state-keys)
|
|
18
|
+
- [Transitions and what the agent sees](#transitions-and-what-the-agent-sees)
|
|
19
|
+
- [Triggers and signatures](#triggers-and-signatures)
|
|
20
|
+
- [Variables](#variables)
|
|
14
21
|
- [Tool governance](#tool-governance)
|
|
15
22
|
- [Skill governance](#skill-governance)
|
|
16
23
|
- [Mount governance](#mount-governance)
|
|
17
|
-
- [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [Workspace layout: skills, scripts](#workspace-layout)
|
|
25
|
-
- [Cases](#cases)
|
|
26
|
-
- [Code description docblocks](#code-description-docblocks)
|
|
27
|
-
- [Read-only zones](#read-only-zones)
|
|
28
|
-
- [Annotated example: order-lookup](#annotated-example-order-lookup)
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## File shape
|
|
33
|
-
|
|
34
|
-
**Two-file layout** under `workflows/<name>/`:
|
|
35
|
-
|
|
36
|
-
- **`workflow.yaml`** — the canonical, enforced machine spec: a **pure YAML
|
|
37
|
-
mapping** (no `---` frontmatter fences, no markdown). Parsed with YAML 1.2
|
|
38
|
-
semantics and validated against the schema. Top-level keys: `title`,
|
|
39
|
-
`instructions`, `disabled`, `runtime`, `settings`, `tests`, `tools`, `skills`,
|
|
40
|
-
`mounts`, `metadata`, `states`.
|
|
41
|
-
- **`WORKFLOW.md`** (optional, beside it) — a plain-markdown prose addendum with
|
|
42
|
-
**no frontmatter**. A frontmatter block in `WORKFLOW.md` when `workflow.yaml`
|
|
43
|
-
exists is a **validation error**. The runtime renders the system prompt's
|
|
44
|
-
workflow header deterministically from the spec (`title` + `instructions`, and
|
|
45
|
-
NO state of the graph) and appends the `WORKFLOW.md` prose after it — prose is
|
|
46
|
-
never load-bearing and cannot disagree with enforcement. The graph is disclosed
|
|
47
|
-
per model call, for the ACTIVE STATE ONLY.
|
|
48
|
-
There is no "frontmatter wins" rule anymore; `workflow.yaml` is the only
|
|
49
|
-
authority.
|
|
50
|
-
|
|
51
|
-
**Removed**: the single-file `WORKFLOW.md` layout (YAML frontmatter + markdown
|
|
52
|
-
body, without a `workflow.yaml`) no longer loads — it is a load error. Migrate by
|
|
53
|
-
moving the frontmatter into `workflow.yaml` and stripping the fences from the
|
|
54
|
-
prose.
|
|
55
|
-
|
|
56
|
-
## Root fields
|
|
57
|
-
|
|
58
|
-
```yaml
|
|
59
|
-
title?: string # the workflow's HUMAN-READABLE name (metadata only).
|
|
60
|
-
# Identity is the WORKFLOW SLUG — the directory name
|
|
61
|
-
# (`workflows/<slug>/`, also the CLI argument), authored
|
|
62
|
-
# in the SAME kebab-case as state slugs
|
|
63
|
-
# (`order-lookup`); validate ERRORS on a missing or
|
|
64
|
-
# non-kebab workflow slug, exactly as for a state slug.
|
|
65
|
-
# A top-level `name:` is REMOVED — it is
|
|
66
|
-
# a load/validate ERROR; rename it to `title:`.
|
|
67
|
-
instructions?: string # the workflow's STANDING instructions — free-form
|
|
68
|
-
# markdown, direction that holds for the WHOLE run
|
|
69
|
-
# (conventions, house rules, what "done" means).
|
|
70
|
-
# Author it as a LITERAL block (`|-`), not folded
|
|
71
|
-
# (`>-`): it renders as markdown, and folding
|
|
72
|
-
# collapses blank lines into one run-on paragraph.
|
|
73
|
-
# Rendered verbatim as an `## Instructions` block at
|
|
74
|
-
# the top of the workflow prompt section (unlike a
|
|
75
|
-
# state's `instructions`, which are per model call) —
|
|
76
|
-
# so it lands in the static,
|
|
77
|
-
# cacheable prefix. validate WARNS when absent,
|
|
78
|
-
# ERRORS when present but not a non-empty string.
|
|
79
|
-
# A top-level `description:` is REMOVED and now
|
|
80
|
-
# simply ignored; use `instructions:`, rereading the
|
|
81
|
-
# text as direction, not narration.
|
|
82
|
-
# A test case's `description` is UNAFFECTED. A
|
|
83
|
-
# TRANSITION's `description` is what the agent routes
|
|
84
|
-
# on (see `transitions` below).
|
|
85
|
-
|
|
86
|
-
disabled?: boolean # `true` = this machine STARTS NO NEW TURNS. Bounds
|
|
87
|
-
# STARTING, not finishing.
|
|
88
|
-
# REFUSES: `archmax run` (one line, non-zero exit, no
|
|
89
|
-
# session minted); a host `invoke`; a firing on a fresh
|
|
90
|
-
# session; a FOLLOW-UP message to a settled session
|
|
91
|
-
# (a new turn is new work) — all rejected AT THE TURN
|
|
92
|
-
# BOUNDARY with `status: rejected` and
|
|
93
|
-
# "Refusing to start: workflow '<slug>' is disabled.",
|
|
94
|
-
# before any state or model call; and a caller's
|
|
95
|
-
# `archmax_workflow_<slug>` call (blocked call, refusal
|
|
96
|
-
# kind `disabled`, before a child is composed — it also
|
|
97
|
-
# beats a declared MOCK, so a case can never assert a
|
|
98
|
-
# delegation production would refuse).
|
|
99
|
-
# STILL WORKS: assembly; `decide` / `reply` / `deliver`
|
|
100
|
-
# (each resumes an unfinished turn, so parked human
|
|
101
|
-
# decisions are never trapped); `archmax validate` (full,
|
|
102
|
-
# plus a warning); and a CALLER's assembly — the tool is
|
|
103
|
-
# still bound, so disabling one leaf never takes its
|
|
104
|
-
# callers offline (validate WARNS on each state that
|
|
105
|
-
# allows a disabled target).
|
|
106
|
-
# `archmax test` SKIPS the suite and exits 0 — not run is
|
|
107
|
-
# not failed. There is NO flag to force cases to run.
|
|
108
|
-
# Read FAIL-CLOSED: anything but an absent key, `null` or
|
|
109
|
-
# boolean `false` disables the workflow, so `disabled:
|
|
110
|
-
# "no"` STOPS runs; validate ERRORS on a non-boolean.
|
|
111
|
-
# NOT a kill switch for work in flight, and NOT a
|
|
112
|
-
# per-state/per-trigger switch (that is a forbid list).
|
|
113
|
-
|
|
114
|
-
runtime?:
|
|
115
|
-
engine?: string # "archmax-harness"
|
|
116
|
-
version?: string | number # target contract — "3", the only one supported (and
|
|
117
|
-
# the default when omitted). "1"/"2" are refused as
|
|
118
|
-
# an unsupported runtime contract, naming the
|
|
119
|
-
# supported set.
|
|
120
|
-
|
|
121
|
-
# There is NO root `budget`: declaring one is an unknown-key load error
|
|
122
|
-
# (`Unrecognized key: "budget"`). A whole turn — one message, decision, reply or
|
|
123
|
-
# delivery, across every state — is bounded by LangGraph's `recursionLimit`:
|
|
124
|
-
# 400 unless the host's invoke config sets its own, which wins. A turn that hits
|
|
125
|
-
# it ends the session `failed` (exit code `recursion-limit`). A state's own
|
|
126
|
-
# `budget` bounds that state (see Budgets & error routing).
|
|
127
|
-
|
|
128
|
-
settings?:
|
|
129
|
-
model?: string # MODEL ID ONLY — the id every state of this workflow runs
|
|
130
|
-
# on, over the endpoint/credentials the assembly already
|
|
131
|
-
# has. Overrides ARCHMAX_MODEL; a state's `model` overrides
|
|
132
|
-
# it. NOT a block: `{ id, temperature }` is a load error —
|
|
133
|
-
# sampling and endpoint are env/host configuration.
|
|
134
|
-
# Opaque: nothing validates the id against a list.
|
|
135
|
-
# A grader state runs on it too unless it declares its
|
|
136
|
-
# own `model`. Does NOT change the case judge
|
|
137
|
-
# (tests.judge.model).
|
|
138
|
-
timeoutMs?: number # default 15000; per sandbox evaluation — one
|
|
139
|
-
# archmax_eval/archmax_run call, or a code state's
|
|
140
|
-
# whole script
|
|
141
|
-
memoryLimitBytes?: number
|
|
142
|
-
maxPtcCalls?: number | null
|
|
143
|
-
maxResultChars?: number
|
|
144
|
-
prompt_cache?:
|
|
145
|
-
enabled?: boolean # default true (provider prompt caching)
|
|
146
|
-
ttl?: "5m" | "1h" # default "5m"
|
|
147
|
-
|
|
148
|
-
tests?: # cases suite config (declarative data;
|
|
149
|
-
# tests.config.js is REMOVED — see Cases)
|
|
150
|
-
maxConcurrency?: number # only 1 (or absent) accepted — concurrency is
|
|
151
|
-
# not implemented; >1 is a validate error and
|
|
152
|
-
# fails `archmax test` loudly
|
|
153
|
-
caseTimeoutMs?: number # per-case WALL-CLOCK budget, default 120000 —
|
|
154
|
-
# deliberately named differently from
|
|
155
|
-
# settings.timeoutMs (per-script sandbox budget)
|
|
156
|
-
judge?: # {} = enable the case judge (`grade:`) with the
|
|
157
|
-
model?: string # env-configured default model — unrelated to
|
|
158
|
-
# grader states
|
|
159
|
-
modelOptions?: { temperature?: number, maxTokens?: number }
|
|
160
|
-
# applied over the env model config, via the
|
|
161
|
-
# factory's `judge` role; other keys are errors
|
|
162
|
-
|
|
163
|
-
tools?:
|
|
164
|
-
allow_always?: AllowEntry[] # the WORKFLOW-WIDE GRANT, in every state. ABSENT = the
|
|
165
|
-
# default tools (ls, read_file, write_file, edit_file,
|
|
166
|
-
# glob, grep, copy_file, move_file, remove_file,
|
|
167
|
-
# write_todos, archmax_eval, archmax_run). DECLARED
|
|
168
|
-
# ([] included) = exactly what it lists: it REPLACES
|
|
169
|
-
# the default tools, so list each one you want. A
|
|
170
|
-
# new workflow lists all twelve.
|
|
171
|
-
forbid_always?: ForbidEntry[]
|
|
172
|
-
# DENIED in EVERY state, and absolute: nothing reaches
|
|
173
|
-
# past it — no state grant, allow_always entry,
|
|
174
|
-
# argument guard or consumer rule. Same entry
|
|
175
|
-
# grammar as allow, plus the tool "*" meaning
|
|
176
|
-
# EVERY tool: { tool: "*", paths: ["logs/**"] }
|
|
177
|
-
# denies that path to everything. "*" in an
|
|
178
|
-
# ALLOW list is a load error.
|
|
179
|
-
# Guards narrow what is denied; globs match
|
|
180
|
-
# dotfiles (secrets/** covers secrets/.env).
|
|
181
|
-
# Inherited by every sub-workflow, naming the
|
|
182
|
-
# workflow that declared it. There is NO
|
|
183
|
-
# `policy:` block — writing one is a load error.
|
|
184
|
-
|
|
185
|
-
skills?:
|
|
186
|
-
allow_always?: string[] # SKILL SLUGS enabled in EVERY state of this workflow —
|
|
187
|
-
# the bundle directory names under skills/, kebab-case.
|
|
188
|
-
# NEVER paths, NEVER globs: the unit you govern is the
|
|
189
|
-
# capability. This is the skills half of
|
|
190
|
-
# tools.allow_always: a GRANT, which a state's own
|
|
191
|
-
# `skills.allow` ADDS to (the enabled set is the UNION),
|
|
192
|
-
# so no state repeats it and no state can subtract from
|
|
193
|
-
# it. ABSENT = none granted workflow-wide, exactly like
|
|
194
|
-
# `allow_always: []` — write the empty list when every
|
|
195
|
-
# capability here is state-specific.
|
|
196
|
-
# validate ERRORS on: a slug no source provides (it
|
|
197
|
-
# lists the known ones), a path/glob/non-string entry,
|
|
198
|
-
# a bundle whose SKILL.md `name:` ≠ its directory; and
|
|
199
|
-
# WARNS on a state entry this list already grants
|
|
200
|
-
# everywhere, and on a state's `allow: []` beside it
|
|
201
|
-
# (an empty state list subtracts nothing).
|
|
202
|
-
forbid_always?: string[] # SKILL SLUGS out of reach of EVERY state, whatever any
|
|
203
|
-
# list grants. Deny beats allow. (The retired root
|
|
204
|
-
# `allow` ceiling is a load error: one skills model.)
|
|
205
|
-
|
|
206
|
-
mounts?:
|
|
207
|
-
allow_always?: MountGrant[] # MOUNT NAMES reachable in EVERY state of this workflow —
|
|
208
|
-
# the keys of the HOST's mount table with their slashes
|
|
209
|
-
# stripped (`reference`, `catalogs/eu`, `AGENTS.md`).
|
|
210
|
-
# NEVER globs: scope paths WITHIN a reachable mount with
|
|
211
|
-
# a tools.allow entry. Only a mount the host declared
|
|
212
|
-
# `governed: true` needs a grant — an UNGOVERNED mount is
|
|
213
|
-
# visible in every state already, and a grant naming one
|
|
214
|
-
# is inert (validate warns). The skills model exactly: a
|
|
215
|
-
# GRANT a state's `mounts.allow` ADDS to (the reachable
|
|
216
|
-
# set is the UNION), never a ceiling. ABSENT = this
|
|
217
|
-
# workflow reaches no governed mount; validate says so
|
|
218
|
-
# ONCE, naming them — write `allow_always: []` to mean it.
|
|
219
|
-
# An entry is a NAME, or `{ mount: <name>, access: read |
|
|
220
|
-
# read_write }` to say what this level gives. A bare NAME
|
|
221
|
-
# asks for NO write: a governed mount is written only
|
|
222
|
-
# where some grant of THIS workflow says `read_write`
|
|
223
|
-
# (a child's or caller's grant never counts). The host's
|
|
224
|
-
# readOnly is a CEILING: `read_write` on a read-only mount
|
|
225
|
-
# opens nothing (validate warns); `read` NARROWS a
|
|
226
|
-
# writable one, and no narrower level widens it back.
|
|
227
|
-
# validate ERRORS on a name the table does not mount.
|
|
228
|
-
forbid_always?: string[] # MOUNT NAMES out of reach of EVERY state, governed or
|
|
229
|
-
# not, whatever any list grants. Deny beats allow.
|
|
230
|
-
# Binds this workflow's grader and code states too,
|
|
231
|
-
# and is inherited by every sub-workflow, naming the workflow
|
|
232
|
-
# that declared it. A NAME only — a denial is total, so
|
|
233
|
-
# there is no `access` to qualify. (A root `mounts.allow`
|
|
234
|
-
# is a load error: one mounts model.)
|
|
235
|
-
|
|
236
|
-
metadata?: Record<string, unknown> # HOST PRESENTATION METADATA — an authoring
|
|
237
|
-
# UI's own state (canvas zoom, pan). Also accepted on
|
|
238
|
-
# a state (its position). The one key the runtime
|
|
239
|
-
# never reads: not shape-checked, not prompted,
|
|
240
|
-
# not governed, and excluded from the spec hash.
|
|
241
|
-
# Do NOT author it by hand and do NOT invent a
|
|
242
|
-
# use for it — but PRESERVE it verbatim when you
|
|
243
|
-
# rewrite a spec that already carries one, or the
|
|
244
|
-
# builder that wrote it loses every state position.
|
|
245
|
-
# The root is otherwise STRICT: any other
|
|
246
|
-
# unrecognized key is a load error — `extensions:`
|
|
247
|
-
# included.
|
|
248
|
-
|
|
249
|
-
states: Record<slug, MachineState> # REQUIRED; ≥1 state, ≥1 with a trigger
|
|
250
|
-
# the KEY is the state's SLUG (see below)
|
|
251
|
-
```
|
|
24
|
+
- [Moving, parking and failing](#moving-parking-and-failing)
|
|
25
|
+
- [Budgets and `on_error`](#budgets-and-on_error)
|
|
26
|
+
- [Sub-workflows](#sub-workflows)
|
|
27
|
+
- [Models](#models)
|
|
28
|
+
- [Prompt economy](#prompt-economy)
|
|
29
|
+
- [Removed keys](#removed-keys)
|
|
30
|
+
- [Paths and zones](#paths-and-zones)
|
|
252
31
|
|
|
253
|
-
##
|
|
32
|
+
## The skeleton
|
|
254
33
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
`archmax_advance`, emitted events, session artifacts, and cases
|
|
258
|
-
`reachedState` assertions are all slugs. Author slugs as **hyphen-separated kebab-case** — lowercase alphanumeric
|
|
259
|
-
segments joined by single hyphens (`^[a-z0-9]+(-[a-z0-9]+)*$`, e.g.
|
|
260
|
-
`identify-case`, `report-requested`); no underscores, capitals, doubled hyphens,
|
|
261
|
-
or leading/trailing hyphens —
|
|
262
|
-
`validate` reports a nonconforming slug — or a state declared under an empty key,
|
|
263
|
-
which has no identity at all — as an **error**. A slug is a reference target, so
|
|
264
|
-
rename one only deliberately, and update every reference to it.
|
|
34
|
+
Every key, in a workflow that validates clean. Start a new workflow from it and delete
|
|
35
|
+
what you do not need.
|
|
265
36
|
|
|
266
37
|
```yaml
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
# variable, guarded or not. One owner per variable;
|
|
287
|
-
# never `trigger`/`title`; not on a human state.
|
|
288
|
-
# `lock: true` is refused for an owned variable, and a
|
|
289
|
-
# host-supplied value with the same name stays final.
|
|
290
|
-
# Pair with `requires` to make the owner set it.
|
|
291
|
-
title?: string # HUMAN-READABLE label. Presentational only: carried
|
|
292
|
-
# in a human state's pending-decision record and shown
|
|
293
|
-
# in host surfaces. REACHES NO MODEL CALL. Never a
|
|
294
|
-
# routing target — `to` always takes a slug — so
|
|
295
|
-
# rewording it changes no behavior. Not unique.
|
|
296
|
-
# NOT a substitute for `instructions` (behavior
|
|
297
|
-
# guidance) or a transition `description` (routing).
|
|
298
|
-
|
|
299
|
-
type?: "agent" | "human" | "grader" | "code"
|
|
300
|
-
# default "agent" = the session's agent works here.
|
|
301
|
-
# "human" = a person routes.
|
|
302
|
-
# "grader" = an isolated grader judges the session
|
|
303
|
-
# and decides the edge (see Grader and code states).
|
|
304
|
-
# "code" = the runtime runs `script` and the
|
|
305
|
-
# script decides the edge.
|
|
306
|
-
# grader + code = AUTOMATED states: run on entry
|
|
307
|
-
# with no call of the agent's model.
|
|
308
|
-
# (`workflow` was removed: a state CALLS another
|
|
309
|
-
# workflow by allowing `archmax_workflow_<slug>`.)
|
|
310
|
-
|
|
311
|
-
script?: string # CODE STATES ONLY, and REQUIRED there: the file the
|
|
312
|
-
# state runs, workflow-relative inside `scripts/`,
|
|
313
|
-
# ending .js or .mjs, no `..` segment —
|
|
314
|
-
# script: scripts/check-refund.js
|
|
315
|
-
# resolves to workflows/<slug>/scripts/check-refund.js
|
|
316
|
-
# on the authoring plane. A code state without it,
|
|
317
|
-
# or `script` on any other type, is a LOAD ERROR.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
triggers?: Record<triggerId, TriggerDeclaration | null>
|
|
321
|
-
# marks a start state. The KEY is the trigger id
|
|
322
|
-
# and THIS state is that trigger's entry state —
|
|
323
|
-
# the one place a trigger is declared.
|
|
324
|
-
# manual: = CLI/Deep-Agent entry
|
|
325
|
-
# (prompt = input)
|
|
326
|
-
# <other id>: = tool/event trigger
|
|
327
|
-
# a null/empty value = the id and nothing more
|
|
328
|
-
# SEVERAL keys = the state is the entry for EVERY
|
|
329
|
-
# id, in declaration order (one state, one
|
|
330
|
-
# behavior, several ways in).
|
|
331
|
-
# The value is the declaration (see below):
|
|
332
|
-
# session/message/connection/description/
|
|
333
|
-
# requires/returns
|
|
334
|
-
# plus any key the HOST adds — preserved,
|
|
335
|
-
# lint-warned, never interpreted.
|
|
336
|
-
# NO `entry:` and NO `name:` inside a declaration:
|
|
337
|
-
# the state it sits on IS the entry and the key
|
|
338
|
-
# it sits under IS the id. Either one fails load
|
|
339
|
-
# naming the key. There is also no root
|
|
340
|
-
# `triggers:` block.
|
|
341
|
-
# An id is AUTHORED, so deleting or reordering a
|
|
342
|
-
# sibling never moves it — hosts key deployments
|
|
343
|
-
# on that id.
|
|
344
|
-
# two states declaring the same trigger id is a
|
|
345
|
-
# validation ERROR (start states must be
|
|
346
|
-
# unambiguous). A trigger has ONE entry state; a
|
|
347
|
-
# mapping with several keys is several triggers
|
|
348
|
-
# entering one state. A repeated id on one state
|
|
349
|
-
# is impossible — YAML mappings cannot repeat a key.
|
|
350
|
-
# A shared entry state has ONE behavior: branch on
|
|
351
|
-
# `${{trigger}}` / `args.trigger` where the paths
|
|
352
|
-
# differ, or use separate states.
|
|
353
|
-
|
|
354
|
-
summary?: string # ONE-LINE label for AUTHORS AND HOSTS (an editor's
|
|
355
|
-
# node caption, a generated diagram). RENDERED INTO
|
|
356
|
-
# NO PART OF THE PROMPT — the model never reads it,
|
|
357
|
-
# so it is free, and it can NOT carry routing logic.
|
|
358
|
-
# The routing text the agent reads is the SOURCE
|
|
359
|
-
# state's transition `description`s, or a target's
|
|
360
|
-
# `instructions` where an edge is undescribed; a
|
|
361
|
-
# target's summary is never disclosed.
|
|
362
|
-
|
|
363
|
-
instructions?: string # behavior guidance.
|
|
364
|
-
# agent state: surfaced to the LLM while active.
|
|
365
|
-
# Write the state's behavior INLINE here; a
|
|
366
|
-
# skill bundle (skills/<slug>/) is a capability
|
|
367
|
-
# with data or scripts, enabled by slug — not a
|
|
368
|
-
# per-state instruction sheet.
|
|
369
|
-
# human state: REQUIRED — the decision for the packet
|
|
370
|
-
# grader state: REQUIRED — its criteria, the
|
|
371
|
-
# grader's system prompt after a compiled-in
|
|
372
|
-
# preamble; never shown to the agent
|
|
373
|
-
# code state: a validate ERROR (nothing reads it)
|
|
374
|
-
# There is NO `before:`/`after:` on a state: hooks
|
|
375
|
-
# are removed, and a leftover key is a load error
|
|
376
|
-
# naming it (rewrite it as a grader or code state).
|
|
377
|
-
|
|
378
|
-
tools?:
|
|
379
|
-
allow?: AllowEntry[] # grants beyond the workflow-wide grant (plus the
|
|
380
|
-
# always-allowed archmax_advance); an entry naming
|
|
381
|
-
# a tool that grant includes NARROWS it, and one
|
|
382
|
-
# naming a default tool a declared allow_always
|
|
383
|
-
# leaves out GRANTS it here.
|
|
384
|
-
forbid?: ForbidEntry[] # what this state DENIES while active — beating the
|
|
385
|
-
# workflow-wide grant, this state's own allow,
|
|
386
|
-
# and a host's essentialTools alike. Deny beats allow.
|
|
387
|
-
# Not inherited by a sub-workflow. Binds a grader's
|
|
388
|
-
# and a code state's calls exactly as the agent's.
|
|
389
|
-
|
|
390
|
-
skills?:
|
|
391
|
-
allow?: string[] # which CAPABILITIES this state ADDS, by SLUG — on top
|
|
392
|
-
# of the root's `skills.allow_always`, which is enabled
|
|
393
|
-
# here whatever this says. ABSENT = adds none, the same
|
|
394
|
-
# as `allow: []`. An empty list is NOT a deny (validate
|
|
395
|
-
# warns about one that reads like it) — `forbid` is.
|
|
396
|
-
forbid?: string[] # SLUGS this state does NOT get, subtracting from the
|
|
397
|
-
# workflow's allow_always and from its own allow.
|
|
398
|
-
# Refused as `skill.forbidden` (vs `skill.not-allowed`
|
|
399
|
-
# where nothing granted it).
|
|
400
|
-
# Enabling a slug is a GRANT: skills/<slug>/** becomes
|
|
401
|
-
# readable and its scripts runnable with NO tools.allow
|
|
402
|
-
# path entry. Not enabling one makes the bundle
|
|
403
|
-
# unreachable (read/ls/glob/grep/archmax_run all
|
|
404
|
-
# blocked, rule `skill.not-allowed`, above every
|
|
405
|
-
# declaration) AND undisclosed — its slug and
|
|
406
|
-
# description never enter this state's prompt, and a
|
|
407
|
-
# listing of skills/ does not show it.
|
|
408
|
-
# A tools.allow entry still NARROWS within the enabled
|
|
409
|
-
# set: the two compose as enabled AND allowed.
|
|
410
|
-
# A grader or code state is bound like the agent: a
|
|
411
|
-
# code state reading skills/order-data/assets/orders.json
|
|
412
|
-
# enables `order-data` in ITS OWN list. To gate the
|
|
413
|
-
# agent's use of a capability while a check still
|
|
414
|
-
# reads it, enable it on the check's state only.
|
|
415
|
-
|
|
416
|
-
mounts?:
|
|
417
|
-
allow?: MountGrant[] # which MOUNTS this state ADDS, by NAME — on top of the
|
|
418
|
-
# root's `mounts.allow_always`, which is reachable here
|
|
419
|
-
# whatever this says. ABSENT = adds none, the same as
|
|
420
|
-
# `allow: []`. An empty list is NOT a deny (validate
|
|
421
|
-
# warns about one that reads like it) — `forbid` is.
|
|
422
|
-
# An entry may name its access:
|
|
423
|
-
# allow: [{ mount: shared, access: read }]
|
|
424
|
-
# reads a writable mount without writing it here, and
|
|
425
|
-
# allow: [{ mount: shared, access: read_write }]
|
|
426
|
-
# asks to write it — which a bare name never does. The
|
|
427
|
-
# host's posture is the ceiling — `read_write` cannot
|
|
428
|
-
# open a read-only mount — and a `read` at either level
|
|
429
|
-
# holds, so a state never widens what the workflow
|
|
430
|
-
# narrowed. A write refused by THIS key is
|
|
431
|
-
# `mount.read-only`; one refused by the WIRING is
|
|
432
|
-
# `zone.read-only`.
|
|
433
|
-
forbid?: string[] # MOUNT NAMES this state does NOT get, subtracting from
|
|
434
|
-
# the workflow's allow_always, from its own allow, and
|
|
435
|
-
# from an UNGOVERNED mount's standing visibility alike.
|
|
436
|
-
# Refused as `mount.forbidden` (vs `mount.not-allowed`
|
|
437
|
-
# where nothing enabled it).
|
|
438
|
-
# A governed mount this state does not have is
|
|
439
|
-
# unreachable (read/ls/glob/grep all blocked, above
|
|
440
|
-
# every declaration) AND undisclosed — absent from the
|
|
441
|
-
# volatile prompt block, and filtered out of every
|
|
442
|
-
# ls/glob/grep result, the WORKSPACE ROOT LISTING
|
|
443
|
-
# included. Binds a grader's and a code state's
|
|
444
|
-
# calls exactly as the agent's: grant the mount on
|
|
445
|
-
# the automated state that reads it.
|
|
446
|
-
|
|
447
|
-
model?: string # MODEL ID ONLY — this state's turns run on it (and the
|
|
448
|
-
# handoff message of a park it holds). Most specific of
|
|
449
|
-
# state.model > settings.model > the assembly's default
|
|
450
|
-
# (createAgent `model`/`modelFactory`, else ARCHMAX_MODEL).
|
|
451
|
-
# Same id as settings.model ⇒ validate WARNS (inert).
|
|
452
|
-
# On a GRADER state: the id the grader runs on, resolved
|
|
453
|
-
# under the `grader` model role
|
|
454
|
-
# (modelFactory("grader", env, id)). On a CODE state: a
|
|
455
|
-
# validate ERROR. A sub-workflow resolves its OWN
|
|
456
|
-
# spec's ids — nothing is inherited across it.
|
|
457
|
-
# A host passing an explicit `model` INSTANCE to
|
|
458
|
-
# createAgent outranks every declared id (one assembly
|
|
459
|
-
# warning names them); `modelFactory` is what honours one.
|
|
460
|
-
|
|
461
|
-
budget?: # per-state execution budget
|
|
462
|
-
maxTurns?: number # bounds this turn's agent loop (on a grader: the
|
|
463
|
-
# grader's own model calls)
|
|
464
|
-
timeoutMs?: number # aborts the turn when exceeded (on a grader: its run)
|
|
465
|
-
maxParks?: number # bounds archmax_wait parks here (see Parking); an
|
|
466
|
-
# ERROR on a grader, which cannot park
|
|
467
|
-
# Any `budget` on a CODE state is a validate ERROR:
|
|
468
|
-
# settings.timeoutMs bounds its script.
|
|
469
|
-
on_error?: string # state to route to on a TERMINAL failure
|
|
470
|
-
# (see Budgets & error routing)
|
|
471
|
-
|
|
472
|
-
transitions?: Transition[] # outgoing edges. EMPTY/absent ⇒ terminal state.
|
|
473
|
-
# `parallel:`/`join:` are REMOVED — a run traverses
|
|
474
|
-
# one state at a time; sequence what were branches.
|
|
475
|
-
#
|
|
476
|
-
# Transition = {
|
|
477
|
-
# to: string # a declared state's SLUG
|
|
478
|
-
# description?: string # what the agent routes on
|
|
479
|
-
# type?: "approve" | "reject" | "refine" | "none"
|
|
480
|
-
# }
|
|
481
|
-
#
|
|
482
|
-
# THE AGENT SEES THIS STATE'S EDGES AND NOTHING
|
|
483
|
-
# ELSE OF THE GRAPH — no other state's slug, title,
|
|
484
|
-
# summary, script, criteria or transitions, and no
|
|
485
|
-
# count of the states. So `description` is what it
|
|
486
|
-
# knows about where an edge leads. An UNDESCRIBED
|
|
487
|
-
# edge (absent/empty/blank) routes on the TARGET's
|
|
488
|
-
# `instructions` instead: the edge's line carries
|
|
489
|
-
# them, quoted, and nothing else of the target.
|
|
490
|
-
# LOAD ERRORS (states.<slug>.transitions.<i>.description):
|
|
491
|
-
# an undescribed edge whose target has no
|
|
492
|
-
# instructions; an undescribed transition of a
|
|
493
|
-
# HUMAN state (it is the button label); an
|
|
494
|
-
# undescribed edge INTO a GRADER (its instructions
|
|
495
|
-
# are criteria the work it judges is never shown)
|
|
496
|
-
# or a CODE state (it has none); a non-string. A
|
|
497
|
-
# code state's OWN edges may go undescribed: its
|
|
498
|
-
# script routes, and nothing reads them. An
|
|
499
|
-
# undescribed edge INTO a human state discloses
|
|
500
|
-
# that state's reviewer-facing instructions to the
|
|
501
|
-
# agent — describe it to keep them private.
|
|
502
|
-
#
|
|
503
|
-
# Write it for a reader standing in THIS state,
|
|
504
|
-
# which knows nothing about the target:
|
|
505
|
-
# GOOD: "The refund is over $50." (when)
|
|
506
|
-
# BAD: "Go to refund-review." (where)
|
|
507
|
-
# A routing condition CANNOT live in the target's
|
|
508
|
-
# summary, which is never disclosed; it lives in the
|
|
509
|
-
# target's instructions only when the edge is left
|
|
510
|
-
# undescribed.
|
|
511
|
-
#
|
|
512
|
-
# The rendered line marks a target that is a human
|
|
513
|
-
# decision node, a grader, a code state or terminal
|
|
514
|
-
# (each changes the call or what follows it);
|
|
515
|
-
# nothing else of the target.
|
|
516
|
-
# A GRADER needs at least one transition (validate
|
|
517
|
-
# ERROR otherwise): with nowhere to send the session
|
|
518
|
-
# it could only approve by saying nothing.
|
|
519
|
-
|
|
520
|
-
# human states only (ignored on agent states; a validate ERROR on grader and
|
|
521
|
-
# code states):
|
|
522
|
-
evidence?: string[] # workspace-relative paths always shown in the packet —
|
|
523
|
-
# a baseline the advancing agent adds to (see Human states)
|
|
524
|
-
approvers?: string[] # informational; not enforced
|
|
525
|
-
```
|
|
38
|
+
title: Refund desk # a label; the directory name is the workflow's slug
|
|
39
|
+
instructions: |- # rules for the whole session (a warning when absent)
|
|
40
|
+
Decide only from records you have read; never guess an order.
|
|
41
|
+
|
|
42
|
+
Write every file under scratchpad/.
|
|
43
|
+
runtime: { engine: archmax-harness, version: "3" } # "3" is the only contract; omit the block for it
|
|
44
|
+
disabled: false # true: no new turns start; parked sessions still finish
|
|
45
|
+
settings:
|
|
46
|
+
model: gpt-5-mini # the model id every state runs on; overrides ARCHMAX_MODEL
|
|
47
|
+
timeoutMs: 15000 # per sandbox evaluation: one archmax_run call, one code-state script
|
|
48
|
+
memoryLimitBytes: 67108864 # sandbox memory
|
|
49
|
+
maxPtcCalls: 200 # tools.* calls per evaluation
|
|
50
|
+
maxResultChars: 20000 # characters of an evaluation's result
|
|
51
|
+
prompt_cache: { enabled: true, ttl: 5m } # provider prompt caching; ttl 5m or 1h
|
|
52
|
+
tests: # the case suite
|
|
53
|
+
maxConcurrency: 1 # only 1 is accepted
|
|
54
|
+
caseTimeoutMs: 120000 # wall clock per case
|
|
55
|
+
judge: {} # required by grade: ({} = the env model, or { model, modelOptions })
|
|
56
|
+
metadata: { canvas: { zoom: 1 } } # host data the runtime never reads; keep it as you find it
|
|
526
57
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
What to know:
|
|
548
|
-
|
|
549
|
-
- `archmax_wait` is available in **every** state, terminal ones included — a run
|
|
550
|
-
that has answered and now awaits a reply parks where it answered instead of
|
|
551
|
-
finishing. `tools: { forbid_always: [archmax_wait] }` removes it workflow-wide.
|
|
552
|
-
- The park is durable: status `awaiting_input` (classified **open**, never
|
|
553
|
-
finished), a checkpointed record of the state and the reason, and the process
|
|
554
|
-
may exit.
|
|
555
|
-
- **Any** delivered trigger id resumes a park — the id is recorded as the run's
|
|
556
|
-
current trigger (`args.trigger`, `${{trigger}}`), never matched against an edge.
|
|
557
|
-
The delivered variables arrive **locked**, and the arrival is written into the
|
|
558
|
-
transcript as an `[event]` **runtime note** — a synthetic `archmax_note` tool
|
|
559
|
-
call and its result, which is how the runtime says something to a run in a
|
|
560
|
-
channel a model cannot read as a person. Nothing may call `archmax_note`, and
|
|
561
|
-
derived views (`calledTool`, `usedNoTools`, the CLI's flow) skip the pair.
|
|
562
|
-
- The removed `type: wait` state kind is now an unknown state kind (`validate`
|
|
563
|
-
reports it as one), and a per-transition `on:` key is ignored. Call
|
|
564
|
-
`archmax_wait` instead.
|
|
565
|
-
- When what the state waits for is a **time** rather than a reply, the call says so:
|
|
566
|
-
`archmax_wait({ reason, until: "1d" })` — a duration (`30m`, `2h`, `1d`) or an
|
|
567
|
-
ISO-8601 instant, normalized to an absolute `resumeAt` on the park record, the
|
|
568
|
-
session summary, and the park event. The runtime **schedules nothing**: `resumeAt`
|
|
569
|
-
tells the host's cron when to `deliver`, and an earlier event resumes the park
|
|
570
|
-
regardless. An unparseable `until` is a tool error and the run does not park.
|
|
571
|
-
- A state that polls ("look for the transaction, else wait a day and retry") bounds
|
|
572
|
-
its own loop with `budget.maxParks` and sends the overrun somewhere with
|
|
573
|
-
`on_error`; the runtime counts parks per state in the checkpoint and refuses the
|
|
574
|
-
park that would exceed the bound as a turn failure:
|
|
575
|
-
|
|
576
|
-
```yaml
|
|
577
|
-
check-bank:
|
|
578
|
-
summary: Look for the transaction; wait a day and retry if it has not posted.
|
|
579
|
-
budget: { maxParks: 5 }
|
|
580
|
-
on_error: escalate
|
|
581
|
-
transitions:
|
|
582
|
-
- { to: reconcile, description: The transaction was found. }
|
|
583
|
-
```
|
|
584
|
-
- A finished session re-invoked is a **new turn** — it continues in the state the
|
|
585
|
-
previous turn ended in, *not* at the trigger's entry state, which applies only to
|
|
586
|
-
a session's first turn. Give a state re-entry transitions if a conversation must
|
|
587
|
-
move on from it; otherwise the agent's way out is `archmax_reset`. To continue
|
|
588
|
-
mid-state instead, park rather than finish.
|
|
589
|
-
|
|
590
|
-
A trigger's **declaration** sits under the `triggers:` of the state it enters,
|
|
591
|
-
keyed by its id. It says which conversation a firing belongs to and what the run
|
|
592
|
-
owes its caller:
|
|
58
|
+
tools:
|
|
59
|
+
allow_always: # the workflow-wide grant. Declared, it REPLACES the twelve
|
|
60
|
+
- ls # default tools, so list them all and drop only what this
|
|
61
|
+
- read_file # workflow must not have. A host tool every state needs
|
|
62
|
+
- write_file # goes here too.
|
|
63
|
+
- edit_file
|
|
64
|
+
- glob
|
|
65
|
+
- grep
|
|
66
|
+
- copy_file
|
|
67
|
+
- move_file
|
|
68
|
+
- remove_file
|
|
69
|
+
- write_todos
|
|
70
|
+
- archmax_eval
|
|
71
|
+
- archmax_run
|
|
72
|
+
forbid_always: [archmax_reset] # denied in every state, whatever grants it; "*" = every tool
|
|
73
|
+
skills:
|
|
74
|
+
allow_always: [order-data] # skill bundles enabled in every state, by slug
|
|
75
|
+
forbid_always: [] # skill bundles no state may reach
|
|
593
76
|
|
|
594
|
-
```yaml
|
|
595
77
|
states:
|
|
596
|
-
|
|
78
|
+
decide: # the key is the state's slug
|
|
79
|
+
title: Decide the refund # a label for people; no model reads it
|
|
80
|
+
summary: Record the decision and draft the email. # a label for editors; no model reads it
|
|
597
81
|
triggers:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
82
|
+
manual: # the entry the CLI, a host and a calling workflow use
|
|
83
|
+
description: Decide one refund request. # for callers; never shown to this session
|
|
84
|
+
requires: # what a start must supply
|
|
85
|
+
- email # a bare name takes any value
|
|
86
|
+
- { name: order_id, type: string, description: The order to refund. }
|
|
87
|
+
- { name: contact, type: string, format: email } # a rule beside the type
|
|
88
|
+
returns: [decision] # what the session guarantees when it completes
|
|
89
|
+
email_reply: # a host trigger; its payload arrives as locked variables
|
|
90
|
+
session: conversation_id # a dotted path over the variables: the session it joins
|
|
91
|
+
message: false # host-resolved: where the message is, or false for none
|
|
92
|
+
connection: mail # host-resolved: an access connection
|
|
93
|
+
instructions: >-
|
|
94
|
+
Look the customer up with lookup_customer. Record the decision with
|
|
95
|
+
archmax_set_variables as `decision` (approved or denied), draft the email
|
|
96
|
+
in scratchpad/email.md, then advance to check-decision. If a note sends
|
|
97
|
+
you back, fix what it names and advance again.
|
|
98
|
+
tools:
|
|
99
|
+
allow: # adds to allow_always; naming a granted tool narrows it here
|
|
100
|
+
- lookup_customer
|
|
101
|
+
- { tool: write_file, paths: ["reports/${{order_id}}/*.md"] }
|
|
102
|
+
- { tool: archmax_run, paths: ["skills/refund-policy/scripts/**"] }
|
|
103
|
+
- archmax_workflow_enrich-order # calls workflows/enrich-order/ as a sub-workflow
|
|
104
|
+
forbid: # denied while this state is active; beats every grant
|
|
105
|
+
- { tool: read_file, paths: ["skills/order-data/assets/archive/**"] }
|
|
106
|
+
skills:
|
|
107
|
+
allow: [refund-policy] # adds to skills.allow_always
|
|
108
|
+
requires: [decision] # must be set before the session may leave this state
|
|
109
|
+
locks: [decision] # only this state may write it
|
|
110
|
+
model: gpt-5 # this state's model id; beats settings.model
|
|
111
|
+
budget: { timeoutMs: 60000, maxParks: 3 } # per model call; archmax_wait parks here
|
|
112
|
+
on_error: escalate # where a failure of this state routes
|
|
113
|
+
transitions: # none: a terminal state
|
|
114
|
+
- to: check-decision
|
|
115
|
+
description: The decision is recorded and the email is drafted. # required; say WHEN
|
|
116
|
+
|
|
117
|
+
check-decision: # a code state: runs its script, no model call
|
|
118
|
+
type: code
|
|
119
|
+
script: scripts/check-decision.js # workflows/<slug>/scripts/check-decision.js
|
|
120
|
+
tools: { allow: [lookup_customer] } # what the script calls is granted HERE
|
|
121
|
+
on_error: escalate
|
|
122
|
+
transitions:
|
|
123
|
+
- { to: review, description: The decision matches the refund policy. }
|
|
124
|
+
- { to: decide, description: The decision contradicts the refund policy. }
|
|
125
|
+
|
|
126
|
+
review: # a human state: a person picks the transition
|
|
127
|
+
type: human
|
|
128
|
+
instructions: Confirm the decision before the email goes out. # required: the decision
|
|
129
|
+
evidence: [scratchpad/email.md] # always shown to the person
|
|
130
|
+
approvers: [finance@example.com] # informational; not enforced
|
|
131
|
+
transitions: # one per labelled type; never to itself
|
|
132
|
+
- { to: review-email, type: approve, description: Approve the decision. }
|
|
133
|
+
- { to: decide, type: refine, description: Send it back with a comment. }
|
|
134
|
+
|
|
135
|
+
review-email: # a grader state: an isolated subagent decides
|
|
136
|
+
type: grader
|
|
137
|
+
instructions: |- # required: its criteria
|
|
138
|
+
Read scratchpad/email.md and the `decision` variable.
|
|
139
|
+
- Polite, states the decision plainly and names the order: advance to `send`.
|
|
140
|
+
- Otherwise advance back to `decide`, saying exactly what to change. If the
|
|
141
|
+
trail already holds two steps with kind "grader" and to "decide", raise
|
|
142
|
+
`email-rejected` instead.
|
|
143
|
+
model: gpt-5 # resolved under the grader model role
|
|
144
|
+
budget: { timeoutMs: 60000 } # bounds its whole run
|
|
145
|
+
on_error: escalate
|
|
146
|
+
transitions:
|
|
147
|
+
- { to: send, description: The email is ready to send. }
|
|
148
|
+
- { to: decide, description: The email needs changes. }
|
|
614
149
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
wiring the SDK already reads, and a loose declaration that swallowed one would
|
|
619
|
-
start the run somewhere you did not say.
|
|
620
|
-
- `requires:` / `returns:` are the run's **signature**, and both are enforced. A
|
|
621
|
-
firing that does not supply every `requires` name does not start. `returns`
|
|
622
|
-
is checked once as the run finishes, and **nothing is handed back**: a
|
|
623
|
-
sub-workflow child with a name unset **completes**, and its caller gets
|
|
624
|
-
every declared return, the unset ones `null`, plus `note: "Not all return
|
|
625
|
-
variables were set by the sub-workflow: '<name>' was not set."`; a top-level
|
|
626
|
-
run with a name unset is **rejected**; a typed one mistyped rejects either
|
|
627
|
-
way. A run that *parks* is not checked — it has not finished. A return is
|
|
628
|
-
owed **only at the finish**: `archmax_advance` checks a state's own
|
|
629
|
-
`requires:`, never the returns, a later write replaces an unlocked earlier
|
|
630
|
-
one, and a seed of one (a name in both lists) lands **unlocked** and does not
|
|
631
|
-
overwrite a value the session already holds. So do not list a return in a
|
|
632
|
-
state's `requires:` unless that state must settle it, and do not tell the
|
|
633
|
-
agent to lock one before its value is final. `note` is reserved: a `returns`
|
|
634
|
-
list may not declare it. Want the model to try again? Route the finishing
|
|
635
|
-
state through a code state that checks the returns and sends the work back
|
|
636
|
-
with what is missing. Each entry is a bare
|
|
637
|
-
run-variable name (**untyped**: any value, `null` included) or a strict object
|
|
638
|
-
`{ name, type?, description? }` naming one; the spellings mix in one list and
|
|
639
|
-
names are distinct across both. What the agent must *do* to produce a
|
|
640
|
-
variable still belongs in the `instructions` that set it. The names — with
|
|
641
|
-
type and description when declared, as `name (type) — description` — are
|
|
642
|
-
rendered into the prompt under the trigger's entry state, so the agent is told
|
|
643
|
-
what to produce before it can be failed for omitting it. A trigger's
|
|
644
|
-
`requires:` also **guarantees** the variable for `${{…}}` guards, so `validate`
|
|
645
|
-
stops warning about a guard bound to it. This is what makes delegation
|
|
646
|
-
checkable — see [Sub-workflows are tools](#sub-workflows-are-tools).
|
|
647
|
-
- **`type`** is one of `string`, `integer` (no fractional part), `number`
|
|
648
|
-
(finite), `boolean`, `date` (RFC 3339 `YYYY-MM-DD`, a real calendar day),
|
|
649
|
-
`date-time` (RFC 3339 with a mandatory `Z`/`±hh:mm` offset), `object` (not an
|
|
650
|
-
array), `array` or `file` (a path from the workspace root, such as
|
|
651
|
-
`scratchpad/attachments/invoice.pdf` or `skills/billing/assets/template.pdf`; see
|
|
652
|
-
[Passing files](#passing-files)). Nothing is coerced (`"4"` is not an `integer`) and a typed
|
|
653
|
-
entry never takes `null`. It is held **at the start** (a mistyped seed is
|
|
654
|
-
refused before any model call), **at the write** (`archmax_set_variables` of a
|
|
655
|
-
mistyped typed return of the current trigger is a correctable refusal,
|
|
656
|
-
nothing written) and **at completion** (a mistyped typed return rejects the
|
|
657
|
-
run — in practice a code state's write). There are no enums, item or
|
|
658
|
-
property schemas, ranges or optional inputs. An unknown `type`, an unknown
|
|
659
|
-
entry key (`default:`) or an empty `description` fails load.
|
|
660
|
-
- **A `file` value must name a readable file.** The pure rule checks only the
|
|
661
|
-
path's shape (a host runs it in a browser). At the start, at the write, at
|
|
662
|
-
completion and at a delegation call the runtime also requires a **file** (not a
|
|
663
|
-
folder) at the path that the session may read **on runtime authority** — the
|
|
664
|
-
runtime's own `file`-value read check, the one call that runs outside a state's
|
|
665
|
-
surface: zone rules (`checkpoints/`, `artifacts/`, `_specs/`
|
|
666
|
-
never), the workflow's mount grants taken across all its states (a governed
|
|
667
|
-
mount no state is granted is out of reach), and every workflow-wide
|
|
668
|
-
`forbid_always`, inherited ones included; the state's allow list and its own
|
|
669
|
-
mount/skill grants do not narrow it. So tell the agent to
|
|
670
|
-
write the file *before* it sets a `file` return: `archmax_set_variables` refuses
|
|
671
|
-
a path with nothing there. A host hands a file in with `TurnInput.files` (CLI:
|
|
672
|
-
`archmax run … --file invoice=./invoice.pdf`), which places it under
|
|
673
|
-
`scratchpad/attachments/` and seeds the path.
|
|
674
|
-
- **When to type.** Type an entry when something outside the session reads the
|
|
675
|
-
contract: a delegating model that must build the argument, a host publishing an
|
|
676
|
-
MCP tool or a start form. Leave it bare when any value will do. Typing a
|
|
677
|
-
`returns` entry also tells the agent the shape to write.
|
|
678
|
-
- **`description` on the declaration** says, for a **caller**, what calling this
|
|
679
|
-
entry does. It leads the delegation tool's description and a host's MCP tool
|
|
680
|
-
description; it never reaches the model of the session it starts (that
|
|
681
|
-
session's brief is its `instructions`). An entry's own `description` is
|
|
682
|
-
different: it *is* shown to the agent, beside the name.
|
|
683
|
-
- `session:` is a **dotted path over the run's variables** — the same syntax
|
|
684
|
-
`${{…}}` guards use, including array indices and negative indices
|
|
685
|
-
(`-1` = last). No script, no callback: name where the host already puts the
|
|
686
|
-
conversation id. A `${{…}}`-wrapped value is rejected — write the bare path.
|
|
687
|
-
- Related triggers that must land in the same conversation declare the **same
|
|
688
|
-
path**. Each declares it itself: there is no shared lookup to change from
|
|
689
|
-
elsewhere in the document, and the duplication is the deliberate cost of a
|
|
690
|
-
state being legible on its own.
|
|
691
|
-
- A trigger with no `session:` path — or a firing whose path does not resolve —
|
|
692
|
-
gives that run a session of its own (its session id). Every run has exactly one
|
|
693
|
-
session, so there is no absent case.
|
|
694
|
-
- `message:` and `connection:` are **host-resolved** keys: shape-checked,
|
|
695
|
-
preserved and readable back (`messagePathForTrigger`, `connectionForTrigger`),
|
|
696
|
-
but nothing in the runtime acts on them. `message:` takes `session:`'s dotted
|
|
697
|
-
path — where a firing's words are — or the literal `false` for a trigger whose
|
|
698
|
-
firings carry no message; `connection:` is an opaque non-empty slug naming an
|
|
699
|
-
access connection in the host's own configuration. Author them only if the host
|
|
700
|
-
you are targeting reads them.
|
|
701
|
-
- A declaration key the SDK does not define is a **warning**, not a load failure:
|
|
702
|
-
it is ignored, and the workflow still runs. Structural problems — a
|
|
703
|
-
non-mapping `triggers:`, a declaration that is neither a mapping nor empty, one
|
|
704
|
-
id declared by two states, a malformed `session`/`message` path, an empty
|
|
705
|
-
`connection`, an `entry:` or `name:` key — still fail load.
|
|
706
|
-
- A host's own keys ride **inside the declaration**, keyed by the trigger id:
|
|
707
|
-
|
|
708
|
-
```yaml
|
|
709
|
-
states:
|
|
710
|
-
intake:
|
|
711
|
-
triggers: # one state, two event sources
|
|
712
|
-
outlook-mail: { type: ap, piece: microsoft-outlook, event: newEmail }
|
|
713
|
-
slack-message:
|
|
714
|
-
type: ap
|
|
715
|
-
piece: slack
|
|
716
|
-
event: newMessage
|
|
717
|
-
session: triggers.-1.threadId
|
|
718
|
-
connection: acme-slack
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
One state, one behavior, one governance block; `${{trigger}}` and a code
|
|
722
|
-
state's `trigger` input still read which one actually fired.
|
|
723
|
-
- Every declared trigger **starts** a session. An event that must only ever reach
|
|
724
|
-
a live one — a ticket closing, a payment settling — is declared nowhere: the
|
|
725
|
-
host names the session it delivers into (`archmax deliver <session> --trigger
|
|
726
|
-
<id>`, `agent.workflow.send(sessionId, …)`), or passes a `sessionPath` with the
|
|
727
|
-
firing. A firing naming a trigger no state declares, with no session to reach,
|
|
728
|
-
is refused.
|
|
729
|
-
|
|
730
|
-
Call `archmax_wait` when an **external event** is what lets the work continue (an
|
|
731
|
-
email reply, a webhook, a scheduled reminder the host fires); author a `human`
|
|
732
|
-
state when a **person** picks the edge. Both park the session durably; they differ
|
|
733
|
-
in the resolution channel and in where the run continues — a decision advances
|
|
734
|
-
along the chosen edge, a delivery resumes the parked state.
|
|
735
|
-
|
|
736
|
-
**Runtime control tools** carry the `archmax_` prefix — `archmax_advance`,
|
|
737
|
-
`archmax_reset`, `archmax_wait`, `archmax_raise`, `archmax_run`,
|
|
738
|
-
`archmax_get_variables`, `archmax_set_variables`, and `archmax_get_history`
|
|
739
|
-
(grader states only) — and are the names to write in `tools.allow`,
|
|
740
|
-
`tools.allow_always`, and the `forbid`/`forbid_always` lists. These are the only spellings:
|
|
741
|
-
an entry naming an unprefixed variant governs a tool that does not exist,
|
|
742
|
-
silently granting nothing. A host tool may never claim the prefix. Deep Agents' built-ins
|
|
743
|
-
(`read_file`, `task`, …) keep their own names.
|
|
744
|
-
|
|
745
|
-
- A state with **no `tools` block** gets the **workflow-wide grant only**: what
|
|
746
|
-
`tools.allow_always` lists, or the default tools (`ls`, `read_file`, `write_file`,
|
|
747
|
-
`edit_file`, `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`,
|
|
748
|
-
`archmax_eval`, `archmax_run`) when the key is absent, plus the always-allowed
|
|
749
|
-
`archmax_advance` and a host's `essentialTools`, minus whatever a `forbid` list
|
|
750
|
-
denies there, and read-only-zone protection. A grader or code state never has
|
|
751
|
-
`write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait` or `archmax_reset`,
|
|
752
|
-
even when `allow_always` lists one; a grader also has `archmax_get_history`.
|
|
753
|
-
Governance is closed by default; there is no way to declare a fully open state.
|
|
754
|
-
- `tools.forbid` on a state **denies** while that state is active, beating every
|
|
755
|
-
grant that would otherwise reach it (see [Tool governance](#tool-governance)).
|
|
756
|
-
- A state with **no transitions** is **terminal**: the run ends when the agent
|
|
757
|
-
finishes its turn there; it cannot call `archmax_advance`.
|
|
758
|
-
|
|
759
|
-
## Ending a run as a failure: `archmax_raise`
|
|
760
|
-
|
|
761
|
-
When the work **cannot be completed** — a system it depends on keeps failing, the
|
|
762
|
-
thing asked about does not exist — the agent calls
|
|
763
|
-
`archmax_raise({ code, reason })`. It is in every state (terminal ones included),
|
|
764
|
-
never declared, and only for failure: a session that ends without it is a
|
|
765
|
-
success.
|
|
766
|
-
|
|
767
|
-
- The call must be the **only** tool call in its message; one sent beside other
|
|
768
|
-
calls is refused, and the model may raise again on its own.
|
|
769
|
-
- The session ends **at once** with status `failed` (classified **finished**): no
|
|
770
|
-
further model call, no `on_error` route, no further state (an automated one
|
|
771
|
-
included), no check of the trigger's `returns`. A rejection still
|
|
772
|
-
pending from earlier in the turn is replaced. The position stays where the
|
|
773
|
-
agent raised; the trail records no step.
|
|
774
|
-
- The host reads `Outcome.kind === "failed"` and `Outcome.exit`
|
|
775
|
-
(`{ success: false, code, reason }`); a completed turn carries
|
|
776
|
-
`exit: { success: true }`. `code` is the agent's own free-form token (≤ 64
|
|
777
|
-
characters, one line), so if hosts branch on particular codes, name them in
|
|
778
|
-
the state's `instructions` ("if the order does not exist, raise
|
|
779
|
-
`order-not-found`").
|
|
780
|
-
- A **child** session that raises fails its caller's delegation call with kind
|
|
781
|
-
`raised`, naming the code and reason; the caller may recover, route through
|
|
782
|
-
its own `on_error`, or raise in turn.
|
|
783
|
-
- Ban it with `tools: { forbid_always: [archmax_raise] }` (or one state's
|
|
784
|
-
`tools.forbid`) where the agent must not end the run itself — say, a state that
|
|
785
|
-
must always reach a person instead. An agent's script cannot call it; a code
|
|
786
|
-
state's script (`tools.archmax_raise`) and a grader can, and their raise ends the
|
|
787
|
-
session the same way (the note and `exit` carry their code).
|
|
788
|
-
- Test it with the structural `raised` assertion; `succeeded: true` fails on a
|
|
789
|
-
raise (it passes only on a `completed` outcome).
|
|
790
|
-
|
|
791
|
-
## Transitions
|
|
150
|
+
send: # terminal: no transitions
|
|
151
|
+
tools: { allow: [send_email] }
|
|
152
|
+
instructions: Send scratchpad/email.md to `email` with send_email.
|
|
792
153
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
- to: string # REQUIRED target state SLUG (must exist); never a title
|
|
796
|
-
description?: string # shown to the agent (or human) to pick the edge; absent =
|
|
797
|
-
# the agent is shown the target's instructions instead.
|
|
798
|
-
# REQUIRED on a human state's edges and on an edge INTO
|
|
799
|
-
# a grader or code state; a code state's own may omit it
|
|
800
|
-
type?: "approve" | "reject" | "refine" | "none" # default "none"
|
|
154
|
+
escalate:
|
|
155
|
+
instructions: Tell the customer that a person will follow up.
|
|
801
156
|
```
|
|
802
157
|
|
|
803
|
-
|
|
804
|
-
`archmax_advance({ to, reason })`; `type` is metadata only.
|
|
805
|
-
- On **grader** and **code** states the grader or the script chooses one the same
|
|
806
|
-
way (`archmax_advance` / `tools.archmax_advance`), and its `reason` is what the
|
|
807
|
-
next state reads.
|
|
808
|
-
- On **human** states each transition is a selectable outcome (a UI renders one
|
|
809
|
-
button per edge, labeled by `type`). `refine` conventionally loops back to a
|
|
810
|
-
prior state with the reviewer's comment as correction. Max one transition per
|
|
811
|
-
`type` on a human state.
|
|
158
|
+
## Files
|
|
812
159
|
|
|
813
|
-
|
|
160
|
+
- `workflows/<slug>/workflow.yaml` is the machine: a plain YAML mapping with no
|
|
161
|
+
frontmatter. The directory name is the workflow's slug, kebab-case
|
|
162
|
+
(`^[a-z0-9]+(-[a-z0-9]+)*$`). `title` is only a label.
|
|
163
|
+
- `WORKFLOW.md` beside it is optional Markdown, appended to the prompt after the
|
|
164
|
+
graph section. Frontmatter in it is a load error. Put a diagram for human readers
|
|
165
|
+
inside an HTML comment: comments are stripped from the prompt.
|
|
166
|
+
- `scripts/` holds the code-state scripts and `tests/` the cases.
|
|
814
167
|
|
|
815
|
-
|
|
168
|
+
## Root keys
|
|
816
169
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
170
|
+
| Key | What it does |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `title` | A label for people. |
|
|
173
|
+
| `instructions` | Rules for the whole session, rendered once at the top of the cacheable prompt prefix. Write a literal block (`\|-`): a folded block (`>-`) joins paragraphs. A warning when absent. |
|
|
174
|
+
| `runtime` | `{ engine: archmax-harness, version: "3" }`. `3` is the only contract and the default. `1` and `2` are refused. |
|
|
175
|
+
| `disabled` | `true` starts no new turns: `archmax run`, a host's turn, a follow-up message and a calling workflow's `archmax_workflow_<slug>` call are refused. `decide`, `reply` and `deliver` still work, so parked sessions finish. `archmax test` skips the suite and exits 0. Any value but absent, `null` or `false` disables, and a non-boolean is a validate error. |
|
|
176
|
+
| `settings.model` | The model id every state runs on, over the endpoint the host configured. It overrides `ARCHMAX_MODEL`. An id only: a mapping with `temperature` is a load error. |
|
|
177
|
+
| `settings.timeoutMs` | Bounds one sandbox evaluation (default 15000): an `archmax_eval` or `archmax_run` call, or a code state's whole script. `memoryLimitBytes`, `maxPtcCalls` and `maxResultChars` bound it too. |
|
|
178
|
+
| `settings.prompt_cache` | `{ enabled, ttl: 5m \| 1h }`: provider prompt caching, on by default. |
|
|
179
|
+
| `tests` | The case suite: `maxConcurrency` (only `1`), `caseTimeoutMs` (default 120000, per case) and `judge`, which `grade:` requires (`{}` is the env model; or `{ model, modelOptions: { temperature, maxTokens } }`). |
|
|
180
|
+
| `tools` | `allow_always` and `forbid_always`: [Tool governance](#tool-governance). |
|
|
181
|
+
| `skills` | `allow_always` and `forbid_always`, by slug: [Skill governance](#skill-governance). |
|
|
182
|
+
| `mounts` | `allow_always` and `forbid_always`, by mount name: [Mount governance](#mount-governance). |
|
|
183
|
+
| `metadata` | Host data, such as an editor's canvas. Never read or checked. Keep it verbatim when you rewrite a spec. |
|
|
184
|
+
| `states` | Required: at least one state, and at least one with a trigger. |
|
|
185
|
+
|
|
186
|
+
## State keys
|
|
187
|
+
|
|
188
|
+
The key under `states` is the state's slug, kebab-case. It is the only token that `to`,
|
|
189
|
+
`on_error`, `archmax_advance`, events and `reachedState` resolve, so rename a state only
|
|
190
|
+
together with every reference to it.
|
|
191
|
+
|
|
192
|
+
| Key | On | What it does |
|
|
193
|
+
| --- | --- | --- |
|
|
194
|
+
| `type` | — | `agent` (the default), `human`, `grader` or `code`. |
|
|
195
|
+
| `title`, `summary` | all | Labels for people and editors. No model reads them, so they cannot carry routing. |
|
|
196
|
+
| `triggers` | all | Marks an entry state: [Triggers and signatures](#triggers-and-signatures). |
|
|
197
|
+
| `instructions` | agent, human, grader | An agent state's behaviour, shown while it is active. Required on a human state (the decision to present) and on a grader (its criteria). |
|
|
198
|
+
| `script` | code | Required on a code state and refused elsewhere: `scripts/<file>.js` or `.mjs`, no `..`, resolving to `workflows/<slug>/scripts/<file>`. |
|
|
199
|
+
| `tools`, `skills`, `mounts` | all | This state's `allow` and `forbid` lists. A grader's and a code state's calls are held to their own state's lists. |
|
|
200
|
+
| `requires` | all | Variables that must be set before the session may leave the state. |
|
|
201
|
+
| `locks` | agent, grader, code | Variables only this state may write. One owner per variable, and never `trigger` or `title`. |
|
|
202
|
+
| `model` | agent, human, grader | This state's model id; it beats `settings.model`. Naming the workflow's own id is a warning. |
|
|
203
|
+
| `budget` | agent, grader | `timeoutMs` and `maxParks`: [Budgets and `on_error`](#budgets-and-on_error). |
|
|
204
|
+
| `on_error` | all | The state a failure of this state routes to. |
|
|
205
|
+
| `transitions` | all | `[{ to, description?, type? }]`. A state with none is terminal. `description` may be left out only on an agent state's lone edge into another agent state. |
|
|
206
|
+
| `evidence`, `approvers` | human | Paths always shown in the decision record; who decides (informational). |
|
|
207
|
+
| `metadata` | all | Host data, never read. |
|
|
208
|
+
|
|
209
|
+
What each type needs and refuses:
|
|
210
|
+
|
|
211
|
+
| Type | Who runs it | Requires | A validate error |
|
|
212
|
+
| --- | --- | --- | --- |
|
|
213
|
+
| `agent` | the session's agent | — | — |
|
|
214
|
+
| `human` | a person picks the transition | `instructions`, at least one transition | a transition to itself, two transitions of one labelled `type` |
|
|
215
|
+
| `grader` | an isolated subagent | `instructions`, at least one transition | `evidence`, `approvers`, `budget.maxParks` |
|
|
216
|
+
| `code` | its script, in the sandbox | `script` | `instructions`, `model`, `budget`, `evidence`, `approvers` |
|
|
217
|
+
|
|
218
|
+
Grader and code states are **automated states**: the runtime runs them on entry, with no
|
|
219
|
+
call of the agent's model. [`code-states-and-scripts.md`](code-states-and-scripts.md)
|
|
220
|
+
says how they run, what they are offered and how they settle.
|
|
221
|
+
|
|
222
|
+
## Transitions and what the agent sees
|
|
223
|
+
|
|
224
|
+
The agent is shown the active state and nothing else of the graph. Each model call carries
|
|
225
|
+
that state's transitions:
|
|
825
226
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
227
|
+
```
|
|
228
|
+
Transitions — choose one with `archmax_advance` when this state's work is done:
|
|
229
|
+
- to `check-decision` (code) — The decision is recorded and the email is drafted.
|
|
230
|
+
- to `review` (human decision node) — The decision needs a person's approval.
|
|
231
|
+
- to `review-email` (grader) — The email is drafted.
|
|
232
|
+
- to `escalate` (terminal) — The order does not exist.
|
|
829
233
|
```
|
|
830
234
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
`
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
**The model is closed by default, and a denial beats every grant.** Every state has the
|
|
837
|
-
**workflow-wide grant**: what `tools.allow_always` lists, or, when the workflow does not
|
|
838
|
-
declare that key, the **default tools** (`ls`, `read_file`, `write_file`, `edit_file`,
|
|
839
|
-
`copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`, `archmax_eval`,
|
|
840
|
-
`archmax_run`). A declared list **replaces** the default tools, never adds to them:
|
|
841
|
-
`allow_always: []` grants no tool workflow-wide, and a list without `write_file` leaves
|
|
842
|
-
every state that does not name it unable to write. So a new workflow lists all twelve and
|
|
843
|
-
removes only what it must not have. Every state also has its type's control tools and
|
|
844
|
-
any tool the host passes as `essentialTools`, whatever the workflow declares. A grader or
|
|
845
|
-
code state is never offered `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait` or
|
|
846
|
-
`archmax_reset`, even when `allow_always` lists them, and naming one in its own `tools.allow`
|
|
847
|
-
is a `validate` error.
|
|
848
|
-
One built-in is **never disclosed** and grantable by nothing: `task`, Deep Agents'
|
|
849
|
-
subagent dispatch — grading is a grader state and delegating is a sub-workflow, so naming
|
|
850
|
-
`task` in an allow entry is a `validate` error and the kernel refuses the call.
|
|
851
|
-
The planning scratchpad `write_todos` and the sandbox
|
|
852
|
-
`archmax_eval` are default tools: granted wherever the workflow-wide grant includes
|
|
853
|
-
them (everywhere, in a workflow without `allow_always`). `write_todos`'s prompt
|
|
854
|
-
guidance appears only when some state is granted it, and code states run in the
|
|
855
|
-
sandbox whether or not any state is granted `archmax_eval`. The interpreter is safe as a
|
|
856
|
-
default because the `tools.*` calls its code makes are decided against the active
|
|
857
|
-
state by the same kernel: it adds computation over what the state permits, not
|
|
858
|
-
access. `archmax_run` — executing an authored script **file** — is a default tool on
|
|
859
|
-
the same terms (the script is a file the state could already read); declare it
|
|
860
|
-
per state to **narrow** which files may run there. The file operations —
|
|
861
|
-
`copy_file({ source, destination, overwrite? })`, `move_file` (same arguments)
|
|
862
|
-
and `remove_file({ file_path })` — are default tools on the same terms: every tool
|
|
863
|
-
**declares its path arguments** with how the call uses each (`read`, `list`,
|
|
864
|
-
`search`, `write`, `remove`, `execute`), and every path rule reads that
|
|
865
|
-
declaration. `copy_file` reads `source` and writes `destination`; `move_file`
|
|
866
|
-
**removes** `source`, so a move out of a read-only mount is refused before
|
|
867
|
-
anything is written; a removal is refused wherever a write is. A call naming
|
|
868
|
-
several paths is refused when any one is, naming the argument. `paths:` in an
|
|
869
|
-
entry guards every declared path argument: a grant needs all of them to match
|
|
870
|
-
(`{ tool: copy_file, paths: [skills/**, reports/**] }`), a denial needs one
|
|
871
|
-
(`{ tool: "*", paths: [secrets/**] }` blocks a copy in or out). A host tool that
|
|
872
|
-
declares its paths (`toolPaths`, or a descriptor's `paths`) is governed the same
|
|
873
|
-
way; an argument may hold a list of paths, each one governed (and each one
|
|
874
|
-
tested by a `paths:` guard), and any other shape is refused
|
|
875
|
-
(`tool.path-argument`). `scratchpad/` is open in every state to each default file tool the state
|
|
876
|
-
is granted, whatever the granting entry narrows, and to a host's `essentialTools`.
|
|
877
|
-
A file tool the state is not granted gets nothing from it, and a host tool a state
|
|
878
|
-
must grant stays refused where it is not granted, whatever its paths. Any other tool
|
|
879
|
-
must be declared in the state's `tools.allow` or workflow-level
|
|
880
|
-
`tools.allow_always`. A `tools.allow` entry naming a tool the workflow-wide grant
|
|
881
|
-
includes **narrows** it for that state. A guard on an `allow_always` entry is
|
|
882
|
-
enforced like any other (`{ tool: write_file, paths: [reports/**] }` binds in every
|
|
883
|
-
state that does not name `write_file`).
|
|
884
|
-
Denial is declared beside the grant, in the same two positions and with the same
|
|
885
|
-
entries: `tools.forbid_always` at the root denies in every state,
|
|
886
|
-
`tools.forbid` on a state denies while it is active — subtracting from the
|
|
887
|
-
workflow-wide grant, the state's own `allow` and a host's `essentialTools` alike.
|
|
888
|
-
**Deny beats allow, and no narrower level widens a denial.** A forbid entry may
|
|
889
|
-
name the tool `*` (every tool); `*` in an allow list is a load error. A grader's
|
|
890
|
-
calls and a code state's script are governed against **their own state** exactly
|
|
891
|
-
as the agent is — its `allow`, its `forbid`, `forbid_always` and every ancestor's.
|
|
892
|
-
The only call on runtime authority is the runtime's own `file`-value read check,
|
|
893
|
-
held to the workflow's grants across all its states and to every `forbid_always`.
|
|
894
|
-
|
|
895
|
-
Precedence (highest first):
|
|
896
|
-
|
|
897
|
-
0. **Skill and mount governance** — a path inside a skill bundle the active state
|
|
898
|
-
does not enable is refused before anything below is consulted (rule
|
|
899
|
-
`skill.not-allowed`), and so is a path under a mount the state does not have
|
|
900
|
-
(`mount.not-allowed` / `mount.forbidden`, or `mount.read-only` for a write into
|
|
901
|
-
one a grant narrowed to reads). Nothing widens either: not a wildcard
|
|
902
|
-
`tools.allow` entry, not `allow_always`, not a consumer rule.
|
|
903
|
-
1. **Declared denials** — `tools.forbid_always` denies in every state,
|
|
904
|
-
`tools.forbid` denies in the active state, both regardless of any allow list
|
|
905
|
-
(and regardless of always-allowed): deny beats allow, and no narrower level
|
|
906
|
-
widens a denial.
|
|
907
|
-
2. **The state type's control tools** (baked in): an agent or human state has
|
|
908
|
-
`archmax_advance`, `archmax_reset`, `archmax_wait`, `archmax_raise` and the
|
|
909
|
-
variable tools; a code state `archmax_advance`, `archmax_raise` and the
|
|
910
|
-
variable tools; a grader state those plus `archmax_get_history`.
|
|
911
|
-
3. **Per-state `tools.allow`** — the listed tools/arg-shapes. An entry for a
|
|
912
|
-
tool the workflow-wide grant includes takes precedence (**narrower wins** —
|
|
913
|
-
the state's arg globs apply, not the broader grant); an entry for a default
|
|
914
|
-
tool a declared `allow_always` omits grants it in this state.
|
|
915
|
-
4. **The workflow-wide grant** — the declared `tools.allow_always`, guards
|
|
916
|
-
included, or the default tools, unconstrained, when the key is absent; in a
|
|
917
|
-
grader or code state, minus `write_todos`, `archmax_eval` and `archmax_run`.
|
|
918
|
-
5. **The host's `essentialTools`** — always permitted when no state entry
|
|
919
|
-
narrows them, whatever the workflow declares.
|
|
920
|
-
6. **Default**: everything else is blocked.
|
|
921
|
-
|
|
922
|
-
**Progressive disclosure.** The model is only shown the active state's tool
|
|
923
|
-
surface: the workflow-wide grant + a host's `essentialTools` + the state's entries +
|
|
924
|
-
its control tools (`archmax_advance` hidden in terminal states), minus every tool
|
|
925
|
-
denied there by name. A grader is offered, and a code state's script reaches,
|
|
926
|
-
exactly the same derivation for its own state — anything else is absent
|
|
927
|
-
(`undefined` on `tools`). Disclosure is
|
|
928
|
-
name-level (arg-constrained tools stay visible; the kernel enforces args at
|
|
929
|
-
call time) and never replaces enforcement. **Skills are disclosed the same way**:
|
|
930
|
-
each model call names only the skills the active state enables, with their
|
|
931
|
-
descriptions — a disabled bundle's slug never reaches the prompt, and `ls skills/`
|
|
932
|
-
does not list it.
|
|
235
|
+
The marker says what taking the transition leads to: a person decides next, a grader or
|
|
236
|
+
code state runs at once and may send the session straight back, or the session ends. A
|
|
237
|
+
transition's `type`, other than `none`, is shown too. Nothing else of the target is
|
|
238
|
+
disclosed: not its title, summary, instructions, script or transitions.
|
|
933
239
|
|
|
934
|
-
|
|
240
|
+
So a transition needs a `description`, a non-empty string. It is all the agent learns
|
|
241
|
+
about the transition. Write it for a reader standing in the source state:
|
|
935
242
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
`tools.allow` grants no tool — and a capability is declared **once**, at the level
|
|
941
|
-
where the decision was made:
|
|
243
|
+
- Say **when** to take it: `The refund is over $50, or the customer has asked twice.`
|
|
244
|
+
- Never say **where** it goes: `Go to refund-review.` tells this reader nothing.
|
|
245
|
+
- Keep routing conditions out of the target. A target's `summary` and `instructions`
|
|
246
|
+
never reach the state that routes into it.
|
|
942
247
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
248
|
+
**A lone edge between agent states may go undescribed.** When an agent state with
|
|
249
|
+
`instructions` has a single transition, into another agent state, write `- to: send` and
|
|
250
|
+
no description, never a filler like `Proceed`. The agent is told to take it as soon as
|
|
251
|
+
the state's instructions are done, without asking anyone whether to move on:
|
|
946
252
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
skills:
|
|
951
|
-
allow: [refund-policy] # this state adds one, and has both
|
|
952
|
-
identify-case:
|
|
953
|
-
skills:
|
|
954
|
-
forbid: [order-data] # routes only: opts out of the grant
|
|
253
|
+
```
|
|
254
|
+
Transition — the only one out of this state, with no condition of its own: take it with `archmax_advance` as soon as this state's instructions are done, without asking anyone whether to move on.
|
|
255
|
+
- to `send` (terminal)
|
|
955
256
|
```
|
|
956
257
|
|
|
957
|
-
|
|
958
|
-
(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
- **`
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
`
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
`
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
258
|
+
So the instructions are the whole condition. When moving on waits for something they do
|
|
259
|
+
not say (a reply, an approval), describe the edge. The description stays required on one
|
|
260
|
+
of several edges, on every edge of a human, grader or code state, on an edge into one of
|
|
261
|
+
those (say what must be ready), on an edge back into its own state, and out of a state
|
|
262
|
+
with no `instructions`.
|
|
263
|
+
|
|
264
|
+
At a human state the description is also the button's label, and `type` (`approve`,
|
|
265
|
+
`reject`, `refine`, `none`) labels the choice. Each labelled type maps to one target. A
|
|
266
|
+
`refine` transition carries the person's comment back as a correction. On other states
|
|
267
|
+
`type` is metadata the marker shows.
|
|
268
|
+
|
|
269
|
+
A session is in one state at a time: there is no fan-out. Sequence what would be
|
|
270
|
+
branches; concurrency belongs to sub-workflow calls.
|
|
271
|
+
|
|
272
|
+
## Triggers and signatures
|
|
273
|
+
|
|
274
|
+
A trigger is declared under the `triggers:` of the state it enters, keyed by its id. That
|
|
275
|
+
state is its entry. There is no root `triggers:` block, and `entry:` or `name:` inside a
|
|
276
|
+
declaration is a load error. Several keys on one state make it the entry for each; where
|
|
277
|
+
their paths differ, branch on `${{trigger}}` (a script reads `trigger`). Each trigger id
|
|
278
|
+
belongs to one state.
|
|
279
|
+
|
|
280
|
+
- **`manual`** is the entry the CLI, a host's plain turn and a calling workflow use.
|
|
281
|
+
- **Any other id** is a host trigger. Its payload arrives as locked variables.
|
|
282
|
+
- **`session:`** is a dotted path over the variables that names the session the trigger
|
|
283
|
+
joins (`conversation_id`, or `triggers.-1.conversationId`, where `-1` is the last
|
|
284
|
+
element). Write the bare path, not `${{…}}`. Without one, or when it does not resolve,
|
|
285
|
+
the trigger starts a session of its own.
|
|
286
|
+
- **`message:`** (a dotted path, or `false`) and **`connection:`** (a slug) are read by
|
|
287
|
+
the host, not by the runtime.
|
|
288
|
+
- **A key the harness does not define** is kept, with a warning. A host's own keys go
|
|
289
|
+
there.
|
|
290
|
+
- **Every declared trigger starts a session.** An event that must only reach a live
|
|
291
|
+
session is declared nowhere: the host delivers it to that session by id.
|
|
292
|
+
|
|
293
|
+
**The signature.** `requires` and `returns` are the session's contract with its caller,
|
|
294
|
+
and both are enforced.
|
|
295
|
+
|
|
296
|
+
- An entry is a bare variable name, which takes any value (`null` included), or
|
|
297
|
+
`{ name, type?, description?, …rules }`. The two forms mix in one list.
|
|
298
|
+
- `type` is one of `string`, `integer`, `number`, `boolean`, `date` (`YYYY-MM-DD`),
|
|
299
|
+
`date-time` (RFC 3339 with an offset), `object`, `array` or `file`. Nothing is coerced
|
|
300
|
+
(`"4"` is not an `integer`), and a typed entry never takes `null`. A typed entry is
|
|
301
|
+
held at the start, at each `archmax_set_variables` of a typed return, and at
|
|
302
|
+
completion.
|
|
303
|
+
- **Rules** say what a typed value must look like beyond its type, as JSON Schema
|
|
304
|
+
keywords, and are held wherever the type is. Each entry reports the first rule it
|
|
305
|
+
breaks (`'quantity' must be at most 100`).
|
|
306
|
+
|
|
307
|
+
| Rule | On | Value |
|
|
308
|
+
| --- | --- | --- |
|
|
309
|
+
| `format` | `string` | `email`, `url` (absolute `http(s)://`) or `phone` (E.164) |
|
|
310
|
+
| `pattern` | `string` | a regex found anywhere in the value (anchor it with `^…$`) |
|
|
311
|
+
| `minLength`, `maxLength` | `string` | counts of code points |
|
|
312
|
+
| `enum` | `string` | a list of distinct strings, matched exactly |
|
|
313
|
+
| `content` | `string` | `text`, `markdown` or `html`: for the host, never checked |
|
|
314
|
+
| `minimum`, `maximum` | `integer`, `number`, `date`, `date-time` | inclusive; `today` and `now` for the dates |
|
|
315
|
+
| `minItems`, `maxItems` | `array` | counts of items |
|
|
316
|
+
| `accept`, `maxSize` | `file` | extensions like `.pdf`; bytes, or `<n>KB` / `<n>MB` |
|
|
317
|
+
|
|
318
|
+
A rule on a type that does not take it or on an untyped entry, `format` with
|
|
319
|
+
`pattern`, a pattern with a lookaround or a backreference, and inconsistent bounds are
|
|
320
|
+
load errors. A `pattern` runs in RE2, so no input can make it slow.
|
|
321
|
+
- A start that omits a `requires` name, or supplies a mistyped one, is refused before
|
|
322
|
+
any model call.
|
|
323
|
+
- `returns` is checked when the session completes. A top-level session with a return
|
|
324
|
+
unset is rejected. A sub-workflow child completes anyway, and its caller gets the unset
|
|
325
|
+
ones as `null` plus a `note`. A mistyped return is rejected either way. A parked session
|
|
326
|
+
is not checked.
|
|
327
|
+
- A return is owed only at the finish: no state needs it to leave, and a later write
|
|
328
|
+
replaces an earlier unlocked one. Lock a return only once it is final. `note` is
|
|
329
|
+
reserved and may not be declared.
|
|
330
|
+
- A `file` value is a workspace path to a file the session may read. Tell the agent to
|
|
331
|
+
write the file before it sets the variable: `archmax_set_variables` refuses a path with
|
|
332
|
+
nothing there.
|
|
333
|
+
- The trigger's `description` is for callers: a calling model, an MCP client. The session
|
|
334
|
+
it starts never sees it. An entry's own `description` is shown to the agent beside the
|
|
335
|
+
name.
|
|
336
|
+
- Type an entry when something outside the session builds or reads the value. Leave it
|
|
337
|
+
bare when any value will do.
|
|
338
|
+
|
|
339
|
+
## Variables
|
|
340
|
+
|
|
341
|
+
A session has one flat store of variables, `name → value`, never declared. A variable
|
|
342
|
+
exists once something sets it:
|
|
343
|
+
|
|
344
|
+
- **The host** seeds it: `--variables`, `createAgent({ variables })`, a turn's
|
|
345
|
+
`variables`, a delivery. Every seed is **locked**, so `archmax_set_variables` refuses
|
|
346
|
+
to change it.
|
|
347
|
+
- **The agent** sets it with `archmax_set_variables`; `lock: true` freezes it until
|
|
348
|
+
`archmax_reset`. A misspelled name creates a new variable, and a state's `requires` is
|
|
349
|
+
what surfaces the mistake.
|
|
350
|
+
- **A state owns it** with `locks: [name]`: only that state writes it, so changing it
|
|
351
|
+
means going back there. Give the owner a transition back from the states that use it,
|
|
352
|
+
and pair `locks` with `requires`.
|
|
353
|
+
|
|
354
|
+
Two names are reserved. `trigger` holds the current trigger id and is always locked.
|
|
355
|
+
`title` is the session's one-line task label: the agent sets it (a sub-workflow child is
|
|
356
|
+
not asked to), it is never locked, and a host's seed of it lands unlocked.
|
|
357
|
+
|
|
358
|
+
**References.** `${{name}}` or `${{a.b.-1.c}}` resolves against the variables in two
|
|
359
|
+
places:
|
|
360
|
+
|
|
361
|
+
- **In a `tools.allow` glob**, as a guard:
|
|
362
|
+
`{ tool: gmail__send, args: { to: ["${{from_email}}"] } }`. Lock what a guard reads,
|
|
363
|
+
and guarantee it: name it in an earlier state's or the trigger's `requires`, or seed
|
|
364
|
+
it. A guard that cannot resolve fails the session.
|
|
365
|
+
- **Inside any text argument the model writes**, mid-string and as often as needed. The
|
|
366
|
+
tool receives the value, while the transcript keeps the reference, so a stored value
|
|
367
|
+
costs its tokens once. Tell a state that must emit a stored value to write the
|
|
368
|
+
reference (`Quote the customer's message by writing ${{inbound.text}}`): a reference is
|
|
369
|
+
exact where a retyped copy drifts. A reference that cannot resolve is refused, and the
|
|
370
|
+
agent retries.
|
|
371
|
+
|
|
372
|
+
A value is matched literally, so a variable never widens a guard. A path reads own
|
|
373
|
+
properties and must end at a scalar. Write `$${{name}}` for the literal characters.
|
|
374
|
+
Scripts get no references: they read `variables` (a code state) or `args.variables`
|
|
375
|
+
(`archmax_run`).
|
|
1028
376
|
|
|
1029
|
-
|
|
377
|
+
## Tool governance
|
|
1030
378
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
allow_always: [reference] # reachable in every state, named once
|
|
1034
|
-
forbid_always: [catalogs/uk] # out of reach everywhere, and in every
|
|
1035
|
-
# child session this workflow delegates to
|
|
379
|
+
Governance is closed by default. **A denial beats every grant, and no narrower level
|
|
380
|
+
widens a denial.**
|
|
1036
381
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
mounts:
|
|
1044
|
-
forbid: [reference] # opts out of the workflow's grant
|
|
1045
|
-
```
|
|
382
|
+
**The workflow-wide grant** is `tools.allow_always`, granted in every state. When the
|
|
383
|
+
key is absent, it is the twelve **default tools**: `ls`, `read_file`, `write_file`,
|
|
384
|
+
`edit_file`, `glob`, `grep`, `copy_file`, `move_file`, `remove_file`, `write_todos`,
|
|
385
|
+
`archmax_eval` and `archmax_run`. When it is declared, an empty list included, it is
|
|
386
|
+
exactly what it lists: it *replaces* the default tools. So a new workflow lists all
|
|
387
|
+
twelve and drops only what it must not have.
|
|
1046
388
|
|
|
1047
|
-
A state's `allow
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
error.
|
|
389
|
+
**A state's `tools.allow`** adds to that grant. An entry naming a tool the workflow-wide
|
|
390
|
+
grant includes **narrows** it in that state; an entry naming a default tool that a
|
|
391
|
+
declared `allow_always` leaves out grants it there. Every other tool, a host tool or a
|
|
392
|
+
delegation tool, needs an entry.
|
|
1052
393
|
|
|
1053
|
-
|
|
394
|
+
**Denials** sit beside the grants. `tools.forbid_always` at the root denies in every
|
|
395
|
+
state, and in every sub-workflow the session calls. A state's `tools.forbid` denies
|
|
396
|
+
while it is active. Both beat the workflow-wide grant, the state's `allow` and a host's
|
|
397
|
+
`essentialTools`.
|
|
1054
398
|
|
|
1055
|
-
|
|
1056
|
-
|
|
399
|
+
**Each state type's own tools** come from one table, `STATE_TYPE_TOOLS` on
|
|
400
|
+
`@archmax-ai/harness/spec`. A type's controls need no grant, though a `forbid` can still
|
|
401
|
+
block one. What a type withholds, no grant can give.
|
|
1057
402
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
- { mount: shared, access: read_write } # writable where the host allows it
|
|
403
|
+
| State type | Controls it always has | Never offered |
|
|
404
|
+
| --- | --- | --- |
|
|
405
|
+
| `agent`, `human` | `archmax_advance`, `archmax_reset`, `archmax_wait`, `archmax_raise`, `archmax_get_variables`, `archmax_set_variables` | — |
|
|
406
|
+
| `grader`, `code` | `archmax_advance`, `archmax_raise`, `archmax_get_variables`, `archmax_set_variables`, `archmax_get_history` | `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait`, `archmax_reset` |
|
|
1063
407
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
408
|
+
- `archmax_advance` is absent in a terminal state.
|
|
409
|
+
- A withheld tool listed in `allow_always` is left out of a grader or code state. One
|
|
410
|
+
named in that state's own `tools.allow` is a validate error.
|
|
411
|
+
- Deep Agents' `task` is callable in no state, and naming it in any allow list is a
|
|
412
|
+
validate error: grading is a grader state, and delegating is a sub-workflow.
|
|
413
|
+
- A host's `essentialTools` are on in every state.
|
|
1069
414
|
|
|
1070
|
-
**
|
|
1071
|
-
writable in a state when the host serves it writable, some grant of *this*
|
|
1072
|
-
workflow says `access: read_write` (in `allow_always` or any state's `allow`), and
|
|
1073
|
-
no grant applicable in the state says `access: read`. So a bare grant in one state
|
|
1074
|
-
and a `read_write` grant in another both write. One mount table serves a session
|
|
1075
|
-
and every child it dispatches, and the host serves a route writable when any
|
|
1076
|
-
workflow in that tree asks; a mount still belongs to the workflow that grants it,
|
|
1077
|
-
so a caller's `read_write` never lets its child write through a bare grant, nor the
|
|
1078
|
-
other way round. An ungoverned mount takes the host's posture: it has no grant to
|
|
1079
|
-
ask with.
|
|
1080
|
-
|
|
1081
|
-
The host's `readOnly` is a **ceiling**, not a default to argue with:
|
|
1082
|
-
|
|
1083
|
-
- `access: read_write` on a mount the workspace serves read-only opens nothing,
|
|
1084
|
-
and `validate` warns that the entry is inert. Ask the host to mount it
|
|
1085
|
-
`{ readOnly: false }` instead.
|
|
1086
|
-
- `access: read` is a restriction, so it behaves like every other restriction
|
|
1087
|
-
here: whichever level asks for it gets it, and **no narrower level widens it
|
|
1088
|
-
back**. A state cannot re-open what the workflow narrowed.
|
|
1089
|
-
- A `forbid` entry is a name only. A denial is total, so there is no access to
|
|
1090
|
-
qualify — `{ mount: x, access: read }` in a `forbid` list is a load error.
|
|
1091
|
-
|
|
1092
|
-
Two refusals, because they are two different edits: a write into a mount the
|
|
1093
|
-
**host** serves read-only is `zone.read-only` (fix the wiring), and a write into
|
|
1094
|
-
one a **grant** narrowed, or one no grant of the workflow asks to write, is
|
|
1095
|
-
`mount.read-only` (fix the spec; the refusal says which).
|
|
1096
|
-
|
|
1097
|
-
### What the agent sees
|
|
1098
|
-
|
|
1099
|
-
A governed mount the state does not have is not merely refused — it is not shown:
|
|
1100
|
-
|
|
1101
|
-
- `ls`, `glob` and `grep` results lose every entry under it, the **workspace root
|
|
1102
|
-
listing included**, so the agent is never shown a directory it then cannot read.
|
|
1103
|
-
- The static "Workspace zones" prompt section names ungoverned mounts only, one
|
|
1104
|
-
line each with a `read-only` or `read/write` marker. The
|
|
1105
|
-
governed mounts of *this* state are listed in the volatile "Current state"
|
|
1106
|
-
block, read-only or read/write per the state's own grant — which is why the
|
|
1107
|
-
cacheable prompt prefix stays byte-identical across states.
|
|
1108
|
-
- Every call is still decided by the kernel (`mount.not-allowed`,
|
|
1109
|
-
`mount.forbidden`, `mount.read-only`), whether or not a listing showed the path.
|
|
1110
|
-
Redaction is the courtesy; the kernel is the enforcement.
|
|
1111
|
-
|
|
1112
|
-
### Nested and file mounts
|
|
1113
|
-
|
|
1114
|
-
A mount key may be several segments deep (`catalogs/eu`), and a mount name is that
|
|
1115
|
-
key with its slashes stripped — that is the token a `mounts` list takes, never a
|
|
1116
|
-
glob (`reference/**` is a load error; scope paths *within* an enabled mount with a
|
|
1117
|
-
`tools.allow` entry). A path is matched to a mount by **longest prefix**, so
|
|
1118
|
-
`catalogs/eu` wins over a `catalogs` mounted beside it, and a sibling
|
|
1119
|
-
`catalogs/uk` matches neither. A file mount (`AGENTS.md`) matches its exact path,
|
|
1120
|
-
so `AGENTS.md.bak` is not captured.
|
|
1121
|
-
|
|
1122
|
-
### Diagnostics
|
|
1123
|
-
|
|
1124
|
-
`archmax validate` reports, given the host's mount table:
|
|
1125
|
-
|
|
1126
|
-
- **error** — a name the table does not mount at all, naming the mounts it does;
|
|
1127
|
-
- a grant naming an **ungoverned** mount (it grants nothing: the mount is visible
|
|
1128
|
-
everywhere already — use `forbid` to take one away);
|
|
1129
|
-
- an `access: read_write` on a mount the host serves **read-only**;
|
|
1130
|
-
- a name in both `allow_always` and a state's `allow` (unless the state entry
|
|
1131
|
-
narrows the access to `read`, or asks `read_write` of a mount the root grants
|
|
1132
|
-
bare: both do real work);
|
|
1133
|
-
- a state's `allow: []` beside a non-empty `allow_always`;
|
|
1134
|
-
- a `forbid` naming a governed mount nothing would have enabled there;
|
|
1135
|
-
- a `forbid_always` name some `allow` list also grants;
|
|
1136
|
-
- a `tools.allow` path entry that can only match inside a mount the state does not
|
|
1137
|
-
enable (the grant reaches nothing);
|
|
1138
|
-
- **once**, when a spec declares no root `mounts` block at all in a workspace that
|
|
1139
|
-
governs mounts, naming what the workflow cannot reach. Any root block silences
|
|
1140
|
-
it — write `mounts: { allow_always: [] }` when a workflow reaches none.
|
|
1141
|
-
|
|
1142
|
-
Grader and code states are bound by **their own state's** mount grants, exactly
|
|
1143
|
-
like the agent: a code state reads a governed mount only where `allow_always` or
|
|
1144
|
-
its own `mounts.allow` grants it, and writes one only where that grant asks
|
|
1145
|
-
`access: read_write`. So when a check needs a mount no agent state should see,
|
|
1146
|
-
grant it on the check's code state alone. The one read on runtime authority is
|
|
1147
|
-
the runtime's own `file`-value check, held to the **workflow's** grants taken
|
|
1148
|
-
across every state and to every `forbid_always`.
|
|
1149
|
-
|
|
1150
|
-
## Session variables in guards
|
|
1151
|
-
|
|
1152
|
-
A `tools.allow` argument glob may reference a **run variable** — `${{name}}` or
|
|
1153
|
-
`${{name.dotted.path}}` — resolved against the run's variables at call time:
|
|
415
|
+
**Entries.**
|
|
1154
416
|
|
|
1155
417
|
```yaml
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
- { tool: write_file, paths: ["reports/${{case_id}}/**"] }
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
There is **no `variables:` block** — a variable exists once something sets it:
|
|
1164
|
-
the host at assembly (`variables`, every seed locked), the agent mid-run
|
|
1165
|
-
(`archmax_set_variables`), or one of the two built-ins.
|
|
1166
|
-
|
|
1167
|
-
**Two reserved names.** Do not claim either for something else:
|
|
1168
|
-
|
|
1169
|
-
| | `trigger` | `title` |
|
|
1170
|
-
|---|---|---|
|
|
1171
|
-
| Set by | the runtime, at every arrival | the **agent**, prompted by the platform prompt (a sub-workflow child is not) |
|
|
1172
|
-
| Locked | always | **never**, by any route |
|
|
1173
|
-
| Guaranteed set | yes | no — absent until written |
|
|
1174
|
-
| In a trigger's `requires:` | accepted | accepted (seeded unlocked) |
|
|
1175
|
-
| In a trigger's `returns:` | refused | refused |
|
|
1176
|
-
|
|
1177
|
-
`trigger` holds the current turn's trigger id. `title` holds a short one-line label
|
|
1178
|
-
naming the task the run is doing; the platform system prompt asks the agent to set
|
|
1179
|
-
one as its first activity, and to rename it only when the whole conversation has
|
|
1180
|
-
moved to a different subject (a follow-up on the same subject keeps it), so a
|
|
1181
|
-
host can name a run instead of showing a session id. A sub-workflow's child is not
|
|
1182
|
-
asked — its title names a session nothing lists and is never returned — so its
|
|
1183
|
-
prompt leaves the step out; do not write "set the title" into a child's state
|
|
1184
|
-
`instructions` to bring it back. A write must be a non-empty
|
|
1185
|
-
single-line string of at most 200 characters (stored trimmed) and must not pass
|
|
1186
|
-
`lock: true` — both are refused, atomically, like every other `set_variables`
|
|
1187
|
-
rejection. A host may seed a `title`; it lands **unlocked** (the one exception to
|
|
1188
|
-
"every seed is locked") as an opening label, and it will not overwrite one an
|
|
1189
|
-
earlier turn established.
|
|
1190
|
-
|
|
1191
|
-
Do not confuse it with the spec-root and per-state `title:` keys, which are static
|
|
1192
|
-
human labels for the *workflow* and the *state*. The run variable names the **run**.
|
|
1193
|
-
|
|
1194
|
-
Because nothing guarantees `title` is set, a guard on `${{title}}` still draws the
|
|
1195
|
-
assembly-time warning — correctly.
|
|
1196
|
-
|
|
1197
|
-
Two declarations *guarantee* one: a state's `requires:` (checked when leaving that
|
|
1198
|
-
state) and a **trigger's** `requires:` (checked before the run starts, so a firing
|
|
1199
|
-
that omits it never runs). A trigger's `returns:` is the mirror — the variables a
|
|
1200
|
-
run guarantees it has set by the time it completes. See a trigger's declaration.
|
|
1201
|
-
|
|
1202
|
-
A value belongs to the **session**: what one state records is readable by every
|
|
1203
|
-
state after it and by every later turn, so a downstream guard can bind to an
|
|
1204
|
-
upstream fact. Design for that — `requires` in an early state plus `lock: true` is
|
|
1205
|
-
how a later `${{name}}` becomes guaranteed rather than hopeful.
|
|
1206
|
-
|
|
1207
|
-
**Paths.** Segments descend one level; a numeric segment indexes an array and a
|
|
1208
|
-
negative one counts from the end (`${{order.items.-1.sku}}` is the last item).
|
|
1209
|
-
Traversal reads own properties only, so `${{tags.length}}` and
|
|
1210
|
-
`${{v.constructor}}` do **not** resolve. A reference must resolve to a single
|
|
1211
|
-
scalar.
|
|
1212
|
-
|
|
1213
|
-
**Authoring rules that matter:**
|
|
1214
|
-
|
|
1215
|
-
- **Lock anything a guard reads.** Host seeds are locked automatically (`title`
|
|
1216
|
-
and a seed of a trigger's `returns` excepted, which land unlocked); for an
|
|
1217
|
-
agent-established fact pass `lock: true` — on a return, only once it is final. An unlocked variable can be
|
|
1218
|
-
overwritten by any later state — that rewrite is permitted, being one writer
|
|
1219
|
-
twice, and the later value simply wins — so a guard bound to an unlocked name is
|
|
1220
|
-
a weak guard. A lock is the only thing that makes a fact settled.
|
|
1221
|
-
- **Guarantee the reference.** Name it in the `requires` of a state that runs
|
|
1222
|
-
first, or seed it. Otherwise assembly only *warns*, and the run **fails** at
|
|
1223
|
-
the guarded call if the agent never set it — an unresolvable reference is
|
|
1224
|
-
terminal, not a retryable block, because a guard the runtime cannot evaluate
|
|
1225
|
-
is not a guard.
|
|
1226
|
-
- **A positional index makes the guard move.** `${{order.items.-1.sku}}` permits
|
|
1227
|
-
a different value as the array grows. Sometimes exactly right ("the newest
|
|
1228
|
-
attachment"); when you want a *stable* guard, store the value you mean as its
|
|
1229
|
-
own named variable.
|
|
1230
|
-
- **An `archmax_set_variables` typo creates a new variable** rather than erroring — there
|
|
1231
|
-
are no declarations to check against. `requires` is the safety net that turns
|
|
1232
|
-
that into a visible failure.
|
|
1233
|
-
|
|
1234
|
-
Substituted values are matched **literally** (a value holding `*` matches `*`,
|
|
1235
|
-
not everything), so a variable can never widen the guard it lands in.
|
|
1236
|
-
|
|
1237
|
-
### The same references work in the agent's own tool arguments
|
|
1238
|
-
|
|
1239
|
-
`${{…}}` is not only an authoring syntax. Anywhere a tool argument is text, the
|
|
1240
|
-
runtime substitutes references from the run's variables **before** the call is
|
|
1241
|
-
governed or executed — and a reference may sit *inside* a longer string, and
|
|
1242
|
-
repeat:
|
|
1243
|
-
|
|
418
|
+
- read_file # a bare name: any arguments
|
|
419
|
+
- { tool: write_file, args: { file_path: ["reports/*.json"] } } # matches when any glob matches
|
|
420
|
+
- { tool: copy_file, paths: ["skills/**", "reports/**"] } # every path argument must match
|
|
421
|
+
- { tool: "*", paths: ["secrets/**"] } # forbid lists only: every tool
|
|
1244
422
|
```
|
|
1245
|
-
"Thanks — your refund is on its way.
|
|
1246
423
|
|
|
1247
|
-
|
|
424
|
+
- A path argument (a tool's declared paths, else `file_path`) is canonicalized before it
|
|
425
|
+
is matched. Any other argument, such as a URL or an address, is matched as written.
|
|
426
|
+
- `*` never crosses `/`; `**` does. Globs match dot-files, so `secrets/**` covers
|
|
427
|
+
`secrets/.env`. `""` matches only an empty value.
|
|
428
|
+
- In an allow entry, `paths:` needs every path argument to match. In a forbid entry, one
|
|
429
|
+
match denies.
|
|
430
|
+
- `"*"` as the tool is legal only in a forbid list.
|
|
431
|
+
- An entry may also carry `connection` (the connection a platform tool resolves through)
|
|
432
|
+
and `source` (a host's label). The runtime matches by `tool` alone.
|
|
433
|
+
|
|
434
|
+
**Path arguments.** Each tool declares how it uses each path argument: `read`, `list`,
|
|
435
|
+
`search`, `write`, `remove` or `execute`. `copy_file` and `move_file` take
|
|
436
|
+
`{ source, destination, overwrite? }`, and `move_file` removes its source. `remove_file`
|
|
437
|
+
takes `{ file_path }`. A removal is refused wherever a write is, a call naming several
|
|
438
|
+
paths is refused when any one is, and an existing destination is refused without
|
|
439
|
+
`overwrite: true`.
|
|
440
|
+
|
|
441
|
+
**The scratchpad.** `scratchpad/` is open in every state to each default file tool the
|
|
442
|
+
state is granted, whatever the granting entry narrows. So an allow entry inside
|
|
443
|
+
`scratchpad/` narrows nothing, and validate warns. To govern *where* a result lands,
|
|
444
|
+
allow a path outside the reserved zones, such as `reports/*.json`.
|
|
445
|
+
|
|
446
|
+
**The order of decision.** The first rule that applies wins:
|
|
447
|
+
|
|
448
|
+
1. Skill and mount governance. A path in a skill bundle or a governed mount the state
|
|
449
|
+
does not have is refused first (`skill.not-allowed`, `mount.not-allowed`,
|
|
450
|
+
`mount.forbidden`, `mount.read-only`).
|
|
451
|
+
2. Declared denials: `forbid_always`, then the state's `forbid`.
|
|
452
|
+
3. The state type's controls.
|
|
453
|
+
4. The state's `tools.allow`. A narrowing entry wins over the broader grant.
|
|
454
|
+
5. The workflow-wide grant, its guards included.
|
|
455
|
+
6. The host's `essentialTools`, where no state entry narrows them.
|
|
456
|
+
7. Everything else is refused.
|
|
457
|
+
|
|
458
|
+
The model is shown only the active state's tools, by name. The kernel still checks every
|
|
459
|
+
argument at call time.
|
|
1248
460
|
|
|
1249
|
-
|
|
1250
|
-
```
|
|
461
|
+
## Skill governance
|
|
1251
462
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
- **A reference is exact.** A copy the model retypes can truncate, paraphrase, or
|
|
1256
|
-
drift; a reference cannot. When a state's job is to emit a large stored value,
|
|
1257
|
-
its `instructions` should **name the variable to interpolate** rather than
|
|
1258
|
-
telling the agent to include the original text.
|
|
1259
|
-
- **The guard and the argument meet on the value.** Substitution runs ahead of the
|
|
1260
|
-
kernel, so `args: { to: ["${{from_email}}"] }` and an argument of
|
|
1261
|
-
`"${{from_email}}"` resolve independently to the same string — they agree by
|
|
1262
|
-
construction, not by the model having retyped an address correctly.
|
|
1263
|
-
- **A bad reference here is recoverable**, unlike a bad guard. The call is refused
|
|
1264
|
-
with the reference named and the agent retries; only an unresolvable *guard* is
|
|
1265
|
-
terminal, because that is a defect the agent cannot fix.
|
|
1266
|
-
|
|
1267
|
-
The transcript keeps the reference — the substituted value goes to the tool and to
|
|
1268
|
-
the `tool-called` event a run trail renders, and no further, so the value's tokens
|
|
1269
|
-
are not re-sent on every later model call. An cases `calledTool` assertion
|
|
1270
|
-
therefore states the **reference** the agent wrote; pin the delivered text through
|
|
1271
|
-
the tool's effect instead (the file it wrote, the variable it set).
|
|
1272
|
-
|
|
1273
|
-
To write the characters `${{…}}` literally — authoring a guard through
|
|
1274
|
-
`write_file`, or writing about the syntax — double the dollar: `$${{name}}`.
|
|
1275
|
-
|
|
1276
|
-
This is **model-facing**: the agent's calls and a grader's get it. An
|
|
1277
|
-
`archmax_run` script (`args.variables`) or a code state's script (`variables`)
|
|
1278
|
-
already holds the run's variables and composes strings in JavaScript, so `${{…}}`
|
|
1279
|
-
in a script's `tools.*` call is ordinary text.
|
|
1280
|
-
|
|
1281
|
-
## Grader and code states
|
|
1282
|
-
|
|
1283
|
-
Two state types are run by the runtime itself, together called **automated
|
|
1284
|
-
states**. Both are ordinary states on the graph: entered by a transition, leaving
|
|
1285
|
-
by `archmax_advance` along one of their own transitions, ending the session with
|
|
1286
|
-
`archmax_raise`, governed by their own tool surface.
|
|
463
|
+
A skill bundle is governed by its slug, never by path. The lists compose like the tool
|
|
464
|
+
lists:
|
|
1287
465
|
|
|
1288
466
|
```yaml
|
|
467
|
+
skills:
|
|
468
|
+
allow_always: [order-data] # enabled in every state; name it on no state
|
|
1289
469
|
states:
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
transitions:
|
|
1296
|
-
- to: refund-review
|
|
1297
|
-
description: The recorded decision matches the refund policy.
|
|
1298
|
-
- to: refund-request
|
|
1299
|
-
description: The recorded decision contradicts the policy; send it back with the fix.
|
|
1300
|
-
|
|
1301
|
-
review-enrichment: # GRADER state: an isolated grader decides the edge
|
|
1302
|
-
type: grader
|
|
1303
|
-
budget: { maxTurns: 8 } # the grader's own model calls
|
|
1304
|
-
on_error: escalation
|
|
1305
|
-
skills: { allow: [] } # the GRADER is governed HERE too: no bundle, and
|
|
1306
|
-
# read_file (workflow-wide) reads the returned files
|
|
1307
|
-
instructions: |- # REQUIRED — its criteria
|
|
1308
|
-
Read `orders_to_enrich` and `enrichments` with archmax_get_variables, then
|
|
1309
|
-
each entry's `enrichment_file` with read_file.
|
|
1310
|
-
- One entry per requested `order_id`, none missing or extra, each `delayed`
|
|
1311
|
-
matching its file: advance to `enrichment-reported`.
|
|
1312
|
-
- Otherwise advance back to `enrich-order`, naming what to fix. Before
|
|
1313
|
-
that, count the steps in archmax_get_history's trail with kind "grader"
|
|
1314
|
-
and to "enrich-order"; if there are already two, raise
|
|
1315
|
-
`enrichment-incorrect` instead.
|
|
1316
|
-
transitions: # REQUIRED — at least one
|
|
1317
|
-
- to: enrichment-reported
|
|
1318
|
-
description: Every requested order has exactly one enrichment, and each matches its file.
|
|
1319
|
-
- to: enrich-order
|
|
1320
|
-
description: The enrichments are incomplete or do not match their files; send them back.
|
|
470
|
+
answer: {} # has order-data
|
|
471
|
+
refund:
|
|
472
|
+
skills: { allow: [refund-policy] } # adds one, and has both
|
|
473
|
+
route:
|
|
474
|
+
skills: { forbid: [order-data] } # opts out of the workflow-wide grant
|
|
1321
475
|
```
|
|
1322
476
|
|
|
1323
|
-
-
|
|
1324
|
-
`
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
`archmax_get_variables`, `archmax_set_variables`, `archmax_get_history` (which
|
|
1339
|
-
no agent or human state is offered). Never `write_todos`, `archmax_eval`,
|
|
1340
|
-
`archmax_run`, `archmax_wait` or `archmax_reset` — not even when `allow_always`
|
|
1341
|
-
lists one (it is left out), and naming one in its own `tools.allow` is a
|
|
1342
|
-
`validate` error. An automated state **cannot park**. Every call is decided by
|
|
1343
|
-
the kernel against that state, like the agent's: its `forbid`, `forbid_always`,
|
|
1344
|
-
its skill and mount grants and every safety rule bind it, and a tool another
|
|
1345
|
-
state grants is not reachable.
|
|
1346
|
-
- **How it moves.** `archmax_advance({ to, reason })` is checked at the call (the
|
|
1347
|
-
edge exists, the state's `requires` hold against the variables at that moment;
|
|
1348
|
-
`evidence` only into a human state); a refusal can be retried. The **first
|
|
1349
|
-
accepted** advance or raise is the decision and every later one is refused. The
|
|
1350
|
-
decision takes effect when the run finishes **cleanly**.
|
|
1351
|
-
- **How it settles** — exactly one of:
|
|
1352
|
-
- **advanced** → the session moves; the trail records `{ to, kind: grader | code,
|
|
1353
|
-
reason }`; a runtime note carries the reason to the next state
|
|
1354
|
-
(`[grader] The '<state>' grader moved the run to '<to>': <reason>. …` /
|
|
1355
|
-
`[code] '<state>' moved the run to '<to>': <reason>. …`); a human target parks
|
|
1356
|
-
after its one reply-only handoff message.
|
|
1357
|
-
- **raised** → the session ends `failed`, its `exit` carrying the code and reason.
|
|
1358
|
-
- **completed** → a terminal state finished without either call; the session
|
|
1359
|
-
completes, held to the trigger's `returns`.
|
|
1360
|
-
- **failed** → it threw, timed out, exhausted its budget, or a **non-terminal**
|
|
1361
|
-
state finished without either call (`<type> state '<slug>' finished without
|
|
1362
|
-
calling archmax_advance or archmax_raise`) — routed through `on_error`, else the
|
|
1363
|
-
session ends rejected. A raise wins over an error; an error discards a recorded
|
|
1364
|
-
advance. Finishing undecided is never read as approval.
|
|
1365
|
-
- **The history.** Both types read the session with `archmax_get_history`, which
|
|
1366
|
-
answers `{ messages, trail }` (`SessionHistory`) as JSON: the session as it
|
|
1367
|
-
stood when the state was entered, built from the checkpointed session, whole
|
|
1368
|
-
and oldest first, nothing windowed. A message is `{ role, text, toolCalls?,
|
|
1369
|
-
tool?, id?, note? }`: an `assistant` entry lists its calls (`{ id?, name, args
|
|
1370
|
-
}`), the `tool` entry with the matching `id` carries the result inline in
|
|
1371
|
-
`text`, and a runtime note is `role: "runtime"` with its `note` kind.
|
|
1372
|
-
- **Code state.** `script` (required, `scripts/<file>.js|.mjs`) runs in the QuickJS
|
|
1373
|
-
sandbox in a fresh REPL per run. Its default export receives `{ state, trigger?,
|
|
1374
|
-
variables, tools }` and reads the history with
|
|
1375
|
-
`JSON.parse(await tools.archmax_get_history())`. Its return value is never
|
|
1376
|
-
read. `instructions`, `model`, `budget`, `evidence` and `approvers` on a code
|
|
1377
|
-
state are `validate` errors; `settings.timeoutMs` bounds the script. A
|
|
1378
|
-
**terminal** code state completes the session when its script returns: approve
|
|
1379
|
-
into one to end without another model call. Script contract and worked
|
|
1380
|
-
examples: [`code-state-and-test-scripts.md`](code-state-and-test-scripts.md).
|
|
1381
|
-
- **Grader state.** A subagent with its own isolated context, built per run: its
|
|
1382
|
-
system prompt is a compiled-in grader preamble followed by the state's
|
|
1383
|
-
`instructions`; its task names the state and lists its transitions; it reads the
|
|
1384
|
-
session only through `archmax_get_history`, and the work with its own state's
|
|
1385
|
-
tools. Its messages never enter the session's transcript, its tokens never
|
|
1386
|
-
stream, it writes no checkpoint; the session reads its decision as the trail
|
|
1387
|
-
step and the note. `instructions` (non-empty) and at least one transition are
|
|
1388
|
-
required; `evidence`, `approvers` and `budget.maxParks` are errors. Every edge
|
|
1389
|
-
into a grader needs a `description` (its instructions are never shown to the
|
|
1390
|
-
work it judges). `budget.maxTurns` bounds its own model calls and
|
|
1391
|
-
`budget.timeoutMs` its run. `model` (else `settings.model`) is resolved under the
|
|
1392
|
-
**`grader`** model role.
|
|
1393
|
-
- **No iteration budget.** Nothing caps how often a state is entered. A
|
|
1394
|
-
state that gives up after N rounds counts them in the trail — each earlier
|
|
1395
|
-
send-back is `{ kind: grader | code, to: <graded state> }` — and raises (or routes
|
|
1396
|
-
to an escalation) on the round after.
|
|
1397
|
-
- **Events.** `code-start { state, script }`, `code-output { state, line }`,
|
|
1398
|
-
`code-result { state, status, durationMs, reason? }`, `grader-start { state }`,
|
|
1399
|
-
`grader-result { state, status, durationMs, reason? }`; a grader's model calls are
|
|
1400
|
-
`model-usage` events under its state, added to the session's usage.
|
|
1401
|
-
|
|
1402
|
-
**Patterns.**
|
|
1403
|
-
|
|
1404
|
-
| Want | Shape |
|
|
1405
|
-
| --- | --- |
|
|
1406
|
-
| A deterministic check **in front of** a state | Make the trigger's entry (or the edge's target) a code state that advances to the work, or to a state that declines it (its agent reads the reason from the note) — or raises |
|
|
1407
|
-
| A check **behind** a state | Point the state's transition at a code state with an edge onward and an edge **back**; a send-back's `reason` is the correction the agent reads |
|
|
1408
|
-
| **Approve, then end** | Approve into a terminal code state (records the result, completes the session, no model call) |
|
|
1409
|
-
| Judgment (tone, completeness) | A grader behind the graded state, with an edge back; its `instructions` say when to raise, counting send-backs in the trail. Grade work the agent hands on (a report, a draft), not a live answer: a turn's `reply` is every text the agent said in it, a draft sent back included |
|
|
1410
|
-
|
|
1411
|
-
**Migrating hooks.** `before:`/`after:` are removed, with every hook kind
|
|
1412
|
-
(`{ script }`, `{ rubric }`, custom kinds), `max_iterations` and the root
|
|
1413
|
-
`extensions:` block; each is a load error naming the key. A `before` check becomes
|
|
1414
|
-
a code state in front of the state; an `after` check a code state behind it; a
|
|
1415
|
-
rubric a grader state behind it. Move `workflows/<slug>/hooks/*.js` to
|
|
1416
|
-
`scripts/` and rewrite each to the code-state contract (it advances or raises
|
|
1417
|
-
instead of returning `ok`/`veto`/`correct`), and grant on the code state whatever
|
|
1418
|
-
the script reads — there is no runtime authority any more.
|
|
1419
|
-
|
|
1420
|
-
## Budgets & error routing
|
|
477
|
+
- Enabling a slug makes `skills/<slug>/**` readable and its scripts runnable, with no
|
|
478
|
+
`tools.allow` entry. A bundle a state does not enable is unreachable and undisclosed:
|
|
479
|
+
its slug is not in the prompt, and a listing of `skills/` does not show it.
|
|
480
|
+
- `allow` only adds, so `allow: []` denies nothing (a warning). `forbid` subtracts, the
|
|
481
|
+
workflow-wide grant included. `forbid_always` puts a bundle out of reach everywhere.
|
|
482
|
+
- A workspace that serves bundles warns once when a spec has no root `skills` block.
|
|
483
|
+
Write `skills: { allow_always: [] }` when every bundle is state-specific.
|
|
484
|
+
- A `tools.allow` entry still narrows inside an enabled bundle:
|
|
485
|
+
`{ tool: archmax_run, paths: ["skills/<slug>/scripts/**"] }` confines execution to that
|
|
486
|
+
bundle's scripts.
|
|
487
|
+
- `archmax_run` runs only skill-bundle scripts (rule `script.skill-only`). A script in
|
|
488
|
+
`scratchpad/` or `workflows/` cannot run, so a state that enables no bundle runs no
|
|
489
|
+
script.
|
|
490
|
+
- A grader or code state enables what it reads in its own `skills.allow`. To let a check
|
|
491
|
+
read a bundle the agent must not use, enable it on the check's state only.
|
|
1421
492
|
|
|
1422
|
-
|
|
1423
|
-
states:
|
|
1424
|
-
refund-request:
|
|
1425
|
-
budget:
|
|
1426
|
-
maxTurns: 24 # bounds this turn's agent loop
|
|
1427
|
-
# timeoutMs: 60000 # aborts the turn when exceeded
|
|
1428
|
-
on_error: escalation # terminal-failure target
|
|
1429
|
-
escalation: { instructions: "Explain the failure and hand off." } # terminal
|
|
1430
|
-
```
|
|
493
|
+
## Mount governance
|
|
1431
494
|
|
|
1432
|
-
A
|
|
1433
|
-
`
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
`
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
495
|
+
A mount is a folder or a file the host routes into the agent's workspace: `skills/`,
|
|
496
|
+
`AGENTS.md`, a `reference/` folder. The host decides its backend, whether it is writable,
|
|
497
|
+
and whether the spec governs it. Naming a folder in a spec does not create it: ask the
|
|
498
|
+
host to mount it.
|
|
499
|
+
|
|
500
|
+
- An **ungoverned** mount, the default, is visible in every state. Only a `forbid` takes
|
|
501
|
+
it away.
|
|
502
|
+
- A **governed** mount (`governed: true` in the host's table) is reachable only where a
|
|
503
|
+
`mounts` list grants it, exactly like a skill bundle: `allow_always` at the root,
|
|
504
|
+
`allow` on a state, `forbid` and `forbid_always` to subtract. A name is the mount's key
|
|
505
|
+
without its slashes (`reference`, `catalogs/eu`), never a glob.
|
|
506
|
+
- An entry is a name, which reads, or `{ mount: <name>, access: read | read_write }`. A
|
|
507
|
+
governed mount is writable in a state only when the host serves it writable, some grant
|
|
508
|
+
of this workflow asks `read_write`, and no grant that applies in the state says `read`.
|
|
509
|
+
The host's read-only is a ceiling no grant lifts.
|
|
510
|
+
- A mount the state does not have is hidden from listings and searches, the workspace
|
|
511
|
+
root's listing included.
|
|
512
|
+
- A write the host's wiring refuses is `zone.read-only`. One the spec refuses is
|
|
513
|
+
`mount.read-only`.
|
|
514
|
+
- Validate errors on a name the host's table does not mount. It warns on a grant to an
|
|
515
|
+
ungoverned mount, on `read_write` for a read-only mount, and, once, on a missing root
|
|
516
|
+
`mounts` block in a workspace that governs mounts.
|
|
517
|
+
|
|
518
|
+
## Moving, parking and failing
|
|
519
|
+
|
|
520
|
+
- **Advance.** The agent moves only with `archmax_advance({ to, reason, evidence? })`, one
|
|
521
|
+
per message. The transition and the state's `requires` are checked, and a refusal leaves
|
|
522
|
+
the agent in the state to try again. Then the target's tools unlock, or a grader or code
|
|
523
|
+
target runs at once. Human decisions, `on_error` routes and automated states' own
|
|
524
|
+
advances are the other ways a session moves.
|
|
525
|
+
- **Reset.** `archmax_reset({ reason })` returns the session to the entry state it began
|
|
526
|
+
in and releases the agent's locks. Forbid it with `forbid_always` when a session must
|
|
527
|
+
never restart.
|
|
528
|
+
- **Wait.** A state that needs an outside event calls `archmax_wait({ reason, until? })`
|
|
529
|
+
in the same message as its question. Any agent state may, terminal ones included.
|
|
530
|
+
`until` (`30m`, `2h`, `1d` or an ISO instant) becomes a `resumeAt` for the host's
|
|
531
|
+
scheduler; the runtime keeps no timer. Any delivered trigger continues the **same
|
|
532
|
+
state**, with its payload as locked variables and an `[event]` runtime note, and the
|
|
533
|
+
state then picks its own transition. Bound a polling loop with `budget.maxParks` and an
|
|
534
|
+
`on_error`.
|
|
535
|
+
- **Human states.** Entering one parks the session with a decision record: the state's
|
|
536
|
+
`instructions`, its `evidence` plus any paths the advancing agent attached with
|
|
537
|
+
`archmax_advance({ evidence })`, and its transitions. A person picks the transition,
|
|
538
|
+
and the target state reads the choice and the comment as a `[decision]` runtime note.
|
|
539
|
+
Entering the state spends one reply-only model call, so the park carries a handoff
|
|
540
|
+
message. Do not tell the agent to announce the handoff, or it says it twice. While the
|
|
541
|
+
session is parked, a `reply` is answered on a reply-only turn (every tool call is
|
|
542
|
+
refused) and the session stays parked. Use a human state when a person picks the
|
|
543
|
+
transition, and `archmax_wait` when an event or a time continues the work.
|
|
544
|
+
- **Raise.** When the work cannot be completed, `archmax_raise({ code, reason })`, alone
|
|
545
|
+
in its message, ends the session `failed` at once: no `on_error` route, no `returns`
|
|
546
|
+
check. `code` is a free-form token of up to 64 characters, so name the codes a host
|
|
547
|
+
branches on in the state's `instructions`. Raise is for failure only: a session that
|
|
548
|
+
never raises succeeds. A child that raises fails its caller's call with kind `raised`.
|
|
549
|
+
- **The next turn.** A finished session given a new message takes its next turn in the
|
|
550
|
+
state it ended in, not at the entry. A session that completed in a terminal automated
|
|
551
|
+
state opens at its trigger's entry. Give a state transitions onward when a conversation
|
|
552
|
+
must move on from it, or park instead of finishing.
|
|
553
|
+
|
|
554
|
+
## Budgets and `on_error`
|
|
555
|
+
|
|
556
|
+
- `budget.timeoutMs` bounds each model call of an agent state, and a grader's whole run.
|
|
557
|
+
`budget.maxParks` bounds how often an agent state parks with `archmax_wait`. A code
|
|
558
|
+
state takes no `budget`: `settings.timeoutMs` bounds its script.
|
|
559
|
+
- A **turn** is bounded by LangGraph's `recursionLimit`: 400 graph steps unless the
|
|
560
|
+
host's invoke config sets one. Every model call, tool batch and automated state is a
|
|
561
|
+
step. A turn that hits the limit, a cycle of automated states included, ends the
|
|
562
|
+
session `failed` with `recursion-limit`. It does not route through `on_error`.
|
|
563
|
+
- **Failures route to `on_error`**: an exceeded `timeoutMs` or `maxParks`, a failed
|
|
564
|
+
sub-workflow call nothing recovered, and a grader or code state that fails (it threw,
|
|
565
|
+
timed out, or finished without advancing or raising). The target state reads the
|
|
566
|
+
reason as an `[error]` runtime note. Without an `on_error`, the failure ends the
|
|
567
|
+
session rejected.
|
|
568
|
+
- **Refusals leave the agent in place**: an undeclared transition, unmet `requires`, an
|
|
569
|
+
ordinary governance refusal. A tool that throws is answered with its error, and the
|
|
570
|
+
agent may retry or change course; `on_error` never sees it.
|
|
571
|
+
- Declare `on_error` on every grader and code state, and on every bounded loop.
|
|
572
|
+
|
|
573
|
+
## Sub-workflows
|
|
574
|
+
|
|
575
|
+
A state calls another workflow of the workspace by allowing `archmax_workflow_<slug>`,
|
|
576
|
+
with the slug verbatim. That entry is the whole declaration.
|
|
1494
577
|
|
|
1495
578
|
```yaml
|
|
1496
579
|
enrich:
|
|
1497
580
|
instructions: >-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
requires:
|
|
1501
|
-
- enrichment # nothing captures the result — this makes it mandatory
|
|
581
|
+
Call archmax_workflow_enrich-order with the order id, then record what it
|
|
582
|
+
returns as `enrichment` with archmax_set_variables.
|
|
583
|
+
requires: [enrichment] # nothing records the result for you
|
|
1502
584
|
tools:
|
|
1503
|
-
allow:
|
|
1504
|
-
- archmax_workflow_enrich-account # the slug, verbatim
|
|
1505
|
-
budget: { timeoutMs: 120000 } # bounds the whole turn, calls included
|
|
585
|
+
allow: [archmax_workflow_enrich-order]
|
|
1506
586
|
on_error: enrichment-failed
|
|
1507
587
|
transitions:
|
|
1508
|
-
- to:
|
|
1509
|
-
description: The account has been enriched.
|
|
1510
|
-
```
|
|
1511
|
-
|
|
1512
|
-
- **The target's signature is the call signature.** Its `manual` trigger's
|
|
1513
|
-
`requires:` are the tool's required parameters (typed in the tool schema
|
|
1514
|
-
where the entries are typed, by the same mapping `signatureJsonSchema`
|
|
1515
|
-
publishes); its `returns:` are what the result carries; its `description`
|
|
1516
|
-
leads the tool description, and its `instructions` never reach the caller:
|
|
1517
|
-
|
|
1518
|
-
```
|
|
1519
|
-
archmax_workflow_enrich-account({ account_id: "acct-42" })
|
|
1520
|
-
→ { message: "Enriched acct-42.",
|
|
1521
|
-
returns: { enrichment_file: "scratchpad/e/acct-42.json", risk_level: "low" } }
|
|
1522
|
-
```
|
|
1523
|
-
|
|
1524
|
-
A target declaring no `returns:` answers with the closing message alone.
|
|
1525
|
-
- **Nothing captures the result.** The agent records what it needs with
|
|
1526
|
-
`archmax_set_variables`, or a script writes it. Declare the state's `requires:`
|
|
1527
|
-
so it cannot advance without doing so — that is what replaces the old automatic
|
|
1528
|
-
capture.
|
|
1529
|
-
- **One call runs one sub-run.** There is no fan-out argument: an agent fans out
|
|
1530
|
-
with parallel tool calls, a script with `Promise.all`. Both are bounded by
|
|
1531
|
-
the dispatcher's `maxConcurrent`, and each call succeeds or fails on its
|
|
1532
|
-
own.
|
|
1533
|
-
- **A script is the deterministic fan-out.** Delegation tools are on the PTC
|
|
1534
|
-
surface, so a script decides how many calls to make:
|
|
1535
|
-
|
|
1536
|
-
```js
|
|
1537
|
-
// the bridge camelCases tool names; a tool result arrives as text
|
|
1538
|
-
const results = await Promise.all(
|
|
1539
|
-
orders.map((o) => tools.archmaxWorkflowEnrichAccount({ account_id: o.id })),
|
|
1540
|
-
);
|
|
1541
|
-
const returns = results.map((r) => JSON.parse(r).returns);
|
|
1542
|
-
```
|
|
1543
|
-
|
|
1544
|
-
A script's delegation **cannot park**: a child that reaches a human state fails
|
|
1545
|
-
that call closed with `parked`.
|
|
1546
|
-
- **Governance is ordinary.** The surface is closed by default, so a state naming
|
|
1547
|
-
no target may call none; `tools.forbid_always` blocks a delegation tool like any
|
|
1548
|
-
other.
|
|
1549
|
-
- **A failure is a tool error** the calling agent can handle; the state's
|
|
1550
|
-
`on_error` catches the turn if it cannot. Each failed call is recorded on
|
|
1551
|
-
its own, with its arguments, and stays the calling state's failure until a
|
|
1552
|
-
**later** successful call of the same workflow from the same state recovers
|
|
1553
|
-
it. One success recovers **one** failed call: the one whose arguments it
|
|
1554
|
-
repeats (compared after `${{…}}` resolves, key order ignored), else the
|
|
1555
|
-
oldest — so two parallel calls that failed need two successes. A sibling
|
|
1556
|
-
call in the same batch, another workflow's success, or a rejection with
|
|
1557
|
-
another cause (a terminal kernel block, exhausted parks) recovers nothing,
|
|
1558
|
-
and the route names every outstanding failure with its arguments. An
|
|
1559
|
-
advance, a park or a reset clears them, as it clears any pending rejection. Depth, cycle, missing-input
|
|
1560
|
-
(`missing-param`), mistyped-input (`invalid-param`), unresolvable-reference
|
|
1561
|
-
(`unresolved-param`) and **disabled-target** refusals are blocked calls —
|
|
1562
|
-
nothing ran, so correct the call and retry. A child whose returns are
|
|
1563
|
-
mistyped (`invalid-return`) is a failure: it ran. One that left returns unset
|
|
1564
|
-
is not: the call answers with them `null` and a `note`.
|
|
1565
|
-
- **A whole-argument reference keeps its type.** `{ quantity: "${{count}}" }`
|
|
1566
|
-
seeds the child with the number `count` holds, so it satisfies an `integer`
|
|
1567
|
-
parameter; `{ note: "order ${{order_id}}" }` is substituted as text.
|
|
1568
|
-
- **A `disabled: true` target is refused at dispatch, not at assembly.** The tool is
|
|
1569
|
-
still bound and the caller still assembles (disabling one leaf must not take every
|
|
1570
|
-
caller — or its parked sessions — offline); the call is a blocked call of kind
|
|
1571
|
-
`disabled`, and it beats a declared mock. `validate` **warns** on each state that
|
|
1572
|
-
allows a disabled target: the caller's spec is not wrong.
|
|
1573
|
-
- **A caller enters the child's `manual` entry** — the one entry the CLI, a host
|
|
1574
|
-
firing and a call all use. There is no opt-in on the target: whether a
|
|
1575
|
-
delegation may happen is decided by the caller's `tools.allow` entry. A target
|
|
1576
|
-
with no `manual` entry has no state to start in and fails closed. `session:` and
|
|
1577
|
-
`message:` on that trigger are host-facing keys a delegation ignores: a sub-run
|
|
1578
|
-
is its own session, derived from its caller's, and carries no message. `sub-workflow` is no longer
|
|
1579
|
-
a trigger id — load and `validate` report it with the rename.
|
|
1580
|
-
- **Declare the child's signature** on that trigger — the contract, enforced both
|
|
1581
|
-
ways and at both ingresses:
|
|
1582
|
-
|
|
1583
|
-
```yaml
|
|
1584
|
-
states:
|
|
1585
|
-
enrich:
|
|
1586
|
-
triggers:
|
|
1587
|
-
manual:
|
|
1588
|
-
description: Enrich one order. # leads the caller's tool description
|
|
1589
|
-
requires: # a caller must supply these
|
|
1590
|
-
- { name: order_id, type: string, description: The order to enrich. }
|
|
1591
|
-
returns: # the child guarantees it sets these
|
|
1592
|
-
- enrichment_file
|
|
1593
|
-
- { name: delayed, type: boolean }
|
|
1594
|
-
```
|
|
1595
|
-
|
|
1596
|
-
Develop it standalone on the same contract:
|
|
1597
|
-
`archmax run enrich-order --variables '{"order_id":"ORD-1003"}'`. `${{trigger}}`
|
|
1598
|
-
reads `manual` either way, so the machine cannot branch on having been called.
|
|
1599
|
-
|
|
1600
|
-
A dispatch missing a `requires` name is **refused before the child is
|
|
1601
|
-
composed**; a child that completes without every `returns` name set
|
|
1602
|
-
**completes anyway**, and the call answers with every declared return, the
|
|
1603
|
-
unset ones `null`, plus a `note` naming them — no second attempt is spent, and
|
|
1604
|
-
the caller decides
|
|
1605
|
-
(a child that *parks* is not checked — it has not finished; a child that
|
|
1606
|
-
raises fails the call `raised`). A typed entry is held to its type at both
|
|
1607
|
-
ends: a mistyped argument is refused `invalid-param` before the child runs, and
|
|
1608
|
-
a mistyped return fails the call `invalid-return`. What the child must *do* to
|
|
1609
|
-
produce a variable belongs in the state `instructions` that set it. The names,
|
|
1610
|
-
with type and description where declared, are rendered into the child's prompt
|
|
1611
|
-
in every state, so it is told what to produce before it can be failed for
|
|
1612
|
-
omitting it — and to set each once its final value is known, locking one only
|
|
1613
|
-
when it is final.
|
|
1614
|
-
- **The call answers with the returns, by name.** A signed target answers
|
|
1615
|
-
`{ message, returns: { enrichment_file, delayed } }`; one declaring no
|
|
1616
|
-
`returns:` answers with the closing message alone. The message keeps its own
|
|
1617
|
-
key rather than being folded into the data — prose the model reads, values a
|
|
1618
|
-
guard or a script reads by name. **Nothing captures it into a variable**: the
|
|
1619
|
-
agent records what it needs with `archmax_set_variables`, or a script writes it,
|
|
1620
|
-
and the calling state's `requires:` is what makes that mandatory.
|
|
1621
|
-
- **Mocks are held to the same contract.** An `archmax_workflow_<slug>` mock for a
|
|
1622
|
-
signed target supplies `returns:`. One that supplies a mistyped typed one
|
|
1623
|
-
(`invalid-return`) fails the mocked dispatch, and one that omits a declared
|
|
1624
|
-
name answers like a real child that left it unset: that name `null`, plus a
|
|
1625
|
-
`note` — a mock stands in for the sub-run, never for its agreement.
|
|
1626
|
-
- **No prose crosses the boundary.** A sub-run gets no instruction from its
|
|
1627
|
-
caller — it works from its own state `instructions` (system prompt) plus the
|
|
1628
|
-
arguments it was seeded with as locked variables, whose values its opening
|
|
1629
|
-
message lists: strings JSON-quoted, numbers and booleans as written. A list,
|
|
1630
|
-
an object, `null`, a value rendering past 200 characters, or one past the
|
|
1631
|
-
message's 1,000-character total is only named, with `archmax_get_variables`
|
|
1632
|
-
as the way to read it. Nothing to author, nothing to keep in sync, and nothing
|
|
1633
|
-
in the child's state `instructions` should say "read your inputs first".
|
|
1634
|
-
- **A child goes straight to its work.** Its prompt leaves out the platform
|
|
1635
|
-
prompt's "Name the run first" step (a child's `title` is never returned), so a
|
|
1636
|
-
one-state child with short scalar inputs spends its first model call on the
|
|
1637
|
-
state's work. A workspace overriding the platform prompt
|
|
1638
|
-
(`.platform/system/GRAPH_STATE.md`) keeps that by wrapping what children skip
|
|
1639
|
-
in `<!-- top-level-only -->` … `<!-- /top-level-only -->` lines.
|
|
1640
|
-
- **Same agent, isolated context, own session.** Same model, backend, host
|
|
1641
|
-
tools; its **own** grader and code states, from its own spec; a fresh transcript; the child's *own* per-state
|
|
1642
|
-
`tools.allow`. It is a separate session (`<parent>~<n>`, `n` the dispatch's
|
|
1643
|
-
number in the caller, counted across its turns) with its own checkpoints and its own `scratchpad/`: a path it reads or writes
|
|
1644
|
-
resolves inside its own session, so it can neither see nor overwrite a file of
|
|
1645
|
-
its caller's, and concurrent sub-runs cannot collide on a path.
|
|
1646
|
-
- **Nothing crosses implicitly.** Down: only the arguments the call passed —
|
|
1647
|
-
the calling run's own variables are not copied. Up: only the result message
|
|
1648
|
-
and the declared returns. Everything else the child sets or writes stays in its
|
|
1649
|
-
session. The only files that cross are runtime-made copies of `file` arguments
|
|
1650
|
-
and `file` returns (see "Passing files" below).
|
|
1651
|
-
- **Bounds** — the dispatcher's `bounds: { maxDepth: 3, maxConcurrent: 4 }`. No longer a spec key: the strict schema rejects `settings.sub_workflows` by name.
|
|
1652
|
-
Excess concurrency queues; depth and cycles are refused.
|
|
1653
|
-
- **Params are the sub-run's initial variables** — same construction, same name
|
|
1654
|
-
rule, same lock as a host's `variables`. So a delegated machine also runs
|
|
1655
|
-
**standalone** with those inputs supplied directly:
|
|
1656
|
-
`archmax run <child> --variables '{"order_id":"ORD-1003"}'` — no `--trigger`,
|
|
1657
|
-
because a caller and the CLI enter the same `manual` entry. That is how you
|
|
1658
|
-
develop one in isolation, on exactly the contract a caller holds it to.
|
|
1659
|
-
- **A child may stop for a person.** A `type: human` state in a delegated machine
|
|
1660
|
-
suspends the sub-run *and* the delegating run; `archmax decide <sessionId>
|
|
1661
|
-
<target>` resolves the **child's** decision, the sub-run finishes, and the
|
|
1662
|
-
parent carries on with its result. This holds for a **fan-out** too: each
|
|
1663
|
-
dispatch is its own checkpointed task, so resolving one decision re-runs only
|
|
1664
|
-
that sub-run — siblings that finished keep their results.
|
|
1665
|
-
- **Fails closed** — rejection, budget, refusal or an unresolvable param routes
|
|
1666
|
-
through `on_error` (or ends the run rejected); a suspension for a person is not
|
|
1667
|
-
a failure. Ancestor
|
|
1668
|
-
`forbid_always` rules bind inside the child, naming the workflow that
|
|
1669
|
-
declared them.
|
|
1670
|
-
|
|
1671
|
-
`archmax validate` checks all of this statically: target exists, opted in, no
|
|
1672
|
-
cycle, no over-deep chain, correct state shape — and the signature both ways. A
|
|
1673
|
-
parent reading a field the child does not return is an **error**; a state whose
|
|
1674
|
-
`params:` do not cover the target's `requires:` is a **warning** (the advance may
|
|
1675
|
-
still carry it), and a fan-out is exempt from that warning because its
|
|
1676
|
-
entries' keys are not knowable statically.
|
|
1677
|
-
|
|
1678
|
-
### Passing files
|
|
1679
|
-
|
|
1680
|
-
A sub-run's `scratchpad/` is not its caller's, so a bare path string returned by a
|
|
1681
|
-
child (`returns: [result_file]`, untyped) names a file the caller **cannot
|
|
1682
|
-
read**. Type the entry `file` instead, and the runtime copies the file across:
|
|
1683
|
-
|
|
1684
|
-
```yaml
|
|
1685
|
-
# the child
|
|
1686
|
-
triggers:
|
|
1687
|
-
manual:
|
|
1688
|
-
requires: [{ name: invoice, type: file }]
|
|
1689
|
-
returns: [{ name: report, type: file }]
|
|
1690
|
-
```
|
|
1691
|
-
|
|
1692
|
-
- **Down.** The caller passes any path in its workspace (`scratchpad/inbox/a.pdf`,
|
|
1693
|
-
`skills/billing/assets/template.pdf`). Before the child starts, the runtime
|
|
1694
|
-
copies the exact bytes into the **child's** `scratchpad/attachments/` (same name;
|
|
1695
|
-
two arguments with one name take `-1`, `-2`) and seeds the child with that path,
|
|
1696
|
-
so its opening message shows it. The child edits its own copy and never the
|
|
1697
|
-
caller's file. The argument is a `read` path argument of the delegation tool, so
|
|
1698
|
-
a `paths:` guard on the allow entry and the zone and mount rules bind it. An
|
|
1699
|
-
argument naming nothing is refused `invalid-param` before the child runs. A
|
|
1700
|
-
failed call's record names the caller's path.
|
|
1701
|
-
- **Up.** On completion, each `file` return the child set is copied into the
|
|
1702
|
-
**caller's** `scratchpad/attachments/` and the result names that path. A taken
|
|
1703
|
-
name gets the lowest free `-1`, `-2`, … before the extension (`report-1.pdf`), so
|
|
1704
|
-
repeated or parallel calls never overwrite each other or the caller's files. A
|
|
1705
|
-
return whose file is gone at completion fails the call `invalid-return`.
|
|
1706
|
-
- **No laundering.** A `file` value must name a file the session may read on
|
|
1707
|
-
runtime authority (its own workflow's mount grants and `forbid_always`), and a
|
|
1708
|
-
child inherits every ancestor's `forbid_always`. So a
|
|
1709
|
-
child cannot hand back a file its caller's workflow denies: its agent's write
|
|
1710
|
-
is refused, and a script's write is rejected at completion.
|
|
1711
|
-
- **Instructions to write.** Tell the child to write the file *first* and then set
|
|
1712
|
-
the return to its path. Tell the caller the returned path is under
|
|
1713
|
-
`scratchpad/attachments/`. Name files for what they hold (`<order_id>.json`):
|
|
1714
|
-
the name survives the copy.
|
|
1715
|
-
- **Mocks and cases.** A mocked `file` return names a file in the case's own
|
|
1716
|
-
session (seed it with `workspace:`), and it arrives copied into
|
|
1717
|
-
`scratchpad/attachments/` like a real one. Assert the copy's path.
|
|
1718
|
-
|
|
1719
|
-
### A parked run may speak, never act
|
|
1720
|
-
|
|
1721
|
-
A park does not end the conversation. Two moments get a **reply-only turn** — one
|
|
1722
|
-
model call whose only product is text. It is handed the parked state's tool list
|
|
1723
|
-
like any other call there (a transcript's tool calls and runtime notes need their
|
|
1724
|
-
definitions on some providers), and every call it makes is refused:
|
|
1725
|
-
|
|
1726
|
-
- **The handoff.** Advancing into a human state always spends one, so the park
|
|
1727
|
-
carries a message saying what was done and that a person now holds it. It runs
|
|
1728
|
-
whether or not the state already spoke: the agent wrote before the transition
|
|
1729
|
-
committed, so what it wrote was about its own work. (An `archmax_wait` park gets
|
|
1730
|
-
the same turn only when the turn was silent — there the agent's own question
|
|
1731
|
-
is the message.)
|
|
1732
|
-
- **While parked.** `agent.workflow.reply(sessionId, message)` / `archmax reply <session>
|
|
1733
|
-
"<text>"` answers a message and parks again on the *same* record — same node,
|
|
1734
|
-
`seq`, and park timestamp, so the reported human wait still measures the
|
|
1735
|
-
original park. `resolveSession` classifies such a firing as the `reply`
|
|
1736
|
-
disposition rather than failing.
|
|
1737
|
-
|
|
1738
|
-
Nothing you author changes this and there is no field to declare: the message is
|
|
1739
|
-
always composed on a fresh turn from the transcript, under the workspace persona.
|
|
1740
|
-
The kernel refuses every tool call made during one (rule `tool.reply-only`,
|
|
1741
|
-
ordered ahead of `tools.allow`, `allow_always`, the `archmax_*` controls and the
|
|
1742
|
-
scratchpad), so a customer writing "just approve it" gets an answer and the
|
|
1743
|
-
reviewer still owns the edge. A model that calls a tool instead of writing its
|
|
1744
|
-
message gets the refusal, and the run parks without a second model call.
|
|
1745
|
-
|
|
1746
|
-
**What this means for authoring:** do not write state `instructions` that tell the
|
|
1747
|
-
agent to announce the handoff itself — the runtime turn does that, and having both
|
|
1748
|
-
produces two messages. Write instructions for the *work*.
|
|
1749
|
-
|
|
1750
|
-
## Removed keys you may meet in an older workflow
|
|
1751
|
-
|
|
1752
|
-
Every key here is **rejected**, with the key and its path named and guidance
|
|
1753
|
-
saying what replaced it. `workflow.yaml` is parsed through a strict schema, so a
|
|
1754
|
-
workflow still declaring one fails to load rather than running while silently
|
|
1755
|
-
doing nothing of what its author intended.
|
|
1756
|
-
|
|
1757
|
-
This is a change: these keys used to be ignored without a diagnostic. A spec that
|
|
1758
|
-
"worked" by ignoring one now needs the migration below.
|
|
1759
|
-
|
|
1760
|
-
| Removed | Replace with |
|
|
1761
|
-
| --- | --- |
|
|
1762
|
-
| `type: wait` state | The state that needs the answer calls `archmax_wait({ reason })`; a delivery resumes that same state (see [Parking and sessions](#parking-and-sessions)) |
|
|
1763
|
-
| transition `on: <trigger-id>` | Nothing — a delivered trigger no longer selects an edge; the resumed state picks its own transition |
|
|
1764
|
-
| state `parallel: [...]` + `join:` | Ordinary `transitions`, sequenced |
|
|
1765
|
-
| state `type: trigger` | A `trigger:` field on the state the trigger state routed to |
|
|
1766
|
-
| top-level `name:` | `title:` |
|
|
1767
|
-
| top-level `description:` | `instructions:` |
|
|
1768
|
-
| top-level `policy:` | `tools.forbid_always` (and `skills.forbid_always`) — a load error now |
|
|
1769
|
-
| root `skills.allow:` (the ceiling) | `skills.allow_always: []` plus the state lists — a load error now |
|
|
1770
|
-
| state `type: workflow` | An ordinary agent state allowing `archmax_workflow_<target>` and calling it (see [Sub-workflows are tools](#sub-workflows-are-tools)) |
|
|
1771
|
-
| state `workflow: <slug>` | The `archmax_workflow_<slug>` entry in that state's `tools.allow` — the allow entry *is* the declaration |
|
|
1772
|
-
| state `params:` | The call's arguments |
|
|
1773
|
-
| state `for_each:` | One call per entry — parallel tool calls from the agent, or `Promise.all` in a script |
|
|
1774
|
-
| state `result:` | The tool result; add `requires: [<name>]` to the state so it must record what it needs |
|
|
1775
|
-
| `archmax_advance({ params })` | The call's arguments |
|
|
1776
|
-
| state `before:` (any hook kind) | A **code state in front** of the state — the trigger's entry, or the target of the edge into it — advancing to the work or to a declining state (see [Grader and code states](#grader-and-code-states)) |
|
|
1777
|
-
| state `after: { script: hooks/<x>.js }` | A **code state behind** the state: its transition points at the code state, which advances onward or back with the fix as its `reason` |
|
|
1778
|
-
| `{ rubric: … }` hook | A **grader state** behind the graded state with an edge back; the criteria become its `instructions` |
|
|
1779
|
-
| `max_iterations` / `max_corrections` | Nothing: the grader's `instructions` (or the script) count send-backs in the trail and raise |
|
|
1780
|
-
| root `extensions:` (custom hook kinds) and `createAgent({ hookExecutors })` | Nothing: a custom check is a code state |
|
|
1781
|
-
| `workflows/<slug>/hooks/*.js` returning `ok`/`veto`/`correct` | `workflows/<slug>/scripts/*.js` that call `tools.archmax_advance` / `tools.archmax_raise` |
|
|
1782
|
-
| `runtime.version: "1"` / `"2"` | `"3"` (or omit `runtime`) — 1 and 2 are refused as unsupported |
|
|
1783
|
-
| root `budget: { maxModelCalls }` | Nothing — delete it. A turn is bounded by LangGraph's `recursionLimit` (400 unless the host's invoke config sets one); bound a state with its own `budget.maxTurns` |
|
|
1784
|
-
|
|
1785
|
-
**No fan-out.** There is no concurrent branching: a run traverses **one state at a
|
|
1786
|
-
time**, moved only by `archmax_advance`, a human decision, or `on_error` routing.
|
|
1787
|
-
Sequence the states that were branches:
|
|
1788
|
-
|
|
1789
|
-
```yaml
|
|
1790
|
-
# REMOVED — do not author this
|
|
1791
|
-
states:
|
|
1792
|
-
gather:
|
|
1793
|
-
parallel: [check-inventory, check-credit]
|
|
1794
|
-
join: summarize
|
|
1795
|
-
|
|
1796
|
-
# author this instead
|
|
1797
|
-
states:
|
|
1798
|
-
check-inventory:
|
|
1799
|
-
transitions: [{ to: check-credit }]
|
|
1800
|
-
check-credit:
|
|
1801
|
-
transitions: [{ to: summarize }]
|
|
1802
|
-
summarize: { instructions: "..." }
|
|
1803
|
-
```
|
|
1804
|
-
|
|
1805
|
-
Concurrent composition **is** sub-workflows: a state calling a delegation tool
|
|
1806
|
-
once per unit of work — the calls run concurrently (see
|
|
1807
|
-
[Sub-workflows are tools](#sub-workflows-are-tools)). Sequence anything that is not that —
|
|
1808
|
-
it costs latency, not correctness.
|
|
1809
|
-
|
|
1810
|
-
## Workspace layout
|
|
1811
|
-
|
|
1812
|
-
```
|
|
1813
|
-
workflows/<name>/workflow.yaml # the machine (canonical spec)
|
|
1814
|
-
workflows/<name>/WORKFLOW.md # OPTIONAL prose addendum (no frontmatter)
|
|
1815
|
-
workflows/<name>/scripts/*.js # code-state scripts — run BY THE RUNTIME when a
|
|
1816
|
-
# `type: code` state is entered.
|
|
1817
|
-
# Authoring plane: the agent cannot read or run
|
|
1818
|
-
# anything under workflows/
|
|
1819
|
-
workflows/<name>/tests/ # cases (*.test.yaml, one case per
|
|
1820
|
-
# file; fixture files referenced with `from:`)
|
|
1821
|
-
skills/<capability>/SKILL.md # a capability, bundled (Agent Skills format).
|
|
1822
|
-
# Shared guidance for content that generalizes
|
|
1823
|
-
# across MULTIPLE states — single-state
|
|
1824
|
-
# behavior goes inline in that state's
|
|
1825
|
-
# `instructions` (never one SKILL.md per state)
|
|
1826
|
-
skills/<capability>/scripts/*.js # its archmax_run sources — run BY THE AGENT
|
|
1827
|
-
# (code-state scripts are NOT here: they live in
|
|
1828
|
-
# workflows/<name>/scripts/ — same directory
|
|
1829
|
-
# name, other plane — are run by the runtime,
|
|
1830
|
-
# and the agent cannot read them)
|
|
1831
|
-
skills/<capability>/assets/* # its inputs (read-only)
|
|
1832
|
-
scratchpad/ # THE working area: intermediate files and the
|
|
1833
|
-
# artifacts a state produces. The run IS the
|
|
1834
|
-
# workspace root — resolves to
|
|
1835
|
-
# sessions/<sessionId>/scratchpad/. Always
|
|
1836
|
-
# writable, per-session
|
|
1837
|
-
large_tool_results/ # runtime-offloaded tool results (read-only)
|
|
1838
|
-
conversation_history/ # runtime-offloaded history (read-only)
|
|
1839
|
-
AGENTS.md # workspace persona/system prompt
|
|
1840
|
-
```
|
|
1841
|
-
|
|
1842
|
-
This is the **complete** directory contract — there are no other magic
|
|
1843
|
-
locations, so design the full file set up front instead of exploring.
|
|
1844
|
-
Reference these paths **workspace-relative, without a leading slash**
|
|
1845
|
-
(`skills/order-data/assets/orders.json`, `scratchpad/refund.json`) — in `instructions`, `evidence`,
|
|
1846
|
-
allow-entry globs, and paths referenced inside code-state scripts alike.
|
|
1847
|
-
|
|
1848
|
-
The authoring-plane prefix (`workflows/`) is **reserved**: a
|
|
1849
|
-
host cannot mount it (`MountCollisionError` at assembly), an allow entry naming
|
|
1850
|
-
it is a `validate` **error**, and a script that reads it is blocked with a
|
|
1851
|
-
diagnostic naming the prefix. Never write an allow entry, an `evidence` path, or a
|
|
1852
|
-
case `workspace:` key under it.
|
|
1853
|
-
|
|
1854
|
-
## Cases
|
|
1855
|
-
|
|
1856
|
-
Declarative YAML case documents under `workflows/<name>/tests/*.test.yaml`
|
|
1857
|
-
(or `.test.yml`) — **one case per file**; the case id is the tests/-relative
|
|
1858
|
-
path minus the extension. `archmax test <workflow>` interprets them
|
|
1859
|
-
**host-side**: no QuickJS sandbox runs a case (the sandbox serves only
|
|
1860
|
-
code-state scripts and `archmax_run` inside the driven agent). These are distinct
|
|
1861
|
-
from the runtime's own colocated unit tests. Only `*.test.yaml`/`*.test.yml`
|
|
1862
|
-
files are discovered — a leftover `*.test.js` is ignored without a diagnostic.
|
|
1863
|
-
|
|
1864
|
-
A workflow declaring `disabled: true` runs **no** cases: the suite is reported as
|
|
1865
|
-
**skipped** and the exit code is **0** (not run is not failed, so retiring a
|
|
1866
|
-
workflow never reddens CI). There is no flag to force them; `archmax validate` is
|
|
1867
|
-
the static feedback while a workflow is out of service.
|
|
1868
|
-
|
|
1869
|
-
**Suite config lives in the `tests:` block of `workflow.yaml`**
|
|
1870
|
-
(`maxConcurrency`, `caseTimeoutMs`, `judge` — see [Root fields](#root-fields)).
|
|
1871
|
-
`tests.config.js` is **REMOVED**: its presence is a hard error in
|
|
1872
|
-
`archmax test` and `archmax validate`. There is no `reporters` option.
|
|
1873
|
-
`judge: {}` enables the case judge (`grade:`) with the env-configured default model.
|
|
1874
|
-
|
|
1875
|
-
**The complete case schema (top-level keys, the flat step-entry list, the
|
|
1876
|
-
assertion grammar, start conditions, mocks, `from:` fixture references) plus
|
|
1877
|
-
worked examples — routing,
|
|
1878
|
-
tool mocking, human-in-the-loop decide flows, triggered starts — and the
|
|
1879
|
-
JS→YAML migration table are in
|
|
1880
|
-
[`code-state-and-test-scripts.md`](code-state-and-test-scripts.md).**
|
|
1881
|
-
|
|
1882
|
-
Every case carries two mandatory prose fields, both length-capped: a `title`
|
|
1883
|
-
(a short label, ≤ 60 chars, printed beside the verdict) and a `description`
|
|
1884
|
-
(≤ 200 chars — the scenario driven and what is asserted). `archmax validate`
|
|
1885
|
-
**errors** on a missing or over-long field; longer rationale belongs in a YAML
|
|
1886
|
-
comment above the document (see
|
|
1887
|
-
[Code description docblocks](#code-description-docblocks)). A case is one
|
|
1888
|
-
conversation on one session; `steps` is one flat list where actions (`send`,
|
|
1889
|
-
`decide`) and assertions are peers, each assertion evaluating against the
|
|
1890
|
-
nearest action above it. Unknown keys anywhere in a case are validation
|
|
1891
|
-
errors — fail closed.
|
|
1892
|
-
|
|
1893
|
-
```yaml
|
|
1894
|
-
# Nothing is mocked — the case reads the real orders.json fixture, so the
|
|
1895
|
-
# denial is re-derived by the check-refund code state rather than asserted directly.
|
|
1896
|
-
title: Refund denied for a shipped order
|
|
1897
|
-
description: >-
|
|
1898
|
-
A refund for shipped A-1002 parks at refund-review; the reviewer confirms the
|
|
1899
|
-
denial via refund-closed, the run succeeds, and the reply names the order.
|
|
1900
|
-
steps:
|
|
1901
|
-
- send: "Hi, I'm support@acmecorp.com. Please refund order A-1002."
|
|
1902
|
-
- parked: true
|
|
1903
|
-
- reachedState: refund-review
|
|
1904
|
-
- decide:
|
|
1905
|
-
to: refund-closed
|
|
1906
|
-
comment: "The denial matches policy."
|
|
1907
|
-
- succeeded: true
|
|
1908
|
-
- reply:
|
|
1909
|
-
includes: "A-1002"
|
|
588
|
+
- { to: reply, description: The order is enriched. }
|
|
1910
589
|
```
|
|
1911
590
|
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
`
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
-
|
|
1939
|
-
`
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
matched against the state's `tools.allow` like any other argument. The framework
|
|
1974
|
-
owns no second artifact area.
|
|
1975
|
-
|
|
1976
|
-
## Annotated example: order-lookup
|
|
1977
|
-
|
|
1978
|
-
The bundled reference workspace lives at
|
|
1979
|
-
`examples/customer-support/workflows/order-lookup/` and demonstrates the full
|
|
1980
|
-
v3 layout: `workflow.yaml` (the machine, including the `tests:` block and its
|
|
1981
|
-
grader and code states), the code-state scripts under `scripts/`, plus a
|
|
1982
|
-
frontmatter-free `WORKFLOW.md` prose addendum (ASCII graph + domain constraints).
|
|
1983
|
-
Read them in full for a real, working example. It declares a top-level `title`
|
|
1984
|
-
and a `title` on every state, with the slugs below as the reference targets, plus
|
|
1985
|
-
a top-level `instructions` block holding the rules that hold across every state —
|
|
1986
|
-
where the data and artifacts live, the tenancy rule, and naming an order's exact
|
|
1987
|
-
status — so no state repeats them. Its shape:
|
|
1988
|
-
|
|
1989
|
-
- `screen-request` (code, `triggers: { manual: , email_reply: }`,
|
|
1990
|
-
`scripts/screen-request.js`, enables `order-data`) — a check **in front**: it
|
|
1991
|
-
merges the requester and tenancy checks before any model call, and advances to
|
|
1992
|
-
`identify-case`, or to `decline` (a terminal agent state that tells the person
|
|
1993
|
-
why) with the reason.
|
|
1994
|
-
- `identify-case` routes to one of four paths via transition descriptions.
|
|
1995
|
-
- `orders-question` (terminal) answers from `skills/order-data/assets/orders.json`:
|
|
1996
|
-
a live answer ends where it is given.
|
|
1997
|
-
- `refund-request` records a decision to `scratchpad/refund.json` (no allow entry
|
|
1998
|
-
narrows it — the working area is open to `write_file` wherever it is granted;
|
|
1999
|
-
what holds the agent to a correct file is the check) and advances to
|
|
2000
|
-
`check-refund`, a code state **behind** it (`scripts/check-refund.js`): it
|
|
2001
|
-
re-derives the policy, advances to `refund-review` when the decision matches,
|
|
2002
|
-
sends it back with the fix otherwise, and escalates after two send-backs.
|
|
2003
|
-
`refund-request` pairs `budget.maxTurns: 12` with `on_error: escalation`.
|
|
2004
|
-
- `refund-review` (`type: human`, `evidence: [scratchpad/refund.json]`) — a person
|
|
2005
|
-
approves (→ terminal `refund-closed`) or refines (→ back to `refund-request`).
|
|
2006
|
-
- `escalation` (terminal) is the error handler entered via `on_error` (and from
|
|
2007
|
-
`check-refund`'s give-up edge).
|
|
2008
|
-
- `general-question` (terminal) answers and writes `scratchpad/answer.json`.
|
|
2009
|
-
- `report-requested` (terminal, `triggers: { report_requested: }`) writes
|
|
2010
|
-
`scratchpad/report.json` for the requested `company` and summarizes it; when the
|
|
2011
|
-
order records cannot be read, the agent ends the session with `archmax_raise`
|
|
2012
|
-
instead (case `orders-unavailable`).
|
|
2013
|
-
- `enrich-order` (`triggers: { enrichment_requested: }`) — a host-fired path no
|
|
2014
|
-
person waits on — runs the `order-enrichment` fan-out script with `archmax_run`,
|
|
2015
|
-
records its result as `enrichments` (`requires: [enrichments]`), and advances to
|
|
2016
|
-
`review-enrichment`, a **grader** behind it: it reads `orders_to_enrich` and
|
|
2017
|
-
`enrichments` with `archmax_get_variables` and each returned `enrichment_file`
|
|
2018
|
-
with `read_file`. It approves into `enrichment-reported` (a terminal agent state
|
|
2019
|
-
that summarizes the enrichments), sends them back to `enrich-order` naming what
|
|
2020
|
-
to fix, or raises `enrichment-incorrect` once it has sent them back twice,
|
|
2021
|
-
counted on `archmax_get_history`'s trail. Case `enrichment-fan-out` pins the
|
|
2022
|
-
approval as a `grader` trail step.
|
|
2023
|
-
|
|
2024
|
-
### Source of truth in code
|
|
2025
|
-
|
|
2026
|
-
If you need to confirm a detail, these files define the schema in the runtime:
|
|
2027
|
-
|
|
2028
|
-
| Concern | File |
|
|
591
|
+
- **The child's `manual` trigger is the call's signature.** Its `requires` are the
|
|
592
|
+
parameters (typed where the entries are typed), its `returns` the result, and its
|
|
593
|
+
`description` leads the tool's description. Its `instructions` never reach the caller.
|
|
594
|
+
- **The call answers** `{ message, returns: { … } }`, or the closing message alone when
|
|
595
|
+
the child declares no `returns`. Nothing records the answer: the caller's agent sets
|
|
596
|
+
what it needs, and the calling state's `requires` makes that mandatory.
|
|
597
|
+
- **The child is a separate session** (`<parent>~<n>`) with a fresh transcript, its own
|
|
598
|
+
`scratchpad/`, and its own grader and code states. It receives only its arguments, as
|
|
599
|
+
locked variables. A `file` argument or return is copied into the receiving session's
|
|
600
|
+
`scratchpad/attachments/`, so type file entries `file`: a bare path names a file the
|
|
601
|
+
other session cannot read.
|
|
602
|
+
- **One call runs one child.** Fan out with parallel tool calls, or with `Promise.all` in
|
|
603
|
+
a script. Depth is bounded at 3 and concurrency at 4.
|
|
604
|
+
- **A refused call ran nothing**: a missing, mistyped or unresolvable argument, a
|
|
605
|
+
disabled target, a cycle, a chain too deep. Correct it and retry.
|
|
606
|
+
- **A failed call is the calling state's failure**: the child failed or raised, or
|
|
607
|
+
returned a mistyped value. It routes through `on_error` when the agent finishes, unless
|
|
608
|
+
a later successful call of the same workflow from the same state recovers it. One
|
|
609
|
+
success recovers one failure: the one with the same arguments, else the oldest.
|
|
610
|
+
- **A whole-argument reference keeps its type**: `{ quantity: "${{count}}" }` passes the
|
|
611
|
+
number.
|
|
612
|
+
- **A child's human state parks the child and its caller.** Deciding it finishes the
|
|
613
|
+
child, and the caller carries on. A script's call cannot park: a child that reaches a
|
|
614
|
+
human state fails it.
|
|
615
|
+
- **`forbid_always` binds every child**, naming the workflow that declared it. A child
|
|
616
|
+
inherits no per-state list.
|
|
617
|
+
- **Develop a child on its own**, on the same contract:
|
|
618
|
+
`archmax run <child> --variables '{"order_id":"ORD-1003"}'`.
|
|
619
|
+
|
|
620
|
+
## Models
|
|
621
|
+
|
|
622
|
+
- `settings.model` is the id every state runs on, and a state's `model` overrides it.
|
|
623
|
+
Each is an id over the endpoint the host configured. The order is the state's, then the
|
|
624
|
+
workflow's, then `ARCHMAX_MODEL`.
|
|
625
|
+
- Name one only where the work differs in kind: a small model for classification and
|
|
626
|
+
extraction, the default for drafting, judgment, and states with many tools or
|
|
627
|
+
transitions. A small model that routes badly costs more in retries than it saves.
|
|
628
|
+
- A grader state's model is resolved under the `grader` model role. A sub-workflow uses
|
|
629
|
+
its own spec's ids. A host that passes an explicit `model` instance to `createAgent`
|
|
630
|
+
outranks every declared id.
|
|
631
|
+
|
|
632
|
+
## Prompt economy
|
|
633
|
+
|
|
634
|
+
The static prompt and the tool schemas are sent on every model call, and the static part
|
|
635
|
+
is cached. It carries the workflow's `title` and top-level `instructions`, and no state.
|
|
636
|
+
A state's `instructions` and transitions are sent only while it is active.
|
|
637
|
+
|
|
638
|
+
- Put rules that hold everywhere in the top-level `instructions`, once. Put everything
|
|
639
|
+
else in the state that needs it: splitting a broad state into narrow ones costs nothing
|
|
640
|
+
in the prefix.
|
|
641
|
+
- `title` and `summary` cost nothing: no model reads them.
|
|
642
|
+
- `WORKFLOW.md` carries only what the spec cannot say.
|
|
643
|
+
- Every model call states the current date and time in UTC, rounded down to ten minutes,
|
|
644
|
+
so date arithmetic needs no authored "today".
|
|
645
|
+
|
|
646
|
+
## Removed keys
|
|
647
|
+
|
|
648
|
+
Each key below is a load error that names it. The
|
|
649
|
+
[changelog](https://harness.archmax.ai/reference/changelog/) says what changed and when.
|
|
650
|
+
|
|
651
|
+
| Key | Write instead |
|
|
2029
652
|
| --- | --- |
|
|
2030
|
-
|
|
|
2031
|
-
|
|
|
2032
|
-
|
|
|
2033
|
-
|
|
|
2034
|
-
|
|
|
2035
|
-
|
|
|
2036
|
-
|
|
|
2037
|
-
|
|
|
2038
|
-
|
|
|
2039
|
-
| `
|
|
2040
|
-
|
|
|
653
|
+
| root `name:` / `description:` | `title:` / `instructions:`, reread as direction |
|
|
654
|
+
| root `policy:` | `tools.forbid_always` and `skills.forbid_always` |
|
|
655
|
+
| root `skills.allow` (a ceiling) | `skills.allow_always` plus the state lists |
|
|
656
|
+
| root `budget` | Nothing: the `recursionLimit` bounds a turn |
|
|
657
|
+
| state `budget.maxTurns` | Nothing: the `recursionLimit` bounds a turn |
|
|
658
|
+
| state `before:` / `after:` (any kind, `rubric:` included), `max_iterations`, `max_corrections`, root `extensions:` | A code state in front of the state for a `before` check, a code state behind it for an `after` check, a grader state behind it for a rubric. Count send-backs on the trail. Scripts move from `hooks/` to `scripts/` and call `tools.archmax_advance` or `tools.archmax_raise`. |
|
|
659
|
+
| state `type: wait`, transition `on:` | `archmax_wait` in the state that needs the answer |
|
|
660
|
+
| state `type: trigger` | `triggers:` on the state it entered |
|
|
661
|
+
| state `type: workflow`, `workflow:`, `params:`, `for_each:`, `result:` | An agent state allowing `archmax_workflow_<slug>`, with `requires` |
|
|
662
|
+
| state `parallel:` / `join:` | Sequenced states |
|
|
663
|
+
| `runtime.version` `1` or `2` | `"3"`, or no `runtime` block |
|
|
664
|
+
| a single-file `WORKFLOW.md` with frontmatter | `workflow.yaml` plus a plain `WORKFLOW.md` |
|
|
665
|
+
| `tests.config.js` | The `tests:` block |
|
|
666
|
+
|
|
667
|
+
## Paths and zones
|
|
668
|
+
|
|
669
|
+
- The agent's workspace root is the session. `scratchpad/` is its working zone, writable
|
|
670
|
+
in every state that is granted a file tool. `large_tool_results/` and
|
|
671
|
+
`conversation_history/` are offload zones the agent only reads. `checkpoints/` and
|
|
672
|
+
`artifacts/` belong to the runtime.
|
|
673
|
+
- Mounts are read-only unless the host serves them writable. A write into a read-only
|
|
674
|
+
mount is refused (`zone.read-only`).
|
|
675
|
+
- `workflows/**` is served by the authoring backend: no state may read it, an allow entry
|
|
676
|
+
naming it is a validate error, and a host cannot mount it.
|
|
677
|
+
- Write paths without a leading slash, and never author a `run/` prefix.
|