@archmax-ai/harness 0.9.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -14
- package/dist/agent.d.ts +4 -6
- package/dist/agent.js +4 -6
- package/dist/agent.js.map +1 -1
- package/dist/assembly/compose.d.ts +5 -24
- package/dist/assembly/compose.js +28 -42
- package/dist/assembly/compose.js.map +1 -1
- package/dist/assembly/delegation-registry.js +3 -14
- package/dist/assembly/delegation-registry.js.map +1 -1
- package/dist/assembly/index.d.ts +8 -15
- package/dist/assembly/index.js +21 -28
- package/dist/assembly/index.js.map +1 -1
- package/dist/assembly/plain.js +1 -2
- package/dist/assembly/plain.js.map +1 -1
- package/dist/authoring-skill/archmax-harness/README.md +5 -5
- package/dist/authoring-skill/archmax-harness/SKILL.md +72 -30
- package/dist/authoring-skill/archmax-harness/references/backend-integration.md +33 -24
- package/dist/authoring-skill/archmax-harness/references/cases.md +41 -8
- package/dist/authoring-skill/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +599 -339
- package/dist/authoring-skill/archmax-harness/references/code-states-and-scripts.md +115 -0
- package/dist/authoring-skill/archmax-harness/references/diagnostics.md +40 -7
- package/dist/authoring-skill/archmax-harness/references/wiring.md +19 -10
- package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +404 -293
- package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +151 -62
- package/dist/cli/state-flow.d.ts +1 -1
- package/dist/cli/state-flow.js +18 -28
- package/dist/cli/state-flow.js.map +1 -1
- package/dist/cli/turn.d.ts +1 -1
- package/dist/cli/turn.js +1 -1
- package/dist/core/code-description.d.ts +3 -3
- package/dist/core/code-description.js +3 -3
- package/dist/core/deepagents.d.ts +5 -12
- package/dist/core/deepagents.js +4 -17
- package/dist/core/deepagents.js.map +1 -1
- package/dist/core/events.d.ts +23 -42
- package/dist/core/events.js +21 -24
- package/dist/core/events.js.map +1 -1
- package/dist/core/message-readers.d.ts +4 -5
- package/dist/core/message-readers.js +3 -8
- package/dist/core/message-readers.js.map +1 -1
- package/dist/core/messages.d.ts +0 -22
- package/dist/core/messages.js +1 -32
- package/dist/core/messages.js.map +1 -1
- package/dist/core/mounts.js +3 -3
- package/dist/core/platform-prompt.generated.js +1 -1
- package/dist/core/platform-prompt.generated.js.map +1 -1
- package/dist/core/session-zone.d.ts +1 -1
- package/dist/core/session-zone.js +1 -1
- package/dist/core/workspace-context.d.ts +1 -1
- package/dist/core/workspace.d.ts +1 -1
- package/dist/core/workspace.js +1 -1
- package/dist/core/zones.d.ts +4 -4
- package/dist/core/zones.js +5 -5
- package/dist/core/zones.js.map +1 -1
- package/dist/env.d.ts +9 -9
- package/dist/index.d.ts +3 -7
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/kernel/kernel.d.ts +31 -61
- package/dist/kernel/kernel.js +54 -112
- package/dist/kernel/kernel.js.map +1 -1
- package/dist/machine/lint-spec.js +53 -12
- package/dist/machine/lint-spec.js.map +1 -1
- package/dist/machine/machine.d.ts +36 -18
- package/dist/machine/machine.js +62 -51
- package/dist/machine/machine.js.map +1 -1
- package/dist/machine/spec-schema.d.ts +32 -180
- package/dist/machine/spec-schema.js +52 -113
- package/dist/machine/spec-schema.js.map +1 -1
- package/dist/machine/tool-names.d.ts +34 -16
- package/dist/machine/tool-names.js +41 -16
- package/dist/machine/tool-names.js.map +1 -1
- package/dist/machine/types.d.ts +14 -46
- package/dist/machine/types.js +0 -2
- package/dist/machine/types.js.map +1 -1
- package/dist/machine/validate-spec.d.ts +1 -1
- package/dist/public/sandbox.d.ts +59 -95
- package/dist/public/sandbox.js +22 -23
- package/dist/public/sandbox.js.map +1 -1
- package/dist/public/spec.d.ts +13 -20
- package/dist/public/spec.js +12 -20
- package/dist/public/spec.js.map +1 -1
- package/dist/runtime/contract.d.ts +11 -12
- package/dist/runtime/contract.js +16 -16
- package/dist/runtime/contract.js.map +1 -1
- package/dist/sandbox/assets/parts/code-state.js +17 -0
- package/dist/sandbox/executor.d.ts +25 -7
- package/dist/sandbox/executor.js +56 -57
- package/dist/sandbox/executor.js.map +1 -1
- package/dist/sandbox/imports.d.ts +10 -10
- package/dist/sandbox/imports.js +11 -11
- package/dist/sandbox/imports.js.map +1 -1
- package/dist/sandbox/prelude.d.ts +5 -6
- package/dist/sandbox/prelude.js +38 -35
- package/dist/sandbox/prelude.js.map +1 -1
- package/dist/sandbox/ptc-gateway.d.ts +19 -18
- package/dist/sandbox/ptc-gateway.js +26 -22
- package/dist/sandbox/ptc-gateway.js.map +1 -1
- package/dist/sessions/resume.d.ts +2 -2
- package/dist/sessions/resume.js +1 -1
- package/dist/sessions/resume.js.map +1 -1
- package/dist/sessions/scope.d.ts +0 -10
- package/dist/sessions/scope.js +0 -17
- package/dist/sessions/scope.js.map +1 -1
- package/dist/sessions/summary.d.ts +1 -1
- package/dist/testing/assertions.js +9 -4
- package/dist/testing/assertions.js.map +1 -1
- package/dist/testing/driver.js +3 -0
- package/dist/testing/driver.js.map +1 -1
- package/dist/testing/interpreter.d.ts +2 -2
- package/dist/testing/runner.d.ts +2 -2
- package/dist/validate/validate.d.ts +1 -1
- package/dist/validate/validate.js +39 -70
- package/dist/validate/validate.js.map +1 -1
- package/dist/workflow/automated-controls.d.ts +63 -0
- package/dist/workflow/automated-controls.js +158 -0
- package/dist/workflow/automated-controls.js.map +1 -0
- package/dist/workflow/automated.d.ts +112 -0
- package/dist/workflow/automated.js +163 -0
- package/dist/workflow/automated.js.map +1 -0
- package/dist/workflow/code-state.d.ts +46 -0
- package/dist/workflow/code-state.js +26 -0
- package/dist/workflow/code-state.js.map +1 -0
- package/dist/workflow/control-tools.d.ts +54 -23
- package/dist/workflow/control-tools.js +99 -79
- package/dist/workflow/control-tools.js.map +1 -1
- package/dist/workflow/file-values.d.ts +4 -4
- package/dist/workflow/file-values.js +1 -1
- package/dist/workflow/file-values.js.map +1 -1
- package/dist/workflow/governance.d.ts +11 -22
- package/dist/workflow/governance.js +47 -104
- package/dist/workflow/governance.js.map +1 -1
- package/dist/workflow/grader-state.d.ts +55 -0
- package/dist/workflow/grader-state.js +182 -0
- package/dist/workflow/grader-state.js.map +1 -0
- package/dist/workflow/history.d.ts +11 -0
- package/dist/{lifecycle/transcript.js → workflow/history.js} +24 -8
- package/dist/workflow/history.js.map +1 -0
- package/dist/workflow/middleware.d.ts +14 -17
- package/dist/workflow/middleware.js +109 -95
- package/dist/workflow/middleware.js.map +1 -1
- package/dist/workflow/move-events.d.ts +21 -0
- package/dist/workflow/move-events.js +20 -0
- package/dist/workflow/move-events.js.map +1 -0
- package/dist/workflow/on-error.js +0 -2
- package/dist/workflow/on-error.js.map +1 -1
- package/dist/workflow/parks.d.ts +9 -2
- package/dist/workflow/parks.js +16 -10
- package/dist/workflow/parks.js.map +1 -1
- package/dist/workflow/paths.d.ts +9 -9
- package/dist/workflow/paths.js +19 -19
- package/dist/workflow/paths.js.map +1 -1
- package/dist/workflow/render-prompt.d.ts +7 -5
- package/dist/workflow/render-prompt.js +22 -37
- package/dist/workflow/render-prompt.js.map +1 -1
- package/dist/workflow/signature-checks.d.ts +2 -2
- package/dist/workflow/snapshot.js +5 -29
- package/dist/workflow/snapshot.js.map +1 -1
- package/dist/workflow/state.d.ts +14 -42
- package/dist/workflow/state.js +20 -33
- package/dist/workflow/state.js.map +1 -1
- package/dist/workflow/sub-workflow.d.ts +3 -3
- package/dist/workflow/sub-workflow.js +3 -7
- package/dist/workflow/sub-workflow.js.map +1 -1
- package/dist/workflow/tool-service.d.ts +1 -9
- package/dist/workflow/tool-service.js +21 -24
- package/dist/workflow/tool-service.js.map +1 -1
- package/dist/workflow/turn-boundary.d.ts +2 -2
- package/dist/workflow/turn-boundary.js +10 -15
- package/dist/workflow/turn-boundary.js.map +1 -1
- package/dist/workflow/turn-runner.js +5 -6
- package/dist/workflow/turn-runner.js.map +1 -1
- package/package.json +2 -2
- package/skills/archmax-harness/README.md +5 -5
- package/skills/archmax-harness/SKILL.md +72 -30
- package/skills/archmax-harness/references/backend-integration.md +33 -24
- package/skills/archmax-harness/references/cases.md +41 -8
- package/skills/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +599 -339
- package/skills/archmax-harness/references/code-states-and-scripts.md +115 -0
- package/skills/archmax-harness/references/diagnostics.md +40 -7
- package/skills/archmax-harness/references/wiring.md +19 -10
- package/skills/archmax-harness/references/workflow-schema.md +404 -293
- package/skills/archmax-harness/references/workflow-yaml.md +151 -62
- package/dist/authoring-skill/archmax-harness/references/hooks-and-scripts.md +0 -89
- package/dist/lifecycle/hook-shape.d.ts +0 -44
- package/dist/lifecycle/hook-shape.js +0 -66
- package/dist/lifecycle/hook-shape.js.map +0 -1
- package/dist/lifecycle/runner.d.ts +0 -197
- package/dist/lifecycle/runner.js +0 -419
- package/dist/lifecycle/runner.js.map +0 -1
- package/dist/lifecycle/tool-calls.d.ts +0 -9
- package/dist/lifecycle/tool-calls.js +0 -57
- package/dist/lifecycle/tool-calls.js.map +0 -1
- package/dist/lifecycle/transcript.d.ts +0 -31
- package/dist/lifecycle/transcript.js.map +0 -1
- package/dist/rubrics/rubrics.d.ts +0 -135
- package/dist/rubrics/rubrics.js +0 -145
- package/dist/rubrics/rubrics.js.map +0 -1
- package/dist/sandbox/assets/parts/core.js +0 -22
- package/dist/sandbox/assets/parts/lifecycle-hook.js +0 -47
- package/dist/sandbox/bridge.d.ts +0 -16
- package/dist/sandbox/bridge.js +0 -95
- package/dist/sandbox/bridge.js.map +0 -1
- package/dist/workflow/visits.d.ts +0 -21
- package/dist/workflow/visits.js +0 -45
- package/dist/workflow/visits.js.map +0 -1
- package/skills/archmax-harness/references/hooks-and-scripts.md +0 -89
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Code-state scripts & cases — full reference
|
|
2
2
|
|
|
3
3
|
The complete contract and worked examples for the two authored surfaces this
|
|
4
4
|
reference covers:
|
|
5
5
|
|
|
6
|
-
1. **
|
|
7
|
-
that wires them, on the **authoring plane**) —
|
|
8
|
-
|
|
9
|
-
`
|
|
10
|
-
to that workflow's `
|
|
11
|
-
`workflows/<slug>/
|
|
12
|
-
|
|
13
|
-
QuickJS sandbox: plain JavaScript with
|
|
14
|
-
filesystem beyond the `tools.*` bridge.
|
|
15
|
-
|
|
16
|
-
**A
|
|
17
|
-
runs an `archmax_run` source. The two live in different places
|
|
18
|
-
|
|
19
|
-
`skills/<capability>/scripts/` — and the classes cannot meet: the agent
|
|
6
|
+
1. **Code-state scripts** (`workflows/<slug>/scripts/*.js` — beside the spec
|
|
7
|
+
that wires them, on the **authoring plane**) — the scripts a `type: code`
|
|
8
|
+
state runs, **hand-authored** `.js` files. `workflow.yaml` wires
|
|
9
|
+
`script: scripts/<name>.js` on the code state: the path is
|
|
10
|
+
**workflow-relative**, confined to that workflow's `scripts/` directory and
|
|
11
|
+
ends in `.js` or `.mjs`, so it resolves to `workflows/<slug>/scripts/<name>.js`.
|
|
12
|
+
An absolute path, a `..` segment, or a path outside `scripts/` is a load error
|
|
13
|
+
and a `validate` error. They run in the QuickJS sandbox: plain JavaScript with
|
|
14
|
+
**top-level `await`**, no Node APIs, no filesystem beyond the `tools.*` bridge.
|
|
15
|
+
|
|
16
|
+
**A code-state script is not an agent's script.** The runtime runs a code
|
|
17
|
+
state; the agent runs an `archmax_run` source. The two live in different places
|
|
18
|
+
on purpose — code-state scripts in `workflows/<slug>/scripts/`, runtime scripts
|
|
19
|
+
in `skills/<capability>/scripts/` — and the classes cannot meet: the agent
|
|
20
20
|
cannot read `workflows/**` at all, and `archmax_run` is confined to skill
|
|
21
21
|
bundles by a non-overridable kernel rule (`script.skill-only`). An agent that
|
|
22
|
-
could execute its
|
|
23
|
-
reason for the split. A
|
|
24
|
-
prompt, not in an event, not in a session
|
|
25
|
-
|
|
22
|
+
could execute or read the check that judges its work could probe it for a
|
|
23
|
+
bypass, which is the whole reason for the split. A code-state script's source
|
|
24
|
+
never reaches the model: not in a prompt, not in an event, not in a session
|
|
25
|
+
artifact — the next state reads the reason the script passed to
|
|
26
|
+
`archmax_advance`, never its code.
|
|
26
27
|
2. **Cases** (`workflows/<name>/tests/*.test.yaml`) — declarative
|
|
27
28
|
YAML documents run by `archmax test` and interpreted **host-side**: no
|
|
28
29
|
sandbox executes a case. Suite config is the `tests:` block in
|
|
@@ -32,23 +33,24 @@ Sandbox scripts are self-contained: the only permitted imports are **typed
|
|
|
32
33
|
`@archmax-ai/harness/*` carriers** (stripped before evaluation — see
|
|
33
34
|
[Typed imports](#typed-imports)); importing from any other specifier is an
|
|
34
35
|
**error**. Each sandbox context gets its own globals — nothing leaks between
|
|
35
|
-
them (`SANDBOX_CONTRACT.context` is `"
|
|
36
|
-
for the agent's own `archmax_eval`/`archmax_run` code, which gets **no**
|
|
37
|
-
|
|
36
|
+
them (`SANDBOX_CONTRACT.context` is `"code"` for a code-state script, or `"ptc"`
|
|
37
|
+
for the agent's own `archmax_eval`/`archmax_run` code, which gets **no**
|
|
38
|
+
`defineCodeState`; `SANDBOX_CONTRACT.version` is `3`).
|
|
38
39
|
|
|
39
40
|
## Contents
|
|
40
41
|
|
|
41
42
|
- [Typed imports](#typed-imports)
|
|
42
|
-
- [Part 1 —
|
|
43
|
+
- [Part 1 — Code-state scripts and agent scripts](#part-1--code-state-scripts-and-agent-scripts)
|
|
43
44
|
- [Authoring model: hand-authored](#authoring-model-hand-authored)
|
|
44
|
-
- [The
|
|
45
|
+
- [The code-state script contract](#the-code-state-script-contract)
|
|
45
46
|
- [Governance of `tools.*` calls](#governance-of-tools-calls)
|
|
46
47
|
- [`archmax_run` scripts: the `args` contract](#archmax_run-scripts-the-args-contract)
|
|
47
48
|
- [Scripts and sub-workflow fan-out](#scripts-and-sub-workflow-fan-out)
|
|
48
|
-
- [Worked example: a
|
|
49
|
-
- [Worked example:
|
|
50
|
-
- [
|
|
51
|
-
- [
|
|
49
|
+
- [Worked example: a check in front of a state](#worked-example-a-check-in-front-of-a-state)
|
|
50
|
+
- [Worked example: a check behind a state](#worked-example-a-check-behind-a-state)
|
|
51
|
+
- [Worked example: approve, then end](#worked-example-approve-then-end)
|
|
52
|
+
- [When to use a grader state instead](#when-to-use-a-grader-state-instead)
|
|
53
|
+
- [Part 2 — Cases (declarative YAML)](#part-2--cases-declarative-yaml)
|
|
52
54
|
- [Layout and the prose rules](#layout-and-the-prose-rules)
|
|
53
55
|
- [Suite config: the `tests:` block](#suite-config-the-tests-block)
|
|
54
56
|
- [Top-level keys](#top-level-keys)
|
|
@@ -60,7 +62,7 @@ vocabulary at all; `SANDBOX_CONTRACT.version` is `2`).
|
|
|
60
62
|
- [File fixtures: `from:` references](#file-fixtures-from-references)
|
|
61
63
|
- [Settle the mocking depth before writing a case](#settle-the-mocking-depth-before-writing-a-case)
|
|
62
64
|
- [Worked example: happy path + grade](#worked-example-happy-path--grade)
|
|
63
|
-
- [Worked example:
|
|
65
|
+
- [Worked example: declined at entry, leak check](#worked-example-declined-at-entry-leak-check)
|
|
64
66
|
- [Worked example: mocking tool results](#worked-example-mocking-tool-results)
|
|
65
67
|
- [Worked example: human-in-the-loop decisions (refine loop)](#worked-example-human-in-the-loop-decisions-refine-loop)
|
|
66
68
|
- [Worked example: triggered start + fixture file](#worked-example-triggered-start--fixture-file)
|
|
@@ -70,213 +72,292 @@ vocabulary at all; `SANDBOX_CONTRACT.version` is `2`).
|
|
|
70
72
|
|
|
71
73
|
## Typed imports
|
|
72
74
|
|
|
73
|
-
A
|
|
74
|
-
completion for the
|
|
75
|
-
|
|
75
|
+
A code-state script may open with a **typed import** — a type carrier that gives
|
|
76
|
+
IDE completion for the state's input, and is **stripped before QuickJS
|
|
77
|
+
evaluation** (the runtime implementation is an identity):
|
|
76
78
|
|
|
77
79
|
```js
|
|
78
|
-
import { ok, veto, correct } from "@archmax-ai/harness/sandbox";
|
|
79
80
|
/** … */
|
|
80
|
-
|
|
81
|
+
import { defineCodeState } from "@archmax-ai/harness/sandbox";
|
|
82
|
+
|
|
83
|
+
export default defineCodeState(async ({ state, trigger, variables, tools }) => {
|
|
81
84
|
/* ... */
|
|
82
|
-
}
|
|
85
|
+
});
|
|
83
86
|
```
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
The same subpath types the input and the history (`CodeStateInput`,
|
|
89
|
+
`CodeStateArgs`, `CodeStateFn`, `SandboxTools`, `SessionHistory`,
|
|
90
|
+
`HistoryMessage`, `TrailStep`). Importing from any
|
|
91
|
+
**non-`@archmax-ai/harness/*`** specifier is an **error** — sandbox scripts are
|
|
92
|
+
self-contained. **Bare-globals authoring** (no import) is equally valid:
|
|
93
|
+
`defineCodeState` is a prelude global, and a plain
|
|
94
|
+
`export default async function ({ tools }) { … }` registers the same way.
|
|
88
95
|
|
|
89
96
|
---
|
|
90
97
|
|
|
91
|
-
## Part 1 —
|
|
98
|
+
## Part 1 — Code-state scripts and agent scripts
|
|
92
99
|
|
|
93
100
|
### Authoring model: hand-authored
|
|
94
101
|
|
|
95
|
-
A
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
A code state references an **ordinary hand-authored** `.js` file. You DESIGN AND
|
|
103
|
+
WRITE the script directly — there is no generation step. `workflow.yaml` only
|
|
104
|
+
wires it: the state's type, its script, and its transitions, which are where the
|
|
105
|
+
script may send the session:
|
|
98
106
|
|
|
99
107
|
```yaml
|
|
100
108
|
states:
|
|
101
|
-
|
|
109
|
+
screen-request:
|
|
110
|
+
type: code
|
|
111
|
+
script: scripts/screen-request.js
|
|
102
112
|
triggers:
|
|
103
113
|
manual:
|
|
104
|
-
|
|
105
|
-
- script:
|
|
106
|
-
|
|
114
|
+
skills:
|
|
115
|
+
allow: [order-data] # the script reads the order records: granted here
|
|
116
|
+
on_error: escalation # where a failure of the script routes
|
|
117
|
+
transitions:
|
|
118
|
+
- to: identify-case
|
|
119
|
+
description: The requester is known and asks only about their own company's orders.
|
|
120
|
+
- to: decline
|
|
121
|
+
description: The request names no known requester, or reaches for another company's orders.
|
|
107
122
|
```
|
|
108
123
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
**
|
|
126
|
-
|
|
127
|
-
errors
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
`script` is **required** on a code state and **refused** on every other type
|
|
125
|
+
(both load errors). A code state makes no model call, so `instructions`, `model`,
|
|
126
|
+
`budget`, `evidence` and `approvers` on one are `validate` errors: say what the
|
|
127
|
+
script does in its JSDoc block, and in the state's `summary`.
|
|
128
|
+
|
|
129
|
+
A code-state script **MUST** open with a leading JSDoc block — a title line, a
|
|
130
|
+
blank line, and a description of what the script does _and how_: the inputs it
|
|
131
|
+
reads, the sequence of checks, and **where it sends the session in each case**
|
|
132
|
+
(each `archmax_advance` target and why, each `archmax_raise` code). It is the only
|
|
133
|
+
human-facing contract the file has (admin UIs display it instead of the code), and
|
|
134
|
+
it is what a reviewer refines when the check is wrong. No `@specification` tag, no
|
|
135
|
+
`// archmax:generated` marker, no generation semantics. See
|
|
136
|
+
[SKILL.md → Workspace layout](../SKILL.md#workspace-layout-complete--nothing-else-exists).
|
|
137
|
+
|
|
138
|
+
- `archmax validate` reports a code-state script problem when the script **file
|
|
139
|
+
is missing** from `scripts/` or it has a **foreign (non-`@archmax-ai/harness/*`)
|
|
140
|
+
import**. It does **not** check the JSDoc block on any script — the header is
|
|
141
|
+
an authoring standard you uphold yourself. (Cases are YAML; there
|
|
142
|
+
`archmax validate` **errors** on a missing `description` instead.)
|
|
143
|
+
- The runtime never generates scripts: a missing script file at run time fails
|
|
144
|
+
the state (fail-closed), routed through its `on_error`.
|
|
145
|
+
|
|
146
|
+
### The code-state script contract
|
|
147
|
+
|
|
148
|
+
When the session's position reaches a code state — the agent's `archmax_advance`
|
|
149
|
+
into it, another automated state's advance, a human decision, an `on_error`
|
|
150
|
+
route, a turn that opens there, an `archmax_reset` back to it — the runtime runs
|
|
151
|
+
its script **in place of the next model call**. The agent's model is not called
|
|
152
|
+
while the session stands in a code state, so a chain of code states makes no
|
|
153
|
+
model call at all. The script's **default export** is called with **one object**,
|
|
154
|
+
and the script **moves the session itself**:
|
|
133
155
|
|
|
134
156
|
```js
|
|
135
157
|
/**
|
|
136
|
-
* Requester
|
|
158
|
+
* Requester screen — reject a request whose requester is not a known customer.
|
|
137
159
|
*
|
|
138
|
-
* Reads the first user message from
|
|
139
|
-
* address in it, loads skills/order-data/assets/orders.json
|
|
140
|
-
*
|
|
160
|
+
* Reads the first user message from the session's history, extracts the first
|
|
161
|
+
* email address in it, loads skills/order-data/assets/orders.json (this state
|
|
162
|
+
* enables `order-data`), and looks the address up against every order's `email`.
|
|
141
163
|
*
|
|
142
|
-
*
|
|
143
|
-
* matches no order — each with a reason naming what was missing
|
|
164
|
+
* Advances to decline when the request carries no email address, and when the
|
|
165
|
+
* address matches no order — each with a reason naming what was missing — and
|
|
166
|
+
* otherwise to identify-case.
|
|
144
167
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
import { defineCodeState } from "@archmax-ai/harness/sandbox";
|
|
169
|
+
|
|
170
|
+
export default defineCodeState(async ({ tools }) => {
|
|
171
|
+
const { messages } = JSON.parse(await tools.archmax_get_history());
|
|
172
|
+
const orders = JSON.parse(await tools.read_file({ file_path: "skills/order-data/assets/orders.json" }));
|
|
147
173
|
const request = messages.find((m) => m.role === "user")?.text ?? "";
|
|
148
174
|
const email = request.match(/[\w.+-]+@[\w.-]+/)?.[0];
|
|
149
|
-
if (!email) return
|
|
150
|
-
if (!orders.some((o) => o.email === email))
|
|
175
|
+
if (!email) return tools.archmax_advance({ to: "decline", reason: "No email address found in the request." });
|
|
176
|
+
if (!orders.some((o) => o.email === email)) {
|
|
177
|
+
return tools.archmax_advance({ to: "decline", reason: `"${email}" is not in the order database.` });
|
|
178
|
+
}
|
|
151
179
|
console.log(`Allowed: ${email}`);
|
|
152
|
-
|
|
153
|
-
}
|
|
180
|
+
await tools.archmax_advance({ to: "identify-case", reason: `${email} is a known requester.` });
|
|
181
|
+
});
|
|
154
182
|
```
|
|
155
183
|
|
|
156
|
-
**The input** (`
|
|
157
|
-
`tools` are also the global `args`):
|
|
184
|
+
**The input** (`CodeStateInput` in `@archmax-ai/harness/sandbox`; the same fields
|
|
185
|
+
minus `tools` are also the global `args`):
|
|
158
186
|
|
|
159
187
|
| Field | Meaning |
|
|
160
188
|
| --- | --- |
|
|
161
|
-
| `state
|
|
162
|
-
| `trigger
|
|
163
|
-
| `variables` | The
|
|
164
|
-
| `
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
| `state` | The code state's own slug. |
|
|
190
|
+
| `trigger?` | The id **string** of the trigger the turn carries. It carries no payload — the session's input is in `variables`. |
|
|
191
|
+
| `variables` | The session's variables as a plain `name → value` map, including the built-in `trigger`. Structured values are present whole (`variables.order.items[0].sku`). **Read-only**: assigning changes nothing. Write with `tools.archmax_set_variables`, which holds locks, `title` and typed returns exactly as the agent's call does. |
|
|
192
|
+
| `tools` | **Exactly this state's tool surface** (async): its grants (the workflow-wide grant included), the host's essential tools, and its controls. A tool answers to its own name (`tools.read_file`, `tools["archmax_workflow_enrich-order"]`) and to its camelCased key (`tools.readFile`); `Object.keys(tools)` lists the camelCased keys. A call with no argument passes `{}` (`tools.archmax_get_history()`, `tools.ls()`). **A name outside the surface is `undefined`.** See [Governance](#governance-of-tools-calls). |
|
|
193
|
+
|
|
194
|
+
**The session's history** is not part of the input: the script reads it the way
|
|
195
|
+
a grader does, with `archmax_get_history` (`SessionHistory`):
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
const { messages, trail } = JSON.parse(await tools.archmax_get_history());
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
It is the session as it stood when the state was entered, built from the
|
|
202
|
+
checkpointed session (never a summarized request), whole and oldest first,
|
|
203
|
+
across every turn:
|
|
204
|
+
|
|
205
|
+
| Field | Meaning |
|
|
206
|
+
| --- | --- |
|
|
207
|
+
| `messages` | The transcript as `{ role, text, toolCalls?, tool?, id?, note? }`. `role` is `user`, `assistant`, `tool`, `system`, or **`runtime`** — a note the runtime wrote (an arrival, a decision, an error route, an automated state's move; `note` names the kind). An `assistant` entry lists the calls it made in `toolCalls`, as `{ id?, name, args }`; the `tool` entry answering one carries the tool's name in `tool`, the call's `id`, and the **result in `text`**, as the tool returned it (`JSON.parse` it when the tool returns JSON; a failed or refused call's `text` is the error). A script that reacts to narration matches `role === "runtime"`, never a `[bracket]` prefix in a user message. Not windowed: the sandbox's `maxResultChars` and `memoryLimitBytes` bound it. |
|
|
208
|
+
| `trail` | The trail as `{ to, kind, reason?, workflow?, status?, ts }`. `kind` is `trigger`, `agent`, `human`, `grader`, `code`, `sub-workflow`, `reset` or `on_error`. Count earlier rounds here: `trail.filter((s) => s.kind === "code" && s.to === "refund-request").length`. |
|
|
209
|
+
|
|
210
|
+
Only what the transcript holds is there: a call a script made through `tools.*`
|
|
211
|
+
(an agent's `archmax_run`, or a code state's own) is not, and neither is a
|
|
212
|
+
grader's.
|
|
213
|
+
|
|
214
|
+
**What a code state checks.** The conversation is the model's memory, not
|
|
215
|
+
evidence: a code state checks the session's records — `variables`, files, and
|
|
216
|
+
the tool results the history carries as the tools returned them. Check a result
|
|
217
|
+
itself, never a copy the model wrote into a variable, a file or its reply, and
|
|
218
|
+
never call a tool again just to see its result (for a host whose tools reach an
|
|
219
|
+
external system, that is a second action). To read a result, find the call in an
|
|
220
|
+
`assistant` entry's `toolCalls`, then the `tool` entry with the same `id`:
|
|
175
221
|
|
|
176
222
|
```js
|
|
177
223
|
/**
|
|
178
|
-
*
|
|
224
|
+
* Status check — the recorded status must be the one get_order returned.
|
|
179
225
|
*
|
|
180
|
-
* Finds the
|
|
181
|
-
*
|
|
226
|
+
* Finds the last get_order call in the session's history and the tool message
|
|
227
|
+
* answering it, and reads scratchpad/status.json ({ status }), which the agent
|
|
228
|
+
* wrote. Advances to notify-customer when the recorded status is the returned
|
|
229
|
+
* one; otherwise back to look-up-order, with the status to record, or with a
|
|
230
|
+
* request to look the order up when no get_order call has a readable result.
|
|
182
231
|
*/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
232
|
+
import { defineCodeState } from "@archmax-ai/harness/sandbox";
|
|
233
|
+
|
|
234
|
+
export default defineCodeState(async ({ tools }) => {
|
|
235
|
+
const { messages } = JSON.parse(await tools.archmax_get_history());
|
|
236
|
+
const call = messages.flatMap((m) => m.toolCalls ?? []).findLast((c) => c.name === "get_order");
|
|
237
|
+
const answer = call && messages.find((m) => m.role === "tool" && m.id === call.id);
|
|
238
|
+
let order;
|
|
239
|
+
try {
|
|
240
|
+
order = JSON.parse(answer.text);
|
|
241
|
+
} catch {
|
|
242
|
+
return tools.archmax_advance({ to: "look-up-order", reason: "Look the order up with get_order first." });
|
|
243
|
+
}
|
|
244
|
+
const recorded = JSON.parse(await tools.read_file({ file_path: "scratchpad/status.json" }));
|
|
245
|
+
if (recorded.status !== order.status) {
|
|
246
|
+
return tools.archmax_advance({
|
|
247
|
+
to: "look-up-order",
|
|
248
|
+
reason: `Order ${call.args.id} is ${order.status}: record that status exactly.`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
await tools.archmax_advance({ to: "notify-customer", reason: `The recorded status ${order.status} is get_order's.` });
|
|
252
|
+
});
|
|
192
253
|
```
|
|
193
254
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
255
|
+
The history spans every turn and every entry into a state, so where only the
|
|
256
|
+
latest round counts, take the last match, as above. What a later state or turn
|
|
257
|
+
needs from the conversation (the person's words, a decision) is still best
|
|
258
|
+
stored in a variable or a file by the state that has it, so no check has to
|
|
259
|
+
search the transcript for it.
|
|
260
|
+
|
|
261
|
+
**The controls** on `tools`:
|
|
262
|
+
|
|
263
|
+
| Control | Effect |
|
|
264
|
+
| --- | --- |
|
|
265
|
+
| `archmax_advance({ to, reason, evidence? })` | Records the move along one of the state's transitions. Checked **at the call**: an undeclared `to` or an unmet `requires` rejects naming the valid targets or the missing variable, and the script may catch it and decide otherwise. `evidence` is accepted only into a human state. The `reason` is what the next state reads. Absent in a terminal state. |
|
|
266
|
+
| `archmax_raise({ code, reason })` | Records the end of the session as **failed**, with the code and reason as its outcome's `exit`. |
|
|
267
|
+
| `archmax_get_variables({ name?, path? })` / `archmax_set_variables({ variables, lock? })` | Read and write the session's variables, as the agent's calls do. A write is visible to the script's later calls, so a script can set what `requires` asks for and then advance. |
|
|
268
|
+
| `archmax_get_history()` | Resolves to the session's history as JSON text, `{ messages, trail }` (above). |
|
|
269
|
+
|
|
270
|
+
These are a grader's controls too: both automated types get the same set. There
|
|
271
|
+
is no `archmax_wait` and no `archmax_reset`: a code state cannot park, since a
|
|
272
|
+
script frame is not durable. To hand work to a person, advance into a human state.
|
|
198
273
|
|
|
199
|
-
**
|
|
274
|
+
**How the run settles** — exactly one outcome:
|
|
200
275
|
|
|
201
|
-
|
|
|
276
|
+
| The run… | Outcome |
|
|
202
277
|
| --- | --- |
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
278
|
+
| called `archmax_advance` and finished cleanly | **advanced**: the session moves. The trail records `{ to, kind: "code", reason }`, and a runtime note carries the reason to the next state: `[code] '<state>' moved the run to '<to>': <reason>. You are now in '<to>'.` Into a human state the session parks after the one reply-only handoff message. Into another automated state, that state runs next, still with no model call. |
|
|
279
|
+
| called `archmax_raise` | **raised**: the session ends `failed` — no `on_error`, no `returns` check, nothing further runs in the turn. |
|
|
280
|
+
| finished without either, in a **terminal** state | **completed**: the session completes, held to the trigger's `returns`. |
|
|
281
|
+
| threw, timed out, could not be read, imported a foreign module, or — in a **non-terminal** state — finished without either call | **failed**: routed through the state's `on_error`, else the session ends rejected. Finishing undecided fails with `code state '<slug>' finished without calling archmax_advance or archmax_raise`. |
|
|
282
|
+
|
|
283
|
+
Three rules hold the outcomes apart:
|
|
284
|
+
|
|
285
|
+
- **The return value is not read.** Returning `false`, a string or an object does
|
|
286
|
+
nothing; there is no verdict to return. Only the controls decide.
|
|
287
|
+
- **One decision per run.** The first accepted `archmax_advance` or `archmax_raise`
|
|
288
|
+
is the state's decision, and every later one is refused (it rejects).
|
|
289
|
+
- **The decision takes effect when the script finishes.** A script that advances
|
|
290
|
+
and then throws **fails**, and the session does not move. A raise wins over a
|
|
291
|
+
later error.
|
|
292
|
+
|
|
293
|
+
So parse defensively where malformed input should *send the work back*, and let a
|
|
294
|
+
genuine infrastructure failure (an unreadable database) throw, so the state fails
|
|
295
|
+
closed through its `on_error`.
|
|
296
|
+
|
|
297
|
+
Other globals: `console.log(...)` (each line a `code-output` event, which the CLI's
|
|
298
|
+
state flow prints under the state), and `defineCodeState(fn)`, an alias for
|
|
299
|
+
`export default fn`. A file with **no default export** is evaluated as a plain
|
|
300
|
+
script: it reads the input from the global `args` and calls the global `tools`.
|
|
301
|
+
**Each run starts clean**: a fresh REPL every time the state is entered, shared
|
|
302
|
+
with neither the agent's `archmax_eval` nor another code state. The script is one
|
|
303
|
+
evaluation, so `settings.timeoutMs` (default 15000), `memoryLimitBytes`,
|
|
304
|
+
`maxPtcCalls` and `maxResultChars` bound it whole, and a cancelled turn stops it at
|
|
305
|
+
once.
|
|
306
|
+
|
|
307
|
+
`${{…}}` does not apply inside a script: the runtime substitutes references in the
|
|
308
|
+
arguments a *model* writes. A script already has the values in `variables` and
|
|
309
|
+
composes strings in JavaScript, so `${{…}}` text in a script's `tools.*` call is
|
|
224
310
|
ordinary data.
|
|
225
311
|
|
|
226
312
|
### Governance of `tools.*` calls
|
|
227
313
|
|
|
228
314
|
Every `tools.*` call is checked by the decision kernel at the moment it runs —
|
|
229
315
|
the same kernel, with the same rules, that governs a tool call the model makes
|
|
230
|
-
directly.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
`
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
`
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
the
|
|
250
|
-
names is refused (`mount.not-allowed`). That includes a mount in the table only
|
|
251
|
-
because a child workflow grants it. So grant a mount the hook needs:
|
|
316
|
+
directly. There is no runtime authority for scripts: **both kinds are governed
|
|
317
|
+
against the state they run in.**
|
|
318
|
+
|
|
319
|
+
| Script kind | Governed against | Must be granted there |
|
|
320
|
+
| --- | --- | --- |
|
|
321
|
+
| Code-state script (`type: code`) | its own code state: `tools.allow`/`forbid`, `skills.*`, `mounts.*`, plus `allow_always`, `forbid_always`, safety rules, consumer rules | every tool, skill and governed mount it uses |
|
|
322
|
+
| `archmax_run` / `archmax_eval` script (the model chose to run it) | the agent's active state, exactly as the model's own calls | every tool it calls |
|
|
323
|
+
|
|
324
|
+
So a code state that reads `skills/order-data/assets/orders.json` enables
|
|
325
|
+
`order-data` in its own `skills.allow`, and one that calls a host tool names it in
|
|
326
|
+
its own `tools.allow`. A tool another state grants is `undefined` on its `tools`;
|
|
327
|
+
a bare `forbid` entry removes a tool from it, and a guarded one (`{ tool:
|
|
328
|
+
write_file, paths: [...] }`) refuses the matching calls. A grant on a code state
|
|
329
|
+
naming `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait` or
|
|
330
|
+
`archmax_reset` is a `validate` error: no automated state is offered them, and
|
|
331
|
+
the same tools in the workflow's `allow_always` are left out of its surface.
|
|
332
|
+
|
|
333
|
+
Mounts work the same way: a governed mount is reachable from a code state only
|
|
334
|
+
when `mounts.allow_always` or that state's own `mounts.allow` grants it, and
|
|
335
|
+
writable only where the grant asks `access: read_write`:
|
|
252
336
|
|
|
253
337
|
```js
|
|
254
|
-
// workflow.yaml: states.
|
|
255
|
-
|
|
256
|
-
const policy = await tools.readFile({ file_path: "contracts/policy.md" });
|
|
338
|
+
// workflow.yaml: states.check-contract.mounts.allow: [contracts]
|
|
339
|
+
const policy = await tools.read_file({ file_path: "contracts/policy.md" });
|
|
257
340
|
```
|
|
258
341
|
|
|
259
342
|
Neither kind may **read** the authoring plane at all. A `tools.*` call naming
|
|
260
|
-
`workflows/**` is blocked (`zone.governance-plane`) with a
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
A refused call **throws** inside the script, carrying the governance reason.
|
|
343
|
+
`workflows/**` is blocked (`zone.governance-plane`) with a diagnostic naming the
|
|
344
|
+
prefix — a script cannot read its own spec, a sibling's, or a grader's
|
|
345
|
+
criteria. There is no `task()` in any sandbox context and no `tools.task`: work
|
|
346
|
+
that needs a model's judgment is a grader state on the graph.
|
|
347
|
+
|
|
348
|
+
A refused call **rejects** inside the script, carrying the governance reason.
|
|
268
349
|
The underlying tool never runs whether or not you catch it:
|
|
269
350
|
|
|
270
351
|
```js
|
|
271
352
|
try {
|
|
272
|
-
await tools.
|
|
353
|
+
await tools.write_file({ file_path: "scratchpad/report.md", content: body });
|
|
273
354
|
} catch (err) {
|
|
274
355
|
console.log(`could not write the report: ${err.message}`);
|
|
275
356
|
}
|
|
276
357
|
```
|
|
277
358
|
|
|
278
359
|
A **binary file** is not a refusal and does not throw: reads are text only, so
|
|
279
|
-
`tools.
|
|
360
|
+
`tools.read_file` on an image, audio, video, PDF or PowerPoint file, or on any
|
|
280
361
|
file whose content holds a NUL byte (an archive, an Office document), resolves to
|
|
281
362
|
the notice `Error: '<path>' is a binary file (<type>, <size>) and was not read;
|
|
282
363
|
read_file returns text files only.` instead of content — an image too, even when
|
|
@@ -287,28 +368,27 @@ an `Error: …` string the same way. A script that parses what it read checks
|
|
|
287
368
|
first:
|
|
288
369
|
|
|
289
370
|
```js
|
|
290
|
-
const text = await tools.
|
|
371
|
+
const text = await tools.read_file({ file_path: "scratchpad/upload.json" });
|
|
291
372
|
const upload = text.startsWith("Error:") ? null : JSON.parse(text);
|
|
292
373
|
```
|
|
293
374
|
|
|
294
375
|
To **duplicate, move or delete** a file — a template, an image, a `.docx` —
|
|
295
|
-
call `tools.
|
|
296
|
-
`tools.
|
|
297
|
-
`tools.
|
|
298
|
-
bytes move, binary files included, with no `
|
|
376
|
+
call `tools.copy_file({ source, destination, overwrite? })`,
|
|
377
|
+
`tools.move_file({ source, destination, overwrite? })` or
|
|
378
|
+
`tools.remove_file({ file_path })` instead of reading and writing it back. The
|
|
379
|
+
bytes move, binary files included, with no `read_file` line limit, and each
|
|
299
380
|
call resolves to one line (`Copied '<source>' to '<destination>' (<size>).`,
|
|
300
381
|
`Moved …`, `Removed '<path>'.`). They are governed by their declared paths like
|
|
301
382
|
any other call, so a governance refusal (a destination in a read-only mount, a
|
|
302
|
-
disabled skill's file)
|
|
383
|
+
disabled skill's file) rejects. A failure the operation itself meets (a missing
|
|
303
384
|
source, a directory, an existing destination without `overwrite: true`)
|
|
304
385
|
resolves to an `Error: …` string instead.
|
|
305
386
|
|
|
306
|
-
**Authoring rule for
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
`tools.allow`, or workflow-wide with `tools.allow_always`; a default tool such
|
|
310
|
-
`read_file` is covered wherever the workflow-wide grant includes it.
|
|
311
|
-
hooks need no such grant.
|
|
387
|
+
**Authoring rule for every script:** if sandboxed code calls `tools.X`, the state
|
|
388
|
+
it runs in must cover `tool: X` (with any argument constraints the call
|
|
389
|
+
satisfies), or `tools.X` is `undefined` (or the call rejects). Grant it per state
|
|
390
|
+
with `tools.allow`, or workflow-wide with `tools.allow_always`; a default tool such
|
|
391
|
+
as `read_file` is covered wherever the workflow-wide grant includes it.
|
|
312
392
|
|
|
313
393
|
### `archmax_run` scripts: the `args` contract
|
|
314
394
|
|
|
@@ -319,9 +399,9 @@ store. It reads its input from the global **`args`**:
|
|
|
319
399
|
|
|
320
400
|
- **the arguments the model passed** to `archmax_run` (`args.<name>`), plus
|
|
321
401
|
- **`args.variables`** — the session's variables as a flat `name → value` map,
|
|
322
|
-
the same map a
|
|
323
|
-
reads the run's input (a seeded list, a delivered payload) without the
|
|
324
|
-
having to retype it.
|
|
402
|
+
the same map a code state gets. The runtime supplies it on every call, so a
|
|
403
|
+
script reads the run's input (a seeded list, a delivered payload) without the
|
|
404
|
+
model having to retype it.
|
|
325
405
|
|
|
326
406
|
```js
|
|
327
407
|
/** One sub-run per requested order. */
|
|
@@ -330,16 +410,18 @@ const orders = args.variables?.orders_to_enrich ?? [];
|
|
|
330
410
|
|
|
331
411
|
`args.variables` is a read-only snapshot; a script that must persist a result
|
|
332
412
|
returns it and the state's `requires:` makes the model record it with
|
|
333
|
-
`archmax_set_variables`.
|
|
334
|
-
`
|
|
335
|
-
|
|
413
|
+
`archmax_set_variables`. An agent's script reaches **no control tool** — not
|
|
414
|
+
`archmax_advance`, not the variable tools — and there is **no `task()`** in an
|
|
415
|
+
`archmax_run` script or in `archmax_eval`. The model has no `task` tool at all: it
|
|
416
|
+
is disclosed in no state and grantable by nothing, because judging work is a
|
|
417
|
+
grader state and delegating it is a sub-workflow.
|
|
336
418
|
|
|
337
419
|
### Scripts and sub-workflow fan-out
|
|
338
420
|
|
|
339
421
|
A script **is** the deterministic way to drive a fan-out. Delegation tools are on the `tools.*`
|
|
340
|
-
surface — a delegation does work and returns a value, unlike the control tools
|
|
341
|
-
call — so a script decides how many sub-runs to start, with what inputs, and what to do
|
|
342
|
-
answer:
|
|
422
|
+
surface — a delegation does work and returns a value, unlike the control tools an agent's script
|
|
423
|
+
may never call — so a script decides how many sub-runs to start, with what inputs, and what to do
|
|
424
|
+
with each answer:
|
|
343
425
|
|
|
344
426
|
```js
|
|
345
427
|
/** One sub-run per delayed order, all in flight at once. */
|
|
@@ -359,59 +441,68 @@ enriched;
|
|
|
359
441
|
`Promise.all` genuinely parallelizes: the bridge starts each host call detached and returns its
|
|
360
442
|
promise handle immediately. The runtime still bounds how many sub-runs are in flight
|
|
361
443
|
(the dispatcher's `maxConcurrent`), queueing the rest. The list never enters the model's
|
|
362
|
-
context.
|
|
444
|
+
context. A code state may drive the same fan-out when its own state grants the delegation tool,
|
|
445
|
+
and then record the results itself with `tools.archmax_set_variables` before it advances.
|
|
363
446
|
|
|
364
447
|
Two limits still hold, both inherited rather than special to delegation. A script may only call a
|
|
365
448
|
delegation tool its **calling state** allows — the same kernel verdict an agent's call gets. And a
|
|
366
449
|
script's delegation **cannot park**: a QuickJS frame is not durable, so a child that reaches a human
|
|
367
450
|
state fails that call closed with `parked`.
|
|
368
451
|
|
|
369
|
-
|
|
370
|
-
decision) or `archmax_set_variables`. Concurrent calls succeed or fail **independently**,
|
|
371
|
-
can act on what worked — `Promise.allSettled` is the honest shape when partial success
|
|
372
|
-
meaningful. Each sub-run is its own session with its own `scratchpad/`, so concurrent sub-runs
|
|
452
|
+
An agent's script still may not call `archmax_advance` (moving the state machine is the model's
|
|
453
|
+
governed decision) or `archmax_set_variables`. Concurrent calls succeed or fail **independently**,
|
|
454
|
+
so a script can act on what worked — `Promise.allSettled` is the honest shape when partial success
|
|
455
|
+
is meaningful. Each sub-run is its own session with its own `scratchpad/`, so concurrent sub-runs
|
|
373
456
|
never collide on a path; a `file` return comes back as a copy under a name no other file holds.
|
|
374
457
|
|
|
375
458
|
A script lives as long as its turn. When the host cancels the turn, a call still in flight fails
|
|
376
459
|
with `Tool '<name>' failed: the turn was aborted`, and every call the script makes after that is
|
|
377
460
|
refused the same way before it reaches the tool. A retry in a `catch` therefore dispatches nothing
|
|
378
|
-
more, so there is no need to tell a cancel apart from an ordinary failure.
|
|
379
|
-
same way.
|
|
461
|
+
more, so there is no need to tell a cancel apart from an ordinary failure. Code-state scripts stop
|
|
462
|
+
the same way.
|
|
380
463
|
|
|
381
|
-
### Worked example: a
|
|
464
|
+
### Worked example: a check in front of a state
|
|
382
465
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
`
|
|
386
|
-
|
|
466
|
+
To screen every request before any model call, make the trigger's entry state a
|
|
467
|
+
code state that advances to where the work starts, or to a state that declines it.
|
|
468
|
+
The bundled example's `screen-request` (`workflows/order-lookup/scripts/screen-request.js`)
|
|
469
|
+
merges a requester check and a tenancy check — wired as in
|
|
470
|
+
[Authoring model](#authoring-model-hand-authored), with `decline` a terminal agent
|
|
471
|
+
state that tells the person why:
|
|
387
472
|
|
|
388
473
|
```js
|
|
389
474
|
/**
|
|
390
|
-
*
|
|
475
|
+
* Request screen — the requester must be known, and may only reach their own
|
|
476
|
+
* company's orders.
|
|
391
477
|
*
|
|
392
478
|
* Binds the requester's email (the first one in the first user message) to the
|
|
393
479
|
* customers whose orders carry it in skills/order-data/assets/orders.json, then
|
|
394
480
|
* checks the request two ways: it names another customer, or it references an
|
|
395
481
|
* order id (`ORD-…`) owned by another customer. Matching is case-insensitive.
|
|
396
482
|
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
483
|
+
* Advances to decline — with the reason the decline state tells the person — when
|
|
484
|
+
* there is no email, the email is unknown, or either tenancy check finds a
|
|
485
|
+
* foreign customer; otherwise to identify-case. An order database that cannot
|
|
486
|
+
* be read throws: the state fails closed through its on_error.
|
|
400
487
|
*/
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return veto("Could not load skills/order-data/assets/orders.json - unable to check tenancy.");
|
|
408
|
-
}
|
|
488
|
+
import { defineCodeState } from "@archmax-ai/harness/sandbox";
|
|
489
|
+
|
|
490
|
+
export default defineCodeState(async ({ tools }) => {
|
|
491
|
+
const { messages } = JSON.parse(await tools.archmax_get_history());
|
|
492
|
+
const orders = JSON.parse(await tools.read_file({ file_path: "skills/order-data/assets/orders.json" }));
|
|
493
|
+
if (!Array.isArray(orders)) throw new Error("skills/order-data/assets/orders.json is not a list of orders");
|
|
409
494
|
|
|
410
495
|
const request = messages.find((m) => m.role === "user")?.text ?? "";
|
|
411
496
|
const email = (request.match(/[\w.+-]+@[\w.-]+\.\w{2,}/)?.[0] ?? "").toLowerCase();
|
|
497
|
+
if (!email) {
|
|
498
|
+
return tools.archmax_advance({ to: "decline", reason: "Include your email address so we can look up your account." });
|
|
499
|
+
}
|
|
412
500
|
const own = new Set(
|
|
413
501
|
orders.filter((o) => String(o.email).toLowerCase() === email).map((o) => String(o.customer)),
|
|
414
502
|
);
|
|
503
|
+
if (own.size === 0) {
|
|
504
|
+
return tools.archmax_advance({ to: "decline", reason: `"${email}" is not in the order database.` });
|
|
505
|
+
}
|
|
415
506
|
|
|
416
507
|
const lower = request.toLowerCase();
|
|
417
508
|
const foreignByName = [...new Set(orders.map((o) => String(o.customer)))].filter(
|
|
@@ -423,35 +514,64 @@ export default async function hook({ messages, tools }) {
|
|
|
423
514
|
.map((o) => String(o.id));
|
|
424
515
|
|
|
425
516
|
if (foreignByName.length > 0 || foreignById.length > 0) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
`
|
|
429
|
-
);
|
|
517
|
+
return tools.archmax_advance({
|
|
518
|
+
to: "decline",
|
|
519
|
+
reason: `"${email}" may only view orders for its own account. Do not name the other customer's orders.`,
|
|
520
|
+
});
|
|
430
521
|
}
|
|
431
|
-
|
|
432
|
-
}
|
|
522
|
+
await tools.archmax_advance({ to: "identify-case", reason: `${email} is authorized for ${[...own].join(", ")}.` });
|
|
523
|
+
});
|
|
433
524
|
```
|
|
434
525
|
|
|
435
|
-
The pattern to internalize: **one `
|
|
436
|
-
|
|
437
|
-
anything worth seeing in a verbose
|
|
526
|
+
The pattern to internalize: **one `archmax_advance` per outcome, each with a
|
|
527
|
+
`reason` the next state can act on** (the `decline` agent reads it from the `[code]`
|
|
528
|
+
note and tells the person); `console.log` for anything worth seeing in a verbose
|
|
529
|
+
run; a `throw` for what should fail the state rather than route it. To end the
|
|
530
|
+
session as a failure instead, with no reply at all, call
|
|
531
|
+
`tools.archmax_raise({ code: "unknown-requester", reason })`.
|
|
438
532
|
|
|
439
|
-
### Worked example:
|
|
533
|
+
### Worked example: a check behind a state
|
|
440
534
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
bundled example's `
|
|
535
|
+
To check the agent's work before the session moves on, route the agent's
|
|
536
|
+
transition into a code state, and give that state an edge onward and an edge
|
|
537
|
+
**back**. The bundled example's `check-refund` sits between `refund-request` (the
|
|
538
|
+
agent records a decision) and `refund-review` (a person reviews it), so a decision
|
|
539
|
+
the policy forbids never reaches the reviewer:
|
|
444
540
|
|
|
445
541
|
```yaml
|
|
446
|
-
|
|
447
|
-
|
|
542
|
+
states:
|
|
543
|
+
refund-request:
|
|
544
|
+
instructions: >-
|
|
545
|
+
… Record the decision in scratchpad/refund.json, tell the customer, and in
|
|
546
|
+
that same message archmax_advance to check-refund. If the check sends the
|
|
547
|
+
case back, its note names the fix: correct the file and advance again.
|
|
548
|
+
skills:
|
|
549
|
+
allow: [order-data, refund-policy]
|
|
550
|
+
transitions:
|
|
551
|
+
- to: check-refund
|
|
552
|
+
description: The refund decision has been recorded and explained to the customer.
|
|
553
|
+
check-refund:
|
|
554
|
+
type: code
|
|
555
|
+
script: scripts/check-refund.js
|
|
556
|
+
skills:
|
|
557
|
+
allow: [order-data]
|
|
558
|
+
on_error: escalation
|
|
559
|
+
transitions:
|
|
560
|
+
- to: refund-review
|
|
561
|
+
description: The recorded decision matches the refund policy.
|
|
562
|
+
- to: refund-request
|
|
563
|
+
description: The recorded decision contradicts the policy; send it back with the fix.
|
|
564
|
+
- to: escalation
|
|
565
|
+
description: The decision still fails the check after two corrections.
|
|
448
566
|
```
|
|
449
567
|
|
|
450
|
-
The check (`workflows/order-lookup/
|
|
568
|
+
The check (`workflows/order-lookup/scripts/check-refund.js`), which counts its own
|
|
569
|
+
earlier send-backs on the trail and escalates on the third:
|
|
451
570
|
|
|
452
571
|
```js
|
|
453
572
|
/**
|
|
454
|
-
* Refund policy
|
|
573
|
+
* Refund policy check — the agent may not hand a refund the policy forbids to
|
|
574
|
+
* the reviewer.
|
|
455
575
|
*
|
|
456
576
|
* Re-derives the refund decision from the order database instead of trusting
|
|
457
577
|
* the one the agent wrote. Reads the recorded ticket from scratchpad/refund.json
|
|
@@ -461,81 +581,183 @@ The check (`workflows/order-lookup/hooks/check-refund.js`):
|
|
|
461
581
|
* Policy: a refund is "approved" only when the order exists with status
|
|
462
582
|
* delivered or delayed; an unknown order and any other status must be "denied".
|
|
463
583
|
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
584
|
+
* Advances to refund-review when the recorded decision matches. Otherwise sends
|
|
585
|
+
* the session back to refund-request with a reason naming the fix — the ticket
|
|
586
|
+
* missing or unparseable, `decision` not "approved"/"denied", or the decision
|
|
587
|
+
* contradicting the policy — and, once it has sent it back twice (counted on
|
|
588
|
+
* the trail), advances to escalation instead. An unreadable order database
|
|
589
|
+
* throws: the state fails through its on_error.
|
|
468
590
|
*/
|
|
469
|
-
|
|
591
|
+
import { defineCodeState } from "@archmax-ai/harness/sandbox";
|
|
592
|
+
|
|
593
|
+
const MAX_SEND_BACKS = 2;
|
|
594
|
+
|
|
595
|
+
export default defineCodeState(async ({ tools }) => {
|
|
596
|
+
const { trail } = JSON.parse(await tools.archmax_get_history());
|
|
597
|
+
const sentBack = trail.filter((s) => s.kind === "code" && s.to === "refund-request").length;
|
|
598
|
+
const sendBack = (reason) =>
|
|
599
|
+
sentBack >= MAX_SEND_BACKS
|
|
600
|
+
? tools.archmax_advance({ to: "escalation", reason: `Still failing after ${sentBack} corrections: ${reason}` })
|
|
601
|
+
: tools.archmax_advance({ to: "refund-request", reason });
|
|
602
|
+
|
|
470
603
|
let refund;
|
|
471
604
|
try {
|
|
472
|
-
refund = JSON.parse(await tools.
|
|
605
|
+
refund = JSON.parse(await tools.read_file({ file_path: "scratchpad/refund.json" }));
|
|
473
606
|
} catch (e) {
|
|
474
|
-
return
|
|
475
|
-
`scratchpad/refund.json could not be read or parsed (${e.message}) - write it as a JSON object with "orderId" and "decision" before leaving this state`,
|
|
476
|
-
);
|
|
607
|
+
return sendBack(`scratchpad/refund.json could not be read or parsed (${e.message}) - write it as a JSON object with "orderId" and "decision", then advance again`);
|
|
477
608
|
}
|
|
478
|
-
|
|
479
609
|
const decision = refund?.decision;
|
|
480
610
|
if (decision !== "approved" && decision !== "denied") {
|
|
481
|
-
return
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
let orders;
|
|
485
|
-
try {
|
|
486
|
-
orders = JSON.parse(await tools.readFile({ file_path: "skills/order-data/assets/orders.json" }));
|
|
487
|
-
} catch {
|
|
488
|
-
return veto("skills/order-data/assets/orders.json could not be read - the order database must be accessible to validate the refund decision");
|
|
611
|
+
return sendBack(`scratchpad/refund.json "decision" must be exactly "approved" or "denied"; got ${JSON.stringify(decision)}`);
|
|
489
612
|
}
|
|
490
613
|
|
|
614
|
+
const orders = JSON.parse(await tools.read_file({ file_path: "skills/order-data/assets/orders.json" }));
|
|
491
615
|
const wanted = typeof refund.orderId === "string" ? refund.orderId.trim().toLowerCase() : "";
|
|
492
616
|
const order = orders.find((o) => typeof o?.id === "string" && o.id.toLowerCase() === wanted);
|
|
493
617
|
const derived = order && ["delivered", "delayed"].includes(order.status) ? "approved" : "denied";
|
|
494
618
|
console.log(`Refund policy check - derived "${derived}", recorded "${decision}"`);
|
|
495
619
|
|
|
496
|
-
if (decision === derived)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return veto(
|
|
503
|
-
`Policy requires "${derived}" for order ${JSON.stringify(refund.orderId)} because ${why}, but the recorded decision is "${decision}" - update scratchpad/refund.json to decision: "${derived}"`,
|
|
620
|
+
if (decision === derived) {
|
|
621
|
+
return tools.archmax_advance({ to: "refund-review", reason: `"${decision}" for ${refund.orderId} matches the refund policy.` });
|
|
622
|
+
}
|
|
623
|
+
const why = !order ? "it does not exist in the order database" : `its status is "${order.status}"`;
|
|
624
|
+
return sendBack(
|
|
625
|
+
`Policy requires "${derived}" for order ${JSON.stringify(refund.orderId)} because ${why}, but the recorded decision is "${decision}" - update scratchpad/refund.json to decision: "${derived}", correct what you told the customer, and advance again`,
|
|
504
626
|
);
|
|
627
|
+
});
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Note how the send-back reasons carry the _fix_: the agent reads the reason as a
|
|
631
|
+
`[code]` note, rewrites `scratchpad/refund.json`, and advances again. There is no
|
|
632
|
+
retry budget to declare: the script counts the rounds itself, from the history's
|
|
633
|
+
`trail`, and decides when to give up.
|
|
634
|
+
|
|
635
|
+
### Worked example: approve, then end
|
|
636
|
+
|
|
637
|
+
A check that approves into a terminal **agent** state costs one more model call:
|
|
638
|
+
that state's agent has to finish the turn. Approve into a **terminal code state**
|
|
639
|
+
instead. Its script runs and the session completes, with no further model call.
|
|
640
|
+
Say the enrichment path's grader approves into `enrichments-approved`, and the
|
|
641
|
+
trigger declares `returns: [delayed_order_ids]`:
|
|
642
|
+
|
|
643
|
+
```yaml
|
|
644
|
+
enrichments-approved:
|
|
645
|
+
type: code
|
|
646
|
+
script: scripts/enrichments-approved.js # no transitions: terminal
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
```js
|
|
650
|
+
/**
|
|
651
|
+
* Approved-enrichments record — the end of the enrichment path.
|
|
652
|
+
*
|
|
653
|
+
* Reads the `enrichments` the grader approved and sets the trigger's declared
|
|
654
|
+
* return `delayed_order_ids` from them, so a caller has the result without
|
|
655
|
+
* walking the list, then returns: the session completes.
|
|
656
|
+
*/
|
|
657
|
+
export default async function ({ variables, tools }) {
|
|
658
|
+
const delayed = variables.enrichments.filter((e) => e.delayed).map((e) => e.order_id);
|
|
659
|
+
await tools.archmax_set_variables({ variables: { delayed_order_ids: delayed } });
|
|
505
660
|
}
|
|
506
661
|
```
|
|
507
662
|
|
|
508
|
-
|
|
509
|
-
|
|
663
|
+
A terminal code state's completion is held to the trigger's `returns` like any
|
|
664
|
+
completion: set a declared return with `tools.archmax_set_variables` before the
|
|
665
|
+
script returns.
|
|
510
666
|
|
|
511
|
-
### When to use a
|
|
667
|
+
### When to use a grader state instead
|
|
512
668
|
|
|
513
|
-
Use a
|
|
669
|
+
Use a **code state** when the check is **deterministic** — derivable from
|
|
514
670
|
workspace data with plain code (policy tables, schema checks, cross-file
|
|
515
|
-
consistency). Use a
|
|
516
|
-
|
|
671
|
+
consistency). Use a **grader state** when the check requires **judgment** (is the
|
|
672
|
+
report complete? is the summary faithful?). A grader is an isolated subagent the
|
|
673
|
+
runtime runs on entry: its system prompt is a compiled-in preamble followed by the
|
|
674
|
+
state's `instructions` (its criteria), it reads the session with
|
|
675
|
+
`archmax_get_history` (the whole transcript, tool results included, and the
|
|
676
|
+
trail) and the work with its own state's tools, and it decides with
|
|
677
|
+
`archmax_advance` along one of its transitions, or ends the session with
|
|
678
|
+
`archmax_raise`. The bundled example's enrichment path:
|
|
517
679
|
|
|
518
680
|
```yaml
|
|
519
681
|
states:
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
682
|
+
enrich-order:
|
|
683
|
+
instructions: >-
|
|
684
|
+
Run skills/order-enrichment/scripts/enrich-orders.js with archmax_run …
|
|
685
|
+
Record that list as the `enrichments` variable with archmax_set_variables,
|
|
686
|
+
then advance to review-enrichment, which checks it. If the review sends it
|
|
687
|
+
back, fix what its note names and advance again.
|
|
688
|
+
requires: [enrichments]
|
|
689
|
+
transitions:
|
|
690
|
+
- to: review-enrichment
|
|
691
|
+
description: Every order has been enriched and `enrichments` is recorded.
|
|
692
|
+
review-enrichment:
|
|
693
|
+
type: grader
|
|
694
|
+
budget: { maxTurns: 8 } # the grader's own model calls
|
|
695
|
+
on_error: escalation # a grader that fails or finishes undecided routes here
|
|
696
|
+
skills:
|
|
697
|
+
allow: [] # read_file, from the workflow-wide grant, reads the returned files
|
|
698
|
+
instructions: |-
|
|
699
|
+
You check the enrichments recorded for this run before they are reported.
|
|
700
|
+
|
|
701
|
+
1. Read `orders_to_enrich` and `enrichments` with `archmax_get_variables`.
|
|
702
|
+
2. The enrichments are correct when `enrichments` holds exactly one entry
|
|
703
|
+
per requested `order_id` — none missing, none extra — and each entry
|
|
704
|
+
has an `enrichment_file` and a boolean `delayed`.
|
|
705
|
+
3. Read each `enrichment_file` with `read_file`: its `delayed` must match
|
|
706
|
+
the entry's.
|
|
707
|
+
|
|
708
|
+
- Correct: advance to `enrichment-reported`.
|
|
709
|
+
- Wrong: advance back to `enrich-order`, naming exactly what is missing or
|
|
710
|
+
does not match.
|
|
711
|
+
- Before sending it back, call `archmax_get_history` and count the steps
|
|
712
|
+
in `trail` with `kind: "grader"` and `to: "enrich-order"`: if there are
|
|
713
|
+
already two, raise `enrichment-incorrect` instead.
|
|
714
|
+
transitions:
|
|
715
|
+
- to: enrichment-reported # a terminal agent state that summarizes the enrichments
|
|
716
|
+
description: Every requested order has exactly one enrichment, and each matches its file.
|
|
717
|
+
- to: enrich-order
|
|
718
|
+
description: >-
|
|
719
|
+
The enrichments are incomplete or do not match their files: send them
|
|
720
|
+
back, naming exactly what to fix.
|
|
527
721
|
```
|
|
528
722
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
`
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
its
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
723
|
+
What to know:
|
|
724
|
+
|
|
725
|
+
- A grader state **must** declare non-empty `instructions` and at least one
|
|
726
|
+
transition (`validate` errors): a grader with nowhere to send the session could
|
|
727
|
+
only approve by saying nothing, which would fail open. `evidence`, `approvers`
|
|
728
|
+
and `budget.maxParks` are errors on it. Every edge **into** a grader carries a
|
|
729
|
+
`description`: its instructions are criteria the work it judges is never shown,
|
|
730
|
+
so an undescribed edge into one is a load error.
|
|
731
|
+
- `archmax_get_history` returns `{ messages, trail }` — the same history a code
|
|
732
|
+
state reads: the whole transcript, a `tool` entry carrying its result in `text`,
|
|
733
|
+
and every trail step. Not windowed: a grader judges the whole session, so its
|
|
734
|
+
criteria say which part counts ("the files `enrichments` names", "the last
|
|
735
|
+
assistant message with text"). Check a claim against the tool result or
|
|
736
|
+
the file itself, never against the agent's retelling of it.
|
|
737
|
+
- There is **no iteration budget**. A grader that should give up after N rounds
|
|
738
|
+
says so in its `instructions` and counts the rounds from the history's trail
|
|
739
|
+
(each earlier send-back is `{ kind: "grader", to: "<graded state>" }`), as
|
|
740
|
+
above. The turn's recursion limit is the only backstop.
|
|
741
|
+
- The grader is offered exactly its state's surface — its grants (never
|
|
742
|
+
`write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait` or `archmax_reset`,
|
|
743
|
+
even when `allow_always` lists them) and the controls a code state gets:
|
|
744
|
+
`archmax_advance`, `archmax_raise`, the variable tools and `archmax_get_history`
|
|
745
|
+
— governed against the grader state. Its messages never enter the session's
|
|
746
|
+
transcript and its tokens never stream; the session reads its decision as a
|
|
747
|
+
`[grader]` note and a `kind: grader` trail step.
|
|
748
|
+
- A grader that answers in text without deciding **fails closed** (`grader state
|
|
749
|
+
'<slug>' finished without calling archmax_advance or archmax_raise`), routed
|
|
750
|
+
through `on_error`. `budget.maxTurns` / `timeoutMs` bound its own model calls.
|
|
751
|
+
- `model` on a grader state names the id it runs on, resolved through the
|
|
752
|
+
assembly's `modelFactory` under the **`grader`** role.
|
|
753
|
+
- The checked state's `instructions` tell the agent that advancing into the
|
|
754
|
+
grader is what hands the work in, and to fix the work when a note sends it back
|
|
755
|
+
— never to argue with the note.
|
|
756
|
+
- **Put a grader where the agent hands work on, not on a live answer.** A turn's
|
|
757
|
+
`reply` is every assistant text of the turn, in order, so a draft the grader
|
|
758
|
+
sends back has still reached the person. Check work that waits for nobody — a
|
|
759
|
+
report, a record, a decision a person reviews next — and let a live answer end
|
|
760
|
+
where it is given.
|
|
539
761
|
|
|
540
762
|
---
|
|
541
763
|
|
|
@@ -555,7 +777,7 @@ workflows/<name>/tests/
|
|
|
555
777
|
`archmax test` filter argument matches that path as a substring.
|
|
556
778
|
- **Cases are data, not code.** The host parses, validates, and interprets
|
|
557
779
|
them directly — no QuickJS sandbox is created for a case. The sandbox still
|
|
558
|
-
runs
|
|
780
|
+
runs code-state scripts and `archmax_run` *inside the driven agent*, unchanged.
|
|
559
781
|
- **A case reads like a script.** `steps` is one flat sequential list where
|
|
560
782
|
actions (`send`, `decide`) and assertions are peers; each assertion
|
|
561
783
|
evaluates against the view of the nearest action above it.
|
|
@@ -590,7 +812,7 @@ workflows/<name>/tests/
|
|
|
590
812
|
(`>-`) and a literal (`|`) block of the same prose score alike.
|
|
591
813
|
- Work title-first: write `title` and `description` before the steps, and
|
|
592
814
|
never leave them contradicting each other. This replaces the JSDoc-header
|
|
593
|
-
rule that applied to JS test files; JSDoc headers now apply to
|
|
815
|
+
rule that applied to JS test files; JSDoc headers now apply to code-state
|
|
594
816
|
scripts and `archmax_run` sources only.
|
|
595
817
|
|
|
596
818
|
Run with `archmax test <workflow> [filter]`. A minimal case:
|
|
@@ -619,8 +841,8 @@ tests:
|
|
|
619
841
|
120000 # per-case WALL-CLOCK budget (default 120000) —
|
|
620
842
|
# deliberately named differently from settings.timeoutMs,
|
|
621
843
|
# the per-script sandbox budget
|
|
622
|
-
judge: {} #
|
|
623
|
-
# or { model, modelOptions })
|
|
844
|
+
judge: {} # the model that scores `grade:` ({} = env-configured
|
|
845
|
+
# default model; or { model, modelOptions }) — not a grader state
|
|
624
846
|
```
|
|
625
847
|
|
|
626
848
|
There is no `reporters` option. A
|
|
@@ -641,7 +863,7 @@ Unknown keys are validation **errors**. All keys except `title` and
|
|
|
641
863
|
| `variables` | `{ <name>: <value> }` | Session variables seeded for every driven turn, supplied as the runtime's `variables` and therefore **locked**. Values may be structured. This is where a case puts the run's input — a trigger carries none. Recover the real payload from a previous run's `artifacts/variables.json` rather than inventing one: see [Recovering a case's inputs from previous runs](#recovering-a-cases-inputs-from-previous-runs). |
|
|
642
864
|
| `trigger` | `{ id }` | The trigger every driven turn of the case runs under. Carries **only an id**; `args` is rejected with a pointer to `variables:`. Validated against the workflow machine before any step — an unknown id fails the case (even with no steps), listing the workflow's declared triggers. Omitted ⇒ the manual trigger. See [Start conditions](#start-conditions-trigger-variables-and-workspace). |
|
|
643
865
|
| `workspace` | `{ <path>: <content> }` | Files seeded into the case's run workspace **before any step**. Strings are written verbatim; non-string YAML values are JSON-serialized; a single-key `{ from: <tests/-relative path> }` mapping copies a file byte-for-byte. Only agent-visible run-zone paths are seedable (root-level files like `trigger.json`, or `scratchpad/…`); authored (`skills/…`), runtime-internal, and escaping paths are rejected before anything is written. |
|
|
644
|
-
| `mocks` | `[{ tool, whenInput?, result }]` | Declarative tool mocks: when
|
|
866
|
+
| `mocks` | `[{ tool, whenInput?, result }]` | Declarative tool mocks: when `tool` is called with input partially matching `whenInput`, return `result` instead of executing. Intercepts **agent-initiated** calls and every programmatic call alike — an agent's script's `tools.*`, a code state's `tools.*`, a grader's calls. A control tool (`archmax_advance`, `archmax_raise`, the variable tools) is never mocked. See [the mocking section](#settle-the-mocking-depth-before-writing-a-case). |
|
|
645
867
|
| `clock` | `"2026-03-20"` / `"…T14:30:00Z"` | The date and time every model call of the case states as "now": an ISO 8601 date (midnight UTC) or a date-time with `Z` or an offset; a zoneless or impossible value is a schema error. Carried on every action, a handoff and a child session included, and outranks the host's `clock` option. See [Start conditions](#start-conditions-trigger-variables-and-workspace). |
|
|
646
868
|
| `steps` | flat list of step entries | The case script: actions and assertions as peers (below). |
|
|
647
869
|
|
|
@@ -696,18 +918,18 @@ failure in one of them **halts the case** — see
|
|
|
696
918
|
|
|
697
919
|
| Assertion | Meaning |
|
|
698
920
|
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
699
|
-
| `succeeded: true` | **Structural.**
|
|
921
|
+
| `succeeded: true` | **Structural.** Reads the action's outcome: passes when it is `completed` — not parked, not raised with `archmax_raise`, not failed, not rejected. A tool call that failed or was blocked along the way, and that the turn recovered from, does not fail it. A miss says why: the raised code and reason, `the session is parked`, or `the turn did not complete`. |
|
|
700
922
|
| `parked: true` | **Structural.** The run is parked at a human state awaiting a decision. |
|
|
701
923
|
| `raised: true` / `raised: <code>` / `raised: { code?, reason? }` | **Structural.** The agent ended the turn with `archmax_raise({ code, reason })`. A string pins the code exactly; the mapping pins the code and matches the reason — exactly, or as a regex when written `"/pattern/flags"`. A miss names the code that was raised, or says the run did not raise. |
|
|
702
924
|
| `variables: { expect, path?, locked? }` | Every named variable is set to the expected value — deep equality, so a structured expectation compares by value. `path` addresses into a value per name (`items.-1.sku` is the last element); `locked` asserts the lock state. |
|
|
703
|
-
| `reachedState: <slug>` | **Structural.** A **committed** (non-trigger) audit-trail step entered the state — agent `archmax_advance`, human decision, `archmax_reset`,
|
|
925
|
+
| `reachedState: <slug>` | **Structural.** A **committed** (non-trigger) audit-trail step entered the state — the agent's `archmax_advance`, a grader's or a code state's advance, a human decision, `archmax_reset`, an `on_error` route. The trigger-arrival step never counts, so a session reaches its start state only by moving into it (a session whose entry code state raised never "reaches" it). |
|
|
704
926
|
| `reply: { includes?, excludes? }` | `includes`: substrings or `/pattern/flags` regex strings that must **all** match; `excludes`: none may match (leak checks — a refusal that quotes the data it refuses still leaks it). Each accepts a **single token or a list** (`includes: "ORD-1003"` works). Matched against **every assistant message of the session so far** (earlier turns included), not just the final message (a parked run often closes with a short "moving on" message while the substantive answer came earlier). |
|
|
705
927
|
| `calledTool: { name, input? }` | The turn recorded a call to tool `name`; `input` is a **partial** match — mappings match on the declared keys (nested mappings partially), arrays by deep equality, and `/pattern/flags` string values match as **regexes** (recommended for file paths, so the model's exact spelling doesn't matter). |
|
|
706
928
|
| `notCalledTool: { name, input? }` | No matching call occurred. |
|
|
707
929
|
| `blockedTool: { name, input? }` | A matching call was made **and refused by governance** — the assertion for testing a guard. Distinct from `notCalledTool` (nothing was tried, which proves nothing about the guard) and from a tool that ran and errored (only a policy rejection counts). Use it to test a state's `tools.allow` narrowing or its `skills.allow` set. |
|
|
708
|
-
| `usedNoTools: true` | The turn used no tools at all. |
|
|
709
|
-
| `trail: { to?, kind?, reason?, count }` | **Structural.** The number of audit-trail steps matching **all** given fields equals `count` (declare at least one of `to`/`kind`/`reason`). `kind`: `trigger` \| `agent` \| `human` \| `
|
|
710
|
-
| `noTraversal: true` | **Structural.** The trail holds nothing but trigger arrivals —
|
|
930
|
+
| `usedNoTools: true` | The turn's agent used no tools at all. An automated state's calls are not in the transcript, so they never count. |
|
|
931
|
+
| `trail: { to?, kind?, reason?, count }` | **Structural.** The number of audit-trail steps matching **all** given fields equals `count` (declare at least one of `to`/`kind`/`reason`). `kind`: `trigger` \| `agent` \| `human` \| `grader` \| `code` \| `sub-workflow` \| `reset` \| `on_error` — a grader's or code state's move is `grader`/`code`, with the `reason` it passed to `archmax_advance`. `count: 0` asserts a state was never entered. The trail accumulates across turns — each turn starts with a `trigger` step. |
|
|
932
|
+
| `noTraversal: true` | **Structural.** The trail holds nothing but trigger arrivals — nothing moved the session (an entry code state that raised, an agent that answered from its start state). A grader's or code state's move is a traversal. |
|
|
711
933
|
| `triggerArrival: <trigger-id>` | **Structural.** Some trail step has `kind: trigger` and that trigger id as its reason — the run really started from the declared trigger. |
|
|
712
934
|
| `grade: { closedQA, atLeast }` | LLM-grade the turn's **run evidence** against the `closedQA` criterion — what the action's turn said (every text of that turn, empty when it said nothing) *plus* a bounded chronological record of the session's assistant messages and tool calls so far (each call with input, output, and status; mocked calls appear like real ones). The two are weighed differently: a criterion about what the user was **told** is met only by the reply, one about what the agent **did** may be met by the record — so `closedQA: looked the order up before answering` is a legitimate criterion, not only phrasing checks. **`atLeast` decides the outcome** — score ≥ `atLeast` passes, below it **fails the case**; the judge model's own opinion never overrides your declared bar, so the record can't disagree with the verdict. The judge is instructed to keep its reason to 1–3 sentences, so it fits on the verdict line. Requires `tests.judge` in `workflow.yaml`. |
|
|
713
935
|
|
|
@@ -719,7 +941,30 @@ validation errors**, never silently matched as literals. Quote them in YAML —
|
|
|
719
941
|
|
|
720
942
|
**There is no generic predicate.** No `satisfies`, no inline JS — that is the
|
|
721
943
|
point of the format. Bespoke logic belongs in a vitest unit test against the
|
|
722
|
-
engine or in a
|
|
944
|
+
engine or in a code state on the graph.
|
|
945
|
+
|
|
946
|
+
**An automated state is asserted by what it committed.** A code state's or a
|
|
947
|
+
grader's own tool calls are not in the transcript, so `calledTool`,
|
|
948
|
+
`notCalledTool`, `blockedTool` and `usedNoTools` see only the agent's, and a
|
|
949
|
+
grader's words are never part of `reply`. Assert its move and reason with
|
|
950
|
+
`reachedState` / `trail: { kind: code | grader, to, reason?, count }`, its raise
|
|
951
|
+
with `raised`, what it set with `variables`, and its dispatches with
|
|
952
|
+
`ranWorkflow`:
|
|
953
|
+
|
|
954
|
+
```yaml
|
|
955
|
+
steps:
|
|
956
|
+
- send: "Hi, I'm support@acmecorp.com. Which orders are delayed?"
|
|
957
|
+
- trail: { kind: code, to: identify-case, count: 1 } # the entry screen passed it
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
```yaml
|
|
961
|
+
trigger: { id: enrichment_requested } # the enrichment path
|
|
962
|
+
variables: { orders_to_enrich: [{ order_id: "ORD-1002" }, { order_id: "ORD-1003" }] }
|
|
963
|
+
steps:
|
|
964
|
+
- send: "Enrich the requested orders."
|
|
965
|
+
- trail: { kind: grader, to: enrichment-reported, count: 1 } # the grader approved the enrichments
|
|
966
|
+
- trail: { kind: grader, to: enrich-order, count: 0 } # without a send-back
|
|
967
|
+
```
|
|
723
968
|
|
|
724
969
|
### Structural assertions halt the case
|
|
725
970
|
|
|
@@ -797,8 +1042,8 @@ steps:
|
|
|
797
1042
|
runtime's `variables` and therefore **locked**, values structured or
|
|
798
1043
|
scalar. A seed here is indistinguishable from a production host seed to
|
|
799
1044
|
everything downstream: a `${{…}}` reference in a `tools.allow` glob resolves
|
|
800
|
-
from it, a state's `requires:` gate is satisfied by it, a
|
|
801
|
-
it in `variables`, and `archmax_set_variables` refuses to rewrite it.
|
|
1045
|
+
from it, a state's `requires:` gate is satisfied by it, a code state's script
|
|
1046
|
+
reads it in `variables`, and `archmax_set_variables` refuses to rewrite it.
|
|
802
1047
|
**Recover the real payload rather than inventing one** — see
|
|
803
1048
|
[Recovering a case's inputs from previous runs](#recovering-a-cases-inputs-from-previous-runs).
|
|
804
1049
|
- **`workspace`** — input files seeded before any step. Keys are run-zone
|
|
@@ -822,7 +1067,7 @@ Two operational notes:
|
|
|
822
1067
|
carries a date (an order placed on `2026-03-15`, "refundable for 30 days"):
|
|
823
1068
|
without it the model reads the real "now" and the verdict drifts with the
|
|
824
1069
|
calendar. Pin on a ten-minute mark (the line is rounded down to ten minutes,
|
|
825
|
-
UTC only). Only the prompt is pinned —
|
|
1070
|
+
UTC only). Only the prompt is pinned — code-state scripts, the sandbox's `Date`, a
|
|
826
1071
|
park's `resumeAt` and every recorded timestamp stay real, so a script that needs
|
|
827
1072
|
the case's "today" reads it from a seeded variable, and a wait resumed in the
|
|
828
1073
|
case is resumed with `deliver`, not by its `until`.
|
|
@@ -907,8 +1152,8 @@ actually calls and propose a split:
|
|
|
907
1152
|
inventory, today's date-dependent data). A test that is not safe to re-run is
|
|
908
1153
|
not a test.
|
|
909
1154
|
- **Leave real by default** reads against fixtures committed to the workspace,
|
|
910
|
-
the workflow's own scripts and
|
|
911
|
-
would test the mock instead of the workflow.
|
|
1155
|
+
the workflow's own scripts and code states, and run-zone file writes — mocking
|
|
1156
|
+
those would test the mock instead of the workflow.
|
|
912
1157
|
- **Ask** about reads against a live system: cheap and stable enough to keep
|
|
913
1158
|
real, or slow, rate-limited, or credential-bound enough to mock?
|
|
914
1159
|
|
|
@@ -918,9 +1163,10 @@ while a reader checking the case against the workflow needs to know what was
|
|
|
918
1163
|
deliberately left real, which is exactly the kind of rationale the comment
|
|
919
1164
|
exists for.
|
|
920
1165
|
|
|
921
|
-
Mocks are the top-level `mocks:` list; they intercept **agent-initiated
|
|
922
|
-
|
|
923
|
-
|
|
1166
|
+
Mocks are the top-level `mocks:` list; they intercept **agent-initiated** calls
|
|
1167
|
+
and every programmatic call — an agent's script, a code state's script, a
|
|
1168
|
+
grader — with the same partial-input matching as `calledTool`. So a mock of a
|
|
1169
|
+
file an entry code state reads changes what that state decides too. Interception is guaranteed under `archmax test`. A host running
|
|
924
1170
|
the same cases through `runTests` with its own `createTarget` must wire
|
|
925
1171
|
`createToolMockMiddleware()` into that agent — a case that declares `mocks:`
|
|
926
1172
|
against a target that cannot intercept them is refused with an error **before
|
|
@@ -936,9 +1182,8 @@ workflow-governed target. See
|
|
|
936
1182
|
# (threshold 0.7) checks the order is attributed to the requester's company.
|
|
937
1183
|
title: Delayed orders answered for a known requester
|
|
938
1184
|
description: >-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
names the one delayed order, ORD-1003.
|
|
1185
|
+
A known requester's question routes into orders-question, and the reply names
|
|
1186
|
+
the one delayed order, ORD-1003.
|
|
942
1187
|
steps:
|
|
943
1188
|
- send: "Hi, I'm support@acmecorp.com. Which orders are delayed for Acme?"
|
|
944
1189
|
- succeeded: true
|
|
@@ -952,27 +1197,33 @@ steps:
|
|
|
952
1197
|
atLeast: 0.7
|
|
953
1198
|
```
|
|
954
1199
|
|
|
955
|
-
### Worked example:
|
|
1200
|
+
### Worked example: declined at entry, leak check
|
|
956
1201
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1202
|
+
An entry code state that declines a request commits a `code` step into the
|
|
1203
|
+
declining state and none into the state the work starts in — `reachedState`
|
|
1204
|
+
and a `count: 0` trail pin exactly that — and `reply.excludes` catches a
|
|
1205
|
+
refusal that quotes the data it refuses to share. (Had the screen ended the
|
|
1206
|
+
session with `tools.archmax_raise` instead, `raised: <code>` and
|
|
1207
|
+
`noTraversal: true` would pin it.)
|
|
960
1208
|
|
|
961
1209
|
```yaml
|
|
962
|
-
# The `
|
|
963
|
-
#
|
|
964
|
-
#
|
|
965
|
-
|
|
1210
|
+
# The `trail` count pins the decline to *entry*: identify-case was never
|
|
1211
|
+
# entered, so no route was taken. The `excludes` tokens matter as much as the
|
|
1212
|
+
# refusal itself: a refusal that quotes the order ids it declines to share
|
|
1213
|
+
# still leaks them. The judge (threshold 0.7) covers the same ground in prose.
|
|
1214
|
+
title: Cross-tenant order access is declined
|
|
966
1215
|
description: >-
|
|
967
|
-
A known requester asking for Globex's delayed orders is declined
|
|
968
|
-
|
|
969
|
-
Globex order id appears in the reply.
|
|
1216
|
+
A known requester asking for Globex's delayed orders is declined at entry,
|
|
1217
|
+
identify-case is never entered, and no Globex order id appears in the reply.
|
|
970
1218
|
steps:
|
|
971
1219
|
- send: "Hi, this is support@acmecorp.com. Show me Globex's delayed orders."
|
|
1220
|
+
- succeeded: true
|
|
1221
|
+
- reachedState: decline
|
|
1222
|
+
- trail: { kind: code, to: decline, count: 1 }
|
|
1223
|
+
- trail: { to: identify-case, count: 0 }
|
|
972
1224
|
- reply:
|
|
973
|
-
includes: "/not authorized|only .* your own|
|
|
1225
|
+
includes: "/not authorized|only .* your own|declined/i"
|
|
974
1226
|
excludes: ["ORD-2001", "ORD-2002"]
|
|
975
|
-
- noTraversal: true
|
|
976
1227
|
- grade:
|
|
977
1228
|
closedQA: >-
|
|
978
1229
|
declines the request because the requester is not authorized to view
|
|
@@ -984,20 +1235,28 @@ steps:
|
|
|
984
1235
|
### Worked example: mocking tool results
|
|
985
1236
|
|
|
986
1237
|
```yaml
|
|
987
|
-
# Only read_file against skills/order-data/assets/orders.json is mocked;
|
|
988
|
-
#
|
|
989
|
-
#
|
|
990
|
-
|
|
1238
|
+
# Only read_file against skills/order-data/assets/orders.json is mocked; the
|
|
1239
|
+
# agent's own writes stay real. The report_requested trigger enters
|
|
1240
|
+
# report-requested directly, so no code state reads the records first — a mock
|
|
1241
|
+
# answers a code state's calls too, and an empty database would make the entry
|
|
1242
|
+
# screen decline the requester. Asserting the lookup *and* the empty answer
|
|
1243
|
+
# catches both a skipped lookup and an invented order.
|
|
1244
|
+
title: Empty order database reports no delayed orders
|
|
991
1245
|
description: >-
|
|
992
|
-
With skills/order-data/assets/orders.json mocked empty, a
|
|
993
|
-
|
|
994
|
-
|
|
1246
|
+
With skills/order-data/assets/orders.json mocked empty, a requested report
|
|
1247
|
+
still consults the database, and the reply reports no delayed orders.
|
|
1248
|
+
trigger:
|
|
1249
|
+
id: report_requested
|
|
1250
|
+
variables:
|
|
1251
|
+
company: "Acme Corp"
|
|
1252
|
+
workspace:
|
|
1253
|
+
trigger.json: { company: "Acme Corp" }
|
|
995
1254
|
mocks:
|
|
996
1255
|
- tool: read_file
|
|
997
1256
|
whenInput: { file_path: skills/order-data/assets/orders.json }
|
|
998
1257
|
result: "[]"
|
|
999
1258
|
steps:
|
|
1000
|
-
- send: "
|
|
1259
|
+
- send: "A delayed-orders report was requested."
|
|
1001
1260
|
- calledTool:
|
|
1002
1261
|
name: read_file
|
|
1003
1262
|
input:
|
|
@@ -1088,8 +1347,8 @@ seeded byte-for-byte before any step:
|
|
|
1088
1347
|
title: Delayed-orders report compiled from a trigger
|
|
1089
1348
|
description: >-
|
|
1090
1349
|
The report_requested trigger starts the run: the agent reads the seeded
|
|
1091
|
-
trigger.json
|
|
1092
|
-
|
|
1350
|
+
trigger.json, writes scratchpad/report.json, and the reply names Acme's one
|
|
1351
|
+
delayed order, ORD-1003.
|
|
1093
1352
|
trigger:
|
|
1094
1353
|
id: report_requested
|
|
1095
1354
|
variables:
|
|
@@ -1109,7 +1368,7 @@ steps:
|
|
|
1109
1368
|
- calledTool:
|
|
1110
1369
|
name: write_file
|
|
1111
1370
|
input:
|
|
1112
|
-
file_path: "/
|
|
1371
|
+
file_path: "/scratchpad\\/report\\.json/"
|
|
1113
1372
|
- reply:
|
|
1114
1373
|
includes: ["ORD-1003", "Acme"]
|
|
1115
1374
|
```
|
|
@@ -1136,7 +1395,7 @@ skipped silently. The mapping:
|
|
|
1136
1395
|
| `t.auditTrail()` + `filter(...).length === n` | `- trail: { to?, kind?, reason?, count }`; "nothing but the trigger arrival" ⇒ `- noTraversal: true`; trigger-arrival reason ⇒ `- triggerArrival: <id>` |
|
|
1137
1396
|
| `t.judge.autoevals.closedQA(c).atLeast(n)` | `- grade: { closedQA: c, atLeast: n }` step entry |
|
|
1138
1397
|
| `t.skip(reason)` | `skip: <reason>` |
|
|
1139
|
-
| bespoke predicates | **No YAML equivalent — deliberate.** Move bespoke logic into a vitest unit test against the engine, or a
|
|
1398
|
+
| bespoke predicates | **No YAML equivalent — deliberate.** Move bespoke logic into a vitest unit test against the engine, or a code state that checks it on the graph. |
|
|
1140
1399
|
| `import { defineEval } from "@archmax-ai/harness/testing"` | Nothing — the subpath is removed; YAML imports nothing |
|
|
1141
1400
|
|
|
1142
1401
|
Removed package exports: `createDriveTool`, `DriveRequest`, `DriveSession`,
|
|
@@ -1145,8 +1404,9 @@ cases themselves: `parseCaseDocument`, `CaseSchemaError`, the case types
|
|
|
1145
1404
|
(`CaseDocument`, `CaseStep`, `CaseExpectation`, …), and the test-spec
|
|
1146
1405
|
versioning surface (`DEFAULT_TEST_SPEC_VERSION`, `SUPPORTED_TEST_SPEC_VERSIONS`,
|
|
1147
1406
|
`resolveTestSpecVersion`, `isTestSpecVersionSupported`,
|
|
1148
|
-
`UnsupportedTestSpecVersionError`). `@archmax-ai/harness/sandbox` carries the
|
|
1149
|
-
contract (`
|
|
1407
|
+
`UnsupportedTestSpecVersionError`). `@archmax-ai/harness/sandbox` carries the
|
|
1408
|
+
code-state contract (`defineCodeState`, `CodeStateInput`, `CodeStateArgs`,
|
|
1409
|
+
`CodeStateFn`, `SandboxTools`, `SessionHistory`, `HistoryMessage`, `TrailStep`).
|
|
1150
1410
|
|
|
1151
1411
|
---
|
|
1152
1412
|
|
|
@@ -1154,11 +1414,11 @@ contract (`ok`/`veto`/`correct`, `defineHook`, `HookInput`).
|
|
|
1154
1414
|
|
|
1155
1415
|
| Concern | File in the SDK |
|
|
1156
1416
|
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
1157
|
-
| Sandbox prelude (
|
|
1158
|
-
|
|
|
1159
|
-
| Executor, sandbox tools, PTC gateway
|
|
1160
|
-
| Typed entry point (`
|
|
1161
|
-
|
|
|
1162
|
-
|
|
|
1417
|
+
| Sandbox prelude (the `code` context, `defineCodeState`, the scoped `tools` view) | `src/sandbox/assets/parts/code-state.js`; `src/sandbox/prelude.ts` |
|
|
1418
|
+
| Script source preparation (`@archmax-ai/harness/*` import stripping, `export default`, foreign-import errors) | `src/sandbox/imports.ts` |
|
|
1419
|
+
| Executor, sandbox tools, PTC gateway | `src/sandbox/executor.ts`, `tools.ts`, `ptc-gateway.ts` |
|
|
1420
|
+
| Typed entry point (`defineCodeState`, `CodeStateInput`, `SessionHistory`, `HistoryMessage`, `TrailStep`) | `src/public/sandbox.ts` |
|
|
1421
|
+
| Running an automated state, its outcomes and controls | `src/workflow/automated.ts`, `automated-controls.ts` |
|
|
1422
|
+
| Code-state and grader runs; the history (`messages`, `trail`) | `src/workflow/code-state.ts`, `grader-state.ts`, `history.ts` |
|
|
1163
1423
|
| Case engine (host-side) | `src/testing/` — case schema `case-schema.ts`, discovery `discovery.ts`, assertion evaluation `assertions.ts`, grader `grade.ts`, runner `runner.ts` |
|
|
1164
|
-
| Validation diagnostics (missing script
|
|
1424
|
+
| Validation diagnostics (missing code-state script, foreign import, YAML case checks) | `src/validate/validate.ts` |
|