@archmax-ai/harness 0.8.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 (219) hide show
  1. package/README.md +58 -14
  2. package/dist/agent.d.ts +4 -4
  3. package/dist/agent.js +4 -4
  4. package/dist/assembly/compose.d.ts +5 -24
  5. package/dist/assembly/compose.js +40 -41
  6. package/dist/assembly/compose.js.map +1 -1
  7. package/dist/assembly/delegation-registry.js +3 -14
  8. package/dist/assembly/delegation-registry.js.map +1 -1
  9. package/dist/assembly/index.d.ts +18 -15
  10. package/dist/assembly/index.js +50 -32
  11. package/dist/assembly/index.js.map +1 -1
  12. package/dist/assembly/plain.js +1 -2
  13. package/dist/assembly/plain.js.map +1 -1
  14. package/dist/authoring-skill/archmax-harness/README.md +5 -5
  15. package/dist/authoring-skill/archmax-harness/SKILL.md +72 -30
  16. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +35 -23
  17. package/dist/authoring-skill/archmax-harness/references/cases.md +44 -10
  18. package/dist/authoring-skill/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +617 -313
  19. package/dist/authoring-skill/archmax-harness/references/code-states-and-scripts.md +115 -0
  20. package/dist/authoring-skill/archmax-harness/references/diagnostics.md +40 -7
  21. package/dist/authoring-skill/archmax-harness/references/wiring.md +19 -10
  22. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +416 -276
  23. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +161 -62
  24. package/dist/cli/state-flow.d.ts +1 -1
  25. package/dist/cli/state-flow.js +18 -28
  26. package/dist/cli/state-flow.js.map +1 -1
  27. package/dist/cli/turn.d.ts +1 -1
  28. package/dist/cli/turn.js +2 -3
  29. package/dist/cli/turn.js.map +1 -1
  30. package/dist/core/clock.d.ts +26 -0
  31. package/dist/core/clock.js +64 -0
  32. package/dist/core/clock.js.map +1 -0
  33. package/dist/core/code-description.d.ts +3 -3
  34. package/dist/core/code-description.js +3 -3
  35. package/dist/core/deepagents.d.ts +5 -5
  36. package/dist/core/deepagents.js +4 -4
  37. package/dist/core/events.d.ts +23 -42
  38. package/dist/core/events.js +21 -24
  39. package/dist/core/events.js.map +1 -1
  40. package/dist/core/message-readers.d.ts +22 -2
  41. package/dist/core/message-readers.js +31 -0
  42. package/dist/core/message-readers.js.map +1 -1
  43. package/dist/core/messages.d.ts +1 -23
  44. package/dist/core/messages.js +2 -35
  45. package/dist/core/messages.js.map +1 -1
  46. package/dist/core/mounts.js +3 -3
  47. package/dist/core/platform-prompt.generated.js +1 -1
  48. package/dist/core/platform-prompt.generated.js.map +1 -1
  49. package/dist/core/session-zone.d.ts +1 -1
  50. package/dist/core/session-zone.js +1 -1
  51. package/dist/core/workspace-context.d.ts +1 -1
  52. package/dist/core/workspace.d.ts +1 -1
  53. package/dist/core/workspace.js +1 -1
  54. package/dist/core/zones.d.ts +4 -4
  55. package/dist/core/zones.js +5 -5
  56. package/dist/core/zones.js.map +1 -1
  57. package/dist/env.d.ts +9 -9
  58. package/dist/index.d.ts +3 -7
  59. package/dist/index.js +2 -4
  60. package/dist/index.js.map +1 -1
  61. package/dist/kernel/kernel.d.ts +31 -61
  62. package/dist/kernel/kernel.js +57 -114
  63. package/dist/kernel/kernel.js.map +1 -1
  64. package/dist/machine/allow.d.ts +17 -2
  65. package/dist/machine/allow.js +16 -6
  66. package/dist/machine/allow.js.map +1 -1
  67. package/dist/machine/lint-spec.js +68 -12
  68. package/dist/machine/lint-spec.js.map +1 -1
  69. package/dist/machine/machine.d.ts +39 -16
  70. package/dist/machine/machine.js +72 -49
  71. package/dist/machine/machine.js.map +1 -1
  72. package/dist/machine/spec-schema.d.ts +34 -179
  73. package/dist/machine/spec-schema.js +93 -106
  74. package/dist/machine/spec-schema.js.map +1 -1
  75. package/dist/machine/tool-names.d.ts +34 -10
  76. package/dist/machine/tool-names.js +41 -10
  77. package/dist/machine/tool-names.js.map +1 -1
  78. package/dist/machine/tool-paths.d.ts +6 -0
  79. package/dist/machine/tool-paths.js +10 -0
  80. package/dist/machine/tool-paths.js.map +1 -1
  81. package/dist/machine/types.d.ts +14 -38
  82. package/dist/machine/types.js.map +1 -1
  83. package/dist/machine/validate-spec.d.ts +1 -1
  84. package/dist/machine/variables.d.ts +2 -0
  85. package/dist/machine/variables.js.map +1 -1
  86. package/dist/public/sandbox.d.ts +62 -66
  87. package/dist/public/sandbox.js +22 -23
  88. package/dist/public/sandbox.js.map +1 -1
  89. package/dist/public/spec.d.ts +12 -19
  90. package/dist/public/spec.js +11 -19
  91. package/dist/public/spec.js.map +1 -1
  92. package/dist/runtime/contract.d.ts +11 -12
  93. package/dist/runtime/contract.js +16 -16
  94. package/dist/runtime/contract.js.map +1 -1
  95. package/dist/sandbox/assets/parts/code-state.js +17 -0
  96. package/dist/sandbox/executor.d.ts +25 -7
  97. package/dist/sandbox/executor.js +56 -47
  98. package/dist/sandbox/executor.js.map +1 -1
  99. package/dist/sandbox/imports.d.ts +10 -10
  100. package/dist/sandbox/imports.js +11 -11
  101. package/dist/sandbox/imports.js.map +1 -1
  102. package/dist/sandbox/prelude.d.ts +5 -6
  103. package/dist/sandbox/prelude.js +38 -35
  104. package/dist/sandbox/prelude.js.map +1 -1
  105. package/dist/sandbox/ptc-gateway.d.ts +19 -18
  106. package/dist/sandbox/ptc-gateway.js +26 -22
  107. package/dist/sandbox/ptc-gateway.js.map +1 -1
  108. package/dist/sessions/operations.js +4 -5
  109. package/dist/sessions/operations.js.map +1 -1
  110. package/dist/sessions/resume.d.ts +12 -7
  111. package/dist/sessions/resume.js +12 -9
  112. package/dist/sessions/resume.js.map +1 -1
  113. package/dist/sessions/summary.d.ts +1 -1
  114. package/dist/testing/assertions.js +9 -4
  115. package/dist/testing/assertions.js.map +1 -1
  116. package/dist/testing/case-document.d.ts +2 -0
  117. package/dist/testing/case-document.js.map +1 -1
  118. package/dist/testing/case-schema.d.ts +1 -1
  119. package/dist/testing/case-schema.js +17 -1
  120. package/dist/testing/case-schema.js.map +1 -1
  121. package/dist/testing/driver.d.ts +4 -0
  122. package/dist/testing/driver.js +22 -3
  123. package/dist/testing/driver.js.map +1 -1
  124. package/dist/testing/grade.js +3 -3
  125. package/dist/testing/grade.js.map +1 -1
  126. package/dist/testing/interpreter.d.ts +2 -2
  127. package/dist/testing/interpreter.js +1 -0
  128. package/dist/testing/interpreter.js.map +1 -1
  129. package/dist/testing/runner.d.ts +2 -2
  130. package/dist/validate/validate.d.ts +1 -1
  131. package/dist/validate/validate.js +39 -70
  132. package/dist/validate/validate.js.map +1 -1
  133. package/dist/workflow/automated-controls.d.ts +63 -0
  134. package/dist/workflow/automated-controls.js +158 -0
  135. package/dist/workflow/automated-controls.js.map +1 -0
  136. package/dist/workflow/automated.d.ts +112 -0
  137. package/dist/workflow/automated.js +163 -0
  138. package/dist/workflow/automated.js.map +1 -0
  139. package/dist/workflow/checkpointer.d.ts +7 -0
  140. package/dist/workflow/checkpointer.js +28 -0
  141. package/dist/workflow/checkpointer.js.map +1 -1
  142. package/dist/workflow/code-state.d.ts +46 -0
  143. package/dist/workflow/code-state.js +26 -0
  144. package/dist/workflow/code-state.js.map +1 -0
  145. package/dist/workflow/control-tools.d.ts +54 -23
  146. package/dist/workflow/control-tools.js +121 -77
  147. package/dist/workflow/control-tools.js.map +1 -1
  148. package/dist/workflow/file-values.d.ts +4 -4
  149. package/dist/workflow/file-values.js +1 -1
  150. package/dist/workflow/file-values.js.map +1 -1
  151. package/dist/workflow/governance.d.ts +13 -22
  152. package/dist/workflow/governance.js +51 -87
  153. package/dist/workflow/governance.js.map +1 -1
  154. package/dist/workflow/grader-state.d.ts +55 -0
  155. package/dist/workflow/grader-state.js +182 -0
  156. package/dist/workflow/grader-state.js.map +1 -0
  157. package/dist/workflow/history.d.ts +11 -0
  158. package/dist/workflow/history.js +64 -0
  159. package/dist/workflow/history.js.map +1 -0
  160. package/dist/workflow/middleware.d.ts +14 -17
  161. package/dist/workflow/middleware.js +106 -74
  162. package/dist/workflow/middleware.js.map +1 -1
  163. package/dist/workflow/move-events.d.ts +21 -0
  164. package/dist/workflow/move-events.js +20 -0
  165. package/dist/workflow/move-events.js.map +1 -0
  166. package/dist/workflow/parks.d.ts +9 -2
  167. package/dist/workflow/parks.js +16 -8
  168. package/dist/workflow/parks.js.map +1 -1
  169. package/dist/workflow/paths.d.ts +9 -9
  170. package/dist/workflow/paths.js +19 -19
  171. package/dist/workflow/paths.js.map +1 -1
  172. package/dist/workflow/render-prompt.d.ts +7 -5
  173. package/dist/workflow/render-prompt.js +22 -37
  174. package/dist/workflow/render-prompt.js.map +1 -1
  175. package/dist/workflow/signature-checks.d.ts +2 -2
  176. package/dist/workflow/snapshot.js +5 -29
  177. package/dist/workflow/snapshot.js.map +1 -1
  178. package/dist/workflow/state.d.ts +19 -6
  179. package/dist/workflow/state.js +24 -8
  180. package/dist/workflow/state.js.map +1 -1
  181. package/dist/workflow/sub-workflow.d.ts +3 -3
  182. package/dist/workflow/tool-service.d.ts +1 -9
  183. package/dist/workflow/tool-service.js +21 -24
  184. package/dist/workflow/tool-service.js.map +1 -1
  185. package/dist/workflow/turn-boundary.d.ts +2 -2
  186. package/dist/workflow/turn-boundary.js +10 -7
  187. package/dist/workflow/turn-boundary.js.map +1 -1
  188. package/dist/workflow/turn-runner.js +30 -2
  189. package/dist/workflow/turn-runner.js.map +1 -1
  190. package/package.json +1 -1
  191. package/skills/archmax-harness/README.md +5 -5
  192. package/skills/archmax-harness/SKILL.md +72 -30
  193. package/skills/archmax-harness/references/backend-integration.md +35 -23
  194. package/skills/archmax-harness/references/cases.md +44 -10
  195. package/skills/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +617 -313
  196. package/skills/archmax-harness/references/code-states-and-scripts.md +115 -0
  197. package/skills/archmax-harness/references/diagnostics.md +40 -7
  198. package/skills/archmax-harness/references/wiring.md +19 -10
  199. package/skills/archmax-harness/references/workflow-schema.md +416 -276
  200. package/skills/archmax-harness/references/workflow-yaml.md +161 -62
  201. package/dist/authoring-skill/archmax-harness/references/hooks-and-scripts.md +0 -88
  202. package/dist/lifecycle/hook-shape.d.ts +0 -44
  203. package/dist/lifecycle/hook-shape.js +0 -66
  204. package/dist/lifecycle/hook-shape.js.map +0 -1
  205. package/dist/lifecycle/runner.d.ts +0 -183
  206. package/dist/lifecycle/runner.js +0 -373
  207. package/dist/lifecycle/runner.js.map +0 -1
  208. package/dist/lifecycle/transcript.d.ts +0 -27
  209. package/dist/lifecycle/transcript.js +0 -43
  210. package/dist/lifecycle/transcript.js.map +0 -1
  211. package/dist/rubrics/rubrics.d.ts +0 -135
  212. package/dist/rubrics/rubrics.js +0 -145
  213. package/dist/rubrics/rubrics.js.map +0 -1
  214. package/dist/sandbox/assets/parts/core.js +0 -22
  215. package/dist/sandbox/assets/parts/lifecycle-hook.js +0 -47
  216. package/dist/sandbox/bridge.d.ts +0 -16
  217. package/dist/sandbox/bridge.js +0 -95
  218. package/dist/sandbox/bridge.js.map +0 -1
  219. package/skills/archmax-harness/references/hooks-and-scripts.md +0 -88
