@archmax-ai/harness 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +58 -14
  2. package/dist/agent.d.ts +4 -6
  3. package/dist/agent.js +4 -6
  4. package/dist/agent.js.map +1 -1
  5. package/dist/assembly/compose.d.ts +5 -24
  6. package/dist/assembly/compose.js +28 -42
  7. package/dist/assembly/compose.js.map +1 -1
  8. package/dist/assembly/delegation-registry.js +3 -14
  9. package/dist/assembly/delegation-registry.js.map +1 -1
  10. package/dist/assembly/index.d.ts +8 -15
  11. package/dist/assembly/index.js +21 -28
  12. package/dist/assembly/index.js.map +1 -1
  13. package/dist/assembly/plain.js +1 -2
  14. package/dist/assembly/plain.js.map +1 -1
  15. package/dist/authoring-skill/archmax-harness/README.md +5 -5
  16. package/dist/authoring-skill/archmax-harness/SKILL.md +72 -30
  17. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +33 -24
  18. package/dist/authoring-skill/archmax-harness/references/cases.md +41 -8
  19. package/dist/authoring-skill/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +599 -339
  20. package/dist/authoring-skill/archmax-harness/references/code-states-and-scripts.md +115 -0
  21. package/dist/authoring-skill/archmax-harness/references/diagnostics.md +40 -7
  22. package/dist/authoring-skill/archmax-harness/references/wiring.md +19 -10
  23. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +404 -293
  24. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +151 -62
  25. package/dist/cli/state-flow.d.ts +1 -1
  26. package/dist/cli/state-flow.js +18 -28
  27. package/dist/cli/state-flow.js.map +1 -1
  28. package/dist/cli/turn.d.ts +1 -1
  29. package/dist/cli/turn.js +1 -1
  30. package/dist/core/code-description.d.ts +3 -3
  31. package/dist/core/code-description.js +3 -3
  32. package/dist/core/deepagents.d.ts +5 -12
  33. package/dist/core/deepagents.js +4 -17
  34. package/dist/core/deepagents.js.map +1 -1
  35. package/dist/core/events.d.ts +23 -42
  36. package/dist/core/events.js +21 -24
  37. package/dist/core/events.js.map +1 -1
  38. package/dist/core/message-readers.d.ts +4 -5
  39. package/dist/core/message-readers.js +3 -8
  40. package/dist/core/message-readers.js.map +1 -1
  41. package/dist/core/messages.d.ts +0 -22
  42. package/dist/core/messages.js +1 -32
  43. package/dist/core/messages.js.map +1 -1
  44. package/dist/core/mounts.js +3 -3
  45. package/dist/core/platform-prompt.generated.js +1 -1
  46. package/dist/core/platform-prompt.generated.js.map +1 -1
  47. package/dist/core/session-zone.d.ts +1 -1
  48. package/dist/core/session-zone.js +1 -1
  49. package/dist/core/workspace-context.d.ts +1 -1
  50. package/dist/core/workspace.d.ts +1 -1
  51. package/dist/core/workspace.js +1 -1
  52. package/dist/core/zones.d.ts +4 -4
  53. package/dist/core/zones.js +5 -5
  54. package/dist/core/zones.js.map +1 -1
  55. package/dist/env.d.ts +9 -9
  56. package/dist/index.d.ts +3 -7
  57. package/dist/index.js +2 -4
  58. package/dist/index.js.map +1 -1
  59. package/dist/kernel/kernel.d.ts +31 -61
  60. package/dist/kernel/kernel.js +54 -112
  61. package/dist/kernel/kernel.js.map +1 -1
  62. package/dist/machine/lint-spec.js +53 -12
  63. package/dist/machine/lint-spec.js.map +1 -1
  64. package/dist/machine/machine.d.ts +36 -18
  65. package/dist/machine/machine.js +62 -51
  66. package/dist/machine/machine.js.map +1 -1
  67. package/dist/machine/spec-schema.d.ts +32 -180
  68. package/dist/machine/spec-schema.js +52 -113
  69. package/dist/machine/spec-schema.js.map +1 -1
  70. package/dist/machine/tool-names.d.ts +34 -16
  71. package/dist/machine/tool-names.js +41 -16
  72. package/dist/machine/tool-names.js.map +1 -1
  73. package/dist/machine/types.d.ts +14 -46
  74. package/dist/machine/types.js +0 -2
  75. package/dist/machine/types.js.map +1 -1
  76. package/dist/machine/validate-spec.d.ts +1 -1
  77. package/dist/public/sandbox.d.ts +59 -95
  78. package/dist/public/sandbox.js +22 -23
  79. package/dist/public/sandbox.js.map +1 -1
  80. package/dist/public/spec.d.ts +13 -20
  81. package/dist/public/spec.js +12 -20
  82. package/dist/public/spec.js.map +1 -1
  83. package/dist/runtime/contract.d.ts +11 -12
  84. package/dist/runtime/contract.js +16 -16
  85. package/dist/runtime/contract.js.map +1 -1
  86. package/dist/sandbox/assets/parts/code-state.js +17 -0
  87. package/dist/sandbox/executor.d.ts +25 -7
  88. package/dist/sandbox/executor.js +56 -57
  89. package/dist/sandbox/executor.js.map +1 -1
  90. package/dist/sandbox/imports.d.ts +10 -10
  91. package/dist/sandbox/imports.js +11 -11
  92. package/dist/sandbox/imports.js.map +1 -1
  93. package/dist/sandbox/prelude.d.ts +5 -6
  94. package/dist/sandbox/prelude.js +38 -35
  95. package/dist/sandbox/prelude.js.map +1 -1
  96. package/dist/sandbox/ptc-gateway.d.ts +19 -18
  97. package/dist/sandbox/ptc-gateway.js +26 -22
  98. package/dist/sandbox/ptc-gateway.js.map +1 -1
  99. package/dist/sessions/resume.d.ts +2 -2
  100. package/dist/sessions/resume.js +1 -1
  101. package/dist/sessions/resume.js.map +1 -1
  102. package/dist/sessions/scope.d.ts +0 -10
  103. package/dist/sessions/scope.js +0 -17
  104. package/dist/sessions/scope.js.map +1 -1
  105. package/dist/sessions/summary.d.ts +1 -1
  106. package/dist/testing/assertions.js +9 -4
  107. package/dist/testing/assertions.js.map +1 -1
  108. package/dist/testing/driver.js +3 -0
  109. package/dist/testing/driver.js.map +1 -1
  110. package/dist/testing/interpreter.d.ts +2 -2
  111. package/dist/testing/runner.d.ts +2 -2
  112. package/dist/validate/validate.d.ts +1 -1
  113. package/dist/validate/validate.js +39 -70
  114. package/dist/validate/validate.js.map +1 -1
  115. package/dist/workflow/automated-controls.d.ts +63 -0
  116. package/dist/workflow/automated-controls.js +158 -0
  117. package/dist/workflow/automated-controls.js.map +1 -0
  118. package/dist/workflow/automated.d.ts +112 -0
  119. package/dist/workflow/automated.js +163 -0
  120. package/dist/workflow/automated.js.map +1 -0
  121. package/dist/workflow/code-state.d.ts +46 -0
  122. package/dist/workflow/code-state.js +26 -0
  123. package/dist/workflow/code-state.js.map +1 -0
  124. package/dist/workflow/control-tools.d.ts +54 -23
  125. package/dist/workflow/control-tools.js +99 -79
  126. package/dist/workflow/control-tools.js.map +1 -1
  127. package/dist/workflow/file-values.d.ts +4 -4
  128. package/dist/workflow/file-values.js +1 -1
  129. package/dist/workflow/file-values.js.map +1 -1
  130. package/dist/workflow/governance.d.ts +11 -22
  131. package/dist/workflow/governance.js +47 -104
  132. package/dist/workflow/governance.js.map +1 -1
  133. package/dist/workflow/grader-state.d.ts +55 -0
  134. package/dist/workflow/grader-state.js +182 -0
  135. package/dist/workflow/grader-state.js.map +1 -0
  136. package/dist/workflow/history.d.ts +11 -0
  137. package/dist/{lifecycle/transcript.js → workflow/history.js} +24 -8
  138. package/dist/workflow/history.js.map +1 -0
  139. package/dist/workflow/middleware.d.ts +14 -17
  140. package/dist/workflow/middleware.js +109 -95
  141. package/dist/workflow/middleware.js.map +1 -1
  142. package/dist/workflow/move-events.d.ts +21 -0
  143. package/dist/workflow/move-events.js +20 -0
  144. package/dist/workflow/move-events.js.map +1 -0
  145. package/dist/workflow/on-error.js +0 -2
  146. package/dist/workflow/on-error.js.map +1 -1
  147. package/dist/workflow/parks.d.ts +9 -2
  148. package/dist/workflow/parks.js +16 -10
  149. package/dist/workflow/parks.js.map +1 -1
  150. package/dist/workflow/paths.d.ts +9 -9
  151. package/dist/workflow/paths.js +19 -19
  152. package/dist/workflow/paths.js.map +1 -1
  153. package/dist/workflow/render-prompt.d.ts +7 -5
  154. package/dist/workflow/render-prompt.js +22 -37
  155. package/dist/workflow/render-prompt.js.map +1 -1
  156. package/dist/workflow/signature-checks.d.ts +2 -2
  157. package/dist/workflow/snapshot.js +5 -29
  158. package/dist/workflow/snapshot.js.map +1 -1
  159. package/dist/workflow/state.d.ts +14 -42
  160. package/dist/workflow/state.js +20 -33
  161. package/dist/workflow/state.js.map +1 -1
  162. package/dist/workflow/sub-workflow.d.ts +3 -3
  163. package/dist/workflow/sub-workflow.js +3 -7
  164. package/dist/workflow/sub-workflow.js.map +1 -1
  165. package/dist/workflow/tool-service.d.ts +1 -9
  166. package/dist/workflow/tool-service.js +21 -24
  167. package/dist/workflow/tool-service.js.map +1 -1
  168. package/dist/workflow/turn-boundary.d.ts +2 -2
  169. package/dist/workflow/turn-boundary.js +10 -15
  170. package/dist/workflow/turn-boundary.js.map +1 -1
  171. package/dist/workflow/turn-runner.js +5 -6
  172. package/dist/workflow/turn-runner.js.map +1 -1
  173. package/package.json +1 -1
  174. package/skills/archmax-harness/README.md +5 -5
  175. package/skills/archmax-harness/SKILL.md +72 -30
  176. package/skills/archmax-harness/references/backend-integration.md +33 -24
  177. package/skills/archmax-harness/references/cases.md +41 -8
  178. package/skills/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +599 -339
  179. package/skills/archmax-harness/references/code-states-and-scripts.md +115 -0
  180. package/skills/archmax-harness/references/diagnostics.md +40 -7
  181. package/skills/archmax-harness/references/wiring.md +19 -10
  182. package/skills/archmax-harness/references/workflow-schema.md +404 -293
  183. package/skills/archmax-harness/references/workflow-yaml.md +151 -62
  184. package/dist/authoring-skill/archmax-harness/references/hooks-and-scripts.md +0 -89
  185. package/dist/lifecycle/hook-shape.d.ts +0 -44
  186. package/dist/lifecycle/hook-shape.js +0 -66
  187. package/dist/lifecycle/hook-shape.js.map +0 -1
  188. package/dist/lifecycle/runner.d.ts +0 -197
  189. package/dist/lifecycle/runner.js +0 -419
  190. package/dist/lifecycle/runner.js.map +0 -1
  191. package/dist/lifecycle/tool-calls.d.ts +0 -9
  192. package/dist/lifecycle/tool-calls.js +0 -57
  193. package/dist/lifecycle/tool-calls.js.map +0 -1
  194. package/dist/lifecycle/transcript.d.ts +0 -31
  195. package/dist/lifecycle/transcript.js.map +0 -1
  196. package/dist/rubrics/rubrics.d.ts +0 -135
  197. package/dist/rubrics/rubrics.js +0 -145
  198. package/dist/rubrics/rubrics.js.map +0 -1
  199. package/dist/sandbox/assets/parts/core.js +0 -22
  200. package/dist/sandbox/assets/parts/lifecycle-hook.js +0 -47
  201. package/dist/sandbox/bridge.d.ts +0 -16
  202. package/dist/sandbox/bridge.js +0 -95
  203. package/dist/sandbox/bridge.js.map +0 -1
  204. package/dist/workflow/visits.d.ts +0 -21
  205. package/dist/workflow/visits.js +0 -45
  206. package/dist/workflow/visits.js.map +0 -1
  207. package/skills/archmax-harness/references/hooks-and-scripts.md +0 -89
