Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ A run executes in one of **three execution modes** behind the one `LLMProvider`
engine is identical across all three. See [ADR-0012](docs/decisions/0012-managed-inference-dual-mode.md) to [ADR-0015](docs/decisions/0015-managed-mode-data-handling-and-compliance.md)
and [docs/architecture/managed-inference.md](docs/architecture/managed-inference.md).

**Status: Phase 1 in progress — milestone M1 (LLM seam proven) reached (PR #9, 2026-06-07); the `FallbackChain` runner (1.K) landed, completing 1.m2 with the cost tracker (PR #13, 2026-06-11); the run loop (1.N — `WorkflowEngine` + `RunEventBus`) landed (PR #17, 2026-06-13) **completing 1.m3** (parse → DAG → run loop emits the canonical event stream), with the built-in `ToolRegistry` (1.T, a 1.m4 component) landing alongside it as the other `AgentRunner` (1.O) join prerequisite; the **`AgentRunner` (1.O) — per-node LLM execution behind the seam — landed (PR #18, 2026-06-14)**; and the **node-type handlers (1.P) — the six non-agent `NodeExecutor` arms (condition / transform / fan_out / fan_in / input / output) behind a dispatching executor — landed (PR #20, 2026-06-14)**; and **checkpoint/resume (1.R) + the human gate (1.Q) landed (PR #22, 2026-06-15)** — the derived `Checkpointer` + cross-process `resumeFromCheckpoint`, and the `human_in_the_loop` gate with the one-shot timeout port. Node retry (1.S) is next, toward M2.**
**Status: Phase 1 in progress — milestone M1 (LLM seam proven) reached (PR #9, 2026-06-07); the `FallbackChain` runner (1.K) landed, completing 1.m2 with the cost tracker (PR #13, 2026-06-11); the run loop (1.N — `WorkflowEngine` + `RunEventBus`) landed (PR #17, 2026-06-13) **completing 1.m3** (parse → DAG → run loop emits the canonical event stream), with the built-in `ToolRegistry` (1.T, a 1.m4 component) landing alongside it as the other `AgentRunner` (1.O) join prerequisite; the **`AgentRunner` (1.O) — per-node LLM execution behind the seam — landed (PR #18, 2026-06-14)**; and the **node-type handlers (1.P) — the six non-agent `NodeExecutor` arms (condition / transform / fan_out / fan_in / input / output) behind a dispatching executor — landed (PR #20, 2026-06-14)**; and **checkpoint/resume (1.R) + the human gate (1.Q) landed (PR #22, 2026-06-15)** — the derived `Checkpointer` + cross-process `resumeFromCheckpoint`, and the `human_in_the_loop` gate with the one-shot timeout port; and **node retry (1.S) — the above-chain whole-node retry budget ([ADR-0040](docs/decisions/0040-node-retry-budget-above-the-chain.md), amending ADR-0038) — landed (PR #24, 2026-06-15)**, re-dispatching a whole node on a retryable failure up to `retry.max` attempts (with `node:retrying`, abort-aware backoff, and `retry_on` filtering), with retry-from-node (ADR-0040 Part B) deferred to Phase-2. The pre-egress budget governor (1.AC) is next, toward M2.**
Phase 0 (M0, 2026-06-04) landed the monorepo, strict toolchain + CI, `@relavium/shared` (the
full Zod contract set), the no-vendor-type seam fence, and `@relavium/db`. Phase 1 has since
landed `@relavium/llm` — the `LLMProvider` seam + all three adapters (Anthropic, OpenAI/DeepSeek,
Expand All @@ -60,8 +60,7 @@ executor-only with a `secretInputNames` masking gate on `NodeExecContext`), and
`run_events` log, no checkpoint table — ADR-0003) + cross-process `resumeFromCheckpoint` with idempotent
re-delivery and a `workflow_mismatch` identity guard, and the `human_in_the_loop` gate's suspend/resume
plus the one-shot `setTimer` timeout port — `approve` auto-resolves, `reject` fails with `run_timeout`).
Active work is now the remaining 1.m4 lane — node retry (1.S) and the pre-egress budget governor
(1.AC) — toward **M2**; see
Active work is now the last 1.m4 workstream — the pre-egress budget governor (1.AC) — toward **M2**; see
[docs/roadmap/current.md](docs/roadmap/current.md). See [README.md](README.md) for the public overview.