@@ -93,14 +93,16 @@ 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`. |
102
103
  | `tools` | `StructuredTool[]` | Host tools, granted per state by `tools.allow`. Build them with `toolsFromMap` — see [host tools](#host-tools). A name starting `archmax_`, or naming a runtime file operation (`copy_file`, `move_file`, `remove_file`), throws `ReservedToolNameError`. |
103
104
  | `essentialTools` | `string[]` | Host tools treated as always on: disclosed and permitted in every state, whatever the workflow's `tools.allow_always` declares; a state's own entry still narrows one. |
105
+ | `clock` | `string` | Pins the "now" every model call states, for cases and evaluations: an ISO 8601 date (`2026-03-20`, midnight UTC) or a date-time with `Z`/an offset; zoneless or impossible values fail assembly, `""` means the real clock. Rendered in UTC, rounded to ten minutes, no local time. Recorded timestamps, `resumeAt` and the sandbox's `Date` stay real. Not for production: a pinned session resumed days later still thinks it is the pinned day. A case's `clock:` outranks it. |
104
106
  | `images` | `boolean \| ImageReadOptions` | Show a model with vision the images it reads: the model's `read_file` of a PNG/JPEG/GIF/WebP answers with one line and the image is added to each later request as a `user` message after that batch of tool results, read per request — no bytes in history, checkpoints, events or scripts. `{ maxBytes?, keep? }` (default 10 MB, every image). Off by default; every other binary stays refused. |
105
107
  | `toolPaths` | `Record<string, ToolPaths>` | Each host tool's path arguments and how a call uses each (`read`, `list`, `search`, `write`, `remove`, `execute`): `{ get_markdown: { path: "read" } }`. Every path rule then governs them like the built-ins'. Wins over a descriptor's `paths`; declaring a built-in's paths throws `ToolPathsError`. |
106
108
  | `checkpointer` | `BaseCheckpointSaver` | Custom-adapter escape hatch; takes precedence over the session store for checkpoint persistence. Defaults to a durable `BackendCheckpointSaver` writing through the session store. Use `MemorySaver` for ephemeral runs. |
@@ -144,7 +146,7 @@ const sessionId = `run-${runId}`;
144
146
  try {
145
147
  const result = await agent.invoke(
146
148
  { messages: [{ role: "user", content: prompt }] },
147
- { configurable: { thread_id: sessionId }, recursionLimit: 50 },
149
+ { configurable: { thread_id: sessionId } },
148
150
  );
149
151
  // result: { messages, status?, workflowState?, pendingDecision? }
150
152
  if (result.status === WORKFLOW_STATUSES.awaitingDecision) {
@@ -153,10 +155,16 @@ try {
153
155
  return lastAssistantText(result.messages);
154
156
  }
155
157
  } finally {
156
- agent.dispose(sessionId); // cleans up sandbox/PTC state for the session
158
+ agent.dispose(sessionId); // frees the session's sandbox/PTC state and its checkpoints in memory; the next turn reloads them from the store
157
159
  }
158
160
  ```