@@ -93,9 +93,10 @@ All optional (`CreateAgentParams`, `src/assembly/index.ts`):
93
93
  | `rootDir` | `string` | Workspace root for the default filesystem backend. Defaults to cwd. |
94
94
  | `workflow` | `string` | Named workflow under `workflows/<name>/`. |
95
95
  | `trigger` | `TriggerInput` | `{ id, args? }`; defaults to `{ id: "manual" }`. Selects the start state. |
96
- | `model` | `BaseChatModel` | Defaults to the env-configured OpenAI-compatible model. Inject your own to bypass env. |
96
+ | `model` | `BaseChatModel` | Defaults to the env-configured OpenAI-compatible model. Inject your own to bypass env. An explicit instance outranks every model id a spec declares, grader states' included. |
97
+ | `modelFactory` | `(role, env, requested?) => BaseChatModel` | Supplies the chat model per role: `agent` (the session's turns), `judge` (the case judge for `grade:`) and `grader` (a grader state's subagent). `requested` is the id the state or `settings.model` declares, for the `agent` and `grader` roles; a grader state's model is `modelFactory("grader", env, id)`, resolved once per composition. `env` is a thunk, so a factory with its own credentials never loads `.env`. |
97
98
  | `backend` | `BackendProtocolV2` | Serves the authored workspace. Defaults to a `FilesystemBackend` over `rootDir`. |
