@archmax-ai/harness 0.1.0 → 0.2.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 +45 -4
- package/dist/assembly/compose.js +3 -5
- package/dist/assembly/compose.js.map +1 -1
- package/dist/assembly/delegation-registry.js +4 -2
- package/dist/assembly/delegation-registry.js.map +1 -1
- package/dist/authoring-skill/archmax-harness/README.md +24 -16
- package/dist/authoring-skill/archmax-harness/SKILL.md +7 -1
- package/dist/authoring-skill/archmax-harness/references/backend-integration.md +58 -2
- package/dist/authoring-skill/archmax-harness/references/hook-and-test-scripts.md +1 -1
- package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +71 -22
- package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +16 -5
- package/dist/cli/turn.d.ts +9 -4
- package/dist/cli/turn.js +35 -8
- package/dist/cli/turn.js.map +1 -1
- package/dist/cli.js +6 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/kernel/kernel.d.ts +2 -2
- package/dist/kernel/kernel.js +3 -2
- package/dist/kernel/kernel.js.map +1 -1
- package/dist/machine/machine.d.ts +9 -0
- package/dist/machine/machine.js +13 -0
- package/dist/machine/machine.js.map +1 -1
- package/dist/machine/signature.d.ts +104 -0
- package/dist/machine/signature.js +210 -0
- package/dist/machine/signature.js.map +1 -0
- package/dist/machine/spec-schema.d.ts +158 -8
- package/dist/machine/spec-schema.js +78 -6
- package/dist/machine/spec-schema.js.map +1 -1
- package/dist/machine/triggers.d.ts +6 -2
- package/dist/machine/triggers.js +7 -2
- package/dist/machine/triggers.js.map +1 -1
- package/dist/machine/types.d.ts +7 -1
- package/dist/machine/types.js.map +1 -1
- package/dist/machine/variables.d.ts +2 -0
- package/dist/machine/variables.js +4 -0
- package/dist/machine/variables.js.map +1 -1
- package/dist/public/spec.d.ts +17 -2
- package/dist/public/spec.js +15 -1
- package/dist/public/spec.js.map +1 -1
- package/dist/sessions/resume.d.ts +4 -0
- package/dist/sessions/resume.js +8 -1
- package/dist/sessions/resume.js.map +1 -1
- package/dist/workflow/control-tools.d.ts +7 -2
- package/dist/workflow/control-tools.js +27 -2
- package/dist/workflow/control-tools.js.map +1 -1
- package/dist/workflow/governance.js +35 -27
- package/dist/workflow/governance.js.map +1 -1
- package/dist/workflow/middleware.js +5 -10
- package/dist/workflow/middleware.js.map +1 -1
- package/dist/workflow/parks.js +8 -6
- package/dist/workflow/parks.js.map +1 -1
- package/dist/workflow/prompt-cache.d.ts +0 -2
- package/dist/workflow/prompt-cache.js +0 -7
- package/dist/workflow/prompt-cache.js.map +1 -1
- package/dist/workflow/render-prompt.js +28 -7
- package/dist/workflow/render-prompt.js.map +1 -1
- package/dist/workflow/signature-checks.d.ts +22 -0
- package/dist/workflow/signature-checks.js +52 -0
- package/dist/workflow/signature-checks.js.map +1 -0
- package/dist/workflow/state.d.ts +2 -2
- package/dist/workflow/state.js +2 -2
- package/dist/workflow/sub-workflow.d.ts +23 -16
- package/dist/workflow/sub-workflow.js +78 -14
- package/dist/workflow/sub-workflow.js.map +1 -1
- package/dist/workflow/tool-service.js +1 -1
- package/dist/workflow/tool-service.js.map +1 -1
- package/dist/workflow/turn-boundary.js +4 -5
- package/dist/workflow/turn-boundary.js.map +1 -1
- package/dist/workflow/workflow-tools.d.ts +17 -9
- package/dist/workflow/workflow-tools.js +30 -17
- package/dist/workflow/workflow-tools.js.map +1 -1
- package/package.json +2 -1
- package/skills/archmax-harness/README.md +82 -0
- package/skills/archmax-harness/SKILL.md +247 -0
- package/skills/archmax-harness/references/backend-integration.md +681 -0
- package/skills/archmax-harness/references/cases.md +41 -0
- package/skills/archmax-harness/references/diagnostics.md +51 -0
- package/skills/archmax-harness/references/hook-and-test-scripts.md +1070 -0
- package/skills/archmax-harness/references/hooks-and-scripts.md +82 -0
- package/skills/archmax-harness/references/wiring.md +101 -0
- package/skills/archmax-harness/references/workflow-schema.md +1680 -0
- package/skills/archmax-harness/references/workflow-yaml.md +252 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Hook scripts and `archmax_run` scripts — contracts
|
|
2
|
+
|
|
3
|
+
Load this when writing a `workflows/<slug>/hooks/*.js` gate or a
|
|
4
|
+
`skills/<cap>/scripts/*.js` source. Worked examples and the sandbox details:
|
|
5
|
+
[`hook-and-test-scripts.md`](hook-and-test-scripts.md) Part 1.
|
|
6
|
+
|
|
7
|
+
## Hook script contract
|
|
8
|
+
|
|
9
|
+
Default-export function; its return value is the verdict. Verdict helpers are
|
|
10
|
+
globals (typed import optional: `import { ok, veto, correct } from "@archmax-ai/harness/sandbox";`,
|
|
11
|
+
placed below the JSDoc; no other import is allowed).
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
/**
|
|
15
|
+
* Requester gate — the request must name a requester the order records know.
|
|
16
|
+
*
|
|
17
|
+
* Reads the first user message, extracts the first email address, loads
|
|
18
|
+
* skills/order-data/assets/orders.json through the bridge and matches the
|
|
19
|
+
* address against every record's `email`, case-insensitively.
|
|
20
|
+
*
|
|
21
|
+
* Vetoes when the file cannot be parsed, no address is present, or none
|
|
22
|
+
* matches — each with the reason. Otherwise ok.
|
|
23
|
+
*/
|
|
24
|
+
export default async function hook({ state, phase, trigger, variables, messages, tools, from, to, reason }) {
|
|
25
|
+
let orders;
|
|
26
|
+
try { orders = JSON.parse(await tools.readFile({ file_path: "skills/order-data/assets/orders.json" })); }
|
|
27
|
+
catch { return veto("Could not load skills/order-data/assets/orders.json."); }
|
|
28
|
+
const text = messages.find((m) => m.role === "user")?.text ?? "";
|
|
29
|
+
const email = text.match(/[\w.+-]+@[\w.-]+\.\w{2,}/)?.[0];
|
|
30
|
+
if (!email) return veto("Include your email address so we can look up your account.");
|
|
31
|
+
if (!orders.some((o) => o.email?.toLowerCase() === email.toLowerCase())) return veto(`"${email}" is not in the order database.`);
|
|
32
|
+
return ok();
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Input | Meaning |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `state`, `phase` | state slug; `"before"` or `"after"` |
|
|
39
|
+
| `trigger` | trigger id string (payload is in `variables`) |
|
|
40
|
+
| `variables` | read-only `name → value` map incl. `trigger`; structured values whole |
|
|
41
|
+
| `messages` | last 24 messages, newest last, `{ role, text, toolCalls? }`; roles `user`/`assistant`/`tool`/`system`/`runtime` (a runtime note; match `role === "runtime"`, never a `[bracket]`) |
|
|
42
|
+
| `from`, `to`, `reason` | `after` only: the attempted transition and the agent's reason |
|
|
43
|
+
| `tools` | async camelCased bridge: `tools.readFile`, `tools.writeFile`, `tools.archmaxWorkflowEnrichOrder(...)`; no variable tools, no `task` |
|
|
44
|
+
|
|
45
|
+
| Return | Effect |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `ok()`, nothing, `true`/number/string | proceed |
|
|
48
|
+
| `veto(reason)` | block; the reason is shown to the agent — make it actionable |
|
|
49
|
+
| `correct(reason)` | `after` only: retry with the reason, within `max_iterations` |
|
|
50
|
+
| `false` | veto ("precondition not met") |
|
|
51
|
+
| any other object, a throw | veto with keys/error named — never silently ok |
|
|
52
|
+
|
|
53
|
+
`console.log` goes to the trail. A file with no default export uses its
|
|
54
|
+
completion value as the verdict. Parse inputs defensively; turn a failure that
|
|
55
|
+
should not block into an explicit `ok()`.
|
|
56
|
+
|
|
57
|
+
## `archmax_run` script contract
|
|
58
|
+
|
|
59
|
+
`skills/<cap>/scripts/<x>.js`: plain script, top-level `await`, `tools.*`,
|
|
60
|
+
`console.log`; the **last expression** is the tool result. Input is the global
|
|
61
|
+
`args`: the arguments the model passed plus `args.variables` (read-only snapshot).
|
|
62
|
+
No `archmax_advance`, `archmax_set_variables` or `task`. Fan-out shape:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
/** One sub-run per requested order, all in flight at once; returns [{ order_id, ...returns }]. */
|
|
66
|
+
const orders = args.variables?.orders_to_enrich ?? [];
|
|
67
|
+
await Promise.all(orders.map(async (o) => {
|
|
68
|
+
const answer = await tools.archmaxWorkflowEnrichOrder({ order_id: o.order_id });
|
|
69
|
+
return { order_id: o.order_id, ...JSON.parse(answer).returns };
|
|
70
|
+
}));
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The calling state must allow every tool the script calls (here
|
|
74
|
+
`archmax_workflow_enrich-order`).
|
|
75
|
+
|
|
76
|
+
**Return into a variable.** A script's result reaches the model as one tool
|
|
77
|
+
message and no further — it is not written to the variable store, and a script
|
|
78
|
+
cannot write there itself. Return structured data and name the variable in the
|
|
79
|
+
calling state's `requires:`; the model then records it with
|
|
80
|
+
`archmax_set_variables`, which is what makes it survive the state, guard a
|
|
81
|
+
`tools.allow` glob, and be quotable downstream as `${{name}}` instead of retyped.
|
|
82
|
+
A result worth computing is worth naming.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Wiring the runtime into a backend — the short form
|
|
2
|
+
|
|
3
|
+
Load this when embedding `@archmax-ai/harness` in a service. Every option shape, the
|
|
4
|
+
event catalogue and recipes: [`backend-integration.md`](backend-integration.md).
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createAgent, WORKFLOW_STATUSES, createUsageTracker } from "@archmax-ai/harness";
|
|
9
|
+
|
|
10
|
+
const usage = createUsageTracker();
|
|
11
|
+
const agent = await createAgent({
|
|
12
|
+
workflow: "order-lookup",
|
|
13
|
+
workspace: { rootDir, /* mounts?, sessionStore? */ },
|
|
14
|
+
onEvent: (e) => { usage.handler(e); publish(e); }, // omit for the console subscriber
|
|
15
|
+
// model | modelFactory, tools, systemPrompt, middleware, backend, checkpointer,
|
|
16
|
+
// trigger, variables, promptCache, pricing, hookExecutors …
|
|
17
|
+
});
|
|
18
|
+
const sessionId = `run-${runId}`;
|
|
19
|
+
const result = await agent.invoke(
|
|
20
|
+
{ messages: [{ role: "user", content: prompt }] },
|
|
21
|
+
{ configurable: { thread_id: sessionId }, recursionLimit: 50 },
|
|
22
|
+
);
|
|
23
|
+
if (result.status === WORKFLOW_STATUSES.awaitingDecision) {
|
|
24
|
+
// result.pendingDecision.transitions → show to a person, then:
|
|
25
|
+
// await agent.workflow.decide(sessionId, { target, comment });
|
|
26
|
+
}
|
|
27
|
+
usage.totals(sessionId); // tokens (+ costUsd only when pricing is configured)
|
|
28
|
+
agent.dispose(sessionId);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **Install**: `npm i @archmax-ai/harness`; pre-publish, `npm pack` and install the
|
|
32
|
+
tarball (a real copy, not a symlink). This skill ships in the package at
|
|
33
|
+
`BUNDLED_AUTHORING_SKILL_DIR` (`<dir>/archmax-harness/SKILL.md`).
|
|
34
|
+
- **Mounts**: `workspace.mounts` is a route table — key → backend, or
|
|
35
|
+
`{ backend, readOnly, governed }`. Mark a mount `governed: true` to hand the
|
|
36
|
+
spec's `mounts` block the decision of which **states** reach it (closed by
|
|
37
|
+
default, like a skill bundle) and whether each may write there; a mount not so
|
|
38
|
+
declared is visible in every state, so an existing table behaves unchanged.
|
|
39
|
+
`readOnly` is a ceiling the spec cannot lift.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
workspace: {
|
|
43
|
+
rootDir,
|
|
44
|
+
mounts: {
|
|
45
|
+
...defaultMounts(rootDir), // skills/, .platform/, AGENTS.md
|
|
46
|
+
"/reference/": { backend: mountSubtree(store, "reference"), governed: true },
|
|
47
|
+
"/shared/": { backend: mountSubtree(store, "shared"), readOnly: false, governed: true },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then in `workflow.yaml`: `mounts: { allow_always: [reference] }`, a state's
|
|
53
|
+
`mounts: { allow: [{ mount: shared, access: read }] }`, and
|
|
54
|
+
`mounts.forbid_always` for what no state (or child session) may reach. Pass the
|
|
55
|
+
same table to `validateWorkflow({ mounts })` so `archmax validate` checks the
|
|
56
|
+
spec's names against the wiring.
|
|
57
|
+
- **Storage**: default filesystem store at `<root>/sessions`; else
|
|
58
|
+
`createFilesystemSessionStore({ dir })`, `createBackendSessionStore({ backend, prefix? })`,
|
|
59
|
+
`createMemorySessionStore()`. A custom `backend` without `sessionStore`
|
|
60
|
+
throws `SessionStoreRequiredError`. Delete sessions via `agent.sessions.delete(id)`.
|
|
61
|
+
- **Agent**: Deep Agents' `invoke`/`stream`/`streamEvents` + checkpoint
|
|
62
|
+
surface; `sessions.list/get/delete`; `emitRunArtifacts`; `dispose`.
|
|
63
|
+
Governance under `agent.workflow`: `machine`, `resolveTrigger`,
|
|
64
|
+
`send(sessionId, input)` → one `Outcome` (`decide`/`reply`/`deliver` are
|
|
65
|
+
conveniences), `resolveSession`.
|
|
66
|
+
- **Multi-turn**: re-invoke the same `thread_id` with only the new message;
|
|
67
|
+
never concatenate a conversation into one string. A spec edit is adopted,
|
|
68
|
+
not refused: the next turn runs under the current `workflow.yaml` (the
|
|
69
|
+
recorded `specHash` says which version each turn ran under), and a state
|
|
70
|
+
the edit removed reopens at the entry state.
|
|
71
|
+
- **Human-in-the-loop**: `status === "awaiting_decision"` →
|
|
72
|
+
`pendingDecision.transitions` → `decide`. `reply(sessionId, text)`
|
|
73
|
+
answers a message without moving the run. An `archmax_wait` park is
|
|
74
|
+
`awaiting_input` → `deliver(sessionId, { trigger, variables, message? })` — the
|
|
75
|
+
person's message travels with the firing; a park with `resumeAt` is the host
|
|
76
|
+
scheduler's cue. A delegated child's park is named on `Outcome.delegation`.
|
|
77
|
+
- **Events**: `onEvent` receives the one typed stream — `state-enter/leave`,
|
|
78
|
+
`advance`, `hook-verdict`, `parked/decided`, `tool-called/result/blocked`
|
|
79
|
+
(every governed call, `archmax_advance` included, grouped by `callId`),
|
|
80
|
+
`agent-text-delta`/`agent-text` (streaming by default, `messageId` groups
|
|
81
|
+
chunks), `model-usage` (provider counts per turn), `title-set`, warnings.
|
|
82
|
+
There is no run-end event; `invoke` returning is the end. Metrics are a
|
|
83
|
+
fold over this stream; only cost is checkpointed (`sessions.get(id).usage`).
|
|
84
|
+
- **Artifacts** per session under `sessions/<id>/artifacts/`: `graph.json`/`.mmd`,
|
|
85
|
+
`trajectory.json`, `trail.json`, `variables.json`, `metadata.json` (incl.
|
|
86
|
+
usage); the agent's files under `scratchpad/`.
|
|
87
|
+
- **Cases against your agent**: `runTests({ workflow, rootDir, createTarget?, sessionIdForCase? })`
|
|
88
|
+
from `@archmax-ai/harness/testing`; wire `createToolMockMiddleware()` into your
|
|
89
|
+
target or a case with `mocks:` is refused before running.
|
|
90
|
+
- **Model/caching**: `ARCHMAX_API_BASE_URL`, `ARCHMAX_API_KEY`, `ARCHMAX_MODEL`
|
|
91
|
+
(+ `ARCHMAX_TEMPERATURE`, `ARCHMAX_MAX_TOKENS`, `ARCHMAX_STREAMING`) for any
|
|
92
|
+
OpenAI-compatible endpoint, or pass `model`. Prompt prefix is cache-marked
|
|
93
|
+
by default (`promptCache: { enabled, ttl: "5m" | "1h" }`); watch
|
|
94
|
+
`cacheReadTokens`. Pricing via `pricing: { "<model-substring>": { input, output, cacheRead, cacheWrite } }`
|
|
95
|
+
or `ARCHMAX_PRICE_*`; without it report tokens, never a guessed cost. A keyed
|
|
96
|
+
table works behind a proxy that echoes no model back — the id falls back to the
|
|
97
|
+
one the runtime was configured to run. Cached tokens are charged once, at the
|
|
98
|
+
cache rate, not at the input rate as well.
|
|
99
|
+
|
|
100
|
+
Typical split: worker = `createAgent` + `invoke` + `decide`/`deliver`; API =
|
|
101
|
+
artifacts + event subscription; frontend = graph + trajectory + live stream.
|