159
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
+
160
168
  To cancel a turn, pass an `AbortSignal` as `signal` in the same config (or in
161
169
  `agent.workflow.send(sessionId, input, { signal })`) and abort it. The turn's
162
170
  scripts stop with it, in the session and its child sessions: a pending `tools.*`
@@ -186,12 +194,12 @@ checkpoint:
186
194
  // turn 1
187
195
  await agent.invoke(
188
196
  { messages: [{ role: "user", content: "Look up order 1234" }] },
189
- { configurable: { thread_id: "chat-42" }, recursionLimit: 50 },
197
+ { configurable: { thread_id: "chat-42" } },
190
198
  );
191
199
  // turn 2 — SAME thread_id; only the new message
192
200
  await agent.invoke(
193
201
  { messages: [{ role: "user", content: "And when did it ship?" }] },
194
- { configurable: { thread_id: "chat-42" }, recursionLimit: 50 },
202
+ { configurable: { thread_id: "chat-42" } },
195
203
  );
196
204
  ```
197
205
 
@@ -214,17 +222,20 @@ await agent.invoke(
214
222
  { role: "user", content: "what was my first message?" }, // the new turn
215
223
  ],
216
224
  },
217
- { configurable: { thread_id: "chat-42" }, recursionLimit: 50 },
225
+ { configurable: { thread_id: "chat-42" } },
218
226
  );
219
227
  ```