98
- | `authoring` | `BackendProtocolV2` | Serves the **authoring plane** — `workflows/**` (machine specs including the rubrics their states declare, `WORKFLOW.md`, `hooks/`, `tests/`) — to the runtime only. Never a route in the agent's workspace, so no tool call or PTC call can reach it. Defaults to `backend` when you supply one, else a `FilesystemBackend` over the resolved root. Handing it to a **writable** mount throws `AuthoringBackendExposedError`; a read-only mount may share it. |
99
+ | `authoring` | `BackendProtocolV2` | Serves the **authoring plane** — `workflows/**` (machine specs, `WORKFLOW.md`, code-state `scripts/`, `tests/`) — to the runtime only. Never a route in the agent's workspace, so no tool call or PTC call can reach it. Defaults to `backend` when you supply one, else a `FilesystemBackend` over the resolved root. Handing it to a **writable** mount throws `AuthoringBackendExposedError`; a read-only mount may share it. |
99
100
  | `mounts` | `Record<string, MountSpec>` | Authored mounts as a `CompositeBackend` route table: key → backend, or `{ backend, readOnly, governed, searchable }`. Trailing slash = directory mount; no slash = exact-path file mount (`"/AGENTS.md"`). Read-only unless declared otherwise. `governed: true` hands the spec's `mounts` block the decision of **which states** may reach it (closed by default, like a skill bundle) and whether each may write there; a mount not so declared is visible in every state exactly as today, so a table that marks nothing behaves unchanged. `readOnly` is a **ceiling** the spec cannot lift: a governed writable mount is written only by a workflow whose own grants ask `access: read_write` (a bare grant reads it, so serving one route writable for one workflow in a delegation tree opens it to no other), and a grant may narrow a writable mount to reads in a state, never open a read-only one. A key may be several segments deep (`"/catalogs/eu/"`); its **first** segment is what is reserved, so `/workflows/x/` fails as `/workflows/` does. Omitted on the default backend: `defaultMounts(rootDir)` (the conventional table — `/skills/`, `/.platform/`, `/AGENTS.md`, none governed — extend with `{ ...defaultMounts(root), "/templates/": … }`). Omitted with a custom `backend`: nothing authored is served; expose subtrees with `mountSubtree(backend, "skills")`. A key colliding with a run area — or naming the authoring-plane prefix (`/workflows/`) — throws `MountCollisionError`. `searchable: false` declares a mount the runtime never searches on its own initiative: a root-wide `grep`/`glob` fans out over the other mounts only, while a search the agent addresses at the mount (or a path inside it) still reaches its backend and returns the backend's own answer — matches or `{ error }` — verbatim. Use it for a backend that must refuse searches (a remote folder served live). Listing and reading are unchanged, the prompt marks it "browse only", names arrive as `MountPrefixes.unsearchable`, and it is ignored on a file mount. |
