@archmax-ai/harness 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -170
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +2 -2
- package/dist/assembly/compose.js +2 -2
- package/dist/assembly/compose.js.map +1 -1
- package/dist/authoring-skill/archmax-harness/README.md +13 -15
- package/dist/authoring-skill/archmax-harness/SKILL.md +174 -264
- package/dist/authoring-skill/archmax-harness/references/backend-integration.md +244 -776
- package/dist/authoring-skill/archmax-harness/references/cases.md +178 -53
- package/dist/authoring-skill/archmax-harness/references/code-states-and-scripts.md +211 -100
- package/dist/authoring-skill/archmax-harness/references/diagnostics.md +57 -78
- package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +613 -2006
- package/dist/core/deepagents.d.ts +1 -2
- package/dist/core/deepagents.js +1 -2
- package/dist/core/deepagents.js.map +1 -1
- package/dist/kernel/kernel.js +2 -2
- package/dist/kernel/kernel.js.map +1 -1
- package/dist/machine/lint-spec.js +6 -5
- package/dist/machine/lint-spec.js.map +1 -1
- package/dist/machine/machine.d.ts +8 -10
- package/dist/machine/machine.js +11 -12
- package/dist/machine/machine.js.map +1 -1
- package/dist/machine/spec-schema.d.ts +1 -4
- package/dist/machine/spec-schema.js +66 -47
- package/dist/machine/spec-schema.js.map +1 -1
- package/dist/machine/tool-names.d.ts +24 -34
- package/dist/machine/tool-names.js +43 -63
- package/dist/machine/tool-names.js.map +1 -1
- package/dist/machine/types.d.ts +11 -10
- package/dist/machine/types.js.map +1 -1
- package/dist/public/spec.d.ts +5 -5
- package/dist/public/spec.js +5 -5
- package/dist/public/spec.js.map +1 -1
- package/dist/validate/validate.js +3 -3
- package/dist/validate/validate.js.map +1 -1
- package/dist/workflow/automated.js +6 -1
- package/dist/workflow/automated.js.map +1 -1
- package/dist/workflow/control-tools.js +0 -2
- package/dist/workflow/control-tools.js.map +1 -1
- package/dist/workflow/grader-state.d.ts +2 -2
- package/dist/workflow/grader-state.js +5 -15
- package/dist/workflow/grader-state.js.map +1 -1
- package/dist/workflow/middleware.js +2 -26
- package/dist/workflow/middleware.js.map +1 -1
- package/dist/workflow/on-error.js +0 -1
- package/dist/workflow/on-error.js.map +1 -1
- package/dist/workflow/parks.js +0 -1
- package/dist/workflow/parks.js.map +1 -1
- package/dist/workflow/render-prompt.d.ts +4 -5
- package/dist/workflow/render-prompt.js +24 -27
- package/dist/workflow/render-prompt.js.map +1 -1
- package/dist/workflow/state.d.ts +0 -9
- package/dist/workflow/state.js +0 -3
- package/dist/workflow/state.js.map +1 -1
- package/dist/workflow/turn-boundary.js +0 -1
- package/dist/workflow/turn-boundary.js.map +1 -1
- package/package.json +1 -2
- package/skills/archmax-harness/README.md +13 -15
- package/skills/archmax-harness/SKILL.md +174 -264
- package/skills/archmax-harness/references/backend-integration.md +244 -776
- package/skills/archmax-harness/references/cases.md +178 -53
- package/skills/archmax-harness/references/code-states-and-scripts.md +211 -100
- package/skills/archmax-harness/references/diagnostics.md +57 -78
- package/skills/archmax-harness/references/workflow-schema.md +613 -2006
- package/dist/authoring-skill/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
- package/dist/authoring-skill/archmax-harness/references/wiring.md +0 -119
- package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +0 -402
- package/skills/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
- package/skills/archmax-harness/references/wiring.md +0 -119
- package/skills/archmax-harness/references/workflow-yaml.md +0 -402
|
@@ -1,76 +1,201 @@
|
|
|
1
|
-
# Cases
|
|
1
|
+
# Cases
|
|
2
2
|
|
|
3
|
-
Load this when
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Load this when you write or fix a case. A case is one YAML document at
|
|
4
|
+
`workflows/<slug>/tests/<case>.test.yaml`, run by `archmax test`: one conversation on
|
|
5
|
+
one session. The harness reads it on the host, and no sandbox runs it. The case id is the
|
|
6
|
+
path under `tests/` without the extension, and `archmax test <workflow> <filter>` matches
|
|
7
|
+
a substring of it.
|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## Decide what to mock first
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
An unmocked tool call in a case is a real call, on every run of the suite: a real email
|
|
12
|
+
sent, a real card charged. Before you write the file, ask the workflow's owner how much
|
|
13
|
+
to mock, naming the tools the workflow calls:
|
|
14
|
+
|
|
15
|
+
- **Mock** anything that sends, pays, posts, deletes or writes outside the session, and
|
|
16
|
+
anything whose result drifts between runs.
|
|
17
|
+
- **Keep real** reads of fixtures committed to the workspace, the workflow's own scripts
|
|
18
|
+
and code states, and writes under `scratchpad/`.
|
|
19
|
+
- **Ask** about reads of a live system: stable enough to keep, or slow, rate-limited or
|
|
20
|
+
credential-bound enough to mock?
|
|
21
|
+
|
|
22
|
+
Record the answer in a YAML comment above the case.
|
|
23
|
+
|
|
24
|
+
## Keys
|
|
12
25
|
|
|
13
26
|
| Key | Shape |
|
|
14
27
|
| --- | --- |
|
|
15
|
-
| `title` |
|
|
16
|
-
| `description` |
|
|
17
|
-
| `skip` |
|
|
18
|
-
| `trigger` | `{ id }
|
|
19
|
-
| `variables` | `{ name: value }
|
|
20
|
-
| `workspace` | `{ <
|
|
21
|
-
| `mocks` | `[{ tool, whenInput?, result }]`
|
|
22
|
-
| `clock` | ISO
|
|
23
|
-
| `steps` |
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`
|
|
37
|
-
`
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
| `title` | Required. One line of at most 60 characters naming the behaviour under test, printed beside the verdict. |
|
|
29
|
+
| `description` | Required. At most 200 characters: the scenario and what it asserts. Longer rationale goes in a YAML comment above the document. |
|
|
30
|
+
| `skip` | A reason. The case reports skipped. |
|
|
31
|
+
| `trigger` | `{ id }`. Every turn of the case runs under it (default `manual`), and an unknown id fails the case. A case that exercises a host trigger must declare it. |
|
|
32
|
+
| `variables` | `{ name: value }`, seeded and **locked** on every turn: the trigger's payload. Never seed what the agent must establish; assert it instead. |
|
|
33
|
+
| `workspace` | `{ <path>: content }`, written before the first step. A string is written verbatim, other YAML as JSON, and `{ from: <path under tests/> }` copies a fixture file. Only session paths: a root file such as `trigger.json`, or `scratchpad/…`. |
|
|
34
|
+
| `mocks` | `[{ tool, whenInput?, result }]`. A call whose input matches `whenInput` (partially) returns `result`. It intercepts the agent's calls and its scripts', code states' and graders' calls alike. A control tool is never mocked. |
|
|
35
|
+
| `clock` | An ISO date, or a date-time with an offset: the "now" every model call states. Pin it whenever fixtures or prompts carry dates. Scripts and recorded timestamps keep the real time. |
|
|
36
|
+
| `steps` | One flat list of actions and assertions. |
|
|
37
|
+
|
|
38
|
+
Unknown keys are errors everywhere. `archmax validate` checks every case: the schema,
|
|
39
|
+
`title` and `description`, regexes, trigger ids and `from:` files. Only `*.test.yaml`
|
|
40
|
+
and `*.test.yml` files are cases. `tests.judge` in `workflow.yaml` enables `grade:`.
|
|
41
|
+
|
|
42
|
+
## Steps
|
|
43
|
+
|
|
44
|
+
Each step is a single-key mapping. An assertion evaluates against the nearest action
|
|
45
|
+
above it, so an assertion before the first action is an error.
|
|
46
|
+
|
|
47
|
+
| Action | Effect |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `send: "<message>"` | One turn. Sent to a session parked at a human state, it is a message answered on a reply-only turn, and the session stays parked. |
|
|
50
|
+
| `decide: { to, comment? }` | A person picks the parked human state's transition `to`. |
|
|
51
|
+
| `deliver: { trigger, variables? }` | Delivers a trigger to a session parked with `archmax_wait`. |
|
|
52
|
+
|
|
53
|
+
Structural assertions are marked ˢ.
|
|
54
|
+
|
|
55
|
+
| Assertion | Passes when |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `succeeded: true`ˢ | The action's outcome is `completed`: not parked, raised, failed or rejected. A tool error the turn recovered from does not fail it. |
|
|
58
|
+
| `parked: true \| decision \| input \| { channel?, state? }`ˢ | The session is parked. |
|
|
59
|
+
| `raised: true \| <code> \| { code?, reason? }`ˢ | The turn ended with `archmax_raise`. `code` is exact; `reason` is exact or a `"/regex/flags"` string. |
|
|
60
|
+
| `reachedState: <slug>`ˢ | A committed trail step entered the state. The trigger's arrival does not count. |
|
|
61
|
+
| `trail: { to?, kind?, reason?, count }`ˢ | Exactly `count` trail steps match every given field. `count: 0` means the state was never entered. |
|
|
62
|
+
| `noTraversal: true`ˢ | Nothing moved the session: the trail holds only trigger arrivals. |
|
|
63
|
+
| `triggerArrival: <id>`ˢ | The session arrived on that trigger. |
|
|
64
|
+
| `reply: { includes?, excludes? }` | Each token or `"/regex/flags"` string, one or a list, matches (or, for `excludes`, matches none of) every assistant message of the session so far. |
|
|
65
|
+
| `calledTool` / `notCalledTool` / `blockedTool: { name, input? }` | The agent called the tool (`input` matches partially), never called it, or called it and governance refused it. `blockedTool` is how you test a guard. |
|
|
66
|
+
| `ranWorkflow: { workflow, status?, count? }` | A sub-workflow ran, whoever called it, a script included. |
|
|
67
|
+
| `usedNoTools: true` | The agent called no tool in the whole conversation. |
|
|
68
|
+
| `variables: { expect, path?, locked? }` | Each named variable equals its expected value (deep equality). `path` steps into a value; `locked` checks the lock. |
|
|
69
|
+
| `grade: { closedQA, atLeast }` | The judge scores the action's turn against the criterion, and a score below `atLeast` fails the case. Needs `tests.judge`. |
|
|
70
|
+
|
|
71
|
+
- **A missed structural assertion halts the case**, and later steps are reported
|
|
72
|
+
`not-executed`. Put the structural assertion right after the action that should
|
|
73
|
+
satisfy it, before content assertions that only make sense there.
|
|
74
|
+
- `calledTool`, `notCalledTool`, `blockedTool` and `usedNoTools` read the agent's calls
|
|
75
|
+
across the whole conversation, not one turn's. They see the `${{name}}` reference the
|
|
76
|
+
agent wrote, not the value: assert a delivered value through its effect instead.
|
|
77
|
+
- A `/pattern/flags` string is a regex: quote it in YAML. A malformed one is a validate
|
|
78
|
+
error. There is no generic predicate: bespoke logic belongs in a code state.
|
|
79
|
+
- `grade` sees what the action's turn said plus a record of the session's messages and
|
|
80
|
+
tool calls, so it can judge what the agent did as well as what it said. It is the one
|
|
81
|
+
non-deterministic assertion.
|
|
82
|
+
|
|
83
|
+
## Examples
|
|
84
|
+
|
|
85
|
+
A routed answer, with a grade:
|
|
42
86
|
|
|
43
87
|
```yaml
|
|
88
|
+
# reachedState pins the route: without it the case passes even when the agent
|
|
89
|
+
# answers from the start state.
|
|
90
|
+
title: Delayed orders answered for a known requester
|
|
91
|
+
description: >-
|
|
92
|
+
A known requester's question routes into orders-question, and the reply names
|
|
93
|
+
the one delayed order, ORD-1003.
|
|
44
94
|
steps:
|
|
45
|
-
- send: "I
|
|
46
|
-
-
|
|
47
|
-
-
|
|
95
|
+
- send: "Hi, I'm support@acmecorp.com. Which orders are delayed for Acme?"
|
|
96
|
+
- succeeded: true
|
|
97
|
+
- reachedState: orders-question
|
|
48
98
|
- reply:
|
|
49
|
-
includes: "
|
|
99
|
+
includes: ["ORD-1003", "/delayed/i"]
|
|
100
|
+
- grade:
|
|
101
|
+
closedQA: confirms that ORD-1003 is the delayed order for Acme Corp
|
|
102
|
+
atLeast: 0.7
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A human state, sent back once and then approved:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
title: Refund sent back for changes, then approved
|
|
109
|
+
description: >-
|
|
110
|
+
The reviewer refines once and approves: the session parks at both reviews,
|
|
111
|
+
refund-review is entered twice, and the final reply still names ORD-1001.
|
|
112
|
+
steps:
|
|
113
|
+
- send: "Hi, I'm support@acmecorp.com. I'd like a refund for order ORD-1001."
|
|
114
|
+
- parked: true
|
|
115
|
+
- decide: { to: refund-request, comment: "Restate the refund amount." }
|
|
116
|
+
- parked: true
|
|
117
|
+
- trail: { to: refund-review, count: 2 }
|
|
50
118
|
- decide: { to: refund-closed }
|
|
51
119
|
- succeeded: true
|
|
120
|
+
- reply:
|
|
121
|
+
includes: "ORD-1001"
|
|
52
122
|
```
|
|
53
123
|
|
|
54
|
-
|
|
55
|
-
calls are not in the transcript (`calledTool`, `usedNoTools` never see them) and a
|
|
56
|
-
grader's words are never the `reply`. Pin the move and its reason with `trail`,
|
|
57
|
-
a raise with `raised`, a write with `variables`:
|
|
124
|
+
A host trigger, a fixture file and a mock:
|
|
58
125
|
|
|
59
126
|
```yaml
|
|
127
|
+
# Only the order database read is mocked; the agent's own writes stay real.
|
|
128
|
+
title: Empty order database reports no delayed orders
|
|
129
|
+
description: >-
|
|
130
|
+
With orders.json mocked empty, a requested report still reads the database
|
|
131
|
+
and reports no delayed orders.
|
|
132
|
+
trigger: { id: report_requested }
|
|
133
|
+
variables: { company: "Acme Corp" }
|
|
134
|
+
workspace:
|
|
135
|
+
trigger.json: { from: fixtures/trigger.json }
|
|
136
|
+
mocks:
|
|
137
|
+
- tool: read_file
|
|
138
|
+
whenInput: { file_path: skills/order-data/assets/orders.json }
|
|
139
|
+
result: "[]"
|
|
60
140
|
steps:
|
|
61
|
-
- send: "
|
|
62
|
-
-
|
|
141
|
+
- send: "A delayed-orders report was requested."
|
|
142
|
+
- triggerArrival: report_requested
|
|
143
|
+
- calledTool: { name: read_file, input: { file_path: "/orders\\.json/" } }
|
|
144
|
+
- reply:
|
|
145
|
+
includes: "/no (matching |delayed )?orders/i"
|
|
63
146
|
```
|
|
64
147
|
|
|
148
|
+
## Grader and code states in a case
|
|
149
|
+
|
|
150
|
+
An automated state's own calls are not in the transcript, and a grader's words are
|
|
151
|
+
never the reply. Assert what it committed: its move with `trail` (`kind: code` or
|
|
152
|
+
`kind: grader`) or `reachedState`, a raise with `raised`, a write with `variables`, and
|
|
153
|
+
its sub-workflows with `ranWorkflow`.
|
|
154
|
+
|
|
65
155
|
```yaml
|
|
66
|
-
trigger: { id: enrichment_requested } # the enrichment path
|
|
67
|
-
variables: { orders_to_enrich: [{ order_id: "ORD-1002" }, { order_id: "ORD-1003" }] }
|
|
68
156
|
steps:
|
|
69
|
-
- send: "
|
|
70
|
-
- trail: { kind:
|
|
71
|
-
|
|
157
|
+
- send: "Hi, I'm support@acmecorp.com. Which orders are delayed?"
|
|
158
|
+
- trail: { kind: code, to: identify-case, count: 1 } # the entry screen passed it
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A case that declines at entry asserts `reachedState: <declining state>` and
|
|
162
|
+
`trail: { to: <first work state>, count: 0 }`. One whose entry code state raised asserts
|
|
163
|
+
`raised: <code>` and `noTraversal: true`. A mock answers a code state's calls too, so
|
|
164
|
+
mocking a file an entry check reads changes what the check decides.
|
|
165
|
+
|
|
166
|
+
## Seed from a real session
|
|
167
|
+
|
|
168
|
+
A trigger carries only an id. Its payload arrives as variables, and their names and
|
|
169
|
+
shapes live in the host's call site and in sessions that already ran. Read one instead
|
|
170
|
+
of inventing a payload:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
archmax sessions # ids, status, state and variable names
|
|
174
|
+
archmax sessions <id> # one session's variables, each with its value and lock
|
|
72
175
|
```
|
|
73
176
|
|
|
74
|
-
A
|
|
75
|
-
`
|
|
76
|
-
|
|
177
|
+
A host that writes artifacts keeps the same record at
|
|
178
|
+
`sessions/<id>/artifacts/variables.json`. Take the newest session whose `trigger` matches
|
|
179
|
+
the case's, then map its variables:
|
|
180
|
+
|
|
181
|
+
| Recorded entry | Goes to |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `trigger` | the case's `trigger: { id }` |
|
|
184
|
+
| any other locked entry | `variables:`, the trigger's payload |
|
|
185
|
+
| an unlocked entry | nothing: the agent set it, so assert it with `variables: { expect }` |
|
|
186
|
+
|
|
187
|
+
Never copy the whole store. A seed is locked, so seeding a name the agent sets hides a
|
|
188
|
+
missing `archmax_set_variables` call and makes the agent's own write fail.
|
|
189
|
+
|
|
190
|
+
With no session to read, the `${{…}}` references and `requires` lists in
|
|
191
|
+
`workflow.yaml`, and the host's `createAgent({ variables })` and `deliver` calls, name
|
|
192
|
+
what a trigger supplies. To produce a session, run one:
|
|
193
|
+
`archmax run <workflow> "<prompt>" --variables '{"email":"ann@example.com"}'`.
|
|
194
|
+
|
|
195
|
+
## Running cases
|
|
196
|
+
|
|
197
|
+
`archmax test <workflow>` runs the suite one case at a time; `tests.caseTimeoutMs`
|
|
198
|
+
bounds each. A workflow with `disabled: true` runs no cases: the suite is reported
|
|
199
|
+
skipped and exits 0. To run the same suite against your own agent, use `runTests` on
|
|
200
|
+
`@archmax-ai/harness/testing`
|
|
201
|
+
([`backend-integration.md`](backend-integration.md#cases-against-your-agent)).
|
|
@@ -1,115 +1,226 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Grader states, code states and scripts
|
|
2
2
|
|
|
3
|
-
Load this when
|
|
4
|
-
`
|
|
5
|
-
[`
|
|
3
|
+
Load this when you write a code state's script, a grader state, or a skill bundle's
|
|
4
|
+
`archmax_run` script. The keys each state type takes are in
|
|
5
|
+
[`workflow-schema.md`](workflow-schema.md#state-keys).
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Pick the mechanism
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
| The work | Use |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| A deterministic check the graph runs at a fixed point: a screen, a policy check, recording a result | a code state |
|
|
12
|
+
| A judgment of work already done: tone, completeness, faithfulness | a grader state |
|
|
13
|
+
| Deterministic work the agent decides to run: a calculation, a fan-out | an `archmax_run` script in a skill bundle |
|
|
14
|
+
| Work that is a process of its own, or shared by several workflows | a sub-workflow |
|
|
15
|
+
|
|
16
|
+
The agent cannot call a grader or a code state. It reaches one only by a transition.
|
|
17
|
+
|
|
18
|
+
## How an automated state runs
|
|
19
|
+
|
|
20
|
+
A grader or code state runs whenever the session reaches it: the agent's advance,
|
|
21
|
+
another automated state's advance, a human decision, an `on_error` route, a turn that
|
|
22
|
+
opens there, a reset. It runs in place of the agent's next model call, so a chain of
|
|
23
|
+
code states makes no model call at all. A chain that never reaches an agent state, a
|
|
24
|
+
human state or a completion ends at the turn's `recursionLimit`, with the session
|
|
25
|
+
`failed` (`recursion-limit`).
|
|
26
|
+
|
|
27
|
+
**What it is offered.** Exactly its own state's tools: its `tools.allow`, the
|
|
28
|
+
workflow-wide grant and the host's `essentialTools`, plus five controls:
|
|
29
|
+
`archmax_advance` (absent in a terminal state), `archmax_raise`,
|
|
30
|
+
`archmax_get_variables`, `archmax_set_variables` and `archmax_get_history`. It is never
|
|
31
|
+
offered `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait` or `archmax_reset`
|
|
32
|
+
([Tool governance](workflow-schema.md#tool-governance) has the table), so it cannot park.
|
|
33
|
+
The kernel decides each call against that state. Grant what a check reads on the
|
|
34
|
+
check's own state: a tool, a skill bundle or a mount that another state grants is out
|
|
35
|
+
of reach.
|
|
36
|
+
|
|
37
|
+
**How it decides.** `archmax_advance({ to, reason })` is checked at the call: the
|
|
38
|
+
transition must exist and the state's `requires` must hold. The first accepted advance
|
|
39
|
+
or raise is the decision, and a later one is refused. The decision takes effect when
|
|
40
|
+
the state finishes cleanly.
|
|
41
|
+
|
|
42
|
+
**How it settles.** Exactly one of:
|
|
43
|
+
|
|
44
|
+
| Outcome | When | Effect |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| advanced | it advanced and finished cleanly | The session moves. The trail records `{ kind: code \| grader, to, reason }`, and the next state reads the reason as a `[code]` or `[grader]` runtime note. A human target parks after its handoff message. |
|
|
47
|
+
| raised | it called `archmax_raise` | The session ends `failed` with that code and reason. |
|
|
48
|
+
| completed | a terminal state finished without either call | The session completes, held to the trigger's `returns`. |
|
|
49
|
+
| failed | it threw, timed out, or (in a non-terminal state) finished without either call | Routed through `on_error`, else the session ends rejected. A raise beats an error; an error discards a recorded advance. |
|
|
50
|
+
|
|
51
|
+
**The history.** Both types read the session with `archmax_get_history`, which answers
|
|
52
|
+
`{ messages, trail }` as JSON: the session as it stood on entry, whole and oldest
|
|
53
|
+
first, across every turn.
|
|
54
|
+
|
|
55
|
+
| Field | Shape |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `messages` | `{ role, text, toolCalls?, tool?, id?, note? }`. `role` is `user`, `assistant`, `tool`, `system` or `runtime` (a runtime note, its kind in `note`). An `assistant` entry lists its calls in `toolCalls` as `{ id?, name, args }`; the `tool` entry with the same `id` carries the result in `text`. |
|
|
58
|
+
| `trail` | `{ to, kind, reason?, workflow?, status?, ts }`, where `kind` is `trigger`, `agent`, `human`, `grader`, `code`, `sub-workflow`, `reset` or `on_error`. Count earlier rounds here. |
|
|
59
|
+
|
|
60
|
+
Check the records, not the agent's retelling of them: a tool's result as the history
|
|
61
|
+
carries it, a variable, a file. Never call a tool again just to see its result; for a
|
|
62
|
+
tool that acts on an outside system, that is a second action. The history spans every
|
|
63
|
+
turn, so take the last match where only the latest round counts. An automated state's
|
|
64
|
+
own calls are not in the transcript.
|
|
65
|
+
|
|
66
|
+
## Code states
|
|
67
|
+
|
|
68
|
+
A code state runs the file its `script` names. Wire it in `workflow.yaml`, here adapted
|
|
69
|
+
from [`examples/refund/`](https://github.com/archmax-ai/harness/tree/main/examples/refund):
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
check-decision:
|
|
73
|
+
type: code
|
|
74
|
+
script: scripts/check-decision.js # workflows/refund/scripts/check-decision.js
|
|
75
|
+
tools:
|
|
76
|
+
allow: [lookup_customer] # the script calls it, so this state grants it
|
|
77
|
+
on_error: escalate
|
|
78
|
+
transitions:
|
|
79
|
+
- to: review-email
|
|
80
|
+
description: The decision matches the refund policy.
|
|
81
|
+
- to: decide
|
|
82
|
+
description: The decision contradicts the refund policy.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The script's default export is called once. It moves the session itself, and the
|
|
86
|
+
runtime ignores what it returns:
|
|
14
87
|
|
|
15
88
|
```js
|
|
16
89
|
/**
|
|
17
|
-
*
|
|
90
|
+
* Refund decision check — the recorded decision must follow the refund policy.
|
|
18
91
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Advances to identify-case when it matches; otherwise to decline, with a reason
|
|
25
|
-
* naming what was missing. An unreadable order database throws, so the state
|
|
26
|
-
* fails through its on_error.
|
|
92
|
+
* Looks the customer up with lookup_customer by the `email` variable. A refund is
|
|
93
|
+
* due ("approved") only when the latest order is delivered, otherwise "denied".
|
|
94
|
+
* Advances to review-email when `decision` matches; otherwise back to decide,
|
|
95
|
+
* naming the decision the policy requires.
|
|
27
96
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
97
|
+
export default async function ({ variables, tools }) {
|
|
98
|
+
const customer = JSON.parse(await tools.lookup_customer({ email: variables.email }));
|
|
99
|
+
const order = customer?.order;
|
|
100
|
+
const due = order?.status === "delivered" ? "approved" : "denied";
|
|
101
|
+
if (variables.decision === due) {
|
|
102
|
+
await tools.archmax_advance({ to: "review-email", reason: `The refund is ${due}, as the policy requires.` });
|
|
103
|
+
} else {
|
|
104
|
+
await tools.archmax_advance({
|
|
105
|
+
to: "decide",
|
|
106
|
+
reason: `Order ${order?.id ?? "(none)"} is ${order?.status ?? "unknown"}, so the refund must be ${due}.`,
|
|
107
|
+
});
|
|
38
108
|
}
|
|
39
|
-
|
|
40
|
-
});
|
|
109
|
+
}
|
|
41
110
|
```
|
|
42
111
|
|
|
43
112
|
| Input | Meaning |
|
|
44
113
|
| --- | --- |
|
|
45
|
-
| `state` |
|
|
46
|
-
| `trigger
|
|
47
|
-
| `variables` | read-only `name → value` map
|
|
48
|
-
| `tools` |
|
|
49
|
-
|
|
50
|
-
**The
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
the
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return { order_id: o.order_id, ...JSON.parse(answer).returns };
|
|
98
|
-
}));
|
|
114
|
+
| `state` | The code state's slug. |
|
|
115
|
+
| `trigger` | The current trigger id. Its payload is in `variables`. |
|
|
116
|
+
| `variables` | The variables as a read-only `name → value` map, structured values whole. Write with `tools.archmax_set_variables`; the script's later calls see the write. |
|
|
117
|
+
| `tools` | The state's tools as async functions, by name (`tools.read_file`, `tools["archmax_workflow_enrich-order"]`) or camelCase (`tools.readFile`). A tool the state does not offer is `undefined`. A call with no argument passes `{}`. A refused call rejects with the reason, which a `try`/`catch` may handle. |
|
|
118
|
+
|
|
119
|
+
- **The return value is not read.** Only `archmax_advance` and `archmax_raise` decide,
|
|
120
|
+
and every path through a non-terminal state's script must call one.
|
|
121
|
+
- **Send work back; throw on infrastructure.** Parse what the agent wrote defensively,
|
|
122
|
+
and send it back with the fix as the `reason`. Let an unreadable database throw, so
|
|
123
|
+
the state fails through `on_error`.
|
|
124
|
+
- **One evaluation in a fresh sandbox.** Each entry runs in a fresh QuickJS REPL, with
|
|
125
|
+
top-level `await` and no Node APIs, bounded as a whole by `settings.timeoutMs`.
|
|
126
|
+
`console.log` lines become `code-output` events, which the CLI prints under the
|
|
127
|
+
state.
|
|
128
|
+
- **Imports** are limited to `@archmax-ai/harness/*` type carriers, which are stripped
|
|
129
|
+
before evaluation. `import { defineCodeState } from "@archmax-ai/harness/sandbox"`
|
|
130
|
+
types the input (`CodeStateInput`, `SessionHistory`). Any other import fails the
|
|
131
|
+
state.
|
|
132
|
+
- **No references.** `${{…}}` in a script's tool call is plain text: compose strings
|
|
133
|
+
from `variables`.
|
|
134
|
+
- **Approve, then end.** A terminal code state completes the session when its script
|
|
135
|
+
returns, with no further model call. Set any declared `returns` with
|
|
136
|
+
`tools.archmax_set_variables` first.
|
|
137
|
+
- **Files.** `tools.read_file` answers a missing or binary file with an `Error: …`
|
|
138
|
+
string instead of throwing, so check before you parse. Use `copy_file`, `move_file`
|
|
139
|
+
and `remove_file` to duplicate, move or delete a file.
|
|
140
|
+
|
|
141
|
+
## Grader states
|
|
142
|
+
|
|
143
|
+
A grader state runs an isolated subagent. Its system prompt is a compiled-in preamble
|
|
144
|
+
followed by the state's `instructions`, which are its criteria, and its task lists the
|
|
145
|
+
state's transitions. It reads the session only through `archmax_get_history`, and the
|
|
146
|
+
work with its own state's tools. It decides with `archmax_advance`, or ends the session
|
|
147
|
+
with `archmax_raise`.
|
|
148
|
+
|
|
149
|
+
The refund example's grader, with a give-up rule and an `on_error` added:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
review-email:
|
|
153
|
+
type: grader
|
|
154
|
+
instructions: |-
|
|
155
|
+
Read scratchpad/email.md and the `decision` variable. Approve the email
|
|
156
|
+
when it is polite, states the decision plainly and names the order.
|
|
157
|
+
Otherwise send it back to `decide`, saying exactly what to change. If the
|
|
158
|
+
trail from archmax_get_history already holds two steps with kind "grader"
|
|
159
|
+
and to "decide", raise `email-rejected` instead.
|
|
160
|
+
on_error: escalate
|
|
161
|
+
transitions:
|
|
162
|
+
- to: send
|
|
163
|
+
description: The email is ready to send.
|
|
164
|
+
- to: decide
|
|
165
|
+
description: The email needs changes.
|
|
99
166
|
```
|
|
100
167
|
|
|
101
|
-
|
|
102
|
-
`
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
168
|
+
- `instructions` and at least one transition are required; `evidence`, `approvers` and
|
|
169
|
+
`budget.maxParks` are errors.
|
|
170
|
+
- Say in the criteria exactly when to take each transition and when to raise. A grader
|
|
171
|
+
that answers in text without deciding fails through `on_error`.
|
|
172
|
+
- There is no iteration budget. To give up after N rounds, count the earlier send-backs
|
|
173
|
+
on the trail (`{ kind: grader, to: <graded state> }`) and raise.
|
|
174
|
+
- `budget.timeoutMs` bounds its whole run, and the turn's `recursionLimit` bounds its
|
|
175
|
+
loop. Its `model`, else `settings.model`, is resolved under the `grader` model role.
|
|
176
|
+
- Its messages never enter the session's transcript, and its tokens never stream. The
|
|
177
|
+
session sees its decision as a trail step and a runtime note. Its model calls are
|
|
178
|
+
`model-usage` events under its state.
|
|
179
|
+
- The history is not windowed, so name the part that counts: "the last assistant
|
|
180
|
+
message", "the files `enrichments` names".
|
|
181
|
+
- Grade work the agent hands on, not a live answer: everything the agent says in a turn
|
|
182
|
+
is that turn's reply, a draft sent back included.
|
|
183
|
+
|
|
184
|
+
## `archmax_run` scripts
|
|
185
|
+
|
|
186
|
+
An `archmax_run` script, `skills/<slug>/scripts/<x>.js`, is deterministic work the agent
|
|
187
|
+
decides to run. It is a plain script with top-level `await`.
|
|
188
|
+
|
|
189
|
+
| It gets | |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| `args` | The arguments the model passed, plus `args.variables`, a read-only snapshot of the variables. |
|
|
192
|
+
| `tools` | The calling state's tools, as for a code state, re-scoped at every evaluation. No control tool and no `task`. |
|
|
193
|
+
| its result | The value of its last expression, returned to the model as the tool result. |
|
|
194
|
+
|
|
195
|
+
Write one in this order:
|
|
196
|
+
|
|
197
|
+
1. **Get the shapes first**: tool inputs and outputs from the host's describe tools, and
|
|
198
|
+
the variables a session carries from `archmax sessions <id>`. A guessed field name
|
|
199
|
+
costs a whole session.
|
|
200
|
+
2. **Write it**, JSDoc header first.
|
|
201
|
+
3. **Land the result in a variable.** The result is one tool message and nothing more,
|
|
202
|
+
and a script cannot set variables. Return structured data and name the variable in
|
|
203
|
+
the calling state's `requires`, so the agent records it with `archmax_set_variables`.
|
|
204
|
+
Later states can then guard on it and quote it as `${{name}}`.
|
|
205
|
+
4. **Announce it in the bundle's `SKILL.md`.** The `description` says when to run it; the
|
|
206
|
+
body gives its path, its arguments and the variable its result belongs in. A script
|
|
207
|
+
nothing points at is never run.
|
|
208
|
+
5. **Grant it.** The calling state enables the bundle and allows every tool the script
|
|
209
|
+
calls. Narrow execution with `{ tool: archmax_run, paths: ["skills/<slug>/scripts/**"] }`.
|
|
210
|
+
Then validate.
|
|
211
|
+
|
|
212
|
+
A script may fan out over a delegation tool with `Promise.all`. Each call runs one child
|
|
213
|
+
session and succeeds or fails on its own, so `Promise.allSettled` keeps the successes.
|
|
214
|
+
When the host cancels the turn, a pending call fails and later calls are refused.
|
|
215
|
+
|
|
216
|
+
## The JSDoc header
|
|
217
|
+
|
|
218
|
+
Both kinds of workspace JavaScript, code-state scripts and `archmax_run` scripts, open
|
|
219
|
+
with a JSDoc block: a title line, a blank line, then what the script reads, what it
|
|
220
|
+
checks or computes, and each outcome. For a code state, that is every `archmax_advance`
|
|
221
|
+
target and why, and every raise. The description ends at the first `@tag`.
|
|
222
|
+
|
|
223
|
+
Admin UIs show this block instead of the code (`parseCodeDescription` extracts it), so
|
|
224
|
+
write it first and keep the code true to it. `archmax validate` does not check it: it is
|
|
225
|
+
an authoring standard. Cases carry the same depth in their `description`, which
|
|
226
|
+
`validate` does check.
|