220
228
 
221
229
  Gotchas:
222
230
 
223
- - **A hook reads the transcript from `messages`, newest last.** A hook that keys
224
- off "the current request" should take the *last* `user` entry, not the first —
225
- the first is the message that opened the session. (A grading rubric still
226
- receives the compact `userRequest`/`history` payload its rubric is written
227
- against; there `userRequest` is the *first* user 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.
228
239
  - **A definition edit is adopted, not refused.** A session takes its next turn
229
240
  under the `workflow.yaml` in force now; the recorded `specHash` says which
230
241
  definition each turn ran under but gates nothing. A turn positioned in a state
@@ -233,7 +244,7 @@ Gotchas:
233
244
  in seeded history, or the provider may reject the turn.
234
245
  - **Not every message in a session's transcript is a person's.** The runtime
235
246
  writes **runtime notes** of its own — `[decision]`, `[event]`, `[error]`,
236
- `[after]`, `[sub-workflow: …]` — carried as a synthetic `archmax_note` tool call
247
+ `[grader]`, `[code]`, `[sub-workflow: …]` — carried as a synthetic `archmax_note` tool call
237
248
  and its result, so a model cannot read them as a person speaking. Ask
238
249
  `isRuntimeNote(message)` / `runtimeNoteKind(message)` rather than inferring
239
250
  authorship from role or a `[bracket]` prefix: a turn's lower bound is a human
@@ -246,7 +257,7 @@ Gotchas:
246
257
  import { isRuntimeNote, runtimeNoteKind } from "@archmax-ai/harness";
247
258
 
248
259
  const opensTurn = (m: unknown) => isRuntimeNote(m) || m.role === "user";
249
- const label = (m: unknown) => runtimeNoteKind(m); // "decision" | "event" | … | null
260
+ const label = (m: unknown) => runtimeNoteKind(m); // "decision" | "event" | "grader" | "code" | … | null
250
261
  ```
251
262
 
252
263
  ## Human-in-the-loop
@@ -261,6 +272,7 @@ if (result.status === WORKFLOW_STATUSES.awaitingDecision && result.pendingDecisi
261
272
  // When they pick:
262
273
  const outcome = await agent.workflow.decide(sessionId, { target: chosenTo, comment });
263
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).
264
276
  // `rejected` says why when status is "rejected"; Outcome from send() carries it too.
265
277
  // `exit` is { success: false, code, reason } when the agent ended the run with
266
278
  // archmax_raise (status "failed"), { success: true } when it completed.
@@ -422,17 +434,17 @@ Pass `onEvent`; every `WorkflowLifecycleEvent` carries a `level`
422
434
  | --- | --- |
423
435
  | `state-enter` / `state-leave` | state entered / left (`{ state, next? }`) |
424
436
  | `advance` | transition committed (`{ from, to, reason?, callId? }` — `callId` names the `archmax_advance` call that drove it; absent on the turn's opening arrival) |
425
- | `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) |
426
439
  | `parked` / `decided` | human state parked / resolved (`{ state, sessionId, awaiting, to? }` — a human park carries no `callId`, since no call asked for it) |