100
101
  | `sessionStore` | `SessionStore` | Physical storage for run state (checkpoints, artifacts, the per-session `scratchpad/`). Zero-config default: `createFilesystemSessionStore({ dir: "<rootDir>/sessions" })`. Required with a custom `backend` (else `SessionStoreRequiredError`). Build with `createBackendSessionStore({ backend, prefix? })` for S3-style storage or `createMemorySessionStore()` for ephemeral runs. |
101
102
  | `systemPrompt` | `string` | Appended after workspace `AGENTS.md`. |
@@ -145,7 +146,7 @@ const sessionId = `run-${runId}`;
145
146
  try {
146
147
  const result = await agent.invoke(
147
148
  { messages: [{ role: "user", content: prompt }] },
148
- { configurable: { thread_id: sessionId }, recursionLimit: 50 },
149
+ { configurable: { thread_id: sessionId } },
149
150
  );
150
151
  // result: { messages, status?, workflowState?, pendingDecision? }
151
152
  if (result.status === WORKFLOW_STATUSES.awaitingDecision) {
@@ -158,6 +159,12 @@ try {
158
159
  }
159
160
  ```
160
161
 
162
+ A turn runs under LangGraph's `recursionLimit` — 400 unless the config passes its
163
+ own, which wins. It is the one bound on a whole turn (every model call, tool batch
164
+ and automated state is a step; a state's `budget.maxTurns`/`maxParks` bound that
165
+ state); a turn that hits it ends the session `failed` with exit code
166
+ `recursion-limit`.
167
+
161
168
  To cancel a turn, pass an `AbortSignal` as `signal` in the same config (or in
162
169
  `agent.workflow.send(sessionId, input, { signal })`) and abort it. The turn's
163
170
  scripts stop with it, in the session and its child sessions: a pending `tools.*`
@@ -187,12 +194,12 @@ checkpoint:
187
194
  // turn 1
188
195
  await agent.invoke(
189
196
  { messages: [{ role: "user", content: "Look up order 1234" }] },
190
- { configurable: { thread_id: "chat-42" }, recursionLimit: 50 },
197
+ { configurable: { thread_id: "chat-42" } },
191
198
  );
192
199
  // turn 2 — SAME thread_id; only the new message
193
200
  await agent.invoke(
194
201
  { messages: [{ role: "user", content: "And when did it ship?" }] },
195
- { configurable: { thread_id: "chat-42" }, recursionLimit: 50 },
202
+ { configurable: { thread_id: "chat-42" } },
196
203
  );
197
204
  ```
198
205
 
@@ -215,19 +222,20 @@ await agent.invoke(
215
222
  { role: "user", content: "what was my first message?" }, // the new turn
216
223
  ],
217
224
  },
218
- { configurable: { thread_id: "chat-42" }, recursionLimit: 50 },
225
+ { configurable: { thread_id: "chat-42" } },
219
226
  );