## Non-negotiable rules for AI agents
Expand Down
26 changes: 22 additions & 4 deletions docs/reference/contracts/agent-session-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,28 @@ stateDiagram-v2
| **export** | Serialize the session to a `.relavium.yaml` scaffold ([export](#export-to-workflow)). |

The turn loop, tool dispatch, streaming, and fallback are the **same** code paths a workflow `agent`
node uses; the session is a thin wrapper over the `AgentRunner` that manages conversation state and
context. The lifecycle emits the `session:*` event namespace — defined, with the run namespace, in
[sse-event-schema.md](sse-event-schema.md#session-event-namespace) (this spec does not enumerate event
names).
node uses: the session is a thin wrapper over the **correlation-agnostic turn core** — the `runAgentTurn`
path the `AgentRunner` (1.O) also wraps for a workflow node — managing conversation state and context.
*(1.V drives that turn core directly; it does **not** route through the run-only `NodeExecutor` the
`AgentRunner` exposes. "Same `AgentRunner` path" means the shared turn-core execution, not the
`NodeExecutor` surface.)* The lifecycle emits the `session:*` event namespace — defined, with the run
namespace, in [sse-event-schema.md](sse-event-schema.md#session-event-namespace) (this spec does not
enumerate event names). 1.V keeps the conversation **in-memory** (the in-flight `LlmMessage`/`ContentPart`
form) and emits session events through an injected sink; wiring that sink onto the shared `RunEventBus`
(per-session `sequenceNumber` + gap/resync) is **1.W**, and the durable [`SessionMessage`](#session-messages)
schema + persistence is **1.X**.

### Hard turn cap

A session carries a **hard turn cap** — a finite DoS fail-safe on the number of turns it will run (engine
default **50**, overridable at construction; **0/absent ⇒ the default**). It is **distinct** from two other
limits and must not be conflated with either: `[chat].max_messages` (a history-**trim** threshold that
silently *continues* the session — [config-spec.md](config-spec.md)) and the turn core's **within-turn**
`maxToolTurns` tool-loop guard. A `sendMessage` past the cap ends **loudly, with no egress**:
`session:turn_completed` carries `stopReason: 'error'` + `error.code: 'turn_limit'`
([sse-event-schema.md](sse-event-schema.md#error-code-taxonomy)) — never a silent stop; the within-turn
`maxToolTurns` guard surfaces the same `turn_limit` code through the same event. The cap is an **engine-API
knob** in 1.V (a surface maps its `[chat]` default onto it); it is **not** a new `[chat]` field in Phase 1.

## Session context

Expand Down
25 changes: 23 additions & 2 deletions docs/reference/contracts/sse-event-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,27 @@ export interface HumanGatePausedEvent extends BaseEvent {
timeoutAction?: 'approve' | 'reject'; // on-timeout policy (present only with timeoutMs); lets a surface show how the gate auto-resolves and a Phase-2 crash-resume re-arm the timer from the log
expiresAt?: string;
}

export interface BudgetWarningEvent extends BaseEvent {
type: 'budget:warning';
spentMicrocents: number;
limitMicrocents: number;
thresholdPct: number; // 0–100, rounded from spent/limit at the pre-egress check point
}

export interface BudgetPausedEvent extends BaseEvent {
type: 'budget:paused';
nodeId: string; // the agent node whose next LLM call would exceed the cap
spentMicrocents: number;
limitMicrocents: number;
gateId: string; // stable id of the budget gate; required by engine.resume(runId, gateId, decision)
}

export interface RunTimeoutEvent extends BaseEvent {
type: 'run:timeout';
elapsedMs: number;
timeoutMs: number;
}
```

### Security: event payloads never carry secrets
Expand Down Expand Up @@ -236,8 +257,8 @@ Within a turn, the conversational work reuses the **same** `agent:token` / `agen

| `type` | Meaning | Key payload fields |
| --- | --- | --- |
| `budget:warning` | Spend crossed the warning threshold. | `spentMicrocents`, `limitMicrocents`, `thresholdPct` |
| `budget:paused` | Spend would exceed the cap with `on_exceed: pause_for_approval`; the run suspends like a human gate and is resumed via the `resume_budget` IPC command. | `spentMicrocents`, `limitMicrocents` |
| `budget:warning` | Pre-egress worst-case cost estimate would exceed the configured cap, and `on_exceed: warn` is set. Emitted once per run before the capped egress; execution continues. `thresholdPct` is `clamp(round(spent / limit * 100), 0, 100)` observed at the pre-egress check point. | `spentMicrocents`, `limitMicrocents`, `thresholdPct` |
| `budget:paused` | Pre-egress estimate would exceed the cap with `on_exceed: pause_for_approval`; the run suspends like a human gate and is resumed via `engine.resume(runId, gateId, decision)`. `decision: approved` continues; `rejected` closes the run with `run:failed{code: budget_exceeded}`. | `nodeId`, `spentMicrocents`, `limitMicrocents`, `gateId` |
| `run:timeout` | The run hit its `timeout_ms`. | `elapsedMs`, `timeoutMs` |

These three (and `run:paused` / `human_gate:paused`) are **non-terminal** — they signal a governance/suspension state, not the run's end. A run that cannot continue past a timeout or budget cap still closes with **exactly one** `run:failed` carrying `code: run_timeout` / `budget_exceeded`. The exactly-one-terminal-event invariant (`run:completed | run:failed | run:cancelled`) and its precedence are owned by [ADR-0036](../../decisions/0036-run-loop-substrate-event-bus-and-execution-host.md).
Expand Down
15 changes: 10 additions & 5 deletions docs/roadmap/current.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,12 @@ loser-cancel deferred), and a pre-merge BLOCKER secret-leak (the `input` handler
the derived `Checkpointer` (state folded from the `run_events` log; no checkpoint table — ADR-0003) +
cross-process `resumeFromCheckpoint` (idempotent re-delivery, `workflow_mismatch` identity guard), and the
`human_in_the_loop` gate (suspend → notify → resume, plus the one-shot `setTimer` timeout port — `approve`
auto-resolves, `reject` fails with `run_timeout`). The lane now continues at the remaining **1.m4**
workstreams toward **M2** — **node retry (1.S)** and the **pre-egress budget governor (1.AC)** — and the
agent-first sub-spine (**1.V–1.AA**, Lane C) opens now that 1.O exists.
auto-resolves, `reject` fails with `run_timeout`). **Node retry (1.S) is ✅ Done (PR #24, 2026-06-15)** — the above-chain whole-node retry budget
([ADR-0040](../decisions/0040-node-retry-budget-above-the-chain.md) Part A: re-dispatch a whole node on a
retryable, `retry_on`-admitted failure up to `retry.max` attempts with abort-aware backoff and the non-terminal
`node:retrying`, `node:failed` staying the single terminal; the user-triggered retry-from-node Part B is
deferred to Phase-2). The lane now continues at the last **1.m4** workstream toward **M2** — the **pre-egress
budget governor (1.AC)** — and the agent-first sub-spine (**1.V–1.AA**, Lane C) is open now that 1.O exists.

> **Multimodal I/O — the shape is landed (1.AD ✅ Done, PR #11, 2026-06-10).** First-class
> image/audio/video I/O (input **and** output, incl. generate-media-by-rule) was decided on 2026-06-08:
Expand Down Expand Up @@ -181,8 +184,10 @@ agent-first sub-spine (**1.V–1.AA**, Lane C) opens now that 1.O exists.
> **1.N (`WorkflowEngine` + `RunEventBus`) and 1.T (the built-in `ToolRegistry`) are ✅ Done (PR #17,
> merged 2026-06-13)** — **1.N closes 1.m3** (its last component); **1.T** (a 1.m4 component) is the
> other 1.O join prerequisite; **the `AgentRunner` join (1.O) is ✅ Done (PR #18, 2026-06-14)**; and the
> **node-type handlers (1.P) are ✅ Done (PR #20, 2026-06-14)**; and **checkpoint/resume (1.R) + the
> human gate (1.Q) are ✅ Done (PR #22, 2026-06-15)**. **Node retry (1.S)** is the next workstream.
> **node-type handlers (1.P) are ✅ Done (PR #20, 2026-06-14)**; **checkpoint/resume (1.R) + the
> human gate (1.Q) are ✅ Done (PR #22, 2026-06-15)**; and **node retry (1.S) is ✅ Done (PR #24, 2026-06-15)**
> (ADR-0040 Part A; the user-triggered retry-from-node Part B is deferred to Phase-2). The **pre-egress budget
> governor (1.AC)** is the next workstream.

Carry-over hardening is tracked in [deferred-tasks.md](deferred-tasks.md) — pick items up as Phase 1
first touches each file.
Expand Down
8 changes: 8 additions & 0 deletions docs/roadmap/deferred-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ Severity is the review's verified rating. Check an item off in the PR that resol
question: does the estimate need provider-accurate token counting (from the seam's model meta /
usage feedback) to avoid systematic over/under-blocking, or is the declared estimate enough?
No change now — re-evaluate with real 1.AC telemetry. *(1.AC; ADR-0028)*
- [ ] **Configurable sub-100% budget warning threshold (ADR-0028 amendment).** `budget:warning`
today is emitted only when a pre-egress estimate would already exceed the cap, on the `on_exceed: warn`
path (`thresholdPct` reports the observed spent/limit fraction at that point). A user-facing
early-warning threshold (e.g. `warn_at_pct: 80`) requires amending ADR-0028 to add both a
config default and a per-workflow `budget.warn_at_pct` field, plus a decision on whether it
throttles/queues subsequent egresses or only surfaces a one-time advisory event. Deferred until
there is concrete surface demand or telemetry showing operators need an earlier signal.
*(1.AC; ADR-0028; config-spec.md; workflow-yaml-spec.md)*

## Interpolation engine (1.L2) follow-ups

Expand Down
Loading
Loading