427
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) |
428
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` |
429
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) |
430
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 |
431
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 |
432
- | `rubric-start` / `rubric-result` | grading-rubric dispatch bracketed by `dispatchId`, with the rubric's `name` |
433
- | `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?` |
434
446
  | `prompt-shaping` | assembly-time payload summary: resolved `profile`, prompt-cache mechanism, `withheld` built-ins |
435
- | `skills-loaded`, `hooks-summary`, `graph-topology`, `workflow-reset`, `warning` | assembly-time diagnostics |
447
+ | `skills-loaded`, `graph-topology`, `workflow-reset`, `warning` | assembly-time diagnostics |
436
448
 
437
449
  For a browser client, forward these over SSE/WebSocket keyed by `sessionId`. To
438
450
  render a live chat transcript: accumulate `agent-text-delta` into a bubble
@@ -505,7 +517,7 @@ gives the same figure live in process.
505
517
  ### Measuring anything else — fold the stream
506
518
 
507
519
  The SDK keeps no metrics ledger. Tool calls, governance refusals, the path a run
508
- 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
509
521
  sat waiting for a person are all already on the typed event stream, and a backend
510
522
  counts what it needs:
511
523
 
@@ -578,8 +590,8 @@ interface RunMetadata { sessionId: string; workflow: string;
578
590
  runtimeContract: ResolvedRuntimeContract; packageVersion: string; }
579
591
  // trail.json: { sessionId, workflow, steps: TrailStep[] } — the audit trail,
580
592
  // re-derived from checkpointed state on every artifact write.
581
- // TrailStep: { to, kind: "trigger"|"agent"|"human"|"reset"|"on_error",
582
- // reason?, ts }
593
+ // TrailStep: { to, kind: "trigger"|"agent"|"human"|"grader"|"code"|
594
+ // "sub-workflow"|"reset"|"on_error", reason?, workflow?, status?, ts }
583
595
  ```