220
227
  ```
221
228
 
222
229
  Gotchas:
223
230
 
224
- - **A hook reads its visit from `messages`, newest last** — the conversation since
225
- the session entered the state it is in, which can span turns. A hook that keys
226
- off "the current request" should take the *last* `user` entry; an earlier
227
- state's messages are not in it, so what a hook needs from there is stored (a
228
- variable, a file) or read with `toolCalls`. (A grading rubric receives the
229
- compact `userRequest`/`history` payload its rubric is written against: `history`
230
- is the same visit, `userRequest` the person's *latest* message.)
231
+ - **An automated state reads the whole session.** A code state and a grader
232
+ both call `archmax_get_history` for `{ messages, trail }` (a script:
233
+ `JSON.parse(await tools.archmax_get_history())`). Its `messages` are the whole
234
+ transcript, oldest first, which can span turns: a script that keys off "the
235
+ current request" should take the *last* `user` entry, not the first — the
236
+ first is the message that opened the session. A tool entry carries its tool,
237
+ the call `id` and the result as its `text`. Neither's own calls or messages
238
+ enter the session's transcript.
231
239
  - **A definition edit is adopted, not refused.** A session takes its next turn
232
240
  under the `workflow.yaml` in force now; the recorded `specHash` says which
233
241
  definition each turn ran under but gates nothing. A turn positioned in a state
@@ -236,7 +244,7 @@ Gotchas:
236
244
  in seeded history, or the provider may reject the turn.
237
245
  - **Not every message in a session's transcript is a person's.** The runtime
238
246
  writes **runtime notes** of its own — `[decision]`, `[event]`, `[error]`,
239
- `[after]`, `[sub-workflow: …]` — carried as a synthetic `archmax_note` tool call
247
+ `[grader]`, `[code]`, `[sub-workflow: …]` — carried as a synthetic `archmax_note` tool call
240
248
  and its result, so a model cannot read them as a person speaking. Ask
241
249
  `isRuntimeNote(message)` / `runtimeNoteKind(message)` rather than inferring
242
250
  authorship from role or a `[bracket]` prefix: a turn's lower bound is a human
@@ -249,7 +257,7 @@ Gotchas:
249
257
  import { isRuntimeNote, runtimeNoteKind } from "@archmax-ai/harness";
250
258
 
251
259
  const opensTurn = (m: unknown) => isRuntimeNote(m) || m.role === "user";
252
- const label = (m: unknown) => runtimeNoteKind(m); // "decision" | "event" | … | null
260
+ const label = (m: unknown) => runtimeNoteKind(m); // "decision" | "event" | "grader" | "code" | … | null
253
261
  ```