584
596
 
585
597
  An **API** reads these files (through the same session store, or via `runArtifactPaths(sessionId)`
@@ -588,7 +600,7 @@ path helpers) to render historical runs: `graph.json`/`graph.mmd` for the diagra
588
600
  path (which edges committed, by whom, and why), `variables.json` for the run's
589
601
  inputs and established facts (also what a test author reads to reproduce a real
590
602
  firing's payload — see
591
- [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)).
592
604
  Note it holds whatever the run's variables held, verbatim: a host that must not
593
605
  persist an input should not seed it. To persist artifacts yourself,
594
606
  stream the run and call `agent.emitRunArtifacts(sessionId, trajectory)`.
@@ -749,7 +761,7 @@ workflow-governed target.
749
761
  an `archmax_workflow_<slug>` tool), the child runtime is
750
762
  composed from the *same* resolved inputs as the parent — model and
751
763
  `modelFactory`, `backend` and `mounts`, `sessionStore` and `checkpointer`, host
752
- `tools` and `essentialTools`, `sandboxRuntime`, `hookExecutors`,
764
+ `tools` and `essentialTools`, `sandboxRuntime`,
753
765
  `policyRules`, `promptCache` and `pricing` — differing only in
754
766
  the machine that governs it and the system prompt that machine renders. So a
755
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,8 @@ 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
+ | `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 |
22
23
  | `steps` | one flat list; actions and assertions are peers; an assertion evaluates against the nearest action above it |
23
24
 
24
25
  Actions: `send: "<message>"` (a new turn; against a parked run, a reply-only
@@ -26,17 +27,50 @@ message that leaves it parked), `decide: { to, comment? }`,
26
27
  `deliver: { trigger, variables? }`.
27
28
 
28
29
  Assertions (**structural** ones halt the case when they miss; later steps are
29
- 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? }`ˢ ·
30
31
  `raised: true | <code> | { code?, reason? }`ˢ (the agent ended the turn with `archmax_raise`; code exact, reason exact or `"/regex/flags"`) ·
31
- `reachedState: <slug>`ˢ (a committed non-trigger trail step) ·
32
- `trail: { to?, kind?, reason?, count }`ˢ (`kind`: trigger/agent/human/on_error) ·
33
- `noTraversal: true`ˢ · `triggerArrival: <id>`ˢ ·
34
- `reply: { includes?, excludes? }` (token or list; `"/regex/flags"` strings are regexes; matched against the whole turn's assistant text) ·
35
- `calledTool` / `notCalledTool` / `blockedTool: { name, input? }` (partial input; `blockedTool` = governance refused it — the assertion that tests a guard) ·
36
- `ranWorkflow: { workflow, status?, count? }` · `usedNoTools: true` (whole conversation) ·
37
- `variables: { expect, path?, locked? }` · `grade: { closedQA, atLeast }` (LLM grades the reply plus the tool record; `atLeast` is a hard bar; needs `tests.judge`).
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
+ `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; the agent's calls only) ·
37
+ `ranWorkflow: { workflow, status?, count? }` (whoever dispatched) · `usedNoTools: true` (whole conversation, the agent's calls only) ·
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`).
38
39
 
39
40
  Put the structural assertion right after the action that should satisfy it,
40
41
  before content assertions that only make sense in that state:
41
42
 
42
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`.