254
262
 
255
263
  ## Human-in-the-loop
@@ -264,6 +272,7 @@ if (result.status === WORKFLOW_STATUSES.awaitingDecision && result.pendingDecisi
264
272
  // When they pick:
265
273
  const outcome = await agent.workflow.decide(sessionId, { target: chosenTo, comment });
266
274
  // DecideOutcome: { status?, workflowState?, reparked, state?, reply, rejected?, exit?, messages }
275
+ // `reply` is every assistant text of the turn, in order ("" when it said nothing).
267
276
  // `rejected` says why when status is "rejected"; Outcome from send() carries it too.
268
277
  // `exit` is { success: false, code, reason } when the agent ended the run with
269
278
  // archmax_raise (status "failed"), { success: true } when it completed.
@@ -425,17 +434,17 @@ Pass `onEvent`; every `WorkflowLifecycleEvent` carries a `level`
425
434
  | --- | --- |
426
435
  | `state-enter` / `state-leave` | state entered / left (`{ state, next? }`) |
427
436
  | `advance` | transition committed (`{ from, to, reason?, callId? }` — `callId` names the `archmax_advance` call that drove it; absent on the turn's opening arrival) |
428
- | `hook-start` / `hook-output` / `hook-passed` / `hook-verdict` / `hook-rejected` | lifecycle hook activity + verdict (`ok`/`correct`/`veto`) |
437
+ | `code-start` / `code-output` / `code-result` | a code state's run: `{ state, script }`, one `{ state, line }` per `console.log` line, then `{ state, status: "ok" \| "error", durationMs, reason? }` |
438
+ | `grader-start` / `grader-result` | a grader state's run: `{ state }`, then `{ state, status, durationMs, reason? }`. `status` is `ok` when the run settled on a decision or a completion; a move then emits `advance`/`state-leave`/`state-enter` (a code state's tool calls carry `origin: "script"`, a grader's none) |
429
439
  | `parked` / `decided` | human state parked / resolved (`{ state, sessionId, awaiting, to? }` — a human park carries no `callId`, since no call asked for it) |
430
440
  | `parked` (`awaiting: "input"`) / `delivered` | the agent parked the run (`{ state, sessionId, reason, resumeAt?, callId }` — the `archmax_wait` call) / an event resumed it (`{ state, trigger, to }`, where `to` is the same state) |
431
441
  | `raised` | the agent ended the session as a failure (`{ state, sessionId, code, reason, callId }` — the `archmax_raise` call); a `state-leave` follows, and the outcome is `failed` |
432
442
  | `agent-text` | complete assistant message in a state (`{ state, text, messageId }` — always the id the message is stored under; absent only on a `partial: true` event from a failed turn) |
433
443
  | `agent-text-delta` | streaming text chunk (`{ state, text, messageId? }`) — flows for every run, even when the graph is driven with `invoke`; a sub-workflow child's chunks carry the child's `sessionId` and `subWorkflowDispatchId`, never the caller's state |
434
444
  | `tool-called` / `tool-result` / `tool-blocked` | tool passed governance (`callId`, `args`) / settled (`callId`, `status`, `durationMs`, `output` preview) / denied. **Every** governed call, `archmax_advance` included — so a UI drawing a row per call draws one per transition; filter it if you already draw transitions from `advance`/`state-leave`. A tool that **throws** settles `status: "error"` with the error's message as `output` while the turn **continues**: the model reads the same message as the call's answer, and nothing is thrown out of `send` — read tool failures here |
435
- | `rubric-start` / `rubric-result` | grading-rubric dispatch bracketed by `dispatchId`, with the rubric's `name` |
436
- | `model-usage` | per model call provider token usage: `inputTokens` (the total, of which the cache counts are a breakdown), `outputTokens`, `cacheReadTokens`, `cacheCreationTokens`, `model?` (the id the call was priced against), `costUsd?` |
445
+ | `model-usage` | per model call — the agent's or a grader's, under its state — provider token usage: `inputTokens` (the total, of which the cache counts are a breakdown), `outputTokens`, `cacheReadTokens`, `cacheCreationTokens`, `model?` (the id the call was priced against), `costUsd?` |
437
446
  | `prompt-shaping` | assembly-time payload summary: resolved `profile`, prompt-cache mechanism, `withheld` built-ins |
438
- | `skills-loaded`, `hooks-summary`, `graph-topology`, `workflow-reset`, `warning` | assembly-time diagnostics |
447
+ | `skills-loaded`, `graph-topology`, `workflow-reset`, `warning` | assembly-time diagnostics |
439
448
 
440
449
  For a browser client, forward these over SSE/WebSocket keyed by `sessionId`. To
441
450
  render a live chat transcript: accumulate `agent-text-delta` into a bubble
@@ -508,7 +517,7 @@ gives the same figure live in process.
508
517
  ### Measuring anything else — fold the stream
509
518
 
510
519
  The SDK keeps no metrics ledger. Tool calls, governance refusals, the path a run
511
- took, corrections and vetoes, rubric dispatches, and how long a session
520
+ took, every send-back by a grader or code state, and how long a session
512
521
  sat waiting for a person are all already on the typed event stream, and a backend
513
522
  counts what it needs:
514
523
 
@@ -581,8 +590,8 @@ interface RunMetadata { sessionId: string; workflow: string;
581
590
  runtimeContract: ResolvedRuntimeContract; packageVersion: string; }
582
591
  // trail.json: { sessionId, workflow, steps: TrailStep[] } — the audit trail,
583
592
  // re-derived from checkpointed state on every artifact write.
584
- // TrailStep: { to, kind: "trigger"|"agent"|"human"|"reset"|"on_error",
585
- // reason?, ts }
593
+ // TrailStep: { to, kind: "trigger"|"agent"|"human"|"grader"|"code"|
594
+ // "sub-workflow"|"reset"|"on_error", reason?, workflow?, status?, ts }
586
595
  ```
587
596
 
588
597
  An **API** reads these files (through the same session store, or via `runArtifactPaths(sessionId)`
@@ -591,7 +600,7 @@ path helpers) to render historical runs: `graph.json`/`graph.mmd` for the diagra
591
600
  path (which edges committed, by whom, and why), `variables.json` for the run's
592
601
  inputs and established facts (also what a test author reads to reproduce a real
593
602
  firing's payload — see
594
- [hook-and-test-scripts.md](hook-and-test-scripts.md#recovering-a-cases-inputs-from-previous-runs)).
603
+ [code-state-and-test-scripts.md](code-state-and-test-scripts.md#recovering-a-cases-inputs-from-previous-runs)).
595
604
  Note it holds whatever the run's variables held, verbatim: a host that must not
596
605
  persist an input should not seed it. To persist artifacts yourself,
597
606
  stream the run and call `agent.emitRunArtifacts(sessionId, trajectory)`.
@@ -752,7 +761,7 @@ workflow-governed target.
752
761
  an `archmax_workflow_<slug>` tool), the child runtime is
753
762
  composed from the *same* resolved inputs as the parent — model and
754
763
  `modelFactory`, `backend` and `mounts`, `sessionStore` and `checkpointer`, host
755
- `tools` and `essentialTools`, `sandboxRuntime`, `hookExecutors`,
764
+ `tools` and `essentialTools`, `sandboxRuntime`,
756
765
  `policyRules`, `promptCache` and `pricing` — differing only in
757
766
  the machine that governs it and the system prompt that machine renders. So a
758
767
  custom backend serves the child's spec too, and a `modelFactory` supplies its
@@ -2,7 +2,7 @@
2
2
 
3
3
  Load this when writing or fixing a case. Worked examples for every shape
4
4
  (mocks, human decisions, triggered starts, fixtures):
5
- [`hook-and-test-scripts.md`](hook-and-test-scripts.md) Part 2.
5
+ [`code-state-and-test-scripts.md`](code-state-and-test-scripts.md) Part 2.
6
6
 
7
7
  ## Keys, actions, assertions
8
8
 
@@ -18,7 +18,7 @@ it). Host-interpreted; no sandbox runs a case. **Unknown keys are errors.**
18
18
  | `trigger` | `{ id }` only — the trigger every turn runs under (default `manual`); unknown id fails the case |
19
19
  | `variables` | `{ name: value }` seeded and **locked** every turn — the firing's payload; never seed what the agent must establish (assert it with `variables:` instead) |
20
20
  | `workspace` | `{ <run path>: content }` — strings verbatim, other YAML → JSON, `{ from: <tests-relative path> }` copies a file; only run-zone paths (`trigger.json`, `scratchpad/…`) |
21
- | `mocks` | `[{ tool, whenInput?, result }]` — intercepts agent and script calls alike (partial-input match) |
21
+ | `mocks` | `[{ tool, whenInput?, result }]` — intercepts the agent's, its scripts', code states' and graders' calls alike (partial-input match); a control tool (`archmax_*`) is never mocked |
22
22
  | `clock` | ISO 8601 date (`2026-03-20`) or date-time with `Z`/offset — the "now" every model call of the case states; pin it whenever fixtures or prompts carry dates. Prompt only: scripts, the sandbox and recorded timestamps stay real |
23
23
  | `steps` | one flat list; actions and assertions are peers; an assertion evaluates against the nearest action above it |
24
24
 
@@ -27,17 +27,50 @@ message that leaves it parked), `decide: { to, comment? }`,
27
27
  `deliver: { trigger, variables? }`.
28
28
 
29
29
  Assertions (**structural** ones halt the case when they miss; later steps are
30
- reported `not-executed`): `succeeded: true`ˢ (fails on a raise too) · `parked: true | decision | input | { channel?, state? }`ˢ ·
30
+ reported `not-executed`): `succeeded: true`ˢ (the action's outcome is `completed` — not parked, raised, failed or rejected; a failed or blocked tool call the turn recovered from does not fail it, and a miss says why) · `parked: true | decision | input | { channel?, state? }`ˢ ·
31
31
  `raised: true | <code> | { code?, reason? }`ˢ (the agent ended the turn with `archmax_raise`; code exact, reason exact or `"/regex/flags"`) ·
32
- `reachedState: <slug>`ˢ (a committed non-trigger trail step) ·
33
- `trail: { to?, kind?, reason?, count }`ˢ (`kind`: trigger/agent/human/on_error) ·
34
- `noTraversal: true`ˢ · `triggerArrival: <id>`ˢ ·
32
+ `reachedState: <slug>`ˢ (a committed non-trigger trail step, an automated state's move included) ·
33
+ `trail: { to?, kind?, reason?, count }`ˢ (`kind`: trigger/agent/human/grader/code/sub-workflow/reset/on_error; `count: 0` = never entered) ·
34
+ `noTraversal: true`ˢ (nothing moved; a code or grader move is a traversal) · `triggerArrival: <id>`ˢ ·
35
35
  `reply: { includes?, excludes? }` (token or list; `"/regex/flags"` strings are regexes; matched against every assistant message of the session so far, earlier turns included) ·
36
- `calledTool` / `notCalledTool` / `blockedTool: { name, input? }` (partial input; `blockedTool` = governance refused it — the assertion that tests a guard) ·
37
- `ranWorkflow: { workflow, status?, count? }` · `usedNoTools: true` (whole conversation) ·
36
+ `calledTool` / `notCalledTool` / `blockedTool: { name, input? }` (partial input; `blockedTool` = governance refused it — the assertion that tests a guard; the agent's calls only) ·
37
+ `ranWorkflow: { workflow, status?, count? }` (whoever dispatched) · `usedNoTools: true` (whole conversation, the agent's calls only) ·
38
38
  `variables: { expect, path?, locked? }` · `grade: { closedQA, atLeast }` (LLM grades what the action's turn said plus the session's tool record so far; `atLeast` is a hard bar; needs `tests.judge`).
39
39
 
40
40
  Put the structural assertion right after the action that should satisfy it,
41
41
  before content assertions that only make sense in that state:
42
42
 
43
43
  ```yaml
44
+ steps:
45
+ - send: "I want a refund for ORD-1001"
46
+ - reachedState: refund-review # structural: if this misses, the case stops here
47
+ - parked: true
48
+ - reply:
49
+ includes: "approved"
50
+ - decide: { to: refund-closed }
51
+ - succeeded: true
52
+ ```
53
+
54
+ **Grader and code states are asserted by what they committed.** Their own tool
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`:
58
+
59
+ ```yaml
60
+ steps:
61
+ - send: "Hi, I'm support@acmecorp.com. Which orders are delayed?"
62
+ - trail: { kind: code, to: identify-case, count: 1 } # the entry screen passed it
63
+ ```
64
+
65
+ ```yaml
66
+ trigger: { id: enrichment_requested } # the enrichment path
67
+ variables: { orders_to_enrich: [{ order_id: "ORD-1002" }, { order_id: "ORD-1003" }] }
68
+ steps:
69
+ - send: "Enrich the requested orders."
70
+ - trail: { kind: grader, to: enrich-order, count: 0 } # no send-back
71
+ - trail: { kind: grader, to: enrichment-reported, count: 1 } # the grader approved
72
+ ```
73
+
74
+ A case that declines at entry asserts `reachedState: <declining state>` plus
75
+ `trail: { to: <first work state>, count: 0 }`; one whose entry code state raised
76
+ asserts `raised: <code>` and `noTraversal: true`.