This is the full developer documentation for AGBoss (Agent Graph Builder Open Source System)
# Apache-2.0 · TypeScript · Built on LangGraph · v0.1.0 (latest)Build complex AI agents. Catch broken workflows before they run.
>
AGBoss brings Spring-style dependency injection and declarative TypeScript components to LangGraph. Its static compiler catches structural workflow bugs before execution, and deterministic test runners let you verify components and workflows offline without live LLM calls.
$`npm install @agboss/core @agboss/cli`⧉
Starting from scratch? Run `npx -p @agboss/cli agboss create my-app`⧉
## Readable workflows. Verified before runtime.
[Section titled “Readable workflows. Verified before runtime.”](#readable-workflows-verified-before-runtime)
In raw shared-state graphs, a router branch that skips a required producer compiles cleanly under `tsc` and fails only in production when that branch is taken. AGBoss tracks typed `@Dto` handoffs across every route and catches structural workflow mistakes before execution with `npx agboss check`:
✓ Valid workflowPasses `agboss check`
* Readable, declarative flow with explicit typed `@Dto` handoffs.
* VIP orders route through `ApplyVipCouponAction` (`CouponDiscountDto`) before `RenderInvoiceAction`; standard orders route to `RenderStandardInvoiceAction`.
missing-input-fixed.workflow\.ts
```ts
@Workflow({
name: "missing-input-fixed-workflow",
version: "0.1.0",
flow: [
start(MissingInputFixedStart)
.returns(CheckoutRequestDto)
.toRouter(TierRouter)
.chooseOne(
// VIP path produces CouponDiscountDto first
route(ApplyVipCouponAction)
.returns(CouponDiscountDto)
.toAction(RenderInvoiceAction)
.returns(InvoiceSummaryDto)
.toFinish(MissingInputFixedFinish),
// Standard path needs only CheckoutRequestDto
route(RenderStandardInvoiceAction)
.returns(InvoiceSummaryDto)
.toFinish(MissingInputFixedFinish),
),
],
})
export class MissingInputFixedWorkflow {}
```
npx agboss check
```text
types: .agboss/types.d.ts
ok: cases/01-missing-input/agboss-fixed/application.ts
— assembly, environment, application, prompts, types
```
✗ Invalid workflowCaught before runtime
* Second branch skips `ApplyVipCouponAction` and routes straight to `RenderInvoiceAction` (which requires `CouponDiscountDto`).
* TypeScript alone does not catch the missing upstream input; `agboss check` reports the exact missing input and path.
missing-input-broken.workflow\.ts
```ts
@Workflow({
name: "missing-input-broken-workflow",
version: "0.1.0",
flow: [
start(MissingInputBrokenStart)
.returns(CheckoutRequestDto)
.toRouter(TierBrokenRouter)
.chooseOne(
route(ApplyVipCouponAction)
.returns(CouponDiscountDto)
.toAction(RenderInvoiceAction),
// Skips ApplyVipCouponAction (CouponDiscountDto)!
route(RenderInvoiceAction),
),
action(RenderInvoiceAction)
.returns(InvoiceSummaryDto)
.toFinish(MissingInputBrokenFinish),
],
})
export class MissingInputBrokenWorkflow {}
```
npx agboss check
```text
error [input.missing-on-path] missing-input-broken-workflow:
render-invoice-action.coupon (CouponDiscountDto)
is not produced on the path:
missing-input-broken-workflow-start
→ tier-broken-router → render-invoice-action
```
[**Run `02-path-scope-proof` on GitHub →**](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof) · [Read Path Scope & Provenance docs →](/concepts/path-scope/)
## Three core capabilities
[Section titled “Three core capabilities”](#three-core-capabilities)
AGBoss layers structured application architecture on top of [LangGraph](https://www.langchain.com/langgraph)’s durable execution runtime:
[1 · DESIGN](/concepts/decorators/)
### [Typed Components & Declarative Workflows](/concepts/decorators/)
[Structure agent systems from focused TypeScript classes instead of monolithic shared state. Wire services with constructor dependency injection (`@Injectable`) and connect `@Agent`, `@Router`, and `@Action` steps through explicit `@Dto` contracts.](/concepts/decorators/)
[Component Architecture →](/concepts/decorators/)[2 · VERIFY](/concepts/verification-funnel/)
### [Static Checking & Deterministic Testing](/concepts/verification-funnel/)
[Catch broken routes and test multi-agent behavior offline before calling a live model. Validate every workflow path at build time with `agboss check`, then run fast, deterministic unit and workflow tests with `testWith()` and recorded fixtures.](/concepts/verification-funnel/)
[Verification & Testing →](/concepts/verification-funnel/)[3 · OPERATE](/guides/error-handling-and-rollback/)
### [Reliable Execution & Recovery](/guides/error-handling-and-rollback/)
[Run long-lived workflows in production with built-in fault tolerance and cost controls. Roll back failed side effects with `compensateWith` sagas, enforce `@Budget` limits, and resume paused or interrupted runs from persistent state.](/guides/error-handling-and-rollback/)
[Production Reliability →](/guides/error-handling-and-rollback/)
## Runnable examples
[Section titled “Runnable examples”](#runnable-examples)
Every example in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) is verified offline with `npm run check`, `npm test`, and `npx tsx src/demo.ts`:
[STARTER APPLICATION](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss)
### [`01-hello-agboss`](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss)
[Minimal end-to-end starter demonstrating constructor dependency injection, `@Router` + `@Agent` + `@Tool` composition, `@Budget` caps, and deterministic offline tests.](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss)
[View 01-hello-agboss →](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss)[STATIC COMPILER PROOF](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)
### [`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)
[Side-by-side comparison of 4 structural workflow bugs (missing inputs, stale loop data, parallel conflicts, uncompensated actions) that pass `tsc` in raw graphs but are caught by `agboss check`.](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)
[View 02-path-scope-proof →](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)[SAGAS & RECOVERY](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)
### [`04-travel-booking-saga`](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)
[Multi-step flight, hotel, and payment workflow demonstrating automatic `compensateWith` rollbacks on failure, human approval pauses, and persistent crash recovery.](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)
[View 04-travel-booking-saga →](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)
[**Browse all 7 verified examples (including `03-support-refund-desk`, `05-cluster-work-stealing`, `06-enterprise-agent-mesh`, and `07-langgraph-migration`) →**](/examples/job-scout/)
## Start building with AGBoss
[Section titled “Start building with AGBoss”](#start-building-with-agboss)
AGBoss is open source under the **Apache-2.0** license and runs inside your own Node.js infrastructure with zero vendor lock-in.
[QUICKSTART](/getting-started/quick-start/)
### [Quickstart Guide](/getting-started/quick-start/)
[Scaffold a new project, wire typed components with dependency injection, and run your first static workflow check in minutes.](/getting-started/quick-start/)
[Start the Quickstart →](/getting-started/quick-start/)[FEATURE EXPLORER](/features/)
### [Interactive Feature Explorer](/features/)
[Inspect 37 interactive TypeScript code examples covering decorators, routing patterns, testing, and production controls.](/features/)
[Open Feature Explorer →](/features/)[APACHE-2.0 · OPEN SOURCE](https://github.com/agbossjs/agboss)
### [GitHub Repository](https://github.com/agbossjs/agboss)
[Explore the `@agboss/core` runtime, `@agboss/cli` static compiler, and executable proof suite on GitHub.](https://github.com/agbossjs/agboss)
[github.com/agbossjs/agboss →](https://github.com/agbossjs/agboss)
# About AGBoss
> Learn what AGBoss (Agent Graph Builder Open Source System) and HarnessDSL are, who maintains the Apache 2.0 TypeScript framework, and how to contribute.
**AGBoss** (**Agent Graph Builder Open Source System**) is an Apache 2.0 licensed TypeScript framework, static graph compiler (`agboss check`), and production runtime layered on top of [LangGraph](https://www.langchain.com/langgraph) (`npm i @agboss/core`). Inside AGBoss, **HarnessDSL (`HDSL`)** is the declarative graph grammar and verification specification (much like JSX in React or PSL in Prisma) that defines how `*.workflow.ts` `flow` chains and contract decorators specify narrow `@Dto` handoffs, `Path Scope`, `in-degree = 1` vs `in-degree >= 2` canonical topology, `{ about }` staleness tracking, quorums, and `compensateWith` / `committedBy` sagas.
It is designed for Senior, Staff, and Principal engineers and CTOs who build multi-agent systems scaling to hundreds of nodes with AI coding agents (Codex, Cursor, Windsurf, Copilot, Gemini CLI, Claude Code) and require compile-time guarantees, predictable token economics, and zero vendor lock-in.
## Why AGBoss & HarnessDSL (HDSL) exist
[Section titled “Why AGBoss & HarnessDSL (HDSL) exist”](#why-agboss--harnessdsl-hdsl-exist)
Engineering teams building production multi-agent systems face a fundamental architectural gap:
1. **Raw graph libraries are too unconstrained for AI coding agents at scale.** When a coding agent modifies a 50-node graph built around a shared mutable state bag (`Annotation.Root`) and arbitrary `(state) => string` edges, it cannot statically prove which upstream fields are populated, whether a review loop is reading stale state from a previous iteration, or whether an error branch bypassed a mandatory rollback action.
2. **Hosted agent SaaS platforms break enterprise data perimeters.** Routing proprietary prompts, customer PII, and durable execution state through a third-party cloud creates compliance blockers and vendor lock-in.
**AGBoss** (the framework, static compiler, and runtime) and its built-in **HarnessDSL (`HDSL`)** grammar solve both problems by bringing the compiler rigor of distributed-systems engineering and structured frameworks (Spring, Angular, NestJS) directly into your Node.js 22+ backend through [7 architectural principles](/concepts/architecture/):
* **Computational Role Segregation:** Distinct, single-responsibility decorators (`@Agent`, `@Router`, `@Arbiter`, `@Action`, `@Tool`, `@ToolInterceptor`) replace monolithic “god agents”.
* **Path-Scoped SSA Dataflow:** Every workflow path (`*.workflow.ts`) is verified by `agboss check`, which generates `.agboss/types.d.ts` with narrowed `Produced` inputs and rejects missing (`[input.missing-on-path]`), dead (`[flow.unused-output]`), or stale (`[input.outdated-on-path]`) state at build time.
* **Canonical Topology Isomorphism:** A strict 1-to-1 mapping between graph topology and code formatting (`in-degree = 1` inline chain, `in-degree >= 2` top-level `agent()`/`router()`/`action()` entry, one chained call per line) eliminates merge conflicts and structural entropy.
* **Prompt Prefix Cache Invariance:** Static system prompt verification (`[prompt.input-in-instructions]`) and deterministic HMAC pseudonymization (`PiiAction.replaceWithToken()`) preserve LLM KV-cache hit rates across turns while keeping raw PII out of external model providers.
* **Transactional LIFO Sagas & Idempotency:** Compensatable `@Action` nodes (`compensateWith`, `committedBy`) and deterministic `step.idempotencyKey` propagation guarantee clean rollback across errors, quorum `Surplus` aborts, and budget cutoffs.
* **Zero-Leakage DI & 4-Stage Scaling:** Class-inheritance DI inside a single `@Application` root scales seamlessly from in-memory unit/integration tests (`testWith(App)`, 10 unit runners) to SQLite WAL durability and multi-pod 30-second execution lease handoff.
* **Progressive Crystallization & Built-in Coding Skills:** Content-addressed revision snapshots (`agboss describe --json` with `configHash`, `promptHash`, `schemaHash`) let teams crystallize settled `DecidedBy.Model` paths into deterministic `DecidedBy.Code` and `@Action` nodes, while offline-synced `AGENTS.md` + 13 skill modules keep every coding agent aligned with the compiler.
## How AGBoss relates to LangGraph
[Section titled “How AGBoss relates to LangGraph”](#how-agboss-relates-to-langgraph)
AGBoss complements LangGraph rather than replacing it. Every `@Workflow` compiles down to a standard `@langchain/langgraph` state graph, and each `@Agent` runs its tool-calling loop as a compiled subgraph. You can inspect any AGBoss workflow in LangGraph Studio while keeping strict TypeScript DTO boundaries, constructor dependency injection, and built-in FinOps accounting.
## Open-source governance and licensing
[Section titled “Open-source governance and licensing”](#open-source-governance-and-licensing)
The entire AGBoss framework (`agboss`), CLI (`@agboss/cli`), documentation verification suite, and built-in AI coding skills (`AGENTS.md`, `CLAUDE.md`, and `.claude/skills/agboss/`) are published under the [Apache License 2.0](https://github.com/agbossjs/agboss/blob/main/LICENSE).
| Component | Package / Location | License | Purpose |
| ---------------------- | ---------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Core Framework | `agboss` | Apache-2.0 | Decorators, HarnessDSL (`HDSL`) flow syntax, DI container, FinOps, PII, SQLite stores, `@agboss/core/testing`, offline skills (`skills/agboss/`) |
| Command-Line Interface | `@agboss/cli` | Apache-2.0 | `agboss create`, `generate`, `check`, `describe`, `chat`, `run`, `eval`, `replay`, `golden`, `compare` |
| Reference Examples | `examples/job-scout`, `examples/flow-patterns` | Apache-2.0 | End-to-end multi-agent workflows and flow pattern blueprints |
| Documentation Site | [agboss.dev](https://agboss.dev) | Apache-2.0 | Compiler-checked guides, API reference, `/llms.txt`, and `/llms-full.txt` |
# Changelog & Release Notes
> Release history, architectural milestones, and npm package versioning (@agboss/core and @agboss/cli) for the AGBoss TypeScript agent framework.
This page tracks releases and architectural milestones of the AGBoss framework. Every code snippet in the AGBoss documentation is compiled and verified against the current public package exports by `check:docs` in CI.
## What is in AGBoss `v0.1.0` (Latest Release)
[Section titled “What is in AGBoss v0.1.0 (Latest Release)”](#what-is-in-agboss-v010-latest-release)
> **Release status:** `@agboss/core@0.1.0` and `@agboss/cli@0.1.0` are published on the public npm registry (`registry.npmjs.org`) under the `latest` dist-tag.
AGBoss is organized as two Apache 2.0 packages — `@agboss/core` (the core runtime, decorators, **HarnessDSL (`HDSL`)** flow syntax, SQLite stores, testing harness, and bundled coding-agent skills) and `@agboss/cli` (the `agboss` / `agb` CLI). Key capabilities in `v0.1.0` include:
| Area | Shipped Capabilities |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Application Root & Governance (`#261`, `#300`, `#388`, `#389`)** | `@Application` (`*.application.ts`) wires `workflows`, typed `environment` (`secret()`, `env()`, `env.optional()`), `providers`, mandatory `@Budget` classes, `piiPolicies`, `guardrails`, `toolInterceptors`, `exposes`, `observers`, `@HealthCheck` (`checkLiveness()`, `checkReadiness()`), `maxConcurrentSteps`, `@agboss/core/eslint` flat config, and `[name.kind-suffix]` enforcement on both `name` and class identifiers |
| **Path Scope HDSL (`#272`, `#289`)** | One-call-per-line `HarnessDSL` paths (`start(Start).returns(Dto).toAgent(Agent).toFinish(Finish)`), `.chooseOne(route())`, `.switch(caseOf())`, and compile-time Path Scope verification via generated `.agboss/types.d.ts` (`Produced`) |
| **Review Loops & Provenance (`#271`)** | `.returns(Dto, { about })`, `caseOf(Dto, { about })`, `optional(Dto)`, `historyOf(Dto, { limit })`, and runtime `outdatedValues` cascades (`[input.outdated-on-path]`) |
| **Parallel, Quorums & Transactional Sagas (`#273`, `#279`, `#339`, `#340`)** | `.toParallelAgents()`, `.toParallelWorkflows()`, `SpawnPolicy.Always` / `OnDemand`, `.acceptedBy().joinQuorum({ min, max, deadline })`, `.joinQuorumGroups(group(...).priority(1))`, `.onJoinError()`, declarative `compensateWith` / `committedBy` on `@Action` (`[flow.uncompensated-action]`), and automatic LIFO saga unwinding on cancellation, `FailFast`, quorum surplus, and limit abort |
| **Child, Recursive & Federated Mesh Flows (`#263`, `#333`, `#387`, `#398`, `#401`, `#457`)** | `.toWorkflow(ChildWorkflow, { placement })`, `.toParallelWorkflows(ChildWorkflow, { placement: ChildPlacement.AnyInstance })`, recursive `ThisWorkflow` (`maxDepth`), multi-tenant `a2aHttpListener(app, { expose, callerResolver })` (`A2ACallerResolver`, `A2ACallerRejectedError.wireCode`), `@A2AExpose`, typed `@A2AAgent({ target, input, output, price })`, signed Agent Mesh Envelope (`agboss-mesh` JWS propagating `origin`, `budget.remaining`, `deadline.remaining`, `callChain`), `agboss check --federation`, `agboss records export`, and `agboss hub show --html` / `agboss hub ui` |
| **Channels, Guardrails & Tool Interceptors (`#267`, `#337`, `#383`, `#384`)** | `@ChannelRole`, `@ReplyInterpreter`, `@Channel` (with `withdraw` on surplus quorum branches), atomic two-phase `app.resume({ pauseId, decision })`, `DecidedBy.Code` `@InputGuardrail` / `@OutputGuardrail` (`GuardrailHandler`, `GuardrailVerdict`), and cascading `@ToolInterceptor` (`BeforeToolCall` / `AfterToolCall`, `ToolCallDecision.replyToModel`) |
| **Model Providers, Custom Compaction & FinOps (`#202`, `#261`, `#266`, `#382`, `#385`, `#395`, `#454`)** | `@ModelProvider` pools (`OpenRouterProvider` with host/quantization pinning, `OpenAiCompatibleProvider`, custom `ModelProviderHandler.complete(request)` transports, and automatic yielding of claimed models by built-in catch-alls `OpenRouterModelProvider` / `DecisionsModelProvider`), `CompactionStrategy.Custom` (`CompactionHandler`), KV-cache prefix invariance (`[prompt.input-in-instructions]`), and pre-call `@Budget` reservation |
| **Durability, Distributed Child Work-Stealing & Multi-Instance (`#265`, `#268`, `#333`, `#360`, `#401`)** | 10 repository contracts (`Sqlite*Repository`, including `SqliteChildExecutionQueueRepository`, `SqliteExecutionHistoryRepository`, `SqliteMeshRequestRepository`), `ChildPlacement.AnyInstance` distributed child queue with parent lease yielding, shared tree concurrency slots in `RateLimitRepository`, per-child 30s leases & per-child crash recovery, cross-instance `app.cancel(threadId)`, CAS completion (`completeIfStatus`), 30s recovery sweeps, and `agboss describe --json` (`configHash`, `promptHash`, `schemaHash` join keys) |
| **Testing, Trajectories & Evals (`#262`, `#297`, `#395`, `#396`, `#397`, `#419`, `#420`, `#458`)** | `testWith(Application, { instances })`, `VirtualClock`, 10 unit runners (`testTool`, `testToolInterceptor`, `testAction`, `testRouter`, `testArbiter`, `testRag`, `testJudge`, `testCompaction`, `testPii`, `testGuardrail`), trajectory matchers (`toHaveCalledTools`, `toHaveToolCallMatching`), VCR `[vcr.stale-cassette]`, `@EvalSuite` (`pass@k` / `pass^k`, `--baseline --fail-on-regression`, Cohen’s $\kappa$ calibration, `--chaos`, `--metamorphic`, `generate eval-case --from-execution`, `agboss eval show --html`, `agboss eval ui`), `$0` `examples/flow-patterns`, and the 7 standalone verified examples in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) |
| **Built-in AI Skills (`#305`, `#424`)** | `npx --yes --package=@agboss/cli agboss create` scaffolds committed `AGENTS.md` and `CLAUDE.md` (`@AGENTS.md`), while `npm install` (`postinstall`), `agboss check`, and `agboss generate` auto-sync gitignored `.claude/skills/agboss/` (`SKILL.md` + 13 reference guides, including `13-migrating-from-langgraph.md`) offline from `node_modules/@agboss/core/skills/agboss/` |
## Package naming and version history
[Section titled “Package naming and version history”](#package-naming-and-version-history)
AGBoss publishes the official scoped [`@agboss/core`](https://www.npmjs.com/package/@agboss/core) and [`@agboss/cli`](https://www.npmjs.com/package/@agboss/cli) packages under the `agboss` npm organization on `registry.npmjs.org`:
* **`0.1.0` (Latest Published Release):** First substantive public release of the `@agboss/core` framework and `@agboss/cli` toolkit on `registry.npmjs.org`, verified against the 7-project executable proof suite in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples).
* **`0.0.1` (Historical npm Placeholder):** Initial package-name reservation published during repository setup; superseded by `0.1.0`.
When upgrading from earlier pre-release layouts that imported deprecated subpath entry points such as `@agboss/core/graph` or `@agboss/core/memory`, run `npx agboss migrate imports` to rewrite imports to the canonical entry points (`@agboss/core`, `@agboss/core/dto`, `@agboss/core/models`, `@agboss/core/units`, `@agboss/core/rag`, `@agboss/core/mcp`, `@agboss/core/a2a`, `@agboss/core/testing`, `@agboss/core/eval`, `@agboss/core/eslint`).
# CLI Reference
> Complete reference for the agboss CLI (@agboss/cli) — project scaffolding, component generators, static flow checking, interactive chat, and evaluation.
The [`@agboss/cli`](https://www.npmjs.com/package/@agboss/cli) package (paired with the [`@agboss/core`](https://www.npmjs.com/package/@agboss/core) runtime) provides the `agboss` command-line interface (short alias `agb`) for scaffolding projects (`agboss create`), generating and wiring components (`agboss generate`), generating `.agboss/types.d.ts` and checking assembly rules (`agboss check`), inspecting workflows (`agboss describe`), running interactive or one-shot turns (`agboss chat`, `agboss run`), and evaluating prompt profiles (`agboss eval`, `agboss compare`).
## How to scaffold projects and generate components
[Section titled “How to scaffold projects and generate components”](#how-to-scaffold-projects-and-generate-components)
Create a new project interactively or via flags in CI with `npm exec --yes --package=@agboss/cli@0.1.0 -- agboss create` (short alias `agb c`):
```bash
npm exec --yes --package=@agboss/cli@0.1.0 -- agboss create my-app
cd my-app
npm install
npm exec --yes --package=@agboss/cli@0.1.0 -- agboss c support-desk --yes --agents "triage:Sorts requests,answerer:Answers questions" --tools "answerer:search_orders" --mcp filesystem:/path/to/docs --rag notes --skip-install
```
Generate components into an existing workflow module with `agboss generate` (alias `agboss g`) using their full names (including kind suffix) or single-letter/short kind aliases (`a` = `agent`, `r` = `router`, `t` = `tool`, `ar` = `arbiter`, `m` = `mcp`, `oa` = `openapi`, `w` = `workflow`, `ec` = `eval-case`). The CLI enforces a strict **`1 class = 1 file`** layout, placing each `@Workflow` in `src/workflows//` alongside its `@WorkflowStart` and `@WorkflowFinish` files, prompt-capable components (`@Agent`, `@Router`, `@Judge`, `@Arbiter`, `@Compaction`, `@InputGuardrail` / `@OutputGuardrail`, `@ReplyInterpreter`) in their own component subdirectory (`src///..ts` + `.prompt.md`), and all DTOs flat in `src/dto/.dto.ts`:
```bash
agboss g a billing-agent --workflow support-desk-workflow --description "Handles invoices"
agboss g r escalation-router --workflow support-desk-workflow
agboss g t refund-tool --workflow support-desk-workflow --agent billing-agent
agboss g ar quorum-arbiter --workflow support-desk-workflow
agboss g m tickets-mcp --workflow support-desk-workflow --command tickets-mcp --tool search-tool --agent billing-agent
agboss g rag policies-rag --workflow support-desk-workflow --folder policies --agent billing-agent
agboss g oa pets --url ./pets.yaml --workflow support-desk-workflow --agent billing-agent
agboss g w onboarding-workflow
agboss g ec refund-edge-eval-case --suite support-desk-eval-suite --from-execution exec-9102
```
### Standard `1 class = 1 file` project layout
[Section titled “Standard 1 class = 1 file project layout”](#standard-1-class--1-file-project-layout)
```text
src/
├── .application.ts
├── environments/
│ ├── app.environment.ts
│ ├── dev.environment.ts
│ ├── prod.environment.ts
│ └── test.environment.ts
├── dto/ # All *Dto contracts (1 DTO class = 1 file)
│ └── .dto.ts
├── agents// # Group A: prompt-capable components (always in subfolder)
│ ├── .agent.ts
│ └── .prompt.md
├── routers//
│ ├── .router.ts
│ └── .prompt.md # (when DecidedBy.Model)
├── judges//
│ ├── .judge.ts
│ └── .prompt.md
├── arbiters//
│ ├── .arbiter.ts
│ └── .prompt.md
├── compactions//
│ ├── .compaction.ts
│ └── .prompt.md
├── guardrails//
│ ├── .guardrail.ts
│ └── .prompt.md
├── reply-interpreters//
│ ├── .reply-interpreter.ts
│ └── .prompt.md
├── workflows// # Each workflow folder owns its starts & finishes
│ ├── .workflow.ts
│ ├── .workflow-start.ts
│ └── .workflow-finish.ts
├── tools/ # Group B: flat inside their kind folder (1 class = 1 file)
│ └── .tool.ts
├── actions/
│ └── .action.ts
├── interceptors/
│ └── .interceptor.ts
├── services/
│ └── .service.ts
├── observers/
│ └── .observer.ts
├── channels/
│ ├── .channel-role.ts
│ └── .channel.ts
├── pii-policies/
│ └── .pii-policy.ts
├── rag/
│ └── .rag.ts
├── retrievers/
│ └── .retriever.ts
├── mcp/
│ ├── .mcp-server.ts
│ ├── .mcp-tool.ts
│ └── .mcp-expose.ts
├── a2a/
│ ├── .a2a-agent.ts
│ └── .a2a-expose.ts
├── model-providers/
│ └── .model-provider.ts
├── budgets/
│ └── .budget.ts
├── health-checks/
│ └── .health-check.ts
└── eval-suites/
└── .eval-suite.ts
```
## All `agboss` CLI commands
[Section titled “All agboss CLI commands”](#all-agboss-cli-commands)
| Command | What It Does |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `agboss create ` (`agboss c`) | Scaffolds a new AGBoss project with `@agboss/core` (`dependencies`), `@agboss/cli` (`devDependencies`), `@Application`, workflow, environments, `eslint.config.js` (`@agboss/core/eslint`), `.agboss/types.d.ts`, `AGENTS.md`, `CLAUDE.md`, 13 AI coding skills, and Vitest tests |
| `agboss generate ` (`agboss g`) | Generates and wires an `agent`, `router`, `tool`, `arbiter`, `mcp`, `rag`, `workflow`, `openapi` client, or `eval-case` (`--from-execution `) |
| `agboss check [--application ]` | Writes `.agboss/types.d.ts`, syncs `.claude/skills/agboss/` (`SKILL.md` + 13 reference modules), and validates all flow, assembly, environment, and prompt rules without calling models |
| `agboss check --strict` | Treats analyser warnings (`[flow.path-too-deep]`, `[judge.same-model-family]`) and assembly warnings (`[application.in-memory-cost-ledger]`, `[application.in-memory-child-queue]`) as errors |
| `agboss check --models` | Verifies every model setting across the application against its resolved `@ModelProvider` |
| `agboss check --cassettes` | Verifies recorded VCR cassettes against current `promptHash` and `schemaHash` fingerprints (`[vcr.stale-cassette]`) |
| `agboss check --federation ` | Verifies cross-repository `@A2AAgent` / `@McpServer` contracts against snapshots from `agboss describe --json` (`[federation.*]`) |
| `agboss dev` / `agboss studio` | Watches `src/` to regenerate `.agboss/types.d.ts` on save (`dev`), or launches LangGraph Studio for every workflow and child subgraph (`studio`) |
| `agboss describe [--application ]` | Prints paths, node inputs by producer, `{ about }` links, `historyOf` limits, model → provider resolution, budgets, and step limits |
| `agboss describe --json` | Exports the full declarative revision snapshot (`configHash`, `maxConcurrentSteps`, `@Action` `compensateWith` / `committedBy` links, per-agent `promptHash`, per-DTO `schemaHash`, and `application.contracts.imports` / `exports`) |
| `agboss chat` / `agboss run` | Starts an interactive terminal chat (`--thread`, `--profile`, `--env`) or executes a single task on the `@Application` and prints the output and spend summary |
| `agboss eval score` / `agboss replay` | Scores recent `Tern`s (`--start`, `--version`, `--limit`, `--profile`) or replays a prompt version against recorded tasks |
| `agboss eval run --suite ` | Runs an `@EvalSuite` across `--runs ` trials (`pass@k` and `pass^k`), `--chaos --chaos-seed `, `--metamorphic`, `--report `, `--baseline --fail-on-regression`, and `--html ` |
| `agboss eval calibrate --report ` | Computes pairwise **Cohen’s kappa ($\kappa$)**, confusion matrices, disagreements, and metric rubrics between `@Judge` verdicts and human `EvalLabel` JSONL annotations (`--labels `) |
| `agboss eval show [--html ]` / `agboss eval ui` | Re-renders a saved `EvalRunReport` artifact into a self-contained static HTML page without calling models, or launches the local interactive Eval UI workbench (`127.0.0.1`) |
| `agboss records export` | Exports completed `ExecutionRecord`s from `ExecutionHistoryRepository` (`--tenant ` or `--all-tenants`, `--since`, `--until`) to `--out ` |
| `agboss hub show [--html ]` / `agboss hub ui ` | Stitches `describe --json` snapshots, `.jsonl` execution records, and eval reports into the **AGBoss Mesh Hub** (Global Agent Mesh Graph, Cross-Service Execution Trees, Progressive Crystallization Radar, and Blast Radius) as a static HTML page or local UI server (`127.0.0.1`) |
| `agboss golden add --name ` | Saves recent real tasks as a reusable golden evaluation dataset |
| `agboss compare --profiles base,
` | Compares YAML configuration profiles side by side against a golden set |
| `agboss rag index` | Builds or updates `@Rag` knowledge-base indexes |
| `agboss migrate imports` | Rewrites imports from deprecated sub-entries to current AGBoss entries |
## Key static analyzer diagnostics (`agboss check`)
[Section titled “Key static analyzer diagnostics (agboss check)”](#key-static-analyzer-diagnostics-agboss-check)
`agboss check` verifies structural, dataflow, security, and governance invariants before a single token is spent:
| Rule Code | What It Catches |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `[name.kind-suffix]` | A decorated component’s `name` option (`"-agent"`, `"-budget"`, `"-tool-interceptor"`) or its TypeScript class identifier (`*Agent`, `*Budget`, `*ToolInterceptor`) is missing its kind suffix. |
| `[input.missing-on-path]` & `[input.outdated-on-path]` | A downstream node reads a DTO that is not produced on every incoming branch or was invalidated by a review loop’s `{ about: Dto }` without being re-produced. |
| `[flow.uncompensated-action]` & `[graph.limit-handler-uses-model]` | A side-effecting `@Action` lacks `compensateWith` / `committedBy`, or a resource-limit `.onError()` handler routes to an LLM node instead of a deterministic `@Action` / `@WorkflowFinish`. |
| `[prompt.input-in-instructions]` | Runtime `input` fields are interpolated into `prompt` / `promptUrls` (which would break LLM KV prefix caching). |
| `[compaction.missing-handler]` & `[compaction.invalid-setting]` | A `CompactionStrategy.Custom` class omits `compact(input)`, or sets model-only options (`model`, `prompt`, `summaryQueueLength`, `summaryMaxTokens`). |
| `[guardrail.missing-handler]` & `[tool-interceptor.missing-handler]` | A `DecidedBy.Code` guardrail omits `check(text)`, or a `@ToolInterceptor` implements neither `BeforeToolCall` nor `AfterToolCall`. |
| `[model-provider.both-modes]` | A `@ModelProvider` mixes HTTP `baseUrl` / `apiKey` options with a custom `ModelProviderHandler.complete(request)` implementation. |
| `[vcr.stale-cassette]` & `[judge.same-model-family]` | A VCR cassette’s recorded `promptHash` / `schemaHash` no longer matches current code, or a `DecidedBy.Model` `@Judge` uses the same model family as the judged `@Agent`. |
| `[flow.conflicting-placement]` & `[application.in-memory-child-queue]` | Two paths to the same child workflow specify different `ChildPlacement` modes, or `ChildPlacement.AnyInstance` is used without a shared `ChildExecutionQueueRepository` in `@Application({ providers })`. |
| `[federation.incompatible-schema]` & `[federation.missing-target]` | Cross-service `@A2AAgent` / `@McpServer` DTO schemas drifted from the producer’s `@A2AExpose` / `@McpExpose` `schemaHash` in `agboss describe --json`. |
## Exporting a declarative revision snapshot with `agboss describe --json`
[Section titled “Exporting a declarative revision snapshot with agboss describe --json”](#exporting-a-declarative-revision-snapshot-with-agboss-describe---json)
Running `agboss describe --json --application ` dumps the complete machine-readable anatomy (`{ application, workflows[], schemas }`) of an application revision without booting model providers:
* **Application & graph topology (`configHash`):** Deterministic hash of the workflow graph, `starts`, `finishes`, `edges`, `onError` handlers, `maxConcurrentSteps`, `compensationTimeout`, and versioned child workflow registrations (`[ChildWorkflowV1, ChildWorkflowV2]`) so sleeping pauses can be verified and resumed across rolling releases.
* **Component & saga metadata (`promptHash`, `schemaHash`, `compensateWith`, `committedBy`):** Full configuration of every `@Agent`, `@Router`, `@Arbiter`, `@Action`, and `@Tool` in `workflows[].nodes[]`, including individual SHA-256 `promptHash` and `schemaHash` fingerprints and saga compensation edges.
* **DTO schema registry & trace join keys:** Every DTO class, field decorator (`@Text`, `@Integer`, `@Float`, `@Enum`, `@List`), constraint, and `schemaHash` in `schemas` joins directly against runtime `ExecutionRecord` (`WorkflowRecord.configHash`), `ComponentRecord` (`{ name, kind, promptHash, schemaHash }`), and `StepRecord` (`{ toolLoopIteration, validationAttempts, branch: { verdict, status } }`) spans in Langfuse / OpenTelemetry to measure real branch, tool, and DTO field coverage.
## AI coding-agent skills and machine-readable docs
[Section titled “AI coding-agent skills and machine-readable docs”](#ai-coding-agent-skills-and-machine-readable-docs)
AGBoss is built for Senior/Staff engineers steering AI coding agents (OpenAI Codex, Cursor, Windsurf, GitHub Copilot, Gemini CLI, Aider, and Claude Code):
* **Universal project entrypoints (`AGENTS.md` & `CLAUDE.md`):** `agboss create ` generates root `AGENTS.md` and a one-line `CLAUDE.md` (`@AGENTS.md`) once and commits them to Git. They are never overwritten, allowing you to add project-specific domain rules alongside the AGBoss skill routing table.
* **Offline, gitignored skill suite (`SKILL.md` + `references/01..13.md`):** Bundled directly inside the `@agboss/core` npm package (`node_modules/@agboss/core/skills/agboss/`) and automatically synced into `.claude/skills/agboss/` (`SKILL.md`, `references/01-composition-and-architecture.md` .. `13-migrating-from-langgraph.md`, and `.agboss-skills.json`) whenever `npm install` (`"postinstall": "npm run types"`, where `"types": "agboss check"`), `agboss check`, or `agboss generate` runs. Opt out anytime via `"agboss": { "skills": false }` in `package.json`. See [AI-Assisted Engineering & Coding Skills](/concepts/ai-coding-skills/) for the full architecture.
* **Machine-readable documentation:** Every documentation page on `agboss.dev` is published in [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt) for instant context ingestion.
# Community
> Join the AGBoss open-source community — report issues, contribute TypeScript agent features, and follow updates on GitHub, X, and LinkedIn.
AGBoss is developed in the open under the Apache 2.0 license for TypeScript engineers building production multi-agent systems on LangGraph. Ask architecture questions, report issues, propose new flow patterns, or submit pull requests directly on GitHub.
## How to get help
### [GitHub Issues](https://github.com/agbossjs/agboss/issues)
[Found a bug, hit a compiler rule, or need a feature? Open an issue — every change on the roadmap starts there.](https://github.com/agbossjs/agboss/issues)
[Browse issues →](https://github.com/agbossjs/agboss/issues)
### [Documentation](/getting-started/introduction/)
[Complete guides for @Application, HarnessDSL (HDSL) Path Scope, DI, FinOps budgets, and the agboss CLI.](/getting-started/introduction/)
[Read the docs →](/getting-started/introduction/)
### [Runnable Examples](/examples/job-scout/)
[Explore the 7 standalone applications in agbossjs/agboss-examples alongside job-scout and flow-patterns.](/examples/job-scout/)
[Inspect examples →](/examples/job-scout/)
## How to contribute
### [Framework repository](https://github.com/agbossjs/agboss)
[The @agboss/core package, @agboss/cli, check:docs verifier, and example applications, Apache 2.0 licensed.](https://github.com/agbossjs/agboss)
[agbossjs/agboss →](https://github.com/agbossjs/agboss)
### [Documentation website](https://github.com/agbossjs/agboss.dev)
[Every page on agboss.dev has an edit link and compiles its TypeScript snippets against the framework.](https://github.com/agbossjs/agboss.dev)
[agbossjs/agboss.dev →](https://github.com/agbossjs/agboss.dev)
### [Roadmap](/roadmap/)
[See what shipped in v0.1.0 and what is currently in progress.](/roadmap/)
[See the roadmap →](/roadmap/)
## Join the conversation
[X](https://x.com/agbossjs)
### [@agbossjs](https://x.com/agbossjs)
[Release notes, HarnessDSL (HDSL) patterns, and engineering updates.](https://x.com/agbossjs)
[LinkedIn](https://www.linkedin.com/company/agbossjs)
### [AGBoss](https://www.linkedin.com/company/agbossjs)
[Deep-dive architecture articles and enterprise announcements.](https://www.linkedin.com/company/agbossjs)
[GitHub](https://github.com/agbossjs/agboss)
### [agbossjs/agboss](https://github.com/agbossjs/agboss)
[Star the repository to follow new releases.](https://github.com/agbossjs/agboss)
# AI-Assisted Engineering & Coding Skills (AGENTS.md)
> How AGBoss keeps AI coding agents (Codex, Cursor, Windsurf, Copilot, Claude Code) in sync via AGENTS.md, .agboss/types.d.ts, and 13 offline skill modules.
In modern engineering teams, Senior, Staff, and Principal engineers rarely type multi-agent boilerplate by hand. Instead, they act as **systems architects** who specify domain boundaries, DTO contracts, and failure invariants while **AI coding agents — OpenAI Codex, Cursor, Windsurf, GitHub Copilot, Gemini CLI, Cline, Aider, or Claude Code —** generate and refactor the implementation.
However, pre-trained coding models do not know AGBoss or **HarnessDSL** out of the box. Left unguided, they fall back to legacy orchestration habits: shared mutable state bags, `new MyDto()` instantiation, raw numbers instead of `seconds()` / `usd()`, `as any` casts, or multi-call lines in `*.workflow.ts` that fail Path Scope compilation.
AGBoss solves this in a **tool-agnostic, cross-agent way** by bundling an authoritative, compiler-verified **AI Skill Suite** (`AGENTS.md` + 13 progressive-disclosure reference modules) directly inside the `@agboss/core` npm package and automatically keeping the reference modules in sync with your installed framework version.
***
## How coding agents verify graphs in practice
[Section titled “How coding agents verify graphs in practice”](#how-coding-agents-verify-graphs-in-practice)
Every rule in `AGENTS.md` and the 13 skill reference modules maps directly to a compile-time check in `agboss check` and a deterministic offline test in `@agboss/core/testing`. When your coding agent scaffolds or edits a workflow, it validates the graph and runs a deterministic Vitest test in milliseconds:
src/triage/triage.workflow\.ts
```ts
import {
Action,
Agent,
Application,
DecidedBy,
Router,
Workflow,
WorkflowFinish,
WorkflowStart,
route,
start,
type ActionHandler,
type InputOf,
type OutputOf,
type Route,
type RouterHandler,
} from "@agboss/core";
import { Integer, Text } from "@agboss/core/dto";
export class TicketDto {
@Text({ prompt: "Support ticket ID" })
ticketId!: string;
@Integer({ min: 1, max: 5, prompt: "Priority 1 (low) to 5 (critical)" })
priority!: number;
}
export class ResolutionDto {
@Text({ prompt: "Resolution summary" })
summary!: string;
}
@WorkflowStart({
name: "ticket-workflow-start",
description: "Accepts an incoming support ticket",
input: TicketDto,
})
export class TicketStart {}
@WorkflowFinish({
name: "ticket-workflow-finish",
description: "Returns the ticket resolution",
output: ResolutionDto,
})
export class TicketFinish {}
@Agent({
name: "escalation-agent",
description: "Drafts an incident response for high-priority tickets",
prompt: "Draft an immediate escalation plan for the critical ticket.",
model: "moonshotai/kimi-k2.6",
input: { ticket: TicketDto },
output: ResolutionDto,
})
export class EscalationAgent {}
@Action({
name: "auto-ack-action",
description: "Acknowledges standard-priority tickets deterministically",
input: { ticket: TicketDto },
output: ResolutionDto,
})
export class AutoAckAction implements ActionHandler {
execute(input: InputOf): Promise> {
return Promise.resolve({
summary: `Auto-acknowledged ${input.ticket.value.ticketId}`,
});
}
}
@Router({
name: "priority-router",
description: "Routes critical tickets to EscalationAgent and others to AutoAckAction",
decidedBy: DecidedBy.Code,
input: { ticket: TicketDto },
routes: [{ target: EscalationAgent }, { target: AutoAckAction }],
})
export class PriorityRouter implements RouterHandler {
route(input: InputOf): Route {
return input.ticket.value.priority >= 4 ? EscalationAgent : AutoAckAction;
}
}
@Workflow({
name: "ticket-triage-workflow",
version: "1.0.0",
flow: [
start(TicketStart)
.returns(TicketDto)
.toRouter(PriorityRouter)
.chooseOne(
route(EscalationAgent)
.returns(ResolutionDto)
.toFinish(TicketFinish),
route(AutoAckAction)
.returns(ResolutionDto)
.toFinish(TicketFinish),
),
],
})
export class TicketTriageWorkflow {}
@Application({
name: "ticket-triage-application",
version: "1.0.0",
environment: EmptyEnvironment,
workflows: [TicketTriageWorkflow],
budgets: [TriageAppBudget, TriageTenantBudget, TriageExecBudget],
})
export class TicketTriageApplication {}
```
test/triage.workflow\.test.ts
```ts
import { expect } from "vitest";
import { replyWith, testRouter, testWith } from "@agboss/core/testing";
import {
AutoAckAction,
EscalationAgent,
PriorityRouter,
ResolutionDto,
TicketStart,
TicketTriageApplication,
} from "../src/triage/triage.workflow.js";
const test = testWith(TicketTriageApplication);
test("routes critical tickets to EscalationAgent and unit-tests PriorityRouter", async ({
app,
mockLlm,
}) => {
const runRouter = testRouter(PriorityRouter);
const { output: selectedRoute } = await runRouter({
ticket: {
value: { ticketId: "T-500", priority: 5 },
producer: TicketStart,
},
});
expect(selectedRoute).toBe(EscalationAgent);
mockLlm(EscalationAgent).thenReturn(
replyWith(ResolutionDto, { summary: "Paged on-call SRE for T-500" }),
);
const result = await app.execute(TicketStart, {
ticketId: "T-500",
priority: 5,
});
expect(result.output).toEqual({ summary: "Paged on-call SRE for T-500" });
const lowPriority = await app.execute(TicketStart, {
ticketId: "T-100",
priority: 2,
});
expect(lowPriority.output).toEqual({
summary: "Auto-acknowledged T-100",
});
expect(AutoAckAction).toBeDefined();
});
```
***
## How universal skills are scaffolded and synced
[Section titled “How universal skills are scaffolded and synced”](#how-universal-skills-are-scaffolded-and-synced)
When you scaffold a new project with [`agboss create`](/cli/):
```bash
cd /tmp && npm exec --yes --package=@agboss/cli@0.1.0 -- agboss create my-app --yes
```
AGBoss sets up a two-tier, tool-agnostic instruction architecture in your repository:
### 1. Universal `AGENTS.md` Entrypoint (Codex, Cursor, Windsurf, Copilot, Gemini, Aider, Claude)
[Section titled “1. Universal AGENTS.md Entrypoint (Codex, Cursor, Windsurf, Copilot, Gemini, Aider, Claude)”](#1-universal-agentsmd-entrypoint-codex-cursor-windsurf-copilot-gemini-aider-claude)
`agboss create` generates two files in your repository root that are **committed to Git** and **created only once** (never overwritten by subsequent upgrades or syncs):
* **`AGENTS.md` (Universal Standard):** The open cross-agent standard read natively by **OpenAI Codex, Cursor, Windsurf, GitHub Copilot, Gemini CLI, Cline, Roo Code, and Aider**. It contains your project-specific architectural rules and a progressive-disclosure routing table pointing to the 13 framework reference modules. Because AGBoss never overwrites `AGENTS.md`, your team can freely append domain-specific conventions or links for your preferred IDE/agent.
* **`CLAUDE.md` (1-Line Bridge):** A single-line pointer file (`@AGENTS.md`) so Claude Code loads the exact same `AGENTS.md` instructions without duplicating a single rule.
### 2. Offline-Synced Progressive-Disclosure Reference Modules
[Section titled “2. Offline-Synced Progressive-Disclosure Reference Modules”](#2-offline-synced-progressive-disclosure-reference-modules)
Why not dump all 50 pages of framework documentation directly into `AGENTS.md`? Because loading 50 pages of docs on every prompt bloats your coding agent’s context window and degrades code quality.
Instead, the 13 modular reference guides live inside `node_modules/@agboss/core/skills/agboss/` (shipped offline inside the `@agboss/core` npm package) and are automatically synced into `.claude/skills/agboss/` (the standard skill discovery path also referenced directly by `AGENTS.md` so Cursor, Codex, Windsurf, Copilot, and Gemini CLI read the exact same markdown files on demand):
```text
my-app/
├── AGENTS.md # Committed once — universal entrypoint (Codex, Cursor, Windsurf, Copilot, Gemini, Aider)
├── CLAUDE.md # Committed once — 1-line `@AGENTS.md` bridge for Claude Code
├── .agboss/
│ └── types.d.ts # Gitignored — generated Path Scope & Produced types
└── .claude/
└── skills/
└── agboss/ # Gitignored — offline-synced from installed `@agboss/core` package
├── SKILL.md # Root dispatcher & 10 non-negotiable invariants
├── .agboss-skills.json # Synced package version stamp & integrity metadata
└── references/
├── 01-composition-and-architecture.md
├── 02-flow-dsl-all-combinations.md
├── 03-components-di-and-lifecycle.md
├── 04-mcp-and-a2a.md
├── 05-channels-and-human-in-the-loop.md
├── 06-finops-providers-and-queues.md
├── 07-guardrails-pii-and-security.md
├── 08-observability-and-tracing.md
├── 09-persistence-k8s-and-deploy.md
├── 10-testing-vcr-and-evals.md
├── 11-cli-studio-and-error-catalog.md
├── 12-end-to-end-recipes.md
└── 13-migrating-from-langgraph.md
```
Why is the synced skill directory gitignored instead of committed?
* **Zero version drift across upgrades:** Every time you upgrade `@agboss/core` and run `npm install` (via `"postinstall": "npm run types"`, where `"types": "agboss check"`), `npx agboss check`, or `npx agboss generate`, the CLI compares the installed `@agboss/core` version against `.agboss-skills.json` and updates the 14 skill files in place.
* **No noisy 14-file PR diffs:** Upgrading `@agboss/core` in `package.json` never pollutes your pull requests with documentation diffs, and accidental local edits to synced skill files are automatically detected (`[skills.local-edit]`) and restored to match the installed runtime.
* **100% offline & supply-chain safe:** `agboss check` never makes network calls to download instructions that steer coding agents; skills always match the exact version of `@agboss/core` installed in `node_modules`.
### Opting out of auto-sync
[Section titled “Opting out of auto-sync”](#opting-out-of-auto-sync)
For environments that prefer not to write synced skill files into the workspace, disable skill syncing in `package.json` (your coding agent can still read `node_modules/@agboss/core/skills/agboss/` directly):
```json
{
"name": "my-app",
"agboss": {
"application": "src/my-app.application.ts",
"skills": false
}
}
```
***
## Inside the 13 modular skill references
[Section titled “Inside the 13 modular skill references”](#inside-the-13-modular-skill-references)
`AGENTS.md` and `SKILL.md` act as a **context-efficient router**. They define the 10 core invariants and instruct your coding agent (whether Cursor, Codex, Windsurf, Copilot, Gemini CLI, or Claude Code) to open only the specific `references/01..13` file relevant to the current task:
| Reference File | Loaded By Coding Agent When… | Key Contracts Covered |
| -------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SKILL.md` | Starting any task in an AGBoss repository | 10 core invariants, CLI workflow (`generate -> check -> describe -> test`), ban on `as any` / `new Dto()` / component state, 1-call-per-line rule |
| `01-composition-and-architecture.md` | Designing workflows, splitting graphs, or authoring DTOs | `@Agent` vs `@Action` vs `@Tool`, `DecidedBy.Code` vs `DecidedBy.Model`, `@Text`/`@Integer`/`@Flag`/`@OneOf`/`@ListOf`/`@Nested`, narrow DTOs vs God DTOs |
| `02-flow-dsl-all-combinations.md` | Writing or editing `*.workflow.ts` flow paths & error handlers | `start()`, `.returns(Dto, { about })`, `.switch(caseOf())`, `.chooseOne(route())`, `historyOf()`, `.toParallelAgents()`, `.joinQuorum()`, `ThisWorkflow`, `.onError()` hierarchy |
| `03-components-di-and-lifecycle.md` | Implementing components, DI services, lifecycle hooks, or `AppEnvironment` | `@Application`, class-inheritance DI, `OnInit -> OnStart -> OnStop -> OnDestroy`, `ExecutionContext`, `SpendRecorder`, `JudgeModel`, `@ToolInterceptor`, `CompactionStrategy.Custom`, `secret()`, `env()`, `seconds()`, `usd()` |
| `04-mcp-and-a2a.md` | Integrating external MCP servers, A2A remote agents, or Agent Mesh | `@McpServer`, `@McpTool`, `@McpExpose`, `createMcpService`, `@A2AAgent`, `a2aHttpListener` (`callerResolver`), `MeshEnvelope` (`agboss-mesh`), `$ agboss check --federation` |
| `05-channels-and-human-in-the-loop.md` | Adding human approvals, pauses, or webhook resume endpoints | `@ChannelRole`, `@ReplyInterpreter`, `@Channel`, `ChannelOutcome.awaitReply()`, `withdraw()`, `result.pending`, `app.resume({ pauseId, reply })` |
| `06-finops-providers-and-queues.md` | Configuring budgets, model providers, rate limits, or circuit breakers | `@Budget` (`Application`, `Tenant`, `Execution`), `@ModelProvider` (`OpenRouterProvider`, `ModelProviderHandler`), `PromptCaching`, `QueueBalancingStrategy.TenantRoundRobin`, shared 429 cooldowns |
| `07-guardrails-pii-and-security.md` | Implementing input/output guardrails (`DecidedBy.Model` / `DecidedBy.Code`) or PII masking | `@InputGuardrail`, `@OutputGuardrail`, `GuardrailHandler`, `GuardrailDecision`, `@PiiPolicy`, deterministic HMAC `PiiAction.replaceWithToken()` |
| `08-observability-and-tracing.md` | Writing `On*` observers, inspecting `result.record`, or Langfuse | 23 `On*` observer interfaces (`OnExecutionEnd`, `OnSpend`, `OnValuesOutdated`, `OnQuorumDecided`), `result.record.pauses`, `scripts/langfuse.sh` |
| `09-persistence-k8s-and-deploy.md` | Configuring SQLite/Postgres stores, leases, or Kubernetes probes | 7 repository contracts, `Sqlite*` WAL providers, 30s execution leases, `SIGTERM` drain, `@HealthCheck`, `checkLiveness()`, `checkReadiness()`, `configHash` rollouts |
| `10-testing-vcr-and-evals.md` | Writing unit tests, trajectory assertions, `@EvalSuite`, or golden evals | `testWith(App, { instances: 2, concurrency: TestConcurrency.inOrder([...]) })`, `toHaveCalledTools`, `toHaveToolCallMatching`, `VirtualClock`, all 10 unit runners (`testTool` .. `testToolInterceptor`), `@EvalSuite` (`pass@k` / `pass^k`, Cohen’s $\kappa$) |
| `11-cli-studio-and-error-catalog.md` | Running CLI commands or fixing `[rule.code]` compiler diagnostics | Complete catalog of `[flow.*]`, `[graph.*]`, `[input.*]`, `[router.*]`, `[di.*]`, `[environment.*]`, `[prompt.*]`, and `[judge.*]` diagnostics with before/after fixes |
| `12-end-to-end-recipes.md` | Scaffolding a complete multi-file feature from scratch | 4 complete multi-file blueprints: review loops with `{ about }`, parallel quorums with `@Channel`, MCP + A2A agents, and multi-tenant Kubernetes apps |
| `13-migrating-from-langgraph.md` | Migrating an existing raw LangGraph (`StateGraph` / `Annotation.Root`) codebase to AGBoss | 3-step migration playbook: decomposing `StateAnnotation` into a read/write matrix and narrow `@Dto` classes, 1-to-1 primitive translation table, and uncovering latent LangGraph bugs via `npx agboss check` |
***
## Inside `13-migrating-from-langgraph.md`: The 3-Step LangGraph Migration Playbook
[Section titled “Inside 13-migrating-from-langgraph.md: The 3-Step LangGraph Migration Playbook”](#inside-13-migrating-from-langgraphmd-the-3-step-langgraph-migration-playbook)
When an engineer or coding agent migrates an existing raw LangGraph (`StateGraph`, `Annotation.Root`) service to AGBoss, `13-migrating-from-langgraph.md` guides a deterministic 3-step transformation:
1. **Step 1 — Decompose `StateAnnotation` via a Read/Write Matrix into narrow `@Dto` classes:** Instead of a single 30-field optional `Annotation.Root({...})` state bag where every node can read or overwrite any field, the agent builds a node-by-node read/write matrix and extracts cohesive, non-optional `@Dto` classes (`OrderQueryDto`, `PolicyVerdictDto`, `RefundReceiptDto`) governed by [Path Scope](/concepts/path-scope/).
2. **Step 2 — 1-to-1 Primitive Translation Table:**
| Raw LangGraph Construct | AGBoss Equivalent |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `new StateGraph(StateAnnotation).addNode("x", fn)` | [`@Agent`](/reference/agent/) (LLM turn) or [`@Action`](/reference/action/) (deterministic TypeScript with `compensateWith`) |
| `addConditionalEdges("x", routerFn, map)` | [`@Router`](/reference/router/) (`DecidedBy.Code` or `DecidedBy.Model`) + `.chooseOne(route(...))` or `.switch(caseOf(...))` |
| `new Send("worker", item)` map-reduce | `.toParallelAgents()` or `.toParallelWorkflows()` with `maxItems`, `maxConcurrency`, and `.join()` / `.joinQuorum()` |
| `interrupt(payload)` + `Command({ resume })` | [`@ChannelRole`](/reference/channel/) + `ChannelOutcome.awaitReply()` + two-phase `app.resume({ pauseId, decision })` |
| `PostgresSaver` / `SqliteSaver` checkpointer | [7 repository contracts](/guides/persistence-and-scaling/) (`SqliteCheckpointRepository`, `ExecutionLeaseRepository`, etc.) |
3. **Step 3 — Expose hidden bugs in the legacy graph with `npx agboss check`:** Running `npx agboss check` on the migrated topology immediately catches latent bugs that raw LangGraph only surfaces in production: branches where a downstream node read a field that was never populated (`[input.missing-on-path]`), review loops where an agent read a stale draft from 2 iterations ago (`[input.outdated-on-path]`), side-effecting nodes lacking rollback handlers (`[flow.uncompensated-action]`), and dead state writes (`[flow.unused-output]`). For a complete runnable side-by-side comparison (`before-langgraph/support.graph.ts` vs `after-agboss/src/`), inspect **[`07-langgraph-migration`](https://github.com/agbossjs/agboss-examples/tree/main/07-langgraph-migration)** in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples).
***
## Closed-loop workflow for any coding agent
[Section titled “Closed-loop workflow for any coding agent”](#closed-loop-workflow-for-any-coding-agent)
Because every rule in the skill suite corresponds to a deterministic check in `agboss check` and `tsc`, any coding agent operates inside a tight, self-correcting feedback loop:
1. **Scaffold with CLI generators:** The coding agent runs `npx agboss generate agent reviewer --workflow src/offer/offer.workflow.ts` (or `router`, `arbiter`, `tool`, `mcp`, `rag`, `openapi`) to create properly named files and wire imports without boilerplate typos.
2. **Compile Path Scope & generate `.agboss/types.d.ts`:** The agent runs `npx agboss check --strict`. If a path is missing an upstream DTO (`[input.missing-on-path]`), reads a stale loop value (`[input.outdated-on-path]`), or interpolates an input into a system prompt (`[prompt.input-in-instructions]`), `agboss check` outputs the exact `[rule.code]` diagnostic.
3. **Typecheck narrowed `Produced` contracts:** With `.agboss/types.d.ts` updated, `npm run typecheck` (`tsc --noEmit`) verifies every `@Action`, `@Router`, and `@WorkflowFinish` handler against the exact upstream producer union.
4. **Verify deterministically offline:** The agent writes isolated unit tests (`testTool`, `testRouter`, `testArbiter`, `testJudge`) and full workflow tests (`testWith(App)`) and runs `npm test` in milliseconds with zero network calls.
5. **CI documentation guarantee (`check:docs`):** Inside the `agboss` repository itself, `npm run check:docs` compiles every TypeScript snippet in the skill references and documentation against the built framework, ensuring the skills given to your coding agent can never drift from the actual TypeScript API.
# @Application & Runtime
> Assemble workflows, typed environments, model providers, budgets, PII policies, channels, health checks, and 4-stage storage scaling in a single @Application root.
Every AGBoss service has a single `@Application` assembly root that lists the workflows, typed environment contract, shared `@ModelProvider` classes, mandatory `@Budget` ceilings (application, tenant, and execution scopes), `@PiiPolicy` rules, `@InputGuardrail` / `@OutputGuardrail` policies, `observers`, `@HealthCheck` probes, and storage repositories. Both `createApp(Application, options)` and `testWith(Application)` boot this container, validating every contract at startup before any workflow executes.
## How to declare an `@Application` and its environment contract
[Section titled “How to declare an @Application and its environment contract”](#how-to-declare-an-application-and-its-environment-contract)
An application’s environment contract is a plain class declaring field types only (`environments/app.environment.ts` next to the application file — never place `secret()` or `env()` property initializers on the class itself, which triggers `[environment.contract-value]`). Each environment file (`dev.environment.ts`, `prod.environment.ts`, `test.environment.ts`) exports a typed `environment` object using `env()`, `env.optional()`, `env.integer()`, `env.flag()`, and `secret()`:
src/audit/audit.application.ts
```ts
import { AppEnvironment } from "./environments/app.environment.js";
@Application({
name: "audit-application",
version: "1.0.0",
environment: AppEnvironment,
workflows: [AuditWorkflow],
budgets: [AuditAppBudget, AuditTenantBudget, AuditExecBudget],
pauseRecordRetention: hours(24),
shutdownTimeout: seconds(25),
})
export class AuditApplication {}
```
src/audit/environments/app.environment.ts
```ts
import type { Secret } from "@agboss/core";
export class AppEnvironment {
auditUrl!: string;
databaseUrl!: Secret;
openRouterApiKey?: Secret;
}
```
src/audit/audit.workflow\.ts
```ts
import { Budget, BudgetPeriod, BudgetScope, Workflow, WorkflowFinish, WorkflowStart, start, type BudgetHandler } from "@agboss/core";
import { WorkflowStartTextDto } from "@agboss/core/dto";
import { usd, type Usd } from "@agboss/core/units";
@WorkflowStart({ name: "audit-workflow-start", description: "A request", input: WorkflowStartTextDto })
export class AuditWorkflowStart {}
@WorkflowFinish({ name: "audit-workflow-finish", description: "The answer", output: WorkflowStartTextDto })
export class AuditWorkflowFinish {}
@Workflow({
name: "audit-workflow",
version: "1.0.0",
flow: [
start(AuditWorkflowStart)
.returns(WorkflowStartTextDto)
.toFinish(AuditWorkflowFinish),
],
})
export class AuditWorkflow {}
@Budget({ name: "audit-app-budget", scope: BudgetScope.Application, period: BudgetPeriod.Day })
export class AuditAppBudget implements BudgetHandler {
maxSpend(): Usd { return usd(10); }
}
```
src/audit/environments/prod.environment.ts
```ts
import { env, secret } from "@agboss/core";
import type { AppEnvironment } from "./app.environment.js";
export const environment: AppEnvironment = {
auditUrl: env("AUDIT_URL"),
databaseUrl: secret("DATABASE_URL"),
openRouterApiKey: secret("OPENROUTER_API_KEY"),
};
```
src/audit/environments/test.environment.ts
```ts
import { secret } from "@agboss/core";
import type { AppEnvironment } from "./app.environment.js";
export const environment: AppEnvironment = {
auditUrl: "http://127.0.0.1:4010/audit",
databaseUrl: secret("DATABASE_URL"),
};
```
test/audit.application.test.ts
```ts
import { expect } from "vitest";
import { ExecutionStatus, HealthStatus } from "@agboss/core";
import { testWith } from "@agboss/core/testing";
import { AuditApplication } from "../src/audit/audit.application.js";
import { AuditWorkflowFinish, AuditWorkflowStart } from "../src/audit/audit.workflow.js";
const test = testWith(AuditApplication, { env: "test" });
test("boots AuditApplication, executes AuditWorkflowStart, and checks probes", async ({
app,
}) => {
const result = await app.execute(AuditWorkflowStart, {
text: "Run compliance audit",
});
expect(result.status).toBe(ExecutionStatus.Finished);
expect(result.finish).toBe(AuditWorkflowFinish);
expect(result.output).toEqual({ text: "Run compliance audit" });
expect(HealthStatus.Healthy).toBeDefined();
});
```
## What `@Application` configures
[Section titled “What @Application configures”](#what-application-configures)
| Option | Purpose |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workflows` | `@Workflow` classes exposed by the application. Calling `app.execute(StartClass, input)` runs the workflow owning that `@WorkflowStart`. |
| `environment` | Environment contract class (or `EmptyEnvironment` when no environment variables are needed). `Secret` fields mask as `***` everywhere except `.reveal()`. |
| `providers` | Infrastructure and shared providers: `@ModelProvider` classes, `@Channel` implementations, custom `Logger` / `Clock`, and `Sqlite*Repository` stores. |
| `budgets` | `@Budget` classes covering `BudgetScope.Application`, `BudgetScope.Tenant`, and `BudgetScope.Execution` (checked at assembly). |
| `piiPolicies` | Application-wide `@PiiPolicy` classes masking personal data at model boundaries and across logs, traces, and records. |
| `guardrails` | Application-wide `@InputGuardrail` and `@OutputGuardrail` classes cascaded to every workflow, agent, and model router. |
| `compaction` / `uncompactedTurns` | Application-wide `@Compaction` strategy (`SummaryQueue`, `SingleSummary`, `None`) and recent turn preservation window. |
| `observers` | `@Injectable()` observer classes implementing hooks like `OnExecutionEnd`, `OnAgentStart`, `OnExecutionRejected`, or `OnExecutionResume`. |
| `healthChecks` | [`@HealthCheck`](/reference/health-check/) classes invoked by `app.checkLiveness()` and `app.checkReadiness()`. |
| `maxConcurrentSteps` | Instance-wide step ceiling; when saturated, `app.checkReadiness()` reports `Degraded` so Kubernetes load balancers shift new traffic to idle pods. |
| `pauseRecordRetention` | How long resolved pause records are kept before the 30-second recovery sweep purges them (default `hours(24)`). |
| `shutdownTimeout` | Graceful drain window on `app.close()` or `SIGTERM` / `SIGINT` (default `seconds(25)`), after which remaining runs release their leases for peer instances. |
## 4-Stage Progressive Infrastructure Scaling
[Section titled “4-Stage Progressive Infrastructure Scaling”](#4-stage-progressive-infrastructure-scaling)
Because `@Application({ providers })` resolves storage contracts by class inheritance (`SqliteCheckpointSaver extends CheckpointSaver`), you can scale from local unit tests to multi-pod Kubernetes clusters without changing a single line of `@Workflow` or `@Agent` code:
1. **Stage 1 — Local Dev & Isolated Tests:** Default `InMemory*` stores, `testWith(App)`, `VirtualClock`, and 8 unit runners.
2. **Stage 2 — Single-Node Durable Production (`node:sqlite` WAL):** Register the 7 `Sqlite*` providers (`SqliteCheckpointSaver`, `SqliteExecutionRecordRepository`, `SqlitePausedExecutionRepository`, `SqliteCostLedgerRepository`, `SqliteExecutionLeaseRepository`, `SqliteExecutionTreeRepository`, `SqliteRateLimitRepository`).
3. **Stage 3 — Multi-Pod Cluster & K8s Probes:** Horizontal replicas coordinate via 30s leases, 30s orphan recovery sweeps, `maxConcurrentSteps`, and `app.checkLiveness()` / `app.checkReadiness()`.
4. **Stage 4 — Custom Enterprise Backends:** Subclass any of the 7 repository contracts (`PostgresCheckpointSaver extends CheckpointSaver`) and list it in `providers`.
## How `app.execute`, `app.resume`, `app.cancel`, and `app.close` work
[Section titled “How app.execute, app.resume, app.cancel, and app.close work”](#how-appexecute-appresume-appcancel-and-appclose-work)
* **`app.execute(StartClass, input, options?)`** — Starts or continues a workflow thread scoped by `{ threadId, ownerId, tenant: { tenantId, weight }, onStream }`, checking all budgets before the first step.
* **`app.resume({ pauseId, reply }, options?)`** — Atomically records a human-in-the-loop reply (`Pending -> Resolved`), acquires the execution tree’s 30-second lease, and continues from the checkpoint.
* **`app.cancel(threadId, { tenantId, ownerId })`** — Aborts a running execution before its next model or tool call (`ExecutionCancelledError`) or withdraws pending pauses (`ChannelHandler.withdraw(info)`) on a paused thread.
* **`app.close({ timeout? })`** — Rejects new executions with `ApplicationStoppingError`, stops the recovery sweep, runs `onStop`, waits up to `timeout` for active steps to finish or pause, releases remaining leases so peer instances can recover them immediately, and runs `onDestroy`.
# Distributed-Systems Architecture (7 Core Invariants)
> How AGBoss and HarnessDSL enforce 7 distributed-systems invariants at compile time so engineers and AI coding agents scale LangGraph graphs without entropy.
**AGBoss** (**Agent Graph Builder Open Source System**) is built on a foundational premise: **a multi-agent system that scales to hundreds of nodes is a distributed dataflow system, not a prompt script.** In this engineering model (much like React and JSX, or Prisma and PSL), **AGBoss** is the TypeScript framework, static graph compiler (`agboss check`), and production runtime on top of LangGraph (`npm i @agboss/core`), while **HarnessDSL (`HDSL`)** is the declarative graph grammar and verification specification **inside `AGBoss`** — defining how `*.workflow.ts` `flow` chains and contract decorators express narrow `@Dto` handoffs, `Path Scope`, `in-degree = 1` vs `in-degree >= 2` canonical topology, `{ about }` staleness tracking, quorums, and `compensateWith` / `committedBy` sagas. Furthermore, in modern engineering organizations, the bulk of orchestration code is generated and refactored by **AI coding agents (Codex, Cursor, Windsurf, Copilot, Gemini CLI, Claude Code)** under the architectural direction of a Senior, Staff, or Principal engineer.
When coding agents operate on unconstrained graph libraries (raw `Annotation.Root` state bags, untyped `(state) => string` edges, and imperative `Promise.all` loops), small local edits silently corrupt downstream branches, destroy LLM KV-cache hit rates, or leave uncommitted side effects dangling on failure.
AGBoss replaces convention with **7 compile-time and runtime architectural invariants**. Every invariant is verified by `npx agboss check` before a single LLM token is spent.
***
## How the 7 Architectural Invariants Work Together
[Section titled “How the 7 Architectural Invariants Work Together”](#how-the-7-architectural-invariants-work-together)
| # | Architectural Principle | Core Primitives & Compiler Rules | Failure Mode Prevented |
| ----- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **1** | **Computational Role Segregation** | [`@Agent`](/reference/agent/), [`@Router`](/reference/router/), [`@Arbiter`](/reference/arbiter/), [`@Action`](/reference/action/), `[graph.limit-handler-uses-model]`, `[router.spawn-required]`, `[router.prompt-on-always]`, `agboss/no-component-state` | Monolithic “god nodes” that mix LLM reasoning, routing conditionals, consensus voting, and stateful side effects |
| **2** | **Path-Scoped SSA Dataflow** | [`.returns(Dto, { about })`](/concepts/path-scope/), `caseOf(Dto, { about })`, `historyOf(Dto)`, `.agboss/types.d.ts`, `Produced`, `[input.missing-on-path]`, `[input.never-produced]`, `[flow.unused-output]`, `[input.outdated-on-path]`, `[launch.branch-outdates-sibling]` | Reading unpopulated fields, dead outputs, or stale loop artifacts from a shared mutable God State Object |
| **3** | **Canonical Topology Isomorphism** | `in-degree = 1` inline vs `in-degree >= 2` top-level entry (`[flow.shared-node-inline]`, `[flow.single-entry-own-path]`), 1 call/line, depth $\le 4$ (`[flow.path-too-deep]`), `ThisWorkflow` | Coding agents formatting identical graphs ten different ways, deep nesting pyramids, and unreviewable git diffs |
| **4** | **Prompt Prefix Cache Invariance** | `[prompt.input-in-instructions]`, sorted `@Tool` & `@Dto` JSON Schemas, deterministic HMAC-SHA256 [`replaceWithToken()`](/reference/pii-policy/) | Dynamic strings or random PII UUIDs invalidating the LLM KV prefix cache on every turn (10x token cost explosion) |
| **5** | **Transactional LIFO Sagas & Idempotency** | `ExecutionContext.step.idempotencyKey`, `compensateWith` / `committedBy`, `[flow.uncompensated-action]`, `[flow.uncommitted-action]`, `[flow.compensation-out-of-order]`, `[action.committer-without-compensator]`, `@agboss/unwinding` | Duplicate side effects on crash-recovery or dangling uncommitted writes on quorum `Surplus` / budget abort |
| **6** | **Zero-Leakage DI & 4-Stage Scaling** | [`@Application`](/concepts/application/), `[di.instance-provider]`, `OnInit..OnDestroy`, `app.close()` drain, 10 `Sqlite*` WAL stores, `ChildPlacement.AnyInstance`, [`@HealthCheck`](/reference/health-check/) | Global singletons leaking across tests, split-brain multi-pod execution, or rewriting code to move from tests to Kubernetes |
| **7** | **Progressive Crystallization & AI Skills** | `agboss describe --json` (`configHash`, `promptHash`, `schemaHash`), `agboss hub`, [`AGENTS.md` & 13 Skill Modules](/concepts/ai-coding-skills/) | Keeping settled Desire Paths on expensive LLM calls forever, or coding agents hallucinating legacy APIs |
***
## 1. Computational Role Segregation
[Section titled “1. Computational Role Segregation”](#1-computational-role-segregation)
In raw orchestration libraries, every node is an arbitrary `async (state) => Partial` function. Under iterative prompting, AI coding agents take the path of least resistance: they stuff LLM calls, `if/else` routing, parallel quorum checks, and database mutations into the same function.
AGBoss enforces **strict role segregation** at the decorator and compiler level (`[graph.limit-handler-uses-model]`, `[router.spawn-required]`, `[router.prompt-on-always]`, and the ESLint rule `agboss/no-component-state` forbidding mutable instance fields on components):
* **[`@Agent`](/reference/agent/) (Stochastic Reasoning & Tool Loops):** Calls a language model with a static system prompt, typed `input` DTOs, optional [`@Tool`](/reference/tool/) (bounded by `maxToolCalls`) / [`@Rag`](/reference/rag/) bindings, and [`@Judge`](/reference/judge/) quality gates. An `@Agent` class body is empty (`export class ScoutAgent {}`) — it cannot contain imperative routing or direct database writes.
* **[`@Router`](/reference/router/) (Branch Selection):** Chooses the next step (`chooseOne` or `chooseAtLeastOne`, requiring `[router.spawn-required]` and `[router.prompt-on-always]` on conditional model routes) either via a calibrated decision model (`DecidedBy.Model`), deterministic TypeScript (`DecidedBy.Code` implementing `RouterHandler` in `0ms`), or direct `oneOf` + `.switch(caseOf(...))` branching.
* **[`@Arbiter`](/reference/arbiter/) (Parallel Quorum Consensus):** Filters parallel branch answers inside `.toParallelAgents()` or `.toParallelWorkflows()` waves (`DecidedBy.Model` `criteria` or `DecidedBy.Code` `@OnAnswer(AgentClass)` methods) before the `.joinQuorum()` barrier.
* **[`@Action`](/reference/action/) (Deterministic Graph Step):** Runs pure, idempotent TypeScript with constructor DI (`ActionHandler`) and zero LLM access — ideal for external side effects, state transformations, and limit-error recovery paths.
src/settlement/settlement.components.ts
```ts
import {
Action,
Agent,
Arbiter,
DecidedBy,
OnAnswer,
Router,
listOf,
type ActionHandler,
type InputOf,
type OutputOf,
type Route,
type RouterHandler,
} from "@agboss/core";
import { Integer, Text } from "@agboss/core/dto";
export class OrderDto {
@Text()
orderId!: string;
@Integer({ min: 1 })
amountCents!: number;
}
export class RiskScoreDto {
@Integer({ min: 0, max: 100 })
score!: number;
}
export class ReceiptDto {
@Text()
txId!: string;
}
@Agent({
name: "fraud-analyst-agent",
description: "Evaluates order risk from signals",
prompt: "Score the fraud risk of the order from 0 to 100.",
model: "moonshotai/kimi-k2.6",
input: { order: OrderDto },
output: RiskScoreDto,
})
export class FraudAnalystAgent {}
@Agent({
name: "secondary-fraud-agent",
description: "Independent backup fraud model for quorum consensus",
prompt: "Score the fraud risk of the order from 0 to 100.",
model: "moonshotai/kimi-k2.6",
input: { order: OrderDto },
output: RiskScoreDto,
})
export class SecondaryFraudAgent {}
@Arbiter({
name: "low-risk-arbiter",
decidedBy: DecidedBy.Code,
})
export class LowRiskArbiter {
@OnAnswer(FraudAnalystAgent)
fromFraudAnalyst(answer: RiskScoreDto): boolean {
return answer.score < 25;
}
@OnAnswer(SecondaryFraudAgent)
fromSecondaryFraud(answer: RiskScoreDto): boolean {
return answer.score < 25;
}
}
@Action({
name: "capture-payment-action",
description: "Captures payment deterministically after fraud quorum",
input: { order: OrderDto, risks: listOf(RiskScoreDto) },
output: ReceiptDto,
})
export class CapturePaymentAction implements ActionHandler {
execute(
input: InputOf,
): Promise> {
return Promise.resolve({
txId: `tx_${input.order.value.orderId}_${String(input.risks[0]?.value.score ?? 0)}`,
});
}
}
@Action({
name: "direct-capture-action",
description: "Captures low-value order directly without invoking an LLM",
input: { order: OrderDto },
output: ReceiptDto,
})
export class DirectCaptureAction implements ActionHandler {
execute(
input: InputOf,
): Promise> {
return Promise.resolve({
txId: `tx_${input.order.value.orderId}`,
});
}
}
@Router({
name: "settlement-router",
description: "Routes high-value orders to fraud analysis before capture",
decidedBy: DecidedBy.Code,
input: { order: OrderDto },
routes: [{ target: CapturePaymentAction }, { target: DirectCaptureAction }],
})
export class SettlementRouter implements RouterHandler {
route(input: InputOf): Route {
return input.order.value.amountCents > 100_000
? CapturePaymentAction
: DirectCaptureAction;
}
}
```
test/settlement.components.test.ts
```ts
import { expect, it } from "vitest";
import { testAction, testArbiter, testRouter } from "@agboss/core/testing";
import {
CapturePaymentAction,
DirectCaptureAction,
FraudAnalystAgent,
LowRiskArbiter,
SettlementRouter,
SettlementStart,
} from "../src/settlement/settlement.components.js";
it("verifies segregated @Arbiter, @Router, and @Action roles in isolation", async () => {
const runArbiter = testArbiter(LowRiskArbiter);
const verdict = await runArbiter(FraudAnalystAgent, { score: 12 });
expect(verdict.output).toBe(true);
const runRouter = testRouter(SettlementRouter);
const { output: selectedRoute } = await runRouter({
order: {
value: { orderId: "ORD-9", amountCents: 250_000 },
producer: SettlementStart,
},
});
expect(selectedRoute).toBe(CapturePaymentAction);
const runAction = testAction(DirectCaptureAction);
const { output } = await runAction({
order: {
value: { orderId: "ORD-1", amountCents: 5_000 },
producer: SettlementStart,
},
});
expect(output).toEqual({ txId: "tx_ORD-1" });
});
```
***
## 2. Path-Scoped SSA Dataflow & Provenance
[Section titled “2. Path-Scoped SSA Dataflow & Provenance”](#2-path-scoped-ssa-dataflow--provenance)
In a graph with hundreds of nodes, branches, and review loops, a shared mutable state object (`Annotation.Root`) makes it impossible to know at compile time whether `state.draft` was actually populated on *every* path reaching `SendOfferAction`, or whether `state.approval` still holds a stale verdict from iteration #1 after `EditorAgent` produced draft v2.
AGBoss models workflow state using **Static Single Assignment (SSA) dominance** and **explicit artifact provenance** ([Path Scope](/concepts/path-scope/)):
1. **Dominator Availability & Chained Outputs (`[input.missing-on-path]`, `[input.never-produced]`, `[flow.unused-output]`):** Every step consumes and returns narrow immutable `@Dto` payloads. Multiple outputs of a step or join are declared strictly by chaining `.returns(DtoA).returns(DtoB)` (never multi-argument `.returns(A, B)`). A step can declare `input: { draft: OfferDraftDto }` only if `OfferDraftDto` is guaranteed to be produced on *every* incoming path that can reach that step (unless wrapped in `optional(OfferDraftDto)`), and every `.returns(Dto)` must be consumed downstream.
2. **Artifact Provenance (`{ about }` & `[input.outdated-on-path]`, `[launch.branch-outdates-sibling]`):** Provenance `{ about: TargetDto | [DtoA, DtoB] }` is declared on a single output `.returns(CritiqueDto, { about: PlanDto })` or on a specific union branch `caseOf(ChangesRequestedDto, { about: OfferDraftDto })` (never on `.returns(oneOf(...))` and never as a positional function). When `EditorAgent` produces a new `OfferDraftDto`, all prior DTOs that were `{ about: OfferDraftDto }` automatically cascade into `outdatedValues`.
3. **Loop Iteration Versioning (`historyOf`):** Steps that inspect prior loop iterations declare `historyOf(OfferDraftDto, { limit?: number })` in `input` to receive `{ current, previous: [...] }` (legacy string tags `{ as: "before" }` do not exist).
4. **Compiler-Generated Narrowed Producer Types (`.agboss/types.d.ts`):** Running `npx agboss check` analyzes every `@Workflow` and writes `.agboss/types.d.ts`. Code nodes that read from Path Scope (`@Action`, `DecidedBy.Code` `@Router`, and `FinishHandler` `@WorkflowFinish`) receive `Produced` (`{ value: Dto, producer: typeof StepA | typeof StepB }`), whereas fixed-source handlers (`@OnAnswer(Agent)`, `ToolHandler.run`, `ChannelAsk.value`, `ReplyInterpreterHandler.interpret({ ask, reply })`) receive the bare DTO directly.
***
## 3. Canonical Topology Isomorphism
[Section titled “3. Canonical Topology Isomorphism”](#3-canonical-topology-isomorphism)
When multiple engineers and coding agents edit a large workflow graph, structural consistency is critical. AGBoss enforces a **canonical 1-to-1 isomorphism** between the directed graph topology and the TypeScript source code in `*.workflow.ts`:
* **Single incoming edge (`in-degree = 1`):** The step **must** be chained inline (`.toAgent(Next)`). Splitting a single-incoming-edge node into a separate top-level `flow` entry is rejected by `agboss check` (`[flow.single-entry-own-path]`).
* **Convergence node (`in-degree >= 2`):** When two or more branches converge on the same node (such as a review loop target or post-branch merge), that node **must** be declared as a top-level entry (`agent(Node)` / `router(Node)` / `action(Node)` / `workflow(Child)`) in the `flow: [...]` array (`[flow.shared-node-inline]`).
* **One chained call per line (`@stylistic/newline-per-chained-call`):** Every builder call (`start()`, `.returns()`, `.toAgent()`, `.switch()`, `.toFinish()`) occupies its own line.
* **Bounded inline depth (`[flow.path-too-deep]`) & `ThisWorkflow` soft stop:** Inline branch nesting is capped at depth $\le 4$ (`[flow.path-too-deep]`); deeper sub-flows are decomposed into a child [`@Workflow`](/reference/workflow/) (`.toWorkflow(ChildWorkflow)`). For self-recursive workflows (`ThisWorkflow`), reaching `depth === maxDepth` softly strips recursive variants from the model’s JSON Schema (warming at most two KV-cache prefixes: `depth < maxDepth` and `depth === maxDepth`) without crashing the branch.
***
## 4. Prompt Prefix Cache Invariance & Boundary PII
[Section titled “4. Prompt Prefix Cache Invariance & Boundary PII”](#4-prompt-prefix-cache-invariance--boundary-pii)
In multi-turn and tool-calling agents, prompt tokens dominate LLM spend. Modern model providers cache the **KV prefix** of a request, cutting input token cost and time-to-first-token by up to 80–90% — *provided the prefix remains byte-for-byte identical across turns*.
Two common mistakes destroy KV prefix caching in production:
1. Interpolating per-request user inputs (`{{input.query}}`) directly into the system prompt.
2. Masking PII with random per-request UUIDs (``), which mutates historical messages on every turn.
AGBoss enforces **Prompt Prefix Cache Invariance** across a two-part byte-stable prefix (Part 1: static system prompt + deterministically sorted `@Tool` schemas and output `@Dto` JSON Schemas; Part 2: completed `threadId` turns):
* **Static System Prompts (`[prompt.input-in-instructions]`):** `agboss check` verifies every prompt template (`prompt` and `promptUrls`). System instructions may only interpolate static [`AppEnvironment`](/concepts/application/) variables (never `{{input.*}}`); dynamic step inputs are appended strictly in the user/turn message frame.
* **Deterministic HMAC-SHA256 Pseudonymization ([`@PiiPolicy`](/reference/pii-policy/)):** `@PiiPolicy` classes and `sensitive: PiiAction.replaceWithToken()` on `@Dto` fields hash each sensitive match with `@Application({ pseudonymKey })` (`secret("PII_PSEUDONYM_KEY")`). The email `jane@acme.com` always maps to the exact same token (``) across turns, preserving 100% of your KV prefix cache, while AGBoss transparently restores `jane@acme.com` inside `@Tool` arguments before calling your database or API.
***
## 5. Transactional LIFO Sagas & Idempotent Execution
[Section titled “5. Transactional LIFO Sagas & Idempotent Execution”](#5-transactional-lifo-sagas--idempotent-execution)
Multi-agent workflows frequently perform external side effects across multiple steps (reserving inventory, creating a provisional ATS offer, posting an approval message). If a later step fails, a parallel branch triggers `FailurePolicy.FailFast`, a quorum cancels slower branches as `BranchVerdict.Surplus`, or a [`@Budget`](/reference/budget/) cap trips, those external writes must be unwound cleanly.
AGBoss provides three layered guarantees:
* **Deterministic Step Idempotency (`this.executionContext.step.idempotencyKey`):** [`ExecutionContext`](/concepts/application/) provides a deterministic key (`${executionId}:${nodeId}:${visitNumber}[:${itemIndex}][:${callId}]`) and end-to-end `signal` that remain stable across crash-recovery sweeps and retries, allowing external APIs to deduplicate retried calls safely.
* **Declarative `@Action` Sagas & Static Verification:** Side-effecting `@Action` nodes declare `@Action({ compensateWith: UndoAction, committedBy?: CommitAction })`. `agboss check` statically verifies saga completeness and ordering across all paths via `[flow.uncompensated-action]`, `[flow.uncommitted-action]`, `[flow.compensation-out-of-order]`, and `[action.committer-without-compensator]`. At runtime, open saga records are automatically unwound in **reverse completion order (LIFO)** on `app.cancel`, `FailurePolicy.FailFast`, `BranchVerdict.Surplus`, `BudgetExceededError`, or `join` failure, with crash recovery of incomplete rollbacks persisted under `@agboss/unwinding`.
* **Zero-Model Limit Recovery (`[graph.limit-handler-uses-model]`):** When a `.onError()` handler catches `LimitExceededError` (`BudgetExceededError`, `StepsLimitExceededError`, `VisitsLimitExceededError`, `DepthLimitExceededError`) or base `AGBossError`, invoking an `@Agent` or `DecidedBy.Model` `@Router` is statically forbidden — only free deterministic `@Action` and `@WorkflowFinish` nodes are permitted.
***
## 6. Zero-Leakage DI & 4-Stage Infrastructure Scaling
[Section titled “6. Zero-Leakage DI & 4-Stage Infrastructure Scaling”](#6-zero-leakage-di--4-stage-infrastructure-scaling)
AGBoss eliminates global singletons and string-token DI magic. Every infrastructure service is wired inside [`@Application({ providers })`](/concepts/application/) by **class inheritance** (`SqliteCheckpointSaver extends CheckpointSaver`; passing a class to `provide(Class, instance)` is rejected by `[di.instance-provider]`) and managed through four deterministic lifecycle phases (`OnInit -> OnStart -> OnStop -> OnDestroy`). During `await app.close()`, the container first drains in-flight steps within the `shutdownTimeout` budget before running `onDestroy` and releasing active leases.
Because infrastructure is injected by contract, the exact same `@Workflow` and `@Agent` classes scale across **4 operational stages** without changing a single line of business logic:
1. **Stage 1 — Local Dev & Isolated Tests:** Default `InMemory*` stores, [`testWith(App, { instances: 2 })`](/guides/testing-agents/), `VirtualClock`, `TestConcurrency.inOrder([step(A), step(B)])`, and the 10 unit runners (`testTool`, `testToolInterceptor`, `testAction`, `testRouter`, `testArbiter`, `testRag`, `testJudge`, `testCompaction`, `testPii`, `testGuardrail`).
2. **Stage 2 — Single-Node Durable Production (`node:sqlite` WAL):** Drop in the 10 built-in zero-dependency `Sqlite*` providers (`SqliteCheckpointSaver`, `SqliteExecutionRecordRepository`, `SqliteExecutionHistoryRepository`, `SqlitePausedExecutionRepository`, `SqliteCostLedgerRepository`, `SqliteExecutionLeaseRepository`, `SqliteExecutionTreeRepository`, `SqliteChildExecutionQueueRepository`, `SqliteRateLimitRepository`, `SqliteMeshRequestRepository`) for crash-safe persistence and two-phase [`app.resume()`](/guides/channels-and-pauses/).
3. **Stage 3 — Multi-Pod Cluster, Distributed Child Work-Stealing & Kubernetes Probes:** Horizontal worker replicas coordinate via 30-second execution-tree leases (`ExecutionLeaseRepository`, `ExecutionLeaseLostError` aborting lost steps mid-tool-loop), `ChildPlacement.AnyInstance` distributed child workflow queues (`ChildExecutionQueueRepository` with parent lease yielding and per-child crash recovery), 30-second orphan recovery sweeps, `maxConcurrentSteps` backpressure, and [`@HealthCheck`](/reference/health-check/) probes (`await app.checkLiveness()`, `await app.checkReadiness()`).
4. **Stage 4 — Custom Enterprise Backends (Postgres / Redis):** Subclass any of the 10 repository contracts (`export class PostgresCheckpointSaver extends CheckpointSaver`) and register it in `@Application({ providers })`.
***
## 7. Progressive Crystallization & Universal Coding Skills
[Section titled “7. Progressive Crystallization & Universal Coding Skills”](#7-progressive-crystallization--universal-coding-skills)
An agentic workflow should not remain 100% stochastic forever. Early in a product’s lifecycle, engineers use [`DecidedBy.Model`](/reference/router/) routers and flexible multi-tool [`@Agent`](/reference/agent/) loops to handle ambiguous real-world inputs. As production traffic accumulates, stabilized **Desire Paths** emerge.
AGBoss is designed for **[Progressive Crystallization](/concepts/progressive-crystallization/)**:
* `npx agboss describe --json` exports the complete static revision snapshot of your application, including content-addressed `configHash` (workflow topology), `promptHash` (per-agent/router prompt), and `schemaHash` (per-DTO schema) for every node.
* Correlating this snapshot with `ExecutionRecord` and spans in Langfuse / OpenTelemetry exposes settled paths so you can crystallize `DecidedBy.Model` $\to$ `DecidedBy.Code`, repetitive `@Tool` chains $\to$ `@Action`, and `ChannelOutcome.awaitReply()` $\to$ `ChannelOutcome.decided(...)` without changing neighbor contracts in the graph.
* Using **[`AGENTS.md` and the 13 offline-synced skill modules](/concepts/ai-coding-skills/)**, an engineer can instruct Codex, Cursor, Windsurf, Copilot, or Claude Code to perform this crystallization and immediately prove equivalence on golden datasets via `npx agboss compare`.
***
## Executable Proofs for All 7 Invariants (`agbossjs/agboss-examples`)
[Section titled “Executable Proofs for All 7 Invariants (agbossjs/agboss-examples)”](#executable-proofs-for-all-7-invariants-agbossjsagboss-examples)
Every invariant above is proven offline for `$0` in the 7 standalone applications of **[`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples)** (see [7 Verified Examples](/examples/job-scout/)):
* **[`01-hello-agboss`](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss)** — Canonical `1 class = 1 file` component layout, real async HTTP service DI (`PricingCatalogService`), production `src/http/server.ts`, and symmetric `tests/`.
* **[`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)** — Static detection of `[input.missing-on-path]`, `[input.outdated-on-path]`, `[launch.branch-outdates-sibling]`, and `[flow.uncompensated-action]` vs raw LangGraph.
* **[`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk)** — Reversible `@PiiPolicy`, `@ToolInterceptor`, `DecidedBy.Code` guardrails, in-memory BM25/TF-IDF `@Rag`, `PolicyAwareCompaction`, and 5-layer `@EvalSuite`.
* **[`04-travel-booking-saga`](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)** — Reverse-order LIFO saga compensation (`compensateWith`, `committedBy`), `.joinQuorum()`, SQLite WAL crash recovery, and HITL `Channel.withdraw()`.
* **[`05-cluster-work-stealing`](https://github.com/agbossjs/agboss-examples/tree/main/05-cluster-work-stealing)** — `ChildPlacement.AnyInstance`, recursive `ThisWorkflow`, 30s SQLite leases & fencing, `maxConcurrentSteps` backpressure, and `@HealthCheck`.
* **[`06-enterprise-agent-mesh`](https://github.com/agbossjs/agboss-examples/tree/main/06-enterprise-agent-mesh)** — 3-service mesh (`1-order-gateway`, `2-billing-a2a`, `3-catalog-mcp`) over A2A + MCP, Ed25519 `agboss-mesh` JWS envelope, `MeshLoopDetectedError`, and `agboss check --federation`.
* **[`07-langgraph-migration`](https://github.com/agbossjs/agboss-examples/tree/main/07-langgraph-migration)** — Side-by-side comparison of `before-langgraph/` (`Annotation.Root` God State) vs `after-agboss/` (`@Dto`, `@Router`, `@Action`, `@Channel`, and `agboss check --strict`).
# Contexts & Authorization
> Understand the three-level AGBoss context hierarchy (ApplicationContext, TenantContext, ExecutionContext), authorization, and step idempotency keys.
AGBoss strictly separates **domain state** (DTOs carried in the Path Scope and persisted in checkpoints) from **runtime identity and metadata** (injected via three hierarchical context classes). Exactly three context tokens exist — `ApplicationContext`, `TenantContext`, and `ExecutionContext` — forming a parent-linked chain inspired by Spring’s context hierarchy.
## How the three context levels are structured
[Section titled “How the three context levels are structured”](#how-the-three-context-levels-are-structured)
Each level links upward to its parent, so a component injecting `ExecutionContext` can reach `executionContext.tenant` and `executionContext.tenant.application`, and `ApplicationContext.environment` provides typed access to the application’s environment contract:
| Context Class | Lifecycle Scope | Key Properties | Typical Consumers |
| -------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `ApplicationContext` | Singleton per booted `App` instance | `name`, `version`, `instanceId`, `startedAt`, `environmentName`, `environment`, `host` (`hostname`, `pid`, `platform`, `runtimeVersion`) | Startup `@Injectable` services, `@Budget` (`Application` scope), observers |
| `TenantContext` | Per organization / tenant | `tenantId` (`"default"` when omitted), `weight` (positive integer for fair `@ModelProvider` queuing), `application`, plus custom subclass fields | `@Budget` (`Tenant` scope), tenant-aware services |
| `ExecutionContext` | Per workflow execution pass | `executionId`, `threadId`, `ownerId`, `tenant`, `signal` (`AbortSignal`), `workflow`, `step` (`nodeId`, `visitNumber`, `idempotencyKey`, `callId`, `itemIndex`, `copyNumber`) | `@Tool`, `@Action`, `@Router`, `@Arbiter`, `@Judge`, `@InputGuardrail`, `@OutputGuardrail`, `@Channel` |
> **Compiler diagnostic `[context.prompt-unused]`:** Because `TenantContext` and `ExecutionContext` fields are supplied by your server caller (`app.execute(Start, input, { tenant, execution })`) and never generated by an LLM, their `@Text()`, `@Integer()`, or `@OneOf()` decorators must omit `prompt` (`[context.prompt-unused]`).
src/config/support-contexts.ts
```ts
import { ExecutionContext, TenantContext, Tool, type ToolHandler } from "@agboss/core";
import { Integer, Text } from "@agboss/core/dto";
export class SupportTenantContext extends TenantContext {
@Text()
planTier!: string;
@Integer()
monthlyQuotaCents!: number;
}
export class SupportExecutionContext extends ExecutionContext {
@Text()
requestTraceId!: string;
}
@Tool({
name: "issue_credit",
description: "Issues a tenant-scoped credit using ExecutionContext idempotencyKey",
input: CreditRequestDto,
output: CreditReceiptDto,
deps: [SupportExecutionContext, SupportTenantContext],
})
export class IssueCreditTool implements ToolHandler {
constructor(
private readonly ctx: SupportExecutionContext,
private readonly tenant: SupportTenantContext,
) {}
async run(input: CreditRequestDto): Promise {
this.ctx.signal.throwIfAborted();
return {
receiptId: `cr_${this.ctx.step.idempotencyKey}`,
tenantId: this.tenant.tenantId,
planTier: this.tenant.planTier,
traceId: this.ctx.requestTraceId,
amountCents: input.amountCents,
};
}
}
```
src/support.application.ts
```ts
import { Application, EmptyEnvironment } from "@agboss/core";
import { SupportTenantContext, SupportExecutionContext } from "./config/support-contexts.js";
@Application({
name: "support-application",
version: "1.0.0",
environment: EmptyEnvironment,
tenantContext: SupportTenantContext,
executionContext: SupportExecutionContext,
workflows: [SupportWorkflow],
budgets: [CtxAppBudget, CtxTenantBudget, CtxExecBudget],
})
export class SupportApplication {}
```
test/support-contexts.test.ts
```ts
import { expect, it } from "vitest";
import { testTool } from "@agboss/core/testing";
import { IssueCreditTool } from "../src/config/support-contexts.js";
it("injects custom TenantContext and ExecutionContext fields into tools", async () => {
const runCredit = testTool(IssueCreditTool, {
tenant: { tenantId: "acme", weight: 4 },
execution: { requestTraceId: "req_9" },
});
const res = await runCredit({ amountCents: 25 });
expect(res.output.tenantId).toBe("acme");
});
```
## How `tenantId` and `ownerId` authorization protects threads and pauses
[Section titled “How tenantId and ownerId authorization protects threads and pauses”](#how-tenantid-and-ownerid-authorization-protects-threads-and-pauses)
When an execution starts with `tenant: { tenantId: "acme" }` and `ownerId: "user-42"`, AGBoss records ownership on the thread and every pause record:
* **Cross-tenant isolation:** Calling `app.execute`, `app.resume`, or `app.cancel` with a mismatched `tenantId` throws `UnauthorizedExecutionAccessError` and fires `OnExecutionRejected` before loading any checkpoint state.
* **Owner vs. channel responder:** A user-owned thread (`ownerId`) can only be continued or cancelled by that `ownerId`, while out-of-process `@Channel` approvals record `answeredBy` in the audit trail.
# Component Decorators
> Overview of AGBoss and HarnessDSL decorators for applications, workflows, agents, routers, tools, actions, guardrails, interceptors, and A2A mesh endpoints.
In **AGBoss** (*Agent Graph Builder Open Source System*) and its declarative specification grammar **HarnessDSL**, every part of a multi-agent harness is organized as a decorated TypeScript class with a single, clear computational role ([Principle 1: Computational Role Segregation](/concepts/architecture/#1-computational-role-segregation)). Instead of mixing prompt templates, HTTP calls, branching logic, and safety filters inside monolithic functions, you declare dedicated classes and let `@Workflow` (`HarnessDSL`) and `@Application` wire and verify them at compile time via `agboss check`.
## What decorators does AGBoss provide?
[Section titled “What decorators does AGBoss provide?”](#what-decorators-does-agboss-provide)
| Decorator | Public Entry & File Convention | Role in the Application |
| ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`@Application`](/reference/application/) | `agboss` (`*.application.ts`) | Single assembly root binding workflows, environment, providers, budgets, PII, guardrails, health checks, and observers |
| [`@Workflow`](/reference/workflow/) | `agboss` (`*.workflow.ts`) | Typed state graph declaring `flow` paths, `defaults`, `compaction`, and `maxSteps` |
| [`@WorkflowStart`](/reference/workflow-start/) / [`@WorkflowFinish`](/reference/workflow-finish/) | `agboss` (`*.workflow-start.ts` / `*.workflow-finish.ts`) | Entry (`input: Dto`) and exit (`output: Dto` / `FinishHandler`) boundaries of a workflow |
| [`@Agent`](/reference/agent/) | `agboss` (`*.agent.ts` + `*.prompt.md`) | LLM reasoning node with `model`, `prompt`/`promptUrls`, `tools`, `rag`, `judges`, `input`, and `output` |
| [`@Action`](/reference/action/) | `agboss` (`*.action.ts`) | Deterministic TypeScript step (`ActionHandler`, `execute(input)`) for code-only workflow nodes |
| [`@Router`](/reference/router/) | `agboss` (`*.router.ts`) | Branching component (`DecidedBy.Model` or `DecidedBy.Code`) selecting one or more `routes` |
| [`@Arbiter`](/reference/arbiter/) / [`@OnAnswer`](/reference/on-answer/) | `agboss` (`*.arbiter.ts`) | Quorum consensus voter attached with `.acceptedBy()` before `.joinQuorum()` |
| [`@Tool`](/reference/tool/) / [`@BindTool`](/reference/bind-tool/) | `agboss` (`*.tool.ts`) | Agent-callable capability (`ToolHandler`, `run(input)`) with DTO schema validation |
| [`@ToolInterceptor`](/reference/tool-interceptor/) | `agboss` (`*.tool-interceptor.ts`) | Cross-cutting pre-call (`BeforeToolCall` $\to$ `ToolCallDecision`) and post-call (`AfterToolCall`) interceptor for `@Tool` and `@McpTool` |
| [`@Judge`](/reference/judge/) | `agboss` (`*.judge.ts`) | Quality gate (`JudgeHandler`, `DecidedBy.Model` or `DecidedBy.Code`) inspecting reply DTOs and `toolCalls` |
| [`@Compaction`](/reference/compaction/) | `agboss` (`*.compaction.ts`) | Conversation history compaction strategy (`SummaryQueue`, `SingleSummary`, `Custom` with `CompactionHandler`, or `None`) |
| [`@Injectable`](/reference/injectable/) | `agboss` (`*.service.ts`) | Injectable service or `ExecutionObserver` with `OnInit`, `OnStart`, `OnStop`, `OnDestroy` hooks |
| [`@HealthCheck`](/reference/health-check/) | `agboss` (`*.health-check.ts`) | Liveness and readiness probe component (`HealthCheckHandler`, `checkLiveness()`, `checkReadiness()`) |
| [`@ModelProvider`](/reference/model-provider/) | `@agboss/core/models` (`*.model-provider.ts`) | Model vendor pool (`-model-provider` suffix) with HTTP or custom `ModelProviderHandler` transport, `serves`, `promptCaching`, `rateLimitPolicy`, and `circuitBreakerPolicy` |
| [`@Budget`](/reference/budget/) | `agboss` (`*.config.ts`) | Spend ceiling (`BudgetHandler`, `maxSpend(): Usd`) for `Application`, `Tenant`, or `Execution` scope |
| [`@PiiPolicy`](/reference/pii-policy/) | `agboss` (`*.pii-policy.ts`) | Declarative `rules: [pii(...)]` or custom `PiiPolicyHandler.findMatches(text)` with deterministic HMAC `replaceWithToken()` |
| [`@InputGuardrail` & `@OutputGuardrail`](/reference/guardrail/) | `agboss` (`*.guardrail.ts`) | Input/output guardrails (`DecidedBy.Model` with `threshold` or `DecidedBy.Code` with `GuardrailHandler`) across `Application`, `Workflow`, and `Agent` scopes |
| [`@ChannelRole`](/reference/channel-role/) / [`@Channel`](/reference/channel/) / [`@ReplyInterpreter`](/reference/reply-interpreter/) | `agboss` (`*.channel.ts`) | Out-of-process pause role (`approves`), free-text `@ReplyInterpreter`, and transport implementation (`send(ask)`, `withdraw(info)`) |
| [`@Rag`](/reference/rag/) | `agboss` / `@agboss/core/rag` (`*.rag.ts`) | Knowledge base (`RagConnector`, `SqliteFtsConnector`) |
| [`@McpServer`](/reference/mcp/) / [`@McpTool`](/reference/mcp-tool/) / [`@McpExpose`](/reference/mcp-expose/) | `agboss` / `@agboss/core/mcp` (`*.mcp.ts`) | Model Context Protocol client servers, typed tools, and server exposure |
| [`@A2AAgent` & `a2aHttpListener`](/reference/a2a-agent/) | `agboss` / `@agboss/core/a2a` (`*.a2a.ts`) | Remote Agent-to-Agent protocol client (`A2AClient`), `@Injectable()` `A2ACallerResolver`, and multi-tenant `a2aHttpListener` |
| [`@Text`, `@ListOf`, `@Nested`, …](/reference/dto/) | `@agboss/core/dto` (`*.dto.ts`) | Field decorators on plain DTO classes compiled to JSON Schema and checked by `agboss check` |
## Naming & suffix invariant (`[name.kind-suffix]`)
[Section titled “Naming & suffix invariant (\[name.kind-suffix\])”](#naming--suffix-invariant-namekind-suffix)
Every component decorator enforces a dual naming rule (`[name.kind-suffix]`):
1. Its kebab-case `name` option must end with the component kind’s suffix (for example, `"scout-agent"`, `"daily-budget"`, `"tenant-order-tool-interceptor"`, `"support-workflow-start"`).
2. Its PascalCase TypeScript class identifier must end with the corresponding suffix (`ScoutAgent`, `DailyBudget`, `TenantOrderToolInterceptor`, `SupportWorkflowStart`).
This guarantees that both humans and AI coding agents can tell a class’s computational role at a glance in every import and `flow` chain.
## How DTO classes and explicit units work
[Section titled “How DTO classes and explicit units work”](#how-dto-classes-and-explicit-units-work)
In AGBoss, DTOs are plain TypeScript classes decorated with field decorators from `@agboss/core/dto` (`@Text`, `@ListOf`, `@Integer`, `@Decimal`, `@Flag`). Runtime values returned from tools and actions are plain object literals `{ ... }` (never `new MyDto()`), and all currency and duration values use factory functions from `@agboss/core/units` (`usd()`, `seconds()`, `minutes()`, `hours()`):
src/tools/lookup-order.tool.ts
```ts
import { Budget, BudgetScope, ExecutionContext, Tool, type BudgetHandler, type ToolHandler } from "@agboss/core";
import { Integer, Text } from "@agboss/core/dto";
import { usd, type Usd } from "@agboss/core/units";
export class OrderLookupInput {
@Text({ prompt: "the order identifier to look up" })
orderId!: string;
}
export class OrderSummaryOutput {
@Text({ prompt: "current fulfillment status" })
status!: string;
@Integer({ prompt: "number of line items" })
itemCount!: number;
}
@Budget({
name: "order-execution-budget",
scope: BudgetScope.Execution,
})
export class OrderExecutionBudget implements BudgetHandler {
maxSpend(): Usd {
return usd(0.1);
}
}
@Tool({
name: "lookup_order",
description: "Looks up an order summary by ID",
input: OrderLookupInput,
output: OrderSummaryOutput,
deps: [ExecutionContext],
})
export class LookupOrderTool
implements ToolHandler
{
constructor(private readonly executionContext: ExecutionContext) {}
run({ orderId }: OrderLookupInput): Promise {
this.executionContext.signal.throwIfAborted();
return Promise.resolve({
status: `shipped:${orderId}`,
itemCount: 2,
});
}
}
```
src/agents/order-assistant.agent.ts
```ts
import { Agent } from "@agboss/core";
import { LookupOrderTool } from "../tools/lookup-order.tool.js";
@Agent({
name: "order-assistant-agent",
description: "Looks up customer order status using LookupOrderTool",
model: "moonshotai/kimi-k2.6",
prompt: "Look up the order status before answering.",
tools: [LookupOrderTool],
})
export class OrderAssistantAgent {}
```
test/lookup-order.tool.test.ts
```ts
import { expect, it } from "vitest";
import { testTool } from "@agboss/core/testing";
import { LookupOrderTool } from "../src/tools/lookup-order.tool.js";
it("executes LookupOrderTool in isolation without booting the workflow", async () => {
const run = testTool(LookupOrderTool);
const res = await run({ orderId: "ord_104" });
expect(res.output.status).toBe("shipped:ord_104");
});
```
## Where to explore individual decorators
[Section titled “Where to explore individual decorators”](#where-to-explore-individual-decorators)
* [Workflow HDSL (`@Workflow`)](/concepts/workflow/) for `HarnessDSL` path chains, `.switch(caseOf())`, `.chooseOne(route())`, parallel launches, and quorums.
* [Dependency Injection (`@Injectable`)](/concepts/dependency-injection/) for constructor injection, `ExecutionContext`, `Clock`, `Logger`, and lifecycle hooks.
* [Agent Reference](/reference/agent/), [Router Reference](/reference/router/), and [Tool Reference](/reference/tool/) for full decorator options.
# Dependency Injection, Custom Implementations & Overrides
> How @Injectable constructor injection, custom @Rag / @ModelProvider / @Channel / Store adapters, class overrides, and lifecycle hooks work in AGBoss.
AGBoss includes a deterministic dependency injection container modeled on Angular and NestJS. Every component (`@Injectable`, `@Tool`, `@ToolInterceptor`, `@Action`, `@Router`, `@Arbiter`, `@Judge`, `@Compaction`, `@InputGuardrail`, `@OutputGuardrail`, `@PiiPolicy`, `@ModelProvider`, `@Channel`, `@ReplyInterpreter`, `@Rag`, `@Budget`, `@HealthCheck`) declares its constructor dependencies in `deps: [...]`, and the container resolves them at application startup.
Because AGBoss resolves infrastructure contracts by **class inheritance** or typed handler interfaces rather than hardcoding vendor SDKs, you can plug in your own custom `@Rag` vector databases, custom `@ModelProvider` gateways (OpenRouter, vLLM, Ollama, LiteLLM, or custom SDK transports via `ModelProviderHandler`), custom `@Compaction` knowledge-graph handlers (`CompactionStrategy.Custom`), custom `@Channel` transports, and custom Postgres/Redis stores — and swap any dependency in tests with `override(Base, Fake)`.
***
## Complete example: DI, custom `@Rag`, custom `@ModelProvider` & `override()` test
[Section titled “Complete example: DI, custom @Rag, custom @ModelProvider & override() test”](#complete-example-di-custom-rag-custom-modelprovider--override-test)
src/services/audit-trail.service.ts
```ts
import {
Clock,
ExecutionContext,
Injectable,
Tool,
type OnDestroy,
type OnInit,
type OnStart,
type OnStop,
type ToolHandler,
} from "@agboss/core";
import { Rag, type RagConnector, type RagRetrieval } from "@agboss/core/rag";
import { Text } from "@agboss/core/dto";
import { minutes, seconds, usd } from "@agboss/core/units";
import {
DecisionsModelProvider,
ModelCost,
ModelProvider,
OpenAiCompatibleProvider,
PromptCaching,
Reasoning,
RetryPolicy,
} from "@agboss/core/models";
export class JobQueryDto {
@Text({ prompt: "Search query for indexed jobs" })
query!: string;
}
@Injectable()
export class ConnectionPool implements OnInit, OnDestroy {
onInit(): Promise {
return this.ping();
}
onDestroy(): Promise {
return this.end();
}
ping(): Promise {
return Promise.resolve();
}
drain(): Promise {
return Promise.resolve();
}
end(): Promise {
return Promise.resolve();
}
queryJobs(q: string): Promise {
return Promise.resolve([`prod:${q}`]);
}
}
@Injectable({ deps: [ConnectionPool, Clock] })
export class JobIndexService implements OnInit, OnStart, OnStop, OnDestroy {
constructor(
private readonly pool: ConnectionPool,
private readonly clock: Clock,
) {}
async onInit(): Promise {
await this.pool.ping();
}
onStart(): void {
void this.clock.now();
}
async onStop(): Promise {
await this.clock.sleep(seconds(1));
await this.pool.drain();
}
async onDestroy(): Promise {
await this.pool.end();
}
}
// Custom @Rag implementation backed by your own vector/hybrid store:
@Rag({
name: "custom_vector_docs",
description: "Custom vector search over architecture runbooks",
topK: 3,
deps: [ConnectionPool, ExecutionContext],
})
export class CustomVectorDocsRag implements RagConnector {
constructor(
private readonly pool: ConnectionPool,
private readonly executionContext: ExecutionContext,
) {}
async retrieve(
query: string,
options: { readonly topK: number; readonly signal: AbortSignal },
): Promise {
void this.executionContext.tenant.tenantId;
const rows = await this.pool.queryJobs(query);
return {
results: rows.slice(0, options.topK).map((text, idx) => ({
text,
source: `doc-${String(idx + 1)}`,
score: 0.95,
})),
costUsd: 0.0002,
};
}
}
// Custom @ModelProvider for a local/self-hosted vLLM or LiteLLM server:
@ModelProvider({
name: "local-vllm-model-provider",
description: "Self-hosted vLLM OpenAI-compatible inference cluster",
serves: [/^local\//, /^moonshotai\//],
baseUrl: "http://localhost:8000/v1",
timeout: seconds(60),
cost: ModelCost.fromPrices({
"local/qwen-32b": {
inputPerMillion: usd(0.1),
outputPerMillion: usd(0.2),
},
"moonshotai/kimi-k2.6": {
inputPerMillion: usd(0.5),
outputPerMillion: usd(1.5),
},
}),
reasoning: Reasoning.off(),
promptCaching: PromptCaching.off(),
retryPolicy: RetryPolicy.none(),
circuitBreakerPolicy: {
failureThreshold: 5,
window: minutes(1),
openFor: seconds(30),
},
})
export class LocalVllmModelProvider extends OpenAiCompatibleProvider {
override readonly requestFields = {};
}
export const modelProviders = [LocalVllmModelProvider, DecisionsModelProvider];
@Tool({
name: "search_jobs",
description: "Searches indexed jobs using a warmed connection pool",
input: JobQueryDto,
output: JobQueryDto,
deps: [ConnectionPool],
})
export class SearchJobsTool
implements ToolHandler, OnInit, OnDestroy
{
constructor(private readonly pool: ConnectionPool) {}
async onInit(): Promise {
await this.pool.ping();
}
async onDestroy(): Promise {
await this.pool.drain();
}
async run(input: JobQueryDto): Promise {
const hits = await this.pool.queryJobs(input.query);
return { query: hits[0] ?? input.query };
}
}
```
src/audit.application.ts
```ts
import { Application, EmptyEnvironment } from "@agboss/core";
import {
AuditWorkflow,
DiAppBudget,
DiExecBudget,
DiTenantBudget,
} from "./services/audit-trail.service.js";
@Application({
name: "audit-application",
version: "1.0.0",
environment: EmptyEnvironment,
workflows: [AuditWorkflow],
budgets: [DiAppBudget, DiTenantBudget, DiExecBudget],
})
export class AuditApplication {}
```
test/custom-overrides.test.ts
```ts
import { expect, it } from "vitest";
import { override, testRag, testTool } from "@agboss/core/testing";
import {
ConnectionPool,
CustomVectorDocsRag,
SearchJobsTool,
} from "../src/services/audit-trail.service.js";
class FakeConnectionPool extends ConnectionPool {
override queryJobs(q: string): Promise {
return Promise.resolve([`fake-hit:${q}`]);
}
}
it("overrides ConnectionPool in both @Tool and custom @Rag unit runners", async () => {
const runTool = testTool(SearchJobsTool, {
overrides: [override(ConnectionPool, FakeConnectionPool)],
});
const toolRes = await runTool({ query: "staff-ts" });
expect(toolRes.output).toEqual({ query: "fake-hit:staff-ts" });
const runRag = testRag(CustomVectorDocsRag, {
overrides: [override(ConnectionPool, FakeConnectionPool)],
});
const ragRes = await runRag("runbook", { topK: 2 });
expect(ragRes.output[0]?.text).toBe("fake-hit:runbook");
expect(ragRes.output[0]?.source).toBe("doc-1");
expect(ragRes.spend.totalUsd).toBeCloseTo(0.0002);
});
```
***
## How custom implementations & overrides work for every contract
[Section titled “How custom implementations & overrides work for every contract”](#how-custom-implementations--overrides-work-for-every-contract)
AGBoss never locks you into built-in adapters. Every external integration is defined as an abstract base class or TypeScript interface that you can implement in production and override in tests:
| Contract Type | How to Implement Your Own in Production | How to Register It | How to Override / Test It |
| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Knowledge Base ([`@Rag`](/reference/rag/))** | Decorate with `@Rag({ name, description, topK, deps })` and either `implements RagConnector` (`retrieve(query, { topK, signal })` for Pinecone, Qdrant, pgvector, Elasticsearch) or `extends SqliteFtsConnector` (built-in BM25). | Referenced in `@Agent({ rag: [{ use: MyRag, mode: RagMode.Tool \| RagMode.Context }] })`; its `deps` go in `@Workflow({ providers })`. | `testRag(MyRag, { overrides: [override(VectorClient, FakeVectorClient)] })` or `mockRag(MyRag)` in `testWith(App)`. |
| **Model Gateway ([`@ModelProvider`](/reference/model-provider/))** | Decorate with `@ModelProvider({ name: "...-model-provider", serves, cost, reasoning, promptCaching, retryPolicy, rateLimitPolicy, circuitBreakerPolicy })` and extend `OpenRouterProvider`, `OpenAiCompatibleProvider` (vLLM, Ollama, LiteLLM, Azure), or `implements ModelProviderHandler` (`complete(request)` with `deps: [...]` for AWS Bedrock / Vertex AI / mTLS gateways). | Listed in `@Application({ providers: [MyModelProvider, DecisionsModelProvider] })`. | `testWith(App)` automatically intercepts model calls above the transport so unit tests never hit live endpoints. |
| **Custom Compaction ([`@Compaction`](/reference/compaction/))** | Decorate with `@Compaction({ name: "...-compaction", strategy: CompactionStrategy.Custom, turnsBetweenCompactions, deps })` and implement `CompactionHandler` (`compact(input)`). | Bound on `@Application`, `@Workflow`, `@Agent`, or `@Router` via `compaction: MyCompaction`. | `testCompaction(MyCompaction, { overrides })` or `mockOf(Service)` in `testWith(App)`. |
| **Tool Interceptor ([`@ToolInterceptor`](/reference/tool-interceptor/))** | Decorate with `@ToolInterceptor({ name: "...-tool-interceptor", description, deps })` and implement `BeforeToolCall` and/or `AfterToolCall`. | Bound via `toolInterceptors: [MyToolInterceptor]` on `@Application`, `@Workflow`, `@Agent`, `@Tool`, or `@McpTool`. | `testToolInterceptor(MyToolInterceptor, { overrides })`. |
| **Approval Channel ([`@Channel`](/reference/channel/))** | Declare `@ChannelRole({ approves, answeredBy, replyInterpreter }) export abstract class MyRole extends ChannelOf {}`, then implement `@Channel({ name, deps }) export class SlackChannel extends MyRole` (`send()` and optional `withdraw()`). | Listed in `@Application({ providers: [SlackChannel] })` (matched to `MyRole` by class inheritance). | In `testWith(App)`, pass `overrides: [override(MyRole, AutoApproveChannel)]` or override `SlackService` via `override(SlackService, FakeSlackService)`. |
| **7 Durable Stores ([Persistence](/guides/persistence-and-recovery/))** | Subclass any of the 7 contracts (`extends BaseCheckpointSaver`, `PausedExecutionRepository`, `ExecutionRecordRepository`, `CostLedgerRepository`, `ExecutionLeaseRepository`, `ExecutionTreeRepository`, `RateLimitRepository`) or subclass a `Sqlite*` class (`extends SqliteExecutionLeaseRepository`). | Listed in `@Application({ providers: [PostgresLeaseRepository, ...] })`. | Default `InMemory*` stores run automatically when omitted; or pass `overrides: [override(ExecutionLeaseRepository, FaultyLeaseRepo)]` in `testWith`. |
| **Domain Services ([`@Injectable`](/reference/injectable/))** | Decorate with `@Injectable({ deps: [...] })` (and optional `OnInit`, `OnStart`, `OnStop`, `OnDestroy` hooks). | Listed in `@Workflow({ providers: [MyService] })` (or `@Application({ providers })` when shared by app-level guards/observers). | `override(MyService, FakeMyService)` or `mockOf(MyService, { method: vi.fn() })` in `testWith` and all 10 unit runners. |
| **Value Tokens (`InjectionToken`)** | Create `const CONFIG = new InjectionToken("CONFIG")` and bind with `provide(CONFIG, { ... })`. (Note: passing a class to `provide(Class, instance)` is rejected by `[di.instance-provider]`). | Listed in `@Workflow({ providers: [provide(CONFIG, value)] })`. | Pass `provide(CONFIG, testValue)` in `overrides`. |
***
## Built-in injectable framework contexts
[Section titled “Built-in injectable framework contexts”](#built-in-injectable-framework-contexts)
Instead of calling `Date.now()`, `process.env`, or global singletons, AGBoss components inject framework contracts through `deps`:
* **`Clock` (`deps: [Clock]`):** Every timestamp and sleep (`this.clock.now()`, `await this.clock.sleep(seconds(30))`) goes through the application’s `Clock`. Under `testWith`, the container provides a `VirtualClock` that advances instantly with `await clock.advance(seconds(30))`.
* **`ApplicationContext` (`deps: [ApplicationContext]`):** Provides `this.applicationContext.environment` (typed by your `AppEnvironment` contract), `environmentName`, and `instanceId`. Components always inject `ApplicationContext` rather than `AppEnvironment` directly.
* **`ExecutionContext` (`deps: [ExecutionContext]`):** Provides read-only access to the current step’s frame (`executionId`, `rootExecutionId`, `parentExecutionId`, `depth`, `threadId`, `ownerId`, `tenant.tenantId`, `signal`, and `step.idempotencyKey`), isolated via `AsyncLocalStorage` across concurrent runs and parallel branches.
* **`TenantContext` (`deps: [MyTenantContext]`):** Subclass `TenantContext` to pass typed per-request tenant metadata (`plan`, `region`, `dailyBudgetUsd`) into tools, actions, and budgets.
***
## When the 4 container lifecycle hooks run
[Section titled “When the 4 container lifecycle hooks run”](#when-the-4-container-lifecycle-hooks-run)
Every component the container creates can implement any combination of `OnInit`, `OnStart`, `OnStop`, and `OnDestroy`:
| Hook | When It Runs | Typical Use Case |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onInit()` | After the component’s own `deps` have been created and completed their `onInit` | Opening database pools, preparing statements, warming caches (failing `onInit` aborts startup and runs `onDestroy` on already-initialized dependencies) |
| `onStart()` | After every component in the application has completed `onInit`, before `execute` | Starting background listeners, readiness checks, or startup audit logs |
| `onStop()` | When `app.close()` or `SIGTERM` arrives (new executions refused with `ApplicationStoppingError`, active ones draining up to `shutdownTimeout`) | Stopping intake queues and flushing pending operations |
| `onDestroy()` | After all running executions have finished, paused, or released their 30s leases to another instance | Closing database pools, sockets, and telemetry exporters |
# Environments, Secrets & Units
> Configure 12-factor typed environments with AppEnvironment, secret(), env(), env.optional(), and explicit unit types (usd, seconds, minutes, hours).
AGBoss enforces strict 12-factor dev/prod parity: workflows and components never read `process.env` directly. Instead, each `@Application` declares a single environment contract class (`environments/app.environment.ts`), and each deployment target (`prod.environment.ts`, `dev.environment.ts`, `test.environment.ts`) provides an object matching that contract.
## How to declare `AppEnvironment`, `secret()`, and `env()`
[Section titled “How to declare AppEnvironment, secret(), and env()”](#how-to-declare-appenvironment-secret-and-env)
At `createApp(Application, { environment })` startup, AGBoss validates every required environment variable and secret before initializing services or running workflows, throwing `EnvironmentError` immediately if any variable is missing or invalid:
src/environments/app.environment.ts
```ts
import type { Secret } from "@agboss/core";
import type { Milliseconds, Usd } from "@agboss/core/units";
export class AppEnvironment {
openRouterApiKey!: Secret;
piiPseudonymKey!: Secret;
sqlitePath!: string;
webhookBaseUrl?: string;
requestTimeout!: Milliseconds;
dailyTenantCap!: Usd;
}
```
src/environments/prod.environment.ts
```ts
import { env, secret } from "@agboss/core";
import { seconds, usd } from "@agboss/core/units";
import type { AppEnvironment } from "./app.environment.js";
export const environment: AppEnvironment = {
openRouterApiKey: secret("OPENROUTER_API_KEY"),
piiPseudonymKey: secret("PII_PSEUDONYM_KEY"),
sqlitePath: env("AGBOSS_SQLITE_PATH", {
default: "/var/lib/agboss/state.sqlite",
}),
webhookBaseUrl: env.optional("WEBHOOK_BASE_URL"),
requestTimeout: seconds(30),
dailyTenantCap: usd(50),
};
```
src/environments/test.environment.ts
```ts
import { secret } from "@agboss/core";
import { seconds, usd } from "@agboss/core/units";
import type { AppEnvironment } from "./app.environment.js";
export const environment: AppEnvironment = {
openRouterApiKey: secret("OPENROUTER_API_KEY"),
piiPseudonymKey: secret("PII_PSEUDONYM_KEY"),
sqlitePath: ":memory:",
requestTimeout: seconds(5),
dailyTenantCap: usd(1),
};
```
test/environments.test.ts
```ts
import { expect, it } from "vitest";
import { hours, minutes, seconds, usd } from "@agboss/core/units";
import { environment as testEnv } from "../src/environments/test.environment.js";
it("validates explicit unit factories and masks Secret instances in string serialization", () => {
expect(testEnv.sqlitePath).toBe(":memory:");
expect(testEnv.requestTimeout).toBe(seconds(5));
expect(testEnv.dailyTenantCap).toBe(usd(1));
expect(minutes(1)).toBe(seconds(60));
expect(hours(1)).toBe(minutes(60));
});
```
> **Compiler diagnostic `[environment.contract-value]`:** `AppEnvironment` is a pure TypeScript contract class (`field!: Type`). Never assign default values or field initializers on `AppEnvironment` itself (`sqlitePath = ":memory:"` fails with `[environment.contract-value]`) — all concrete values and `env(..., { default })` calls belong in `environments/.environment.ts`.
| Helper | Import | Behavior |
| ------------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `secret("ENV_VAR")` | `"@agboss/core"` | Reads a required secret into a `Secret` instance that prints as `"***"` in `String()`, `JSON.stringify()`, `util.inspect`, and logs; call `.reveal()` only when passing to a wire client. |
| `env("ENV_VAR", { default? })` | `"@agboss/core"` | Reads a required string variable (or falls back to `default` when unset). |
| `env.optional("ENV_VAR")` | `"@agboss/core"` | Returns `string \| undefined` without failing startup when the variable is unset. |
| `EmptyEnvironment` | `"@agboss/core"` | Built-in empty contract class for applications that do not require environment variables. |
## Why AGBoss requires explicit unit factories (`@agboss/core/units`)
[Section titled “Why AGBoss requires explicit unit factories (@agboss/core/units)”](#why-agboss-requires-explicit-unit-factories-agbosscoreunits)
Raw numbers in configuration are a major source of production bugs (confusing `30` seconds with `30` milliseconds, or passing negative/NaN floats as dollar budgets). Every timeout, TTL, retention window, and dollar amount in AGBoss uses branded unit factories from `@agboss/core/units`:
| Factory | Return Type | Validation & Usage |
| ------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `usd(amount)` | `Usd` | Verifies finite `amount >= 0`; used in `@Budget.maxSpend()`, `SpendRecorder.record()`, and `replyWith(..., { cost })`. |
| `seconds(n)` | `Milliseconds` | Converts non-negative seconds to branded `Milliseconds` (`timeout`, `shutdownTimeout`, quorum `deadline`). |
| `minutes(n)` | `Milliseconds` | Converts non-negative minutes to branded `Milliseconds` (model provider `timeout`, circuit breaker cooldowns). |
| `hours(n)` | `Milliseconds` | Converts non-negative hours to branded `Milliseconds` (`pauseRecordRetention: hours(24)`, long quorum deadlines). |
# Digital & Physical AI Missions (System-2 Planner)
> How AGBoss orchestrates complex digital workflows and acts as the System-2 high-level mission planner for Physical AI, robotics, and devices.
AGBoss is a strictly typed TypeScript application framework designed for **complex multi-agent workflows and long-running autonomous missions** on top of [LangGraph](https://www.langchain.com/langgraph). While lightweight prompt chains and untyped graphs suffice for conversational chatbots, production digital operations and physical autonomous systems require static graph and saga verification (`agboss check`), automatic LIFO side-effect compensation (`compensateWith` / `committedBy`), pre-call FinOps budget reservations, multi-instance lease work-stealing, and two-phase human-in-the-loop pauses with automatic button withdrawal (`ChannelHandler.withdraw`).
## Why do high-consequence digital and physical AI systems need a System-2 orchestrator?
[Section titled “Why do high-consequence digital and physical AI systems need a System-2 orchestrator?”](#why-do-high-consequence-digital-and-physical-ai-systems-need-a-system-2-orchestrator)
One unified `@Application` architecture in AGBoss governs both long-running enterprise backend pipelines and high-level physical autonomy:
| Capability | Digital Autonomous Missions (FinTech, SecOps, Enterprise Ops) | Physical AI, Robotics & Smart Devices (System-2 Mission Planner) |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Execution Horizon** | Runs lasting minutes to days across distributed child workflows (`.toParallelWorkflows`) claimed by idle Kubernetes pods via `ExecutionLeaseRepository`. | Deliberative mission planning (1–10 Hz) layered above 100–1000 Hz real-time hardware controllers, motor drivers, and local safety reflexes. |
| **Multimodal & Multi-Agent Quorum** | Parallel specialist agents (risk, compliance, fraud) joined via `.acceptedBy(Arbiter).joinQuorum({ min, max, deadline })` with instant `AbortSignal` cancellation of stragglers. | Multiple multimodal vision/spatial models cross-checked by a deterministic `@Arbiter` before executing an irreversible physical maneuver. |
| **Human-in-the-Loop & Withdrawal** | Two-phase `@ChannelRole` pauses (`Pending → Resolved` CAS) in Slack/Teams; `ChannelHandler.withdraw` removes stale buttons when quorum or `FailFast` resolves early. | Safe mission hold requesting operator confirmation with camera/sensor telemetry; `withdraw` dismisses the operator prompt if a sibling branch or abort resolves first. |
| **Static Verification (`agboss check`)** | Compile-time Path Scope proof (`[input.outdated-on-path]`), DTO validation, and static saga verification (`[flow.uncompensated-action]`). | Strict coordinate, velocity, and torque bounds verified via DTO schemas and `agboss check` before any command reaches the hardware bridge. |
| **Automatic LIFO Saga Compensation** | Reverses external API/database mutations (`ReleaseEscrowAction`, `RevokeOfferAction`) in reverse order on `app.cancel`, `FailFast`, `Surplus`, or `@Budget` breach. | Executes deterministic compensating `@Action` nodes (`ReturnToSafeHoldAction`, `DisarmGripperAction`) in LIFO order under a dedicated `compensationTimeout` cleanup window. |
## How does AGBoss orchestrate long-running digital missions with transactional sagas?
[Section titled “How does AGBoss orchestrate long-running digital missions with transactional sagas?”](#how-does-agboss-orchestrate-long-running-digital-missions-with-transactional-sagas)
In enterprise backend, FinTech, and incident-response pipelines, workflows mutate external systems over hours or days while fanning out into child workflows and waiting on human approvals. AGBoss ensures that every uncommitted external mutation declares its `compensateWith` rollback action and optional `committedBy` commit boundary, unwinding side effects in reverse completion order (LIFO) if the mission is cancelled or aborted:
src/missions/escrow-settlement.workflow\.ts
```ts
import { StepFailedError, Workflow, listOf, start, toAction } from "@agboss/core";
import {
CommitSettlementAction,
HoldEscrowFundsAction,
ProvisionLedgerEntryAction,
ReleaseEscrowFundsAction,
ReverseLedgerEntryAction,
} from "./escrow.actions.js";
@Workflow({
name: "escrow-settlement-workflow",
version: "1.0.0",
maxConcurrentExecutions: 12,
flow: [
start(SettlementStart)
.returns(TransferRequestDto)
.toAction(HoldEscrowFundsAction)
.returns(EscrowHoldDto)
.toAction(ProvisionLedgerEntryAction)
.returns(listOf(LedgerEntryDto))
// Distributed child workflows work-stolen across cluster instances:
.toParallelWorkflows(ComplianceCheckWorkflow, {
maxItems: 6,
maxConcurrency: 4,
})
.join()
.onJoinError(
StepFailedError,
toAction(ReverseLedgerEntryAction)
.toAction(ReleaseEscrowFundsAction)
.toFinish(SettlementRolledBackFinish),
)
.returns(listOf(ComplianceClearanceDto))
// Commit boundary: marks both HoldEscrowFundsAction and ProvisionLedgerEntryAction committed
.toAction(CommitSettlementAction)
.toFinish(SettlementFinish),
],
})
export class EscrowSettlementWorkflow {}
```
src/missions/escrow\.actions.ts
```ts
import { Action, ExecutionContext, type ActionHandler, type InputOf, type OutputOf } from "@agboss/core";
@Action({
name: "release-escrow-funds-action",
description: "Compensates HoldEscrowFundsAction by releasing reserved funds back to the source account",
input: { hold: EscrowHoldDto },
deps: [ExecutionContext],
})
export class ReleaseEscrowFundsAction implements ActionHandler {
constructor(private readonly executionContext: ExecutionContext) {}
async execute(input: InputOf): Promise {
// Runs automatically in LIFO order on app.cancel, FailFast, quorum Surplus, or @Budget breach
void input.hold.value.holdId;
void this.executionContext.step.idempotencyKey;
}
}
@Action({
name: "commit-settlement-action",
description: "Finalizes settlement; once completed, earlier compensators are marked committed",
input: { hold: EscrowHoldDto, clearances: listOf(ComplianceClearanceDto) },
})
export class CommitSettlementAction implements ActionHandler {
async execute(): Promise {}
}
@Action({
name: "hold-escrow-funds-action",
description: "Places an external escrow hold that must be released if settlement does not commit",
input: { request: TransferRequestDto },
output: EscrowHoldDto,
compensateWith: ReleaseEscrowFundsAction,
committedBy: CommitSettlementAction,
deps: [ExecutionContext],
})
export class HoldEscrowFundsAction implements ActionHandler {
constructor(private readonly executionContext: ExecutionContext) {}
async execute(input: InputOf): Promise> {
return { holdId: `hold_${this.executionContext.step.idempotencyKey}`, amountUsd: input.request.value.amountUsd };
}
}
```
test/escrow-settlement.test.ts
```ts
import { expect, it } from "vitest";
import { testAction } from "@agboss/core/testing";
import { HoldEscrowFundsAction } from "../src/missions/escrow.actions.js";
import { SettlementStart } from "../src/missions/escrow-settlement.workflow.js";
it("creates an idempotent escrow hold from TransferRequestDto", async () => {
const runHold = testAction(HoldEscrowFundsAction);
const { output } = await runHold({
request: {
value: { amountUsd: 25_000 },
producer: SettlementStart,
},
});
expect(output.amountUsd).toBe(25_000);
expect(output.holdId).toContain("hold_");
});
```
## How does the dual-loop architecture work for Physical AI and robotics?
[Section titled “How does the dual-loop architecture work for Physical AI and robotics?”](#how-does-the-dual-loop-architecture-work-for-physical-ai-and-robotics)
Autonomous robots, drones, and smart industrial devices separate control into two distinct loops:
1. **System 1 — Real-Time Hardware Control Loop (100–1000 Hz):** Low-level firmware, PID/MPC controllers, actuator drivers, and hardware safety interlocks handle sub-millisecond stabilization, collision reflexes, and emergency stops.
2. **System 2 — Deliberative Mission Orchestrator (`agboss`):** Runs as the high-level “cerebral cortex” that sequences multi-stage physical missions, fuses multimodal VLM/LLM assessments via `.joinQuorum()` and `@Arbiter`, pauses for remote human tele-supervision via `@ChannelRole` (with `withdraw` when resolved), verifies all target coordinates through typed DTOs, and triggers deterministic LIFO compensating actions (`SafeParkManipulatorAction`) whenever a mission is aborted:
src/robotics/inspection-mission.workflow\.ts
```ts
import { Arbiter, DecidedBy, OnAnswer, Workflow, listOf, start } from "@agboss/core";
import { seconds } from "@agboss/core/units";
import {
AcquireTelemetryAction,
ArmManipulatorAction,
ExecuteValveTurnAction,
SafeParkManipulatorAction,
} from "./robotics.actions.js";
@Arbiter({
name: "pose-consensus-arbiter",
decidedBy: DecidedBy.Code,
})
export class PoseConsensusArbiter {
@OnAnswer(PrimarySpatialVlmAgent)
fromPrimarySpatialVlm(answer: PoseAssessmentDto): boolean {
return answer.confidence >= 0.92 && answer.obstacleClearanceCm >= 15;
}
@OnAnswer(BackupSpatialVlmAgent)
fromBackupSpatialVlm(answer: PoseAssessmentDto): boolean {
return answer.confidence >= 0.92 && answer.obstacleClearanceCm >= 15;
}
@OnAnswer(ThermalVlmAgent)
fromThermalVlm(answer: PoseAssessmentDto): boolean {
return answer.confidence >= 0.92 && answer.obstacleClearanceCm >= 15;
}
}
@Workflow({
name: "valve-inspection-mission-workflow",
version: "1.0.0",
maxSteps: 16,
flow: [
start(MissionStart)
.returns(MissionTargetDto)
.toAction(AcquireTelemetryAction)
.returns(SensorFrameDto)
// 1. Multimodal perception quorum: 2-of-3 vision models must agree via PoseConsensusArbiter
.toParallelAgents(PrimarySpatialVlmAgent, BackupSpatialVlmAgent, ThermalVlmAgent)
.acceptedBy(PoseConsensusArbiter)
.joinQuorum({
min: 2,
max: 3,
deadline: seconds(8),
})
.returns(listOf(PoseAssessmentDto))
// 2. Compensatable hardware state transition: automatically parked in LIFO order on abort
.toAction(ArmManipulatorAction)
.toAction(ExecuteValveTurnAction)
.onError(
ActionFailedError,
toAction(SafeParkManipulatorAction)
.toFinish(MissionAbortedFinish),
)
.returns(ValveTurnReceiptDto)
.toFinish(MissionCompletedFinish),
],
})
export class ValveInspectionMissionWorkflow {}
```
src/robotics/robotics.actions.ts
```ts
import { Action, ExecutionContext, type ActionHandler, type InputOf } from "@agboss/core";
@Action({
name: "safe-park-manipulator-action",
description: "Returns the robotic arm to locked safe-hold posture when a mission aborts before completion",
input: { poses: listOf(PoseAssessmentDto) },
deps: [ExecutionContext, HardwareBridgeService],
})
export class SafeParkManipulatorAction implements ActionHandler {
constructor(
private readonly executionContext: ExecutionContext,
private readonly hardwareBridgeService: HardwareBridgeService,
) {}
async execute(): Promise {
// Invoked with a live cleanup signal bounded by @Application({ compensationTimeout: seconds(15) })
await this.hardwareBridgeService.transitionToSafeHold({
idempotencyKey: this.executionContext.step.idempotencyKey,
signal: this.executionContext.signal,
});
}
}
@Action({
name: "arm-manipulator-action",
description: "Unlocks and positions the manipulator arm; compensated by SafeParkManipulatorAction",
input: { poses: listOf(PoseAssessmentDto) },
compensateWith: SafeParkManipulatorAction,
committedBy: ExecuteValveTurnAction,
deps: [ExecutionContext, HardwareBridgeService],
})
export class ArmManipulatorAction implements ActionHandler {
constructor(
private readonly executionContext: ExecutionContext,
private readonly hardwareBridgeService: HardwareBridgeService,
) {}
async execute(input: InputOf): Promise {
await this.hardwareBridgeService.moveToPreGrasp(
input.poses.map((p) => p.value),
{ signal: this.executionContext.signal },
);
}
}
```
src/robotics/teleop-approval.channel.ts
```ts
import {
Channel,
ChannelOf,
ChannelOutcome,
ChannelRole,
PauseAnswerer,
type ChannelAsk,
type ChannelWithdrawal,
} from "@agboss/core";
@ChannelRole({
name: "teleop-safety-channel",
description: "Requests human operator confirmation with camera frame before high-torque actuation",
approves: ValveTurnCommandDto,
answeredBy: PauseAnswerer.Channel,
})
export abstract class TeleopSafetyChannel extends ChannelOf {}
@Channel({
name: "console-teleop-safety-channel",
description: "Publishes interactive approval card to operator console and withdraws on abort/surplus",
deps: [OperatorConsoleService],
})
export class ConsoleTeleopSafetyChannel extends TeleopSafetyChannel {
constructor(private readonly operatorConsole: OperatorConsoleService) {
super();
}
async send(ask: ChannelAsk): Promise> {
await this.operatorConsole.showPrompt(ask.pauseId, ask.value.arguments);
return ChannelOutcome.awaitReply();
}
override async withdraw(withdrawal: ChannelWithdrawal): Promise {
// Immediately dismisses the operator prompt if mission is cancelled or quorum resolved
await this.operatorConsole.dismissPrompt(withdrawal.pauseId, withdrawal.resolution);
}
}
```
test/inspection-mission.test.ts
```ts
import { expect, it } from "vitest";
import { testArbiter } from "@agboss/core/testing";
import {
PoseConsensusArbiter,
PrimarySpatialVlmAgent,
} from "../src/robotics/inspection-mission.workflow.js";
it("accepts pose assessments only when confidence and obstacle clearance meet safety bounds", async () => {
const runArbiter = testArbiter(PoseConsensusArbiter);
const safePose = await runArbiter(PrimarySpatialVlmAgent, {
confidence: 0.96,
obstacleClearanceCm: 22,
});
expect(safePose.output).toBe(true);
const tooClose = await runArbiter(PrimarySpatialVlmAgent, {
confidence: 0.96,
obstacleClearanceCm: 8,
});
expect(tooClose.output).toBe(false);
});
```
# Path Scope & Provenance
> How AGBoss and HarnessDSL track Static Single Assignment (SSA) DTO slots, Produced provenance, { about } review loops, and compile-time Path Scope.
Every path in an AGBoss `@Workflow` written in **HarnessDSL (`HDSL`)** carries a compile-time and runtime **Path Scope**: the Static Single Assignment (SSA) set of DTO values guaranteed to have been produced along that path so far. Components declare what they read by DTO class (`input: { draft: OfferDraftDto }`) and what they produce (`output: OfferDraftDto`), while the workflow writes each transition explicitly with `.returns(OfferDraftDto)` so `agboss check` and TypeScript verify every read before a single model call runs.
## How slots, writes, and narrowed `Produced` reads work
[Section titled “How slots, writes, and narrowed Produced\ reads work”](#how-slots-writes-and-narrowed-producedt-p-reads-work)
A Path Scope stores **one slot per shape**: `OfferDraftDto` and `listOf(OfferDraftDto)` are distinct slots, and `listOf(oneOf(ApprovedDto, CritiqueDto))` is a single polymorphic list slot. Multiple outputs from a step or join are declared strictly by chaining `.returns(DtoA).returns(DtoB)` (the multi-argument `.returns(A, B)` form is not supported). Each write (`start(S).returns(...)`, `.returns(Dto)`, or `caseOf(Dto)`) replaces the current value in that slot while preserving earlier versions in the slot history.
When you run `npx agboss check`, the compiler performs dominance analysis (`[input.missing-on-path]`, `[input.never-produced]`, `[flow.unused-output]`, `[input.outdated-on-path]`, `[launch.branch-outdates-sibling]`) over every `@Workflow` and writes `.agboss/types.d.ts`:
* **`@Agent` inputs** receive each declared `input` field rendered as a structured Markdown/JSON section in the user message frame (keeping the system prompt 100% static for KV-cache hits).
* **Code handlers reading from Path Scope (`@Action`, `@Router` with `DecidedBy.Code`, and `FinishHandler` `@WorkflowFinish`)** receive each field from `InputOf` wrapped as `Produced` (`{ value: T, producer: P }`), where `P` is the exact TypeScript union of step classes that could have produced that DTO on the incoming paths (e.g., `typeof DraftAgent | typeof EditorAgent`).
* **Fixed-source handlers (`@OnAnswer(Agent)`, `ToolHandler.run`, `ChannelAsk.value`, `ReplyInterpreterHandler.interpret({ ask, reply })`)** receive the bare `Dto` directly because the producer is single and fixed.
src/workflows/offer-scope.workflow\.ts
```ts
import { Workflow, agent, caseOf, start } from "@agboss/core";
import { oneOf } from "@agboss/core/dto";
@Workflow({
name: "offer-scope-workflow",
version: "1.0.0",
flow: [
start(OfferStart)
.returns(CandidateBriefDto)
.toAgent(DraftAgent)
.returns(OfferDraftDto)
.toAgent(ReviewerAgent),
agent(ReviewerAgent)
.returns(oneOf(DraftApprovedDto, ChangesRequestedDto))
.switch(
caseOf(DraftApprovedDto, { about: OfferDraftDto })
.toAction(SendOfferAction)
.returns(OfferSentDto)
.toFinish(OfferFinish),
caseOf(ChangesRequestedDto, { about: OfferDraftDto })
.toAgent(EditorAgent)
.returns(OfferDraftDto)
.toAgent(ReviewerAgent),
),
],
})
export class OfferScopeWorkflow {}
```
src/workflows/offer.components.ts
```ts
import { Action, Agent, WorkflowFinish, WorkflowStart, type ActionHandler, type InputOf, type OutputOf } from "@agboss/core";
import { Text, historyOf, oneOf } from "@agboss/core/dto";
export class CandidateBriefDto {
@Text({ prompt: "Candidate summary and target role" })
roleTitle!: string;
}
export class OfferDraftDto {
@Text({ prompt: "Offer letter markdown body" })
text!: string;
}
export class DraftApprovedDto {
@Text({ prompt: "Approval sign-off summary" })
summary!: string;
}
export class ChangesRequestedDto {
@Text({ prompt: "Specific edits required before approval" })
notes!: string;
}
export class OfferSentDto {
@Text()
sentText!: string;
@Text()
draftedBy!: string;
}
@WorkflowStart({
name: "offer-workflow-start",
description: "Starts an offer drafting and review flow",
input: CandidateBriefDto,
})
export class OfferStart {}
@WorkflowFinish({
name: "offer-workflow-finish",
description: "Returns the sent offer confirmation",
output: OfferSentDto,
})
export class OfferFinish {}
@Agent({
name: "draft-agent",
description: "Writes the initial offer draft from the candidate brief",
model: "moonshotai/kimi-k2.6",
prompt: "Write an initial offer draft for the candidate.",
input: { brief: CandidateBriefDto },
output: OfferDraftDto,
})
export class DraftAgent {}
@Agent({
name: "reviewer-agent",
description: "Reviews the current offer draft",
model: "moonshotai/kimi-k2.6",
prompt: "Approve the draft or request specific changes.",
input: { draft: OfferDraftDto },
output: oneOf(DraftApprovedDto, ChangesRequestedDto),
maxVisits: 3,
})
export class ReviewerAgent {}
@Agent({
name: "editor-agent",
description: "Revises the offer draft using bounded critique history",
model: "moonshotai/kimi-k2.6",
prompt: "Revise the offer draft addressing all critiques.",
input: {
draft: OfferDraftDto,
critiques: historyOf(ChangesRequestedDto, { limit: 3 }),
},
output: OfferDraftDto,
})
export class EditorAgent {}
@Action({
name: "send-offer-action",
description: "Reads Produced values with compile-checked provenance",
input: {
draft: OfferDraftDto,
approval: DraftApprovedDto,
},
output: OfferSentDto,
})
export class SendOfferAction implements ActionHandler {
execute(
input: InputOf,
): Promise> {
// input.draft.producer is narrowed by .agboss/types.d.ts to:
// typeof DraftAgent | typeof EditorAgent
return Promise.resolve({
sentText: input.draft.value.text,
draftedBy: input.draft.producer.name,
});
}
}
```
test/offer-scope.workflow\.test.ts
```ts
import { Application, EmptyEnvironment } from "@agboss/core";
import { expect } from "vitest";
import { replyWith, testWith } from "@agboss/core/testing";
import { OfferScopeWorkflow } from "./offer-scope.workflow.js";
import {
ChangesRequestedDto,
DraftAgent,
DraftApprovedDto,
EditorAgent,
OfferDraftDto,
OfferFinish,
OfferStart,
ReviewerAgent,
} from "./offer.components.js";
@Application({
name: "offer-scope-application",
version: "1.0.0",
environment: EmptyEnvironment,
workflows: [OfferScopeWorkflow],
budgets: [OfferAppBudget, OfferTenantBudget, OfferExecBudget],
})
export class OfferScopeApplication {}
const test = testWith(OfferScopeApplication);
test("tracks Produced producer across ReviewerAgent -> EditorAgent revision loop", async ({
app,
mockLlm,
}) => {
mockLlm(DraftAgent).thenReturn(
replyWith(OfferDraftDto, { text: "Draft v1" }),
);
mockLlm(ReviewerAgent).thenReturn(
replyWith(ChangesRequestedDto, { notes: "Add equity cliff details" }),
replyWith(DraftApprovedDto, { summary: "Approved v2" }),
);
mockLlm(EditorAgent).thenReturn(
replyWith(OfferDraftDto, { text: "Draft v2 with equity cliff" }),
);
const result = await app.execute(OfferStart, {
roleTitle: "Staff Distributed Systems Engineer",
});
expect(result.finish).toBe(OfferFinish);
expect(result.output).toEqual({
sentText: "Draft v2 with equity cliff",
draftedBy: "EditorAgent",
});
});
```
## How `about`, `historyOf`, and `OutdatedReason` prevent stale-state bugs
[Section titled “How about, historyOf, and OutdatedReason prevent stale-state bugs”](#how-about-historyof-and-outdatedreason-prevent-stale-state-bugs)
In cyclic review graphs (`DraftAgent -> ReviewerAgent -> EditorAgent -> ReviewerAgent`), an older approval or critique must never survive after `EditorAgent` produces a new draft. AGBoss solves this with **artifact provenance binding (`{ about: OfferDraftDto }`)**:
| Mechanism | API | Behavior |
| -------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Artifact binding** | `.returns(VerdictDto, { about: OfferDraftDto })` & `caseOf(VerdictDto, { about: OfferDraftDto })` | Binds the reviewer output to the specific `OfferDraftDto` instance it evaluated (or an array of targets: `{ about: [OfferDraftDto, TermsDto] }`). |
| **Automatic invalidation** | `OutdatedReason.AboutReplaced` & `OutdatedReason.TransitiveOutdated` | When `EditorAgent` runs and replaces `OfferDraftDto`, any value bound `{ about: OfferDraftDto }` (and anything transitively `{ about }` that value) is immediately marked outdated and evicted from current slots. |
| **Compile-time guard** | `[input.outdated-on-path]` & `[input.missing-on-path]` | `agboss check` rejects any workflow path where a downstream step tries to read a slot that was not produced on every incoming branch or was invalidated along that path. |
| **Bounded loop history** | `historyOf(ChangesRequestedDto, { limit: 3 })` | Gives the revising agent `{ current, previous }` over the last `limit` versions of a slot in chronological order, including values that were later outdated when a new draft was written. |
***
## Standalone Executable Proof: [`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)
[Section titled “Standalone Executable Proof: 02-path-scope-proof”](#standalone-executable-proof-02-path-scope-proof)
To see all four static dataflow & compensation diagnostics reproduced side by side against raw LangGraph (`cases/01-missing-input`, `02-outdated-data`, `03-parallel-invalidation`, `04-uncompensated-action`), inspect **[`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)** and **[`07-langgraph-migration`](https://github.com/agbossjs/agboss-examples/tree/main/07-langgraph-migration)** in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) (see [7 Verified Examples](/examples/job-scout/)).
# Progressive Crystallization
> Evolve DecidedBy.Model routers and multi-tool @Agent loops into 0ms, $0 DecidedBy.Code handlers and @Action steps using agboss describe and trace telemetry.
One of the costliest anti-patterns in production AI engineering is **permanent stochasticity**: keeping every routing decision and multi-step tool sequence inside an LLM call long after the underlying domain patterns have stabilized.
Early in a feature’s lifecycle, you *want* the flexibility of [`DecidedBy.Model`](/reference/router/) routers and multi-tool [`@Agent`](/reference/agent/) loops so your system can handle messy, unanticipated user inputs. Over weeks of production traffic, however, 80–95% of executions often follow repeatable structural patterns:
* A `DecidedBy.Model` router consistently picks `EnterpriseApprovalAgent` whenever `order.amountCents > 100_000` or `order.region === "EU"`.
* An `@Agent` with four `@Tool` bindings calls `FetchInvoiceTool` followed immediately by `ValidateTaxIdTool` in the exact same order on 99% of runs.
**Progressive Crystallization** is the architectural practice of systematically identifying these settled stochastic **Desire Paths** via content-addressed revision hashes (`agboss describe --json`), multi-run `pass@k` vs `pass^k` reliability benchmarks, and execution traces (`ExecutionRecord` and Langfuse / OpenTelemetry spans), and replacing them in place without changing neighbor contracts:
* `DecidedBy.Model` `@Router` and `@Arbiter` $\to$ `DecidedBy.Code` (`0ms` LLM latency, `$0.00` token spend)
* `DecidedBy.Model` `@InputGuardrail`, `@OutputGuardrail`, and `@Judge` $\to$ `DecidedBy.Code` (`GuardrailHandler` / deterministic `JudgeHandler` inspecting DTOs and `toolCalls` at `$0.00` cost)
* Repetitive `@Tool` sequences inside an `@Agent` $\to$ deterministic `@Action` steps or `@ToolInterceptor` gates
* Model-summarized `@Compaction` $\to$ `CompactionStrategy.Custom` (`CompactionHandler`) writing structured facts into a knowledge graph or vector store
* Human-in-the-loop `ChannelOutcome.awaitReply()` pauses on low-risk cases $\to$ immediate auto-approved `ChannelOutcome.decided(...)`
***
## Step 1: Content-Addressed Revision Snapshots (`agboss describe --json`)
[Section titled “Step 1: Content-Addressed Revision Snapshots (agboss describe --json)”](#step-1-content-addressed-revision-snapshots-agboss-describe---json)
In typical agent frameworks, production traces in Langfuse or OpenTelemetry quickly drift from the Git repository: when looking at a trace from yesterday, neither an engineer nor an AI coding agent can tell whether the prompt text, DTO schema, or graph topology has changed since that run.
In AGBoss, `npx agboss describe --json` exports a deterministic, machine-readable snapshot of your entire `@Application` without requiring any API keys:
```bash
npx agboss describe --json
```
The JSON envelope includes three cryptographic content hashes:
| Hash Field | Scope | What It Hashes |
| ---------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **`configHash`** | Per `@Workflow` | Complete graph topology: start/finish nodes, step sequence, `.switch()` / `.chooseOne()` edges, `.joinQuorum()` rules, and `.onError()` paths |
| **`promptHash`** | Per `@Agent` / `@Router` / `@Arbiter` / `@Guardrail` | Resolved system prompt template (`prompt` + `promptUrls`) and model configuration |
| **`schemaHash`** | Per DTO Class | Normalized JSON Schema compiled from `@Text`, `@Integer`, `@Decimal`, `@Flag`, `@OneOf`, `@ListOf`, and `@Nested` decorators |
Every `ExecutionRecord` (`result.record`) and step trace emitted to [`OnExecutionEnd`](/guides/observability-and-tracing/) records these exact hashes (`configHash`, `promptHash`, `schemaHash`).
***
## Step 2: Crystallizing a `DecidedBy.Model` Router into `DecidedBy.Code`
[Section titled “Step 2: Crystallizing a DecidedBy.Model Router into DecidedBy.Code”](#step-2-crystallizing-a-decidedbymodel-router-into-decidedbycode)
Suppose you launch a workflow with a `DecidedBy.Model` router (`OrderTriageModelRouter`) that classifies incoming orders using a fast decision model (`typesafe/jev-1.13`). Once execution traces for `OrderTriageModelRouter`’s `promptHash` and `OrderTriageDto`’s `schemaHash` confirm that the routing boundary is a pure function of `OrderTriageDto` fields (or a deterministic database lookup via an `@Injectable()` service), you (or your coding agent) crystallize the router in place by switching `decidedBy` to `DecidedBy.Code`:
src/routers/order-triage-model.router.ts
```ts
import { DecidedBy, Router } from "@agboss/core";
import { Flag, Integer, Text } from "@agboss/core/dto";
export class OrderTriageDto {
@Text({ prompt: "Customer tier: free or enterprise" })
tier!: string;
@Integer({ min: 0, prompt: "Total order value in cents" })
amountCents!: number;
@Flag({ prompt: "Whether the shipping address is international" })
isInternational!: boolean;
}
@Router({
name: "order-triage-model-router",
description: "Routes orders to compliance review or fast-track capture",
decidedBy: DecidedBy.Model,
model: "typesafe/jev-1.13",
prompt: "Route international orders or orders over $5,000 to compliance review; fast-track the rest.",
input: { order: OrderTriageDto },
routes: [
{
target: ComplianceReviewAgent,
prompt: "Order requires manual or automated export/value compliance review",
},
{
target: FastTrackCaptureAction,
prompt: "Domestic standard order eligible for immediate capture",
},
],
})
export class OrderTriageModelRouter {}
```
src/routers/order-triage.router.ts
```ts
import {
DecidedBy,
Router,
type InputOf,
type Route,
type RouterHandler,
} from "@agboss/core";
@Router({
name: "order-triage-router",
description: "Deterministically routes international or high-value orders to compliance review",
decidedBy: DecidedBy.Code,
input: { order: OrderTriageDto },
routes: [
{ target: ComplianceReviewAgent },
{ target: FastTrackCaptureAction },
],
})
export class OrderTriageRouter implements RouterHandler {
route(input: InputOf): Route {
const { amountCents, isInternational } = input.order.value;
return isInternational || amountCents > 500_000
? ComplianceReviewAgent
: FastTrackCaptureAction;
}
}
```
test/order-triage.router.test.ts
```ts
import { expect, it } from "vitest";
import { testRouter } from "@agboss/core/testing";
import {
ComplianceReviewAgent,
FastTrackCaptureAction,
OrderTriageRouter,
TriageStart,
} from "../src/routers/order-triage.router.js";
it("verifies crystallized DecidedBy.Code router with zero LLM calls", async () => {
const runRouter = testRouter(OrderTriageRouter);
const highValue = await runRouter({
order: {
value: { tier: "enterprise", amountCents: 750_000, isInternational: false },
producer: TriageStart,
},
});
expect(highValue.output).toBe(ComplianceReviewAgent);
const standard = await runRouter({
order: {
value: { tier: "free", amountCents: 25_000, isInternational: false },
producer: TriageStart,
},
});
expect(standard.output).toBe(FastTrackCaptureAction);
});
```
Notice what **did not change**:
* The `@Workflow` definition (`*.workflow.ts`) is completely untouched — `.toRouter(OrderTriageRouter).chooseOne(...)` works identically whether `OrderTriageRouter` is `DecidedBy.Model` or `DecidedBy.Code`.
* Downstream Path Scope availability (`Produced`) is preserved.
* Latency for this node drops from \~150–400ms to `<0.1ms`, and token spend drops to `$0.00`.
***
## Step 3: Crystallizing Repetitive `@Tool` Chains into an `@Action`
[Section titled “Step 3: Crystallizing Repetitive @Tool Chains into an @Action”](#step-3-crystallizing-repetitive-tool-chains-into-an-action)
A second prime target for crystallization is an `@Agent` whose traces show a fixed sequence of tool calls before producing a structured DTO.
Instead of paying an LLM to orchestrate `ToolA -> ToolB` on every turn:
1. Extract the deterministic API/database calls into an [`@Injectable()`](/reference/injectable/) service or a dedicated [`@Action`](/reference/action/) node upstream of the `@Agent`.
2. Wire `.toAction(FetchAndEnrichAction).returns(EnrichedContextDto).toAgent(AnalystAgent)` in the workflow.
3. Now the deterministic data fetching executes with `0` LLM tokens and full [`ExecutionContext.step.idempotencyKey`](/concepts/application/) deduplication, while `AnalystAgent` receives the pre-fetched `EnrichedContextDto` in a single, KV-cache-friendly turn.
***
## Step 4: Discovering Crystallization Candidates (`pass@k` vs `pass^k`) & Verifying Zero Regression
[Section titled “Step 4: Discovering Crystallization Candidates (pass@k vs pass^k) & Verifying Zero Regression”](#step-4-discovering-crystallization-candidates-passk-vs-passk--verifying-zero-regression)
When running an `@EvalSuite` with `--runs ` (for example, `k = 5`), AGBoss reports two complementary metrics for every case:
* **`pass@k` ($\ge 1$ of $k$ runs passed):** Measures whether the model *can* solve the task.
* **`pass^k` ($k$ of $k$ runs passed):** Measures whether the workflow solves the task *reliably* on every production execution.
A case with **high `pass@k` (`1.00`) but low `pass^k` (`0.20`–`0.60`)** is the canonical signal of a stochastic step that should be crystallized into a `DecidedBy.Code` router, guardrail, `@ToolInterceptor`, or `@Action`.
Before promoting a crystallized component to production, verify that end-to-end quality, routing accuracy, and [`@Judge`](/reference/judge/) pass rates remain identical (while cost and latency drop):
```bash
# 1. Capture recent production/staging tasks into a reusable golden dataset:
npx agboss golden add --name order-triage-core --from-last 20
# 2. Compare baseline vs candidate profile on the exact same golden tasks:
npx agboss compare --profiles base,candidate --golden order-triage-core
# 3. Run the evaluation suite across k=5 trials and gate against the main branch baseline:
npx agboss eval run --suite order-triage-eval-suite --runs 5 --baseline .artifacts/eval/main.json --fail-on-regression
# 4. Export execution history and inspect the Progressive Crystallization Radar in AGBoss Mesh Hub (#401):
npx agboss describe --json > .artifacts/describe.json
npx agboss records export --all-tenants --out .artifacts/records.jsonl
npx agboss hub show .artifacts --html .artifacts/hub.html
npx agboss hub ui .artifacts
```
The **Progressive Crystallization Radar** in `agboss hub` joins `agboss describe --json` (`configHash`, `promptHash`, `schemaHash`), stored `ExecutionRecord`s (`steps[].decision`, `inputHash`, `toolCalls`), and `EvalRunReport` artifacts to highlight `DecidedBy.Model` routers whose decisions are invariant per `inputHash` and `@Agent` steps with deterministic tool sequences or low `pass^k`. See [Evaluation, Golden Sets & A/B Profiles](/guides/evaluation-and-profiles/) for full details on `@EvalSuite`, `pass@k` / `pass^k`, and CI regression gates.
# The 5-Loop Verification Funnel & Production Feedback Loops
> How AGBoss structures agent verification as a 5-loop cost and determinism funnel — from $0 static agboss check and VCR unit tests to calibrated @EvalSuite.
**AGBoss** (**Agent Graph Builder Open Source System**) replaces the traditional `Unit -> Integration -> E2E` testing pyramid with a **5-loop cost and determinism verification funnel** closed by **two production feedback loops**. In classical software engineering, every function is deterministic (`f(x) = y`); in a multi-agent system, an LLM node costs money, takes seconds, and exhibits variance even at `temperature: 0`. AGBoss therefore enforces one foundational rule across [`agboss check`](/cli/), `@agboss/core/testing`, and `@agboss/core/eval`: **never verify with a paid language model what can be proven by the compiler or checked by deterministic TypeScript; and when you invoke a model, check $0 trajectory invariants and code judges first before calling a calibrated LLM judge.**
***
## Complete example: Fail-Fast Cascade from `$0` code guards to calibrated `@Judge`
[Section titled “Complete example: Fail-Fast Cascade from $0 code guards to calibrated @Judge”](#complete-example-fail-fast-cascade-from-0-code-guards-to-calibrated-judge)
Before looking at each axis of the funnel, here is how Loops 0, 1, and 2 work together in code — combining a deterministic [`@Tool`](/reference/tool/), a deterministic `$0` [`@Judge`](/reference/judge/) (`CitationFormatJudge`, `0ms`), a calibrated `DecidedBy.Model` `@Judge` (`RefundPolicyGroundedJudge`), and a `testWith(Application)` + `testTool` + `testJudge` verification suite:
src/funnel/refund-verification.components.ts
```ts
import {
Agent,
Application,
Budget,
BudgetPeriod,
BudgetScope,
DecidedBy,
Decision,
EmptyEnvironment,
Judge,
JudgeModel,
JudgeVerdict,
Tool,
Workflow,
WorkflowFinish,
WorkflowStart,
start,
type BudgetHandler,
type JudgeHandler,
type JudgeInput,
type ToolHandler,
} from "@agboss/core";
import { Integer, Text } from "@agboss/core/dto";
import { usd, type Usd } from "@agboss/core/units";
export class RefundTaskDto {
@Text({ prompt: "Order ID and customer refund request" })
request!: string;
}
export class RefundToolInputDto {
@Text({ prompt: "Order ID starting with ORD-" })
orderId!: string;
@Integer({ prompt: "Refund amount in cents" })
amountCents!: number;
}
export class RefundReplyDto {
@Text({ prompt: "Refund confirmation citing [ORD-id]" })
confirmation!: string;
}
@Tool({
name: "issue-refund-tool",
description: "Issues a refund for an order",
input: RefundToolInputDto,
output: RefundReplyDto,
})
export class IssueRefundTool implements ToolHandler {
run(input: RefundToolInputDto): Promise {
return Promise.resolve({
confirmation: `Refunded ${String(input.amountCents)} cents for [${input.orderId}]`,
});
}
}
// Cascade Step 1 ($0, 0ms): Deterministic DecidedBy.Code @Judge runs BEFORE any paid LLM judge
@Judge({
name: "citation-format-judge",
description: "Verifies deterministically that confirmation contains an [ORD-...] citation",
decidedBy: DecidedBy.Code,
reply: RefundReplyDto,
})
export class CitationFormatJudge implements JudgeHandler {
judge(input: JudgeInput): Promise {
return Promise.resolve(
/\[ORD-\d+\]/.test(input.reply.confirmation)
? JudgeVerdict.passed()
: JudgeVerdict.failed({ reason: "Confirmation must cite [ORD-]." }),
);
}
}
// Cascade Step 2 (Paid Decision Model): Runs only after $0 code invariants pass
@Judge({
name: "refund-policy-grounded-judge",
description: "Evaluates whether the refund explanation matches tool evidence",
model: "typesafe/jev-1.13",
reply: RefundReplyDto,
deps: [JudgeModel],
})
export class RefundPolicyGroundedJudge implements JudgeHandler {
constructor(private readonly judgeModel: JudgeModel) {}
async judge(input: JudgeInput): Promise {
const res = await this.judgeModel.decide({
state: { task: input.task, confirmation: input.reply.confirmation },
questions: {
grounded: Decision.noul("Does the confirmation match the issue_refund tool output?"),
},
});
return res.grounded.noul >= 0.85
? JudgeVerdict.passed({ metrics: { grounded: res.grounded.noul } })
: JudgeVerdict.failed({ reason: "Confirmation does not match tool output." });
}
}
@WorkflowStart({ name: "refund-workflow-start", description: "Start", input: RefundTaskDto })
export class RefundWorkflowStart {}
@WorkflowFinish({ name: "refund-workflow-finish", description: "Finish", output: RefundReplyDto })
export class RefundWorkflowFinish {}
@Agent({
name: "refund-specialist-agent",
description: "Processes customer refunds using IssueRefundTool",
model: "moonshotai/kimi-k2.6",
prompt: "Call issue_refund and confirm with [ORD-id].",
input: { task: RefundTaskDto },
output: RefundReplyDto,
tools: [IssueRefundTool],
judges: [CitationFormatJudge, RefundPolicyGroundedJudge],
maxRetries: 1,
})
export class RefundSpecialistAgent {}
@Workflow({
name: "refund-verification-workflow",
version: "1.0.0",
flow: [
start(RefundWorkflowStart)
.returns(RefundTaskDto)
.toAgent(RefundSpecialistAgent)
.returns(RefundReplyDto)
.toFinish(RefundWorkflowFinish),
],
})
export class RefundVerificationWorkflow {}
@Budget({ name: "funnel-app-budget", scope: BudgetScope.Application, period: BudgetPeriod.Day })
export class FunnelAppBudget implements BudgetHandler { maxSpend(): Usd { return usd(50); } }
@Budget({ name: "funnel-tenant-budget", scope: BudgetScope.Tenant, period: BudgetPeriod.Day })
export class FunnelTenantBudget implements BudgetHandler { maxSpend(): Usd { return usd(5); } }
@Budget({ name: "funnel-exec-budget", scope: BudgetScope.Execution })
export class FunnelExecBudget implements BudgetHandler { maxSpend(): Usd { return usd(0.5); } }
@Application({
name: "refund-verification-application",
version: "1.0.0",
environment: EmptyEnvironment,
workflows: [RefundVerificationWorkflow],
budgets: [FunnelAppBudget, FunnelTenantBudget, FunnelExecBudget],
})
export class RefundVerificationApplication {}
```
test/refund-verification.funnel.test.ts
```ts
import { expect } from "vitest";
import { decideWith, replyWith, testJudge, testTool, testWith } from "@agboss/core/testing";
import {
CitationFormatJudge,
IssueRefundTool,
RefundPolicyGroundedJudge,
RefundReplyDto,
RefundSpecialistAgent,
RefundVerificationApplication,
RefundWorkflowStart,
} from "../src/funnel/refund-verification.components.js";
const test = testWith(RefundVerificationApplication);
test("runs $0 unit runners and cascading judges in Loop 1", async ({
app,
mockLlm,
}) => {
// 1. Isolated unit runners ($0, 1ms):
const runTool = testTool(IssueRefundTool);
const toolRes = await runTool({ orderId: "ORD-101", amountCents: 2500 });
expect(toolRes.output.confirmation).toContain("[ORD-101]");
const runFormatJudge = testJudge(CitationFormatJudge);
const badFormat = await runFormatJudge({
variant: RefundReplyDto,
reply: { confirmation: "Refunded without order tag" },
task: "Refund order ORD-101",
attempt: 0,
});
expect(badFormat.output.passed).toBe(false);
// 2. Full workflow test with scripted LLM & judge cascade ($0, 2ms):
mockLlm(RefundSpecialistAgent).thenReturn(
replyWith(RefundReplyDto, { confirmation: "Refunded 2500 cents for [ORD-101]" }),
);
mockLlm(RefundPolicyGroundedJudge).thenReturn(decideWith({ grounded: 0.96 }));
const result = await app.execute(RefundWorkflowStart, {
request: "Refund 2500 cents on ORD-101",
});
expect(result.output).toEqual({
confirmation: "Refunded 2500 cents for [ORD-101]",
});
});
```
***
## Axis 1: What are the 4 objects of verification in AGBoss?
[Section titled “Axis 1: What are the 4 objects of verification in AGBoss?”](#axis-1-what-are-the-4-objects-of-verification-in-agboss)
Every failure in a multi-agent system originates in one of **4 distinct architectural layers** ([7 Architectural Invariants](/concepts/architecture/)). Mixing them into a single undifferentiated test suite makes failures noisy and expensive to debug:
| # | Object of Verification | What Can Go Wrong | Primary AGBoss Primitives |
| ----- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1** | **Topology & Mesh Contracts (`Graph & Mesh Contracts`)** | Missing upstream inputs on a branch, reading a stale draft in a review loop (`{ about }`), forgetting a saga compensator, or breaking a schema consumed by a remote server | [`agboss check`](/cli/), `$ agboss check --federation`, `.agboss/types.d.ts`, [`Path Scope`](/concepts/path-scope/), [`compensateWith` / `committedBy`](/guides/error-handling-and-rollback/) |
| **2** | **Harness Code (`Harness Code`)** | Bugs in deterministic TypeScript surrounding the model: [`@Action`](/reference/action/), `DecidedBy.Code` [`@Router`](/reference/router/) & [`@Arbiter`](/reference/arbiter/), [`@Tool`](/reference/tool/), [`@ToolInterceptor`](/reference/tool-interceptor/), code guardrails, [`@PiiPolicy`](/reference/pii-policy/) masking, rate-limit queues, retries, and LIFO rollbacks | [`10 unit runners`](/guides/testing-agents/#all-10-isolated-unit-runners-in-agbosstesting), `testWith(App)`, `mockLlm`, `TestConcurrency.inOrder`, `VirtualClock`, `instances: 2` |
| **3** | **Model & Tool Trajectory (`Model & Trajectory`)** | The model misunderstands the prompt, skips `requiredTools`, calls `forbiddenTools`, passes another tenant’s ID in tool arguments, or hallucinates an ungrounded answer | `toHaveCalledTools`, `toHaveToolCallMatching`, `toolInvariant`, [`@Judge`](/reference/judge/), [`@EvalSuite`](/guides/evaluation-and-profiles/) (`pass@k` & `pass^k`) |
| **4** | **Memory & Compaction (`Memory & Compaction`)** | A critical safety constraint disappears after [`@Compaction`](/reference/compaction/), or the agent relies on an outdated fact from an old turn when a `@Tool` or [`@Rag`](/reference/rag/) returns updated rules | `testCompaction()`, multi-turn `pastTurn()` + `turn()` in `@EvalSuite`, `{ about }` staleness invalidation |
***
## Axis 2: How does the 5-Loop Cost & Determinism Funnel work?
[Section titled “Axis 2: How does the 5-Loop Cost & Determinism Funnel work?”](#axis-2-how-does-the-5-loop-cost--determinism-funnel-work)
AGBoss arranges verification into **5 concentric loops** ordered strictly by **speed (`< 1s` $\to$ `24/7`)** and **token cost (`$0` $\to$ `$$`)**:
```mermaid
flowchart TD
L0["Loop 0: Static Compiler Proof agboss check & --federation · < 1s · $0 · Every save"]
L1["Loop 1: Deterministic Harness & Chaos npm test (10 Unit Runners + testWith + VCR) · ~3s · $0 · Every commit"]
LOCK["Freshness Lock: [vcr.stale-cassette] Compares cassette promptHash & schemaHash against current code"]
L2["Loop 2: Live-Model PR Smoke Gate agboss eval run --runs 5 --baseline · ~1 min · $ · On PR when hash changes"]
L3["Loop 3: Nightly Polygon agboss eval run --runs 20 --metamorphic --chaos · ~15 min · $$ · Nightly / Pre-release"]
L4["Loop 4: Production Telemetry & Mesh Hub PII-scrubbed ExecutionRecord → agboss eval ui · 24/7"]
L0 --> L1 --> LOCK --> L2 --> L3 --> L4
L4 -. "Loop A: Failure Taxonomy & Judge Calibration Tag failures, verify Cohen's κ ≥ 0.80, promote to agboss golden" .-> L2
L2 -. "Loop B: Progressive Crystallization Move settled routes & tool guards from Loop 2 ($ LLM) back to Loop 1 ($0 Code)" .-> L1
```
| Loop | Command / Stage | Latency | LLM Cost | When It Runs | What It Catches |
| ---------- | -------------------------------------------------------------------------------- | ------------- | ----------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Loop 0** | `agboss check` (`--federation`, `--cassettes`) | **`< 1s`** | **`$0.00`** | Every file save & first CI step | 100% of graph wiring errors (`[input.missing-on-path]`), stale loop inputs (`[input.outdated-on-path]`), uncompensated sagas (`[flow.uncompensated-action]`), and broken cross-repo MCP/A2A schemas |
| **Loop 1** | [`npm test` (`@agboss/core/testing`)](/guides/testing-agents/) | **`~3s`** | **`$0.00`** | Every commit (live LLM is **never** called) | Logic bugs in all 10 component types, workflow branching, parallel quorum races (`TestConcurrency.inOrder`), `ModelFailure` retries, 30s lease handoff (`instances: 2`), and VCR schema integration |
| **Loop 2** | [`$ agboss eval run --runs 5 --baseline`](/guides/evaluation-and-profiles/) | **`~1 min`** | **`$`** | Pull Requests that modify `promptHash`, `schemaHash`, or tools | Prompt regressions on the critical smoke suite (`K = 5` trials per case) using a Fail-Fast `$0 -> $$$` judge cascade and `flaky-on-baseline` quarantine |
| **Loop 3** | `$ agboss eval run --runs 20 --metamorphic --chaos` | **`~15 min`** | **`$$`** | Nightly on `main` & before releases / model upgrades | Paraphrase fragility (`--metamorphic`), multi-turn `@Compaction` memory loss, live provider fault recovery (`--chaos`), and silent upstream model quantization drift |
| **Loop 4** | [`ExecutionRecord` $\to$ `$ agboss eval ui`](/guides/observability-and-tracing/) | **`24/7`** | **—** | Continuously on production traffic | Real-world edge cases, new failure modes, `@Judge` rubric drift (`eval calibrate` Cohen’s $\kappa$), and candidates for [Progressive Crystallization](/concepts/progressive-crystallization/) |
***
### Loop 0: Static Compiler Proof (`agboss check` + `--federation`) — `$0`, `< 1s`
[Section titled “Loop 0: Static Compiler Proof (agboss check + --federation) — $0, < 1s”](#loop-0-static-compiler-proof-agboss-check----federation--0--1s)
* **When:** On every file save during development and as the very first gate in CI.
* **What it proves:** 100% of graph edge types, [`Path Scope`](/concepts/path-scope/) dominance, `{ about }` review-loop freshness, saga compensation closure (`[flow.uncompensated-action]`), prompt cache invariance (`[prompt.input-in-instructions]`), and cross-service `@McpServer` / `@A2AAgent` contracts (`$ agboss check --federation`).
* **Why it exists:** Eliminates architectural and contract errors before a single unit test or model token is spent.
### Loop 1: Deterministic Harness & Infrastructure Chaos (`npm test`) — `$0`, `~3s`
[Section titled “Loop 1: Deterministic Harness & Infrastructure Chaos (npm test) — $0, \~3s”](#loop-1-deterministic-harness--infrastructure-chaos-npm-test--0-3s)
* **When:** On every commit. **A live LLM is never called in Loop 1.**
* **What runs inside Loop 1:**
* **[10 isolated unit runners](/guides/testing-agents/#all-10-isolated-unit-runners-in-agbosstesting):** `testTool`, `testToolInterceptor`, `testAction`, `testRouter`, `testArbiter`, `testRag`, `testJudge`, `testCompaction`, `testPii`, and `testGuardrail` validate individual components in `1ms`.
* **Full-graph `testWith(Application)` tests:** Execute complete workflows against scripted `mockLlm` replies and recorded VCR cassettes (`VcrMode.Replay`).
* **Resilience & chaos injection:** Deterministically inject `failWith(ModelFailure.RateLimit)`, `ModelFailure.Timeout`, `ModelFailure.InvalidOutput`, and `ModelFailure.InvalidToolArguments`, control parallel race arrival order via `TestConcurrency.inOrder()`, and verify multi-pod 30-second lease failover (`testWith(App, { instances: 2 })`) on a `VirtualClock`.
* **The Freshness Lock to Loop 2 (`[vcr.stale-cassette]`):** Every VCR cassette embeds the `promptHash` and `schemaHash` of the steps it recorded. As long as you only refactor TypeScript services, `@Action` nodes, or DI wiring, your entire test suite runs for `$0` on cassettes. The moment you change a single word in a prompt or a field on a `@Dto`, `[vcr.stale-cassette]` trips — signaling that the fixture is stale and Loop 2 must run against a live model.
### Loop 2: Live-Model PR Smoke Gate (`npx agboss eval run --runs 5 --baseline`)
[Section titled “Loop 2: Live-Model PR Smoke Gate (npx agboss eval run --runs 5 --baseline)”](#loop-2-live-model-pr-smoke-gate-npx-agboss-eval-run---runs-5---baseline)
* **When:** On Pull Requests (triggered automatically when `promptHash`, `schemaHash`, or tool bindings change).
* **Why `temperature: 0` still requires `--runs 5` (`pass@k` vs `pass^k`):** Modern GPU inference servers use dynamic continuous batching across concurrent requests, and `BF16` / `FP8` matrix reductions are non-associative. As a result, the exact same prompt at `temperature: 0` can take different tool-call trajectories across runs — making single-run (`K=1`) evaluations statistically unreliable. AGBoss measures both **`pass@k`** (passed at least once in $k$ runs — *capability*) and **`pass^k`** (passed all $k$ of $k$ runs — *production reliability*).
* **How Loop 2 maximizes accuracy while minimizing token spend:**
1. **Fail-Fast Cascade inside every trial (`$0 -> $$$`, `EvalCascade.StopAtFirstFailure`):**
* **Step 1 (`$0`, `0ms`):** Verify deterministic trajectory invariants (`checkTrajectory()`) first — were `required` tools called (`toolCall`), were `forbidden` tools avoided, did tool arguments satisfy `toolInvariant` predicates, and did the run stay within its `@Budget`?
* **Step 2 (`$0`, `0ms`):** Run deterministic `DecidedBy.Code` `@Judge` classes (`CitationFormatJudge`, `FairDecisionJudge`).
* **Step 3 (`$`):** Only if Steps 1 and 2 pass does AGBoss invoke the calibrated LLM `@Judge`, passing it the full `toolCalls` trajectory and final `reply`.
2. **Multi-trial reliability (`pass^k`) vs cached `--baseline` (no double spend):**
* Critical smoke cases (up to 10 cases) run $K = 5$ trials each (`--runs 5`), while cases directly touched by the PR run $10\text{–}20$ trials.
* Instead of running every eval twice on every PR (once on `main`, once on the PR branch), `--baseline` compares each case’s `pass^k` against the cached baseline JSON artifact from the latest nightly `main` run.
* If a case had `5/5` (`pass^5 = 1.00`) on `main` and drops in the PR, CI fails with exit code `5` (`--fail-on-regression`). If a case already had `0 < pass^k < 1` on `main`, it is automatically quarantined as **`flaky-on-baseline`** (`CaseComparison.FlakyOnBaseline`) so baseline noise never blocks unrelated PRs.
3. **Artifact output:** Attaches a self-contained single-file `eval-report.html` (`--html`) directly to the Pull Request.
### Loop 3: Nightly Polygon (Regression, `--metamorphic`, Memory Drift & `--chaos`)
[Section titled “Loop 3: Nightly Polygon (Regression, --metamorphic, Memory Drift & --chaos)”](#loop-3-nightly-polygon-regression---metamorphic-memory-drift----chaos)
* **When:** On a nightly schedule on `main` or before a production release / model migration.
* **What it verifies:**
* **Full domain benchmark with pinned hosts (`only`, `quantizations`):** Runs 30–50+ real domain cases with strict [`@ModelProvider`](/reference/model-provider/) host and quantization pinning (`quantizations: ["fp8", "bf16"]`, `allowFallbacks: false`) and exports a fresh `--baseline` artifact for tomorrow’s PRs.
* **Paraphrase invariance (`--metamorphic`):** Varies the phrasing or language of the user prompt (`paraphrases` in `evalCase`) and verifies that the sequence of `@Tool` calls and their semantic arguments remain invariant.
* **Multi-turn memory & `@Compaction` survival:** Uses `pastTurn()` + `turn()` to verify that (a) critical negative constraints survive `@Compaction` summarization, and (b) fresh `@Tool` / `@Rag` evidence overrides outdated facts from earlier turns in the thread.
* **Live-graph fault injection (`--chaos`):** Injects transient upstream `429`s, network timeouts, and malformed tool JSON mid-run on the live graph to prove retry and `.onError()` resilience under real model nondeterminism.
* **Silent provider drift detection:** Catches unannounced upstream model weight or quantization changes when `main` code has not changed but nightly `pass^k` drops.
***
## Axis 3: How do the two production feedback loops improve the graph?
[Section titled “Axis 3: How do the two production feedback loops improve the graph?”](#axis-3-how-do-the-two-production-feedback-loops-improve-the-graph)
The verification pipeline does not end at deployment — Loop 4 (`ExecutionRecord` telemetry) closes **two continuous feedback loops** back into Loops 2 and 1:
### Feedback Loop A: From Production to Golden Sets & `@Judge` Calibration (Failure Taxonomy Loop)
[Section titled “Feedback Loop A: From Production to Golden Sets & @Judge Calibration (Failure Taxonomy Loop)”](#feedback-loop-a-from-production-to-golden-sets--judge-calibration-failure-taxonomy-loop)
1. **PII-scrubbed production traces:** In production, workflows persist [`ExecutionRecord`](/guides/observability-and-tracing/) (`Tern`) entries into `ExecutionRecordRepository`, already pseudonymized at the boundary by [`@PiiPolicy`](/reference/pii-policy/).
2. **Failure taxonomy tagging in Eval UI / Mesh Hub:** Engineers open `$ agboss eval ui` locally (or the AGBoss Mesh Hub across a fleet), filter failed or high-latency trajectories, and tag root causes with concise taxonomy labels (`wrong-tool`, `forgot-compacted-constraint`, `policy-hallucination`).
3. **Calibrating `@Judge` via Cohen’s kappa ($\kappa \ge 0.80$):** On 20–30 real recorded sessions, human domain experts record pass/fail labels (`evals/labels/*.jsonl`) and run `$ agboss eval calibrate --min-kappa 0.80`. AGBoss computes pairwise **Cohen’s kappa ($\kappa$)** and a confusion matrix (`TP`, `FP`, `TN`, `FN`) for each `@Judge` (and warns with `[judge.same-model-family]` if the judge shares the agent’s model family). If $\kappa < 0.80$, you refine the `@Judge` rubric until automated grading aligns with human judgment.
4. **One-click promotion to Golden Sets (`agboss golden`):** Real production failures are promoted into `golden//*.yaml` (`npx agboss golden add`) and `@EvalSuite` cases, becoming permanent regression guards in Loops 2 and 3.
### Feedback Loop B: Progressive Crystallization (From Loop 2 back to Loop 1)
[Section titled “Feedback Loop B: Progressive Crystallization (From Loop 2 back to Loop 1)”](#feedback-loop-b-progressive-crystallization-from-loop-2-back-to-loop-1)
The relationship between **`pass@k`** (passed at least once in $k$ trials — *capability*) and **`pass^k`** (passed all $k$ of $k$ trials — *production reliability*) tells you exactly how to make the graph faster, cheaper, and more deterministic ([Progressive Crystallization](/concepts/progressive-crystallization/)):
| Observed Eval Signal | What It Means | Architectural Action (Loop 2 $\to$ Loop 1) |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **High `pass@k` (`100%`), Low `pass^k` (`40%`)** | The model *knows how* to solve the task, but slips on 3 of 5 attempts (e.g., calling a `@Tool` with an invalid filter or missing prefix). | Do **not** bloat the system prompt. Add a deterministic [`@ToolInterceptor`](/reference/tool-interceptor/) (`ToolCallDecision.replyToModel`) or `DecidedBy.Code` [`@InputGuardrail`](/reference/guardrail/) that catches the mistake in `0ms` for `$0`. |
| **Stable `pass^k = 100%` across thousands of runs** | `agboss describe --json` + `ExecutionRecord` traces show a `DecidedBy.Model` `@Router` or `@Arbiter` making a predictable decision from DTO fields on $>99%$ of runs. | Crystallize the component from `DecidedBy.Model` to **`DecidedBy.Code`** (or collapse a fixed tool chain into an [`@Action`](/reference/action/)). Its verification permanently moves from paid **Loop 2 (`agboss eval`)** into free, `1ms` **Loop 1 (`npm test`)**. |
***
## Summary in Three Lines
[Section titled “Summary in Three Lines”](#summary-in-three-lines)
1. **`agboss check` (`--federation`)** proves statically in `< 1s` (`$0`) that your graph topology, Path Scope DTOs, saga compensations, and cross-service mesh contracts are sound (see [`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof) and [`06-enterprise-agent-mesh`](https://github.com/agbossjs/agboss-examples/tree/main/06-enterprise-agent-mesh)).
2. **`npm test` (`testWith` + 10 unit runners + hash-locked VCR cassettes)** verifies 100% of your TypeScript harness code, chaos recovery, and concurrency deterministically in seconds (`$0`) on every commit, using a symmetric Spring-style `tests/` directory mirroring `src/` (see [`01-hello-agboss`](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss)).
3. **`agboss eval` (`--runs K` + `--baseline` + `--ui` / Mesh Hub)** measures true model reliability (`pass^k`), blocks regressions against a compact git-diff-friendly baseline (`eval/baseline/main-baseline.json`), calibrates judges by Cohen’s $\kappa$, and pinpoints which stochastic nodes are ready to crystallize into deterministic code (see [`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk)).
# Workflow HDSL — HarnessDSL & Canonical Topology
> Master HarnessDSL (HDSL) — the declarative workflow grammar inside AGBoss for canonical topology, Path Scope DTOs, parallel quorums, and LIFO sagas.
In **AGBoss** (*Agent Graph Builder Open Source System*), a workflow is written in **HarnessDSL (`HDSL`)** — the declarative graph grammar and verification specification built into `AGBoss` (analogous to JSX in React or PSL in Prisma). While `AGBoss` is the framework, static compiler (`agboss check`), and production runtime on LangGraph (`npm i @agboss/core`), `HarnessDSL` is the grammar used inside `*.workflow.ts` and contract decorators to specify `flow` chains, narrow `@Dto` contracts, `Path Scope`, `in-degree = 1` vs `in-degree >= 2` canonical topology, `{ about }` staleness tracking, quorums, and `compensateWith` / `committedBy` sagas. In a `@Workflow` class, `flow` is a list of **paths**. Each path reads like a sentence starting at a source (`start(StartClass)`, `agent(AgentClass)`, `router(RouterClass)`, `action(ActionClass)`, `workflow(ChildWorkflow)`), with **strictly one chained call per line** (`@stylistic/newline-per-chained-call`), and every value produced on an edge declared explicitly via `.returns(...)`.
## Canonical Topology Isomorphism (`in-degree = 1` vs `in-degree >= 2`)
[Section titled “Canonical Topology Isomorphism (in-degree = 1 vs in-degree >= 2)”](#canonical-topology-isomorphism-in-degree--1-vs-in-degree--2)
When AI coding agents edit unconstrained graph definitions, they can format the exact same topology in ten different ways — creating deep nesting pyramids and unreviewable Git diffs. AGBoss enforces a **1-to-1 structural isomorphism** ([Principle 3](/concepts/architecture/#3-canonical-topology-isomorphism)) verified by `agboss check`:
1. **Single incoming edge (`in-degree = 1`):** A node reached from only one predecessor **must** be chained inline (`.toAgent(NextStep)`).
2. **Convergence node (`in-degree >= 2`):** A node reached from two or more edges (for example, `MainRouter` reached both from `start(IntakeWorkflowStart)` and from the `caseOf(ProfileDto)` loop-back) **must** be extracted as a top-level entry (`router(MainRouter)`, `agent(ReviewerAgent)`, `action(MergeAction)`) in the `flow: [...]` array (`[flow.shared-node-inline]`).
3. **One chained call per line:** Every builder method call occupies its own line, and inline branch nesting is capped at depth 4 (`[flow.path-too-deep]`).
## How HDSL paths, `.chooseOne(route())`, and `.switch(caseOf())` work
[Section titled “How HDSL paths, .chooseOne(route()), and .switch(caseOf()) work”](#how-hdsl-paths-chooseoneroute-and-switchcaseof-work)
Components declare their typed `input` fields and `output` DTO (or `oneOf(DtoA, DtoB)` union), while the `@Workflow({ flow: [...] })` array holds the entire control-flow topology and data flow:
src/chat.workflow\.ts
```ts
import { Workflow, caseOf, route, router, start } from "@agboss/core";
import { WorkflowStartTextDto, oneOf } from "@agboss/core/dto";
@Workflow({
name: "chat-workflow",
version: "1.0.0",
flow: [
start(IntakeWorkflowStart)
.returns(WorkflowStartTextDto)
.toRouter(MainRouter),
router(MainRouter)
.chooseOne(
route(ProfilerAgent)
.returns(oneOf(ProfileDto, ResumeMissingDto))
.switch(
caseOf(ProfileDto)
.toRouter(MainRouter),
caseOf(ResumeMissingDto)
.toFinish(AskResumeWorkflowFinish),
),
route(ScoutAgent)
.returns(OpeningsDto)
.toFinish(ChatWorkflowFinish),
route(NoReplyWorkflowFinish),
),
],
maxSteps: 8,
})
export class ChatWorkflow {}
```
src/chat.components.ts
```ts
import { Agent, DecidedBy, Router, WorkflowFinish, WorkflowStart } from "@agboss/core";
import { Text, WorkflowStartTextDto, oneOf, optional } from "@agboss/core/dto";
export class ProfileDto {
@Text({ prompt: "the candidate's profile" })
summary!: string;
}
export class ResumeMissingDto {
@Text({ prompt: "why no resume was found", optional: true })
reason?: string;
}
export class OpeningsDto {
@Text({ prompt: "the openings found" })
openings!: string;
}
@WorkflowStart({ name: "intake-workflow-start", description: "A customer's message", input: WorkflowStartTextDto })
export class IntakeWorkflowStart {}
@Agent({
name: "profiler-agent",
description: "Reads the resume into a profile",
prompt: "Read the resume.",
model: "moonshotai/kimi-k2.6",
input: { message: WorkflowStartTextDto },
output: oneOf(ProfileDto, ResumeMissingDto),
})
export class ProfilerAgent {}
@Agent({
name: "scout-agent",
description: "Finds openings for the profile",
prompt: "Find openings.",
model: "moonshotai/kimi-k2.6",
input: { profile: optional(ProfileDto) },
output: OpeningsDto,
})
export class ScoutAgent {}
@WorkflowFinish({ name: "ask-resume-workflow-finish", description: "Asks for a resume", output: ResumeMissingDto })
export class AskResumeWorkflowFinish {}
@WorkflowFinish({ name: "chat-workflow-finish", description: "The openings found", output: OpeningsDto })
export class ChatWorkflowFinish {}
@WorkflowFinish({ name: "no-reply-workflow-finish", description: "Ends without a reply", output: WorkflowStartTextDto })
export class NoReplyWorkflowFinish {}
@Router({
name: "main-router",
description: "Sends the message to the step that handles it",
decidedBy: DecidedBy.Model,
model: "typesafe/jev-1.13",
prompt: "Pick who handles the customer's message next.",
input: { message: WorkflowStartTextDto },
maxVisits: 3,
routes: [
{ target: ProfilerAgent, prompt: "The customer sends or changes their resume" },
{ target: ScoutAgent, prompt: "The customer asks for job openings" },
{ target: NoReplyWorkflowFinish, prompt: "Stop without a reply" },
],
})
export class MainRouter {}
```
test/chat.workflow\.test.ts
```ts
import { Application, EmptyEnvironment } from "@agboss/core";
import { expect } from "vitest";
import { replyWith, routeTo, testWith } from "@agboss/core/testing";
import { ChatWorkflow } from "./chat.workflow.js";
import {
ChatWorkflowFinish,
IntakeWorkflowStart,
MainRouter,
OpeningsDto,
ProfileDto,
ProfilerAgent,
ScoutAgent,
} from "./chat.components.js";
@Application({
name: "chat-workflow-app",
version: "1.0.0",
environment: EmptyEnvironment,
workflows: [ChatWorkflow],
budgets: [ChatAppBudget, ChatTenantBudget, ChatExecBudget],
})
export class ChatWorkflowApp {}
const test = testWith(ChatWorkflowApp);
test("loops from ProfilerAgent back to MainRouter and finishes via ScoutAgent", async ({
app,
mockLlm,
}) => {
mockLlm(MainRouter).thenReturn(
routeTo(ProfilerAgent),
routeTo(ScoutAgent),
);
mockLlm(ProfilerAgent).thenReturn(
replyWith(ProfileDto, { summary: "Principal TypeScript Engineer" }),
);
mockLlm(ScoutAgent).thenReturn(
replyWith(OpeningsDto, { openings: "Staff Backend @ Acme" }),
);
const result = await app.execute(IntakeWorkflowStart, {
text: "Here is my resume, find openings",
});
expect(result.finish).toBe(ChatWorkflowFinish);
expect(result.output).toEqual({ openings: "Staff Backend @ Acme" });
});
```
## How review loops use `{ about }` and `historyOf`
[Section titled “How review loops use { about } and historyOf”](#how-review-loops-use--about--and-historyof)
When a reviewer evaluates an artifact (such as `OfferDraftDto`), attach `{ about: OfferDraftDto }` (or `{ about: [DtoA, DtoB] }`) on a single output `.returns(CritiqueDto, { about: PlanDto })` or on a specific union branch `caseOf(ChangesRequestedDto, { about: OfferDraftDto })` (never on `.returns(oneOf(...))` and never as a positional helper):
* **Automatic outdating of stale reviews:** When the editor produces a fresh `OfferDraftDto`, any earlier `ChangesRequestedDto` or `DraftApprovedDto` that was `{ about: OfferDraftDto }` is automatically marked outdated in Path Scope so downstream nodes can never consume a stale v1 decision paired with a v2 draft (`[input.outdated-on-path]`, `[launch.branch-outdates-sibling]`).
* **Bounded iteration history (`historyOf(Dto, { limit })`):** Steps that need to inspect prior drafts or critique rounds declare `historyOf(OfferDraftDto, { limit: 3 })` in their `input` contract to receive `{ current, previous: [...] }` instead of manual state arrays or legacy string tags.
## How parallel launches, quorums, and recursion work
[Section titled “How parallel launches, quorums, and recursion work”](#how-parallel-launches-quorums-and-recursion-work)
For concurrent execution and hierarchical composition, HarnessDSL (`HDSL`) provides four additional primitives:
1. **Parallel launches (`.toParallelAgents()` / `.toParallelWorkflows()`):** Run multiple distinct agents (`SecurityAgent, LegalAgent, { maxConcurrency: 3 }`) or fan out across a `listOf(ItemDto)` (`{ maxItems: 20, maxConcurrency: 4 }`), followed by `.join()` and `.onJoinError(...)`.
2. **Consensus quorums (`.joinQuorum()` / `.joinQuorumGroups()`):** Continue as soon as `{ min, max, deadline: seconds(30) }` answers are accepted by `@Arbiter` rules attached via `.acceptedBy(ArbiterClass)` or `.acceptedBy(anyOf(A, B))`, cancelling surplus branches immediately.
3. **Workflow as a step (`.toWorkflow(ChildWorkflow)` / `.toParallelWorkflows()`):** Run a child `@Workflow` as an isolated execution with its own `maxSteps` and Path Scope, branching on its finishes via `.returns(oneOf(...)).switch(caseOf(...)...)`.
4. **Bounded self-recursion (`ThisWorkflow`):** Pass `ThisWorkflow` to `.toWorkflow(ThisWorkflow)`, `route(ThisWorkflow)`, or `.toParallelWorkflows(ThisWorkflow, { maxItems })` with `@Workflow({ maxDepth: 3, maxConcurrentExecutions: 6 })`. At `depth === maxDepth` (soft stop), the runtime automatically strips the recursive `oneOf` variant or router target from the model’s schema so the run converges cleanly (warming two prefix cache entries: `depth < maxDepth` and `depth === maxDepth`).
# AGBoss for the enterprise
> AGBoss Mesh Hub and enterprise support — Global Topology, Cross-Service Execution Trees, Crystallization Radar, Blast Radius, and Kubernetes operators.
Coming Enterprise support and the **AGBoss Mesh Hub (Enterprise Control Plane)** for engineering organizations running AGBoss in regulated and high-consequence production environments: fleet-wide mesh observability, cross-service execution trees, progressive crystallization analytics, LTS release lines, and Day-0 security patches.
[Follow for updates →](https://www.linkedin.com/company/agbossjs)[On the roadmap](/roadmap/#enterprise)
Mesh Hub
### Enterprise Control Plane
Fleet-wide topology map, cross-service execution trees across `rootExecutionId`, and progressive crystallization heatmaps.
Day 0
### Immediate security patches
Receive validated security patches the day a vulnerability is disclosed across AGBoss or upstream dependencies.
Extended
### Predictable LTS lifecycles
Stay on a regression-tested release line with guaranteed backports after the standard community window closes.
## What is the AGBoss Mesh Hub (Enterprise Control Plane)?
◈
### Global Agent Mesh Graph
Interactive fleet map of every `@Application` service across your organization built from `agboss describe –json` and signed `MeshEnvelope` traffic, showing live RPS, p95 latency, `pass^k` reliability, and USD spend per node.
⑂
### Cross-Service Execution Trees
Unified distributed execution tree keyed by `rootExecutionId` across `Service A → A2A → Service B → MCP → Service C`, visualizing human approval pauses, tool retries, budget caps, and LIFO saga rollbacks in one timeline.
⊕
### Blast Radius & Federation
Pre-merge contract impact analysis powered by `agboss check –federation` that previews every downstream A2A and MCP consumer affected when a `@Dto` schema or `promptHash` changes.
⚡
### Progressive Crystallization Radar
Automated heatmap flagging `DecidedBy.Model` steps and low-`pass^k` tool loops where the LLM is overpaying for deterministic routing or formatting — with one-click PR generation to crystallize them into `DecidedBy.Code` or `@ToolInterceptor`.
⇄
### Distributed Child Work-Stealing
`ChildPlacement.AnyInstance` scheduling for horizontal fan-out of `.toParallelWorkflows()` across a Kubernetes cluster via atomic `ClaimPendingChild` queue handoffs.
## What enterprise support & SLA guarantees are planned?
LTS
### Long-term support
Release lines with a published support window and backported fixes, so upgrades happen on your engineering schedule rather than ours.
0
### Day-0 security patches
Fixes for AGBoss and its model provider integrations, shipped for supported lines as soon as a vulnerability is disclosed.
↑
### Automated upgrades
Codemod tooling that migrates workflows, YAML profiles, and component decorators to the next major version and opens the pull request for you.
§
### Governance & compliance
Immutable audit trails of every workflow run, PII policy verification reports, and FinOps cost controls your security team can sign off on.
* **Zero core lock-in:** Enterprise capabilities extend the Apache-2.0 open-source core without proprietary workflow syntax changes.
* **Self-hosted deployment:** Run entirely inside your own VPC, Kubernetes cluster, or on-premise Node.js infrastructure.
* **Direct engineering escalation:** Priority architectural review and incident support from the core framework maintainers.
## Running AGBoss in production?
Tell us what your organization needs — your requirements directly shape what ships first.
[Open an issue →](https://github.com/agbossjs/agboss/issues)
# Examples: 7 Verified Applications (01–07), flow-patterns & job-scout
> Explore the 7 standalone verified examples in agbossjs/agboss-examples (01–07) alongside examples/flow-patterns and examples/job-scout in agbossjs/agboss.
AGBoss provides two complementary repositories of runnable, CI-verified examples so you can prove compile-time and runtime guarantees in seconds for `$0` and inspect production-grade reference architectures.
1. **7 Standalone Verified Examples ([`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples)):** Seven self-contained applications (`01-hello-agboss` through `07-langgraph-migration`) following the **`Promise → Failure Injection → Reproducible Proof`** discipline — each with its own `package.json`, canonical `1 class = 1 file` component subfolder layout, symmetric Spring-style `tests/` mirroring `src/`, `.claude/skills/agboss/`, `npx agboss check`, and `$0` offline test & demo suites.
2. **In-Repository Examples (`examples/flow-patterns` & `examples/job-scout` in [`agbossjs/agboss`](https://github.com/agbossjs/agboss)):** `examples/flow-patterns` verifies loops, code/model routers, parallel waves, and `ThisWorkflow` recursion in `< 5 seconds` for `$0`, alongside the interactive `examples/job-scout` career assistant.
***
## The 7 Verified Examples in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples)
[Section titled “The 7 Verified Examples in agbossjs/agboss-examples”](#the-7-verified-examples-in-agbossjsagboss-examples)
| Example | Architectural Proof & Capabilities Demonstrated | Key Primitives & CLI Commands |
| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **[`01-hello-agboss`](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss)** | **First-run, real HTTP service DI, web server & CLI scaffolding:** Demonstrates `PricingCatalogService` (`async onInit()`, `fetch` with `ExecutionContext.signal` + `step.idempotencyKey`), `src/http/server.ts` (multi-tenant headers, `202 Accepted` on pauses, `/api/pauses/:pauseId/resume`, `/health/live`, `/health/ready`), and canonical `1 class = 1 file` + symmetric `tests/` layout. | `@Application`, `@Workflow`, `@Agent`, `@Router`, `@Tool`, `@Injectable`, `testWith`, `mockOf` |
| **[`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)** | **Compile-time Path Scope vs raw LangGraph:** Proves 4 structural graph bug classes (`01-missing-input`, `02-outdated-data`, `03-parallel-invalidation`, `04-uncompensated-action`) that pass `tsc --noEmit` in raw LangGraph `Annotation.Root` (`langgraph-broken/`) and are caught before runtime by `agboss check` (`agboss-broken/` vs `agboss-fixed/`). | `[input.missing-on-path]`, `[input.outdated-on-path]`, `[launch.branch-outdates-sibling]`, `[flow.uncompensated-action]` |
| **[`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk)** | **PII roundtrip, tool interception, in-memory BM25 RAG, compaction & 5-layer evals:** Reversible HMAC `@PiiPolicy` (`replaceWithToken`, `revealPii: true`, `PiiRefusedError`), `@ToolInterceptor` (`replyToModel`, `block`), `@InputGuardrail` (`DecidedBy.Code`), `@OutputGuardrail`, `InMemoryKnowledgeIndexService` + `RefundPolicyRetriever` (`@Rag`), `PolicyAwareCompaction`, and `@EvalSuite` with compact git-tracked `eval/baseline/main-baseline.json` and `agboss eval calibrate` (Cohen’s $\kappa$). | `@PiiPolicy`, `@ToolInterceptor`, `@InputGuardrail`, `@OutputGuardrail`, `@Rag`, `@Compaction`, `@EvalSuite`, `agboss eval` |
| **[`04-travel-booking-saga`](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)** | **Transactional LIFO sagas, idempotency, quorums, HITL withdrawal & HTTP server:** Reverse-order LIFO compensation across `payment-saga`, `budget-saga`, `quorum-saga` (`.joinQuorum({ min: 2, max: 2 })`), and `hitl-hold-saga` (`Channel.withdraw()`, `NotPausedError`), idempotent external ledgers (`step.idempotencyKey`), SQLite WAL crash recovery, and `src/http/server.ts`. | `@Action({ compensateWith, committedBy })`, `.toParallelAgents().acceptedBy().joinQuorum()`, `@ChannelRole`, `@Channel` |
| **[`05-cluster-work-stealing`](https://github.com/agbossjs/agboss-examples/tree/main/05-cluster-work-stealing)** | **Distributed scheduling, 30s leases, fencing & backpressure:** Parent `CoordinatorWorkflow` fans out `ChunkWorkerWorkflow` with `ChildPlacement.AnyInstance` over `SqliteChildExecutionQueueRepository`, runs bounded recursive `ThisWorkflow` (`maxDepth: 2`), enforces 30s SQLite leases & fencing (`ExecutionLeaseLostError`), `maxConcurrentSteps: 2` backpressure (`HealthComponentKind.StepSlots`), `@HealthCheck`, and `IncompatibleResumeError`. | `ChildPlacement.AnyInstance`, `SqliteChildExecutionQueueRepository`, `ThisWorkflow`, `@HealthCheck`, `SqliteExecutionLeaseRepository` |
| **[`06-enterprise-agent-mesh`](https://github.com/agbossjs/agboss-examples/tree/main/06-enterprise-agent-mesh)** | **3-service A2A + MCP mesh, Ed25519 envelopes, loop guard & federation Blast Radius:** Connects `apps/1-order-gateway`, `apps/2-billing-a2a` (`@A2AExpose`), and `apps/3-catalog-mcp` (`@McpExpose`) with Ed25519-signed `agboss-mesh` JWS envelopes, `MeshLoopDetectedError` / `HopsLimitExceededError`, MCP `_meta.callId` deduplication (`SqliteMeshRequestRepository`), and `agboss check --federation` (`federation/`). | `@A2AExpose`, `@A2AAgent`, `@McpExpose`, `@McpTool`, `SqliteMeshRequestRepository`, `agboss check --federation`, `agboss hub` |
| **[`07-langgraph-migration`](https://github.com/agbossjs/agboss-examples/tree/main/07-langgraph-migration)** | **Side-by-side comparison (`before-langgraph/` vs `after-agboss/`):** Compares a raw 15-field LangGraph `Annotation.Root` God State (`before-langgraph/support.graph.ts`) with the migrated AGBoss implementation (`after-agboss/src/` + `after-agboss/tests/`) using 10 narrow `@Dto`s, `DecidedBy.Model` & `DecidedBy.Code` `@Router`s, bounded `.toParallelAgents()`, idempotent `@Action`, typed `@Channel`, and co-located `.prompt.md` files. | `@Dto`, `@Router`, `.switch(caseOf())`, `.toParallelAgents()`, `@Action`, `@Channel`, `13-migrating-from-langgraph.md` |
### Canonical Project Layout (`1 class = 1 file` & Symmetric `tests/`)
[Section titled “Canonical Project Layout (1 class = 1 file & Symmetric tests/)”](#canonical-project-layout-1-class--1-file--symmetric-tests)
Every project in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) enforces a clean, navigable folder convention:
* **1 class per file** with kind-suffixed filenames (`*.application.ts`, `*.workflow.ts`, `*.workflow-start.ts`, `*.workflow-finish.ts`, `*.agent.ts`, `*.router.ts`, `*.action.ts`, `*.tool.ts`, `*.service.ts`, `*.dto.ts`).
* **Component subfolders** grouping each workflow (`src/workflows//` with its workflow, start, and finishes) and each model-driven component (`src/agents//` and `src/routers//` co-locating the `.ts` class with its immutable `.prompt.md` file).
* **Symmetric Spring-style `tests/` directory** mirroring `src/` 1-to-1 (`tests/services/`, `tests/tools/`, `tests/actions/`, `tests/routers/`, `tests/guardrails/`, `tests/interceptors/`, `tests/http/`, `tests/workflows//`) so coding agents and engineers know immediately where unit and workflow tests belong.
### Spotlight: `02-path-scope-proof` (4 Graph Bugs Caught Before Runtime)
[Section titled “Spotlight: 02-path-scope-proof (4 Graph Bugs Caught Before Runtime)”](#spotlight-02-path-scope-proof-4-graph-bugs-caught-before-runtime)
In [`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof), four structural bug cases (`cases/01-missing-input`, `cases/02-outdated-data`, `cases/03-parallel-invalidation`, `cases/04-uncompensated-action`) are each implemented in three variants — `langgraph-broken/` (compiles with `tsc --noEmit` yet fails or corrupts state at runtime), `agboss-broken/` (rejected before runtime by `agboss check --json` with an exact rule code and witness path), and `agboss-fixed/` (passes `agboss check` and executes cleanly):
1. **Missing input on a conditional route (`01-missing-input` → `[input.missing-on-path]`):** The `fast-track` branch of a router skips `ComplianceReview` (`ComplianceApprovalDto`), yet `PublishContract` requires `approval`. Raw LangGraph silently receives `undefined` at runtime; `agboss check` halts at compile time.
2. **Stale data after a revision cycle (`02-outdated-data` → `[input.outdated-on-path]`):** `QualityReviewer` emits `RevisionRequested` (`{ about: ContractDraftDto }`) and `RevisionWriter` replaces `ContractDraftDto`, making the earlier `LegalSignoffDto` stale. Skipping `LegalCheck` on the second pass is rejected by `agboss check`.
3. **Parallel sibling invalidation (`03-parallel-invalidation` → `[launch.branch-outdates-sibling]`):** Inside a parallel launch, `PricingRewriter` replaces `QuoteSheetDto` while its sibling `TaxAudit` concurrently audits the pre-launch `QuoteSheetDto`. `agboss check` rejects the race condition statically.
4. **Uncompensated side effect on an error path (`04-uncompensated-action` → `[flow.uncompensated-action]`):** `ReserveInventory` (`compensateWith: ReleaseInventory`) is followed by `ChargeCard`, whose `.onError(...)` path reaches `OrderFailedFinish` without invoking `ReleaseInventory`. `agboss check` refuses to compile the uncompensated path.
***
## Detailed Guide to All 7 Standalone Examples (`01–07`)
[Section titled “Detailed Guide to All 7 Standalone Examples (01–07)”](#detailed-guide-to-all-7-standalone-examples-0107)
> **Setup (`v0.1.0`):** `@agboss/core@0.1.0` and `@agboss/cli@0.1.0` are published on `registry.npmjs.org`. Run `npm install` inside any example directory (or `node ./scripts/install-published.mjs --live-registry --version 0.1.0` from the [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) repository root) before running per-project commands.
### 1. [`01-hello-agboss`](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss) — Starter, Constructor DI, Budgets & HTTP Server (`P0`)
[Section titled “1. 01-hello-agboss — Starter, Constructor DI, Budgets & HTTP Server (P0)”](#1-01-hello-agboss--starter-constructor-di-budgets--http-server-p0)
* **Problem statement:** First-time multi-agent setups often mix business logic, HTTP transport calls, and prompt strings inside untyped node functions without budget ceilings or cancellation signals.
* **Architecture overview:** `HelloAgbossWorkflow` starts at `HelloWorkflowStart` (`HelloQuestionDto`), routes via `IntentRouter` (`DecidedBy.Model`) to either `SupportSpecialistAgent` or `QuoteAdvisorAgent` (which invokes `CalculateTierQuoteTool` backed by `PricingCatalogService`), and finishes at `HelloWorkflowFinish` (`HelloAnswerDto`).
* **Main mechanisms demonstrated:** `@Application`, `@Workflow`, `@Router`, `@Agent`, `@Tool`, `@Injectable` (`PricingCatalogService` with `async onInit()`, `ExecutionContext.signal`, and `step.idempotencyKey`), `@Budget` (`Application`, `Tenant`, and `Execution` scopes), `SqliteCostLedgerRepository`, and `src/http/server.ts`.
* **Representative code:** `start(HelloWorkflowStart).returns(HelloQuestionDto).toRouter(IntentRouter).chooseOne(route("support").toAgent(SupportSpecialistAgent).toFinish(HelloWorkflowFinish), route("pricing").toAgent(QuoteAdvisorAgent).toFinish(HelloWorkflowFinish))`
* **Quickstart commands:**
```bash
cd 01-hello-agboss
npm run check && npm test && npx tsx src/demo.ts
```
* **Expected proof output:** `npx tsx src/demo.ts` executes both `"support"` and `"pricing"` routes offline against the local pricing HTTP stub server and prints JSON confirming `supportBranch.status: "finished"`, `pricingBranch.status: "finished"`, and `pricingApiRequestsCount: 1`.
* **Failure scenario tested:** `tests/workflows/hello-agboss/hello-agboss.workflow.test.ts` injects a low execution budget ceiling and verifies `BudgetExceededError` aborts execution before overspending.
* **Guarantee boundary:**
* *Verified:* Static workflow assembly, constructor DI lifecycle (`onInit`), tool schema validation, budget enforcement, and deterministic offline execution.
* *Not verified by default offline run:* Live OpenRouter model quality or network latency (requires `npx tsx src/demo.ts --live` with `OPENROUTER_API_KEY`).
* **Repository directory:** [`agbossjs/agboss-examples/01-hello-agboss`](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss)
### 2. [`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof) — Static Graph Safety & Provenance (`P0`)
[Section titled “2. 02-path-scope-proof — Static Graph Safety & Provenance (P0)”](#2-02-path-scope-proof--static-graph-safety--provenance-p0)
* **Problem statement:** In shared-state graph libraries (`Annotation.Root`), conditional branches that skip required producers, revision loops that leave stale approvals active, parallel branches that overwrite shared slots, and error handlers that bypass compensation all compile under `tsc --noEmit` and fail only at runtime.
* **Architecture overview:** Four self-contained case directories (`cases/01-missing-input`, `02-outdated-data`, `03-parallel-invalidation`, `04-uncompensated-action`), each containing `langgraph-broken/`, `agboss-broken/`, and `agboss-fixed/`.
* **Main mechanisms demonstrated:** `agboss check --json` static dominance analysis (`[input.missing-on-path]`, `[input.outdated-on-path]`, `[launch.branch-outdates-sibling]`, `[flow.uncompensated-action]`), `.agboss/types.d.ts` `Produced` narrowing, `{ about }` provenance invalidation, and `compensateWith`.
* **Representative code:** `caseOf(ChangesRequestedDto, { about: ContractDraftDto }).toAgent(RevisionWriterAgent).returns(ContractDraftDto).toAgent(LegalCheckAgent)`
* **Quickstart commands:**
```bash
cd 02-path-scope-proof
npm run check && npm test && npx tsx src/demo.ts
```
* **Expected proof output:** `npx tsx src/demo.ts` runs all 4 cases and outputs JSON proving that `langgraph-broken` passes `tsc` (`passedTsc: true`) and fails at runtime, `agboss-broken` is rejected before runtime with the exact diagnostic code, and `agboss-fixed` finishes cleanly.
* **Failure scenario tested:** All four `agboss-broken/*.workflow.ts` fixtures are intentionally broken workflows verified to fail `agboss check --json` with exit code `1` and exact witness paths.
* **Guarantee boundary:**
* *Verified:* Static path-dominance of declared `@Dto` slots, `{ about }` invalidation along cyclic/parallel paths, and presence of `compensateWith` along declared error paths.
* *Not verified:* Arbitrary imperative mutations inside custom `@Injectable` services or external databases outside AGBoss DTO handoffs.
* **Repository directory:** [`agbossjs/agboss-examples/02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)
### 3. [`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk) — PII Masking, Guardrails, RAG, Compaction & Evals (`P0`)
[Section titled “3. 03-support-refund-desk — PII Masking, Guardrails, RAG, Compaction & Evals (P0)”](#3-03-support-refund-desk--pii-masking-guardrails-rag-compaction--evals-p0)
* **Problem statement:** Customer support agents handle sensitive PII (emails, phone numbers, payment cards), face prompt-injection and policy-violation risks, suffer context drift over multi-turn conversations, and require reproducible regression gating.
* **Architecture overview:** `SupportRefundWorkflow` wraps `RefundSupportAgent` with `CustomerContactPiiPolicy` + `SupportTicketIdPiiPolicy`, `PromptInjectionGuardrail` (`@InputGuardrail`) + `UnauthorizedPayoutGuardrail` (`@OutputGuardrail`), `RefundPolicyToolInterceptor` + `AuditTrailToolInterceptor`, `RefundPolicyRetriever` (`@Rag`), `PolicyAwareCompaction` (`@Compaction`), and `RefundDeskEvalSuite` (`@EvalSuite`).
* **Main mechanisms demonstrated:** Reversible HMAC `@PiiPolicy` (`replaceWithToken`, `revealPii: true` on `IssueRefundTool`, `PiiRefusedError` on card numbers, masked OTel/Langfuse spans), `@InputGuardrail`, `@OutputGuardrail`, `@ToolInterceptor` (`ToolCallDecision.replyToModel` & `block`), `@Rag`, `@Compaction`, and `@EvalSuite` (`pass@k`, `pass^k`, `--baseline`, Cohen’s $\kappa$ calibration, `--chaos`, `--metamorphic`, incident-to-eval drafting).
* **Representative code:** `@PiiPolicy({ name: "customer-contact-pii-policy", match: [PiiKind.Email, PiiKind.Phone], action: PiiAction.ReplaceWithToken })`
* **Quickstart commands:**
```bash
cd 03-support-refund-desk
npm run check && npm test && npx tsx src/demo.ts
```
* **Expected proof output:** `npx tsx src/demo.ts` prints JSON verifying `pii.modelSawMaskedEmailToken: true`, `pii.modelSawRawEmail: false`, `pii.toolReceivedRealEmail: "alice.synth@example.com"`, `pii.langfuseOtelSpansMaskedPii: true`, `policy.overCapFinalDecision: "escalated"`, `memoryDrift.preservedOrderFact: true`, and `evals.calibration.kappaJudgeAnna: 1`.
* **Failure scenarios tested:** Raw credit card refusal (`PiiRefusedError`), over-cap refund interception (`ToolCallDecision.replyToModel`), cross-tenant tool block, prompt-injection input guardrail block, unauthorized settlement output guardrail block, and baseline regression detection (`pass^k` drop under `--chaos`).
* **Guarantee boundary:**
* *Verified:* Deterministic regex/field PII pseudonymization, tool interceptor policy gates, compaction fact retention, and offline `@EvalSuite` statistical calculations (`pass@k`, `pass^k`, Cohen’s $\kappa$).
* *Not verified by default offline run:* Open-ended adversarial jailbreak resilience on live frontier models (requires live model evaluation runs).
* **Repository directory:** [`agbossjs/agboss-examples/03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk)
### 4. [`04-travel-booking-saga`](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga) — Transactional LIFO Sagas, Quorums, HITL & Crash Recovery (`P0`)
[Section titled “4. 04-travel-booking-saga — Transactional LIFO Sagas, Quorums, HITL & Crash Recovery (P0)”](#4-04-travel-booking-saga--transactional-lifo-sagas-quorums-hitl--crash-recovery-p0)
* **Problem statement:** Multi-step workflows that reserve flights, book hotels, and charge cards leave orphaned reservations when a downstream step fails, a budget limit triggers, a user cancels a paused approval, or a process crashes mid-flight.
* **Architecture overview:** Four workflows in `TravelBookingSagaApplication` (`PaymentSagaWorkflow`, `BudgetSagaWorkflow`, `HitlHoldSagaWorkflow`, and `QuorumSagaWorkflow`) backed by `SqliteCheckpointSaver`, `SqlitePausedExecutionRepository`, and `SqliteExecutionLeaseRepository`.
* **Main mechanisms demonstrated:** `@Action({ compensateWith, committedBy })`, `step.idempotencyKey` deduplication, automatic reverse-order (LIFO) compensation on step failure / `BudgetExceededError` / `app.cancel()`, two-phase `@ChannelRole` + `@Channel` (`withdraw` on cancel, `NotPausedError` on late resume), `.toParallelAgents().acceptedBy(QuorumVoteArbiter).joinQuorum({ min: 2, max: 2 })`, and SQLite crash recovery.
* **Representative code:** `@Action({ name: "reserve-flight-action", input: TravelRequestDto, output: FlightReservationDto, compensateWith: ReleaseFlightAction, committedBy: IssueFinalItineraryAction })`
* **Quickstart commands:**
```bash
cd 04-travel-booking-saga
npm run check && npm test && npx tsx src/demo.ts
```
* **Expected proof output:** `npx tsx src/demo.ts` outputs JSON confirming `scenarioA_PaymentDeclined.compensationOrder: ["release-hotel-action", "release-flight-action"]`, `scenarioB_BudgetExceeded.flightState: "RELEASED"`, `scenarioC_CancelAndRollback.withdrawnResolution: "cancelled"`, `scenarioD_RollbackCrashAndSqliteRecovery.sqliteResumedOutputStatus: "CONFIRMED"`, and `scenarioE_QuorumApproval.outcomeStatus: "CONFIRMED_BY_QUORUM"`.
* **Failure scenarios tested:** Payment gateway decline (`PaymentGatewayDeclinedError`), mid-flight `BudgetExceededError`, concurrent `app.cancel()` vs `app.resume()` race, crash inside a compensation handler (`CompensationFailedError`) followed by idempotent recovery replay, and rejected branch inside a 2-of-3 quorum.
* **Guarantee boundary:**
* *Verified:* Single-node SQLite WAL durability, deterministic LIFO compensation ordering, idempotent `step.idempotencyKey` propagation to external ledgers, and lease-fenced recovery within the configured SQLite store.
* *Not verified:* Multi-datacenter distributed consensus or compensation when an external third-party API permanently rejects its own refund/release endpoint without an idempotency contract.
* **Repository directory:** [`agbossjs/agboss-examples/04-travel-booking-saga`](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)
### 5. [`05-cluster-work-stealing`](https://github.com/agbossjs/agboss-examples/tree/main/05-cluster-work-stealing) — Distributed Child Work-Stealing, Leases & Backpressure (`P1`)
[Section titled “5. 05-cluster-work-stealing — Distributed Child Work-Stealing, Leases & Backpressure (P1)”](#5-05-cluster-work-stealing--distributed-child-work-stealing-leases--backpressure-p1)
* **Problem statement:** Fan-out and recursive workflows executing on a single Node.js instance bottleneck CPU/concurrency and lose progress if a worker crashes mid-batch.
* **Architecture overview:** `CoordinatorWorkflow` fans out `ChunkWorkerWorkflow` across cluster instances via `.toParallelWorkflows(ChunkWorkerWorkflow, { placement: ChildPlacement.AnyInstance })`, alongside `RecursiveDecomposerWorkflow` (`ThisWorkflow`, `maxDepth: 2`), coordinated through shared SQLite WAL repositories (`SqliteChildExecutionQueueRepository`, `SqliteExecutionLeaseRepository`, `SqliteExecutionTreeRepository`, `SqliteRateLimitRepository`).
* **Main mechanisms demonstrated:** `ChildPlacement.AnyInstance` work-stealing, parent lease yielding while waiting for children, 30s lease heartbeat & stale-lease reclamation (`recoverApp`), `ExecutionLeaseLostError` split-brain fencing, `maxConcurrentSteps: 2` backpressure (`HealthComponentKind.StepSlots`), `@HealthCheck` (`checkLiveness`, `checkReadiness`), and `IncompatibleResumeError`.
* **Representative code:** `.toParallelWorkflows(ChunkWorkerWorkflow, { placement: ChildPlacement.AnyInstance, maxItems: 8, onBranchError: ParallelErrorPolicy.CollectAll })`
* **Quickstart commands:**
```bash
cd 05-cluster-work-stealing
npm run check && npm test && npx tsx src/demo.ts
```
* **Expected proof output:** `npx tsx src/demo.ts` runs a 2-instance cluster over shared SQLite and prints JSON with `workStealing.batchSummary.succeededCount: 4`, `committedChunkEffects: 4`, `recursiveStatus: "finished"`, and `health.readinessStatus: "healthy"`.
* **Failure scenarios tested:** Worker crash mid-batch with 30s lease expiry and takeover by a surviving instance, zombie worker split-brain write fenced by `ExecutionLeaseLostError`, `maxConcurrentSteps` saturation flipping readiness to `unhealthy` while liveness stays `healthy`, and `IncompatibleResumeError` on workflow version drift.
* **Guarantee boundary:**
* *Verified:* Multi-instance coordination over shared SQLite WAL storage (single host or shared POSIX/local disk), lease fencing, and step-slot backpressure.
* *Not verified:* Distributed network database backends (PostgreSQL/Redis adapters are on the roadmap and not shipped in `0.1.0`).
* **Repository directory:** [`agbossjs/agboss-examples/05-cluster-work-stealing`](https://github.com/agbossjs/agboss-examples/tree/main/05-cluster-work-stealing)
### 6. [`06-enterprise-agent-mesh`](https://github.com/agbossjs/agboss-examples/tree/main/06-enterprise-agent-mesh) — Multi-Service A2A + MCP Mesh & Federation Check (`P1`)
[Section titled “6. 06-enterprise-agent-mesh — Multi-Service A2A + MCP Mesh & Federation Check (P1)”](#6-06-enterprise-agent-mesh--multi-service-a2a--mcp-mesh--federation-check-p1)
* **Problem statement:** When independent microservices expose agents (`A2A`) and tools (`MCP`), untyped payloads, missing authentication, circular cross-service loops, duplicate retries, and breaking schema changes cause cascading outages.
* **Architecture overview:** Three separate AGBoss applications in one workspace — `apps/1-order-gateway` (`TriageGatewayApplication`), `apps/2-billing-a2a` (`BillingApplication` exposing `@A2AExpose`), and `apps/3-catalog-mcp` (`CatalogApplication` exposing `@McpExpose`).
* **Main mechanisms demonstrated:** `@A2AExpose` + `a2aHttpListener(app, { expose, callerResolver })`, `@A2AAgent`, `@McpExpose` + `@McpServer` + `@McpTool`, Ed25519-signed `agboss-mesh` JWS envelope propagation (`origin`, `budget.remaining`, `deadline.remaining`, `callChain`), `MeshLoopDetectedError` / `HopsLimitExceededError`, `SqliteMeshRequestRepository` idempotency deduplication, `agboss check --federation`, and `agboss hub`.
* **Representative code:** `@A2AAgent({ name: "remote-billing-agent", description: "Calls Billing service over A2A", target: "billing-application/billing-workflow", url: "billingA2aUrl", input: BillingInquiryDto, output: BillingResolutionDto, price: usd(0.05) })`
* **Quickstart commands:**
```bash
cd 06-enterprise-agent-mesh
npm run check && npm test && npx tsx apps/1-order-gateway/src/demo.ts
```
* **Expected proof output:** `npx tsx apps/1-order-gateway/src/demo.ts` boots the A2A + MCP services on loopback HTTP ports and prints JSON confirming `a2aResolution.creditedUsd: 45`, `federationProof.compatibleOk: true`, `federationProof.breakingExitCode: 3`, and `federationProof.breakingCodes: ["federation.incompatible-schema", "federation.missing-target"]`.
* **Failure scenarios tested:** Unauthenticated A2A caller rejection (`A2ACallerRejectedError`), cross-service call-chain cycle (`MeshLoopDetectedError`), hop limit exhaustion (`HopsLimitExceededError`), and breaking cross-service contract drift caught by `agboss check --federation`.
* **Guarantee boundary:**
* *Verified:* Cross-service contract verification (`agboss check --federation`), Ed25519 JWS mesh envelope verification, loop/hop guards, and MCP/A2A request deduplication.
* *Not verified:* External service mesh mTLS/ingress infrastructure or non-AGBoss third-party A2A implementations.
* **Repository directory:** [`agbossjs/agboss-examples/06-enterprise-agent-mesh`](https://github.com/agbossjs/agboss-examples/tree/main/06-enterprise-agent-mesh)
### 7. [`07-langgraph-migration`](https://github.com/agbossjs/agboss-examples/tree/main/07-langgraph-migration) — Deterministic Reference Migration (`before-langgraph/` vs `after-agboss/`) (`P0`)
[Section titled “7. 07-langgraph-migration — Deterministic Reference Migration (before-langgraph/ vs after-agboss/) (P0)”](#7-07-langgraph-migration--deterministic-reference-migration-before-langgraph-vs-after-agboss-p0)
* **Problem statement:** Raw LangGraph workflows often accumulate a 15+ field mutable `Annotation.Root` God State where optional fields are read before they are initialized, stale loop critiques persist across iterations, and side effects lack idempotency keys.
* **Architecture overview:** Side-by-side implementation of the same customer support & refund workflow in `before-langgraph/` (raw `@langchain/langgraph` `StateGraph` with a 15-field `Annotation.Root`) and `after-agboss/` (canonical AGBoss application with 10 narrow `@Dto` classes, `@Router`, `@Agent`, `@Action`, and `@Channel`).
* **Main mechanisms demonstrated:** Migrating `Annotation.Root` to narrow `@Dto` handoffs, replacing conditional edges with `.toRouter().chooseOne()` and `.switch(caseOf(..., { about }))`, replacing parallel node fan-out with `.toParallelAgents()`, and adding `step.idempotencyKey` to `@Action`.
* **Representative code:** `agent(ReplyReviewerAgent).returns(oneOf(ReplyApprovedDto, CritiqueNotesDto)).switch(caseOf(ReplyApprovedDto, { about: DraftReplyDto }).toRouter(RefundSizeRouter), caseOf(CritiqueNotesDto, { about: DraftReplyDto }).toAgent(ReplyWriterAgent))`
* **Quickstart commands:**
```bash
cd 07-langgraph-migration
npm run check && npm test && npx tsx after-agboss/src/demo.ts
```
* **Expected proof output:** `npx tsx after-agboss/src/demo.ts` runs both `before-langgraph` and `after-agboss` deterministically and outputs JSON comparing `beforeLangGraph.godStateFieldsCount: 15`, `firstWriteSawUndefinedDraft: true`, and `unusedFieldPersistedInState: true` against `afterAgboss.status: "finished"` and `afterAgboss.path`.
* **Failure scenario tested:** `tests/migration-comparison.test.ts` proves that the raw LangGraph state exposes uninitialized `undefined` fields on first pass and retains dead state fields, whereas the AGBoss workflow enforces strict Path Scope provenance and `maxVisits` bounds.
* **Guarantee boundary:**
* *Verified:* Deterministic structural comparison between a representative shared-state `StateGraph` and its narrow-DTO AGBoss counterpart.
* *Not verified:* This is a **deterministic reference migration**, not a universal benchmark across all possible LangGraph coding styles.
* **Repository directory:** [`agbossjs/agboss-examples/07-langgraph-migration`](https://github.com/agbossjs/agboss-examples/tree/main/07-langgraph-migration)
***
## How the `examples/job-scout` application works
[Section titled “How the examples/job-scout application works”](#how-the-examplesjob-scout-application-works)
`examples/job-scout` reads a candidate’s resume from disk, proposes a structured search brief, fetches live openings from public Greenhouse boards, ranks every match using the Jev decision model (`JobFitJudge`), explains why top matches fit using a SQLite FTS5 `@Rag` knowledge base (`company_notes`), and saves selected jobs through the official filesystem `@McpServer` after pausing for user approval (`TerminalUserChannel`):
src/job-scout.workflow\.ts
```ts
import { Workflow, route, start } from "@agboss/core";
import { WorkflowStartTextDto } from "@agboss/core/dto";
@Workflow({
name: "job-scout-workflow",
version: "1.0.0",
flow: [
start(ChatWorkflowStart)
.returns(WorkflowStartTextDto)
.toRouter(MainRouter)
.chooseOne(
route(ProfilerAgent)
.toFinish(ChatWorkflowFinish),
route(ScoutAgent)
.toFinish(ChatWorkflowFinish),
route(ShortlistAgent)
.toFinish(ChatWorkflowFinish),
),
],
defaults: {
models: { temperature: 0 },
router: { model: "typesafe/jev-1.13" },
tools: { maxToolCalls: 8 },
},
providers: [JobFitJudge],
})
export class JobScout {}
```
src/job-scout.components.ts
```ts
import { Agent, DecidedBy, Injectable, Router, WorkflowFinish, WorkflowStart } from "@agboss/core";
import { WorkflowFinishTextDto, WorkflowStartTextDto } from "@agboss/core/dto";
@WorkflowStart({ name: "chat-workflow-start", description: "A message", input: WorkflowStartTextDto })
export class ChatWorkflowStart {}
@WorkflowFinish({ name: "chat-workflow-finish", description: "The answer", output: WorkflowFinishTextDto })
export class ChatWorkflowFinish {}
@Agent({ name: "profiler-agent", description: "Reads the resume", prompt: "Read the resume.", model: "moonshotai/kimi-k2.6" })
export class ProfilerAgent {}
@Agent({ name: "scout-agent", description: "Finds jobs", prompt: "Find jobs.", model: "moonshotai/kimi-k2.6" })
export class ScoutAgent {}
@Agent({ name: "shortlist-agent", description: "Saves jobs", prompt: "Save shortlist.", model: "moonshotai/kimi-k2.6" })
export class ShortlistAgent {}
@Router({
name: "main-router",
description: "Sends the message to the agent that answers it",
decidedBy: DecidedBy.Model,
prompt: "Pick the agent that answers the message.",
input: { message: WorkflowStartTextDto },
routes: [
{ prompt: "Reading the resume", target: ProfilerAgent },
{ prompt: "Finding jobs", target: ScoutAgent },
{ prompt: "Managing the shortlist", target: ShortlistAgent },
],
})
export class MainRouter {}
@Injectable()
export class JobFitJudge {}
```
test/job-scout.workflow\.test.ts
```ts
import { Application, EmptyEnvironment } from "@agboss/core";
import { expect } from "vitest";
import { replyWith, routeTo, testWith } from "@agboss/core/testing";
import { JobScout } from "./job-scout.workflow.js";
import {
ChatWorkflowFinish,
ChatWorkflowStart,
MainRouter,
ScoutAgent,
} from "./job-scout.components.js";
@Application({
name: "job-scout-example-app",
version: "1.0.0",
environment: EmptyEnvironment,
workflows: [JobScout],
budgets: [JsExAppBudget, JsExTenantBudget, JsExExecBudget],
})
export class JobScoutExampleApp {}
const test = testWith(JobScoutExampleApp);
test("routes job search queries through MainRouter to ScoutAgent", async ({
app,
mockLlm,
}) => {
mockLlm(MainRouter).thenReturn(routeTo(ScoutAgent));
mockLlm(ScoutAgent).thenReturn(
replyWith("Found 2 matching Staff TypeScript roles."),
);
const result = await app.execute(ChatWorkflowStart, {
text: "Find Staff TypeScript roles",
});
expect(result.finish).toBe(ChatWorkflowFinish);
expect(result.output).toEqual({
text: "Found 2 matching Staff TypeScript roles.",
});
});
```
Run `examples/job-scout` from the repository root:
```bash
npm install && npm run build
cd examples/job-scout
npm run chat
npm run describe
```
## What patterns are demonstrated in `examples/flow-patterns`?
[Section titled “What patterns are demonstrated in examples/flow-patterns?”](#what-patterns-are-demonstrated-in-examplesflow-patterns)
`examples/flow-patterns` (`src/flow-patterns.application.ts`) demonstrates four advanced AGBoss workflow patterns tested 100% offline with `testWith` and `agboss check`:
* **`src/offer/` (Review loop with `{ about }`):** `draft-agent` → `reviewer-agent` returning `oneOf(ChangesRequestedDto, DraftApprovedDto)`. When changes are requested, `caseOf(ChangesRequestedDto, { about: OfferDraftDto })` routes to `editor-agent` (`maxVisits: 2`), with `.onError(VisitsLimitExceededError, ...)` handling exhausted review cycles.
* **`src/credit/` (Code router beside model router):** Combines a Jev model router with `credit-limit-router` (`DecidedBy.Code`), routing small orders to `charge-card-action` and larger orders to `approval-agent`.
* **`src/review/` (Parallel launches in waves):** Runs a reviewer per document section via `.toParallelAgents(...)`, joins their outputs, and launches a second wave with a critic alongside parallel fact-checker copies.
* **`src/topic/` (Recursive `ThisWorkflow`):** Researches broad topics by launching `.toParallelWorkflows(ThisWorkflow, { maxItems })` with `@Workflow({ maxDepth: 3 })`.
Inspect the pattern application directly with the CLI:
```bash
cd examples/flow-patterns
npx agboss describe --application src/flow-patterns.application.ts
npm test
```
# Interactive Feature Explorer
> Interactive walkthrough of AGBoss components, Path Scope workflow verification, LIFO sagas, dependency injection, budgets, PII masking, testing, and multi-run evaluations.
AGBoss is an open-source TypeScript framework with static verification (`agboss check`) for complex multi-agent workflows on top of LangGraph. Use the interactive Feature Explorer below to inspect how each decorator, workflow primitive, and testing loop works in real application code.
## How does each AGBoss capability work in code?
[Section titled “How does each AGBoss capability work in code?”](#how-does-each-agboss-capability-work-in-code)
Select a capability category on the left and switch between the code tabs on the right to see the exact production problem it addresses, the AGBoss solution, and the corresponding TypeScript implementation.
Components & Skills COREDecorated contracts, HarnessDSL (HDSL) paths, @Application assembly & AGENTS.md skills.
@Application\@Workflow (HDSL)@Agent\@Router\@Tool\@McpServer\@Rag\@ModelProviderAI Skills
Loops & QuorumsCompile-time HDSL Path Scope, { about } review loops, and @Arbiter quorums.
Review loopProvenanceParallelQuorum & @ArbiterChild Workflow
Transactional Sagas NEWDeclarative compensateWith & committedBy with automatic LIFO rollback.
compensateWith & committedByLIFO Unwinding
Lifecycle & DIOnInit..OnDestroy, 23 graph hooks, and @HealthCheck probes.
Lifecycle23 Graph HooksScoped DI & Health
Typed Inputs & EnvDecorated DTOs, fail-fast secret()/env(), and typed units.
DTOs & PII FieldsEnvironments & Units
Cache, PII & ApprovalKV-cache invariant HMAC masking, guardrails, and two-phase pauses.
@PiiPolicy & KV Cache\@InputGuardrailHuman Approval
Cost & DurabilityPre-call @Budget reservation, SQLite WAL stores, and 30s pod leases.
@Budget CapsSQLite Stores30s LeasesWork-Stealing
Loop 0–1: Harness & VCR$0 compiler proof, deterministic testWith races, 10 unit runners & VCR hash locks.
Workflow & Races10 Unit Runners
Loop 2–4: Eval FunnelFail-fast @EvalSuite cascades, pass^k vs baseline, Cohen's kappa & crystallization.
Langfuse & AuditCrystallizationEvals & Profiles
Production Problem
AGBoss Solution
job-scout.application.ts
```
```
## Where can you run these capabilities end to end?
[Section titled “Where can you run these capabilities end to end?”](#where-can-you-run-these-capabilities-end-to-end)
Every mechanism in the explorer above is backed by runnable verification suites in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples):
* **[`01-hello-agboss`](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss):** Dependency injection, `@Router` + `@Agent` + `@Tool`, SQLite cost ledger, and HTTP server integration.
* **[`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof):** Build-time `[input.missing-on-path]`, `[input.outdated-on-path]`, `[launch.branch-outdates-sibling]`, and `[flow.uncompensated-action]` proofs.
* **[`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk):** Reversible HMAC `@PiiPolicy`, `@InputGuardrail` / `@OutputGuardrail`, `@ToolInterceptor`, `@Compaction`, and `@EvalSuite`.
* **[`04-travel-booking-saga`](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga):** Declarative `compensateWith` / `committedBy` LIFO sagas, two-phase `@ChannelRole` pauses, `@Arbiter` quorums, and SQLite crash recovery.
* **[`05-cluster-work-stealing`](https://github.com/agbossjs/agboss-examples/tree/main/05-cluster-work-stealing):** `ChildPlacement.AnyInstance` distributed child workflows, 30s leases, and `@HealthCheck` liveness/readiness probes.
* **[`06-enterprise-agent-mesh`](https://github.com/agbossjs/agboss-examples/tree/main/06-enterprise-agent-mesh):** Multi-service A2A + MCP mesh, `agboss check --federation`, and `agboss hub`.
* **[`07-langgraph-migration`](https://github.com/agbossjs/agboss-examples/tree/main/07-langgraph-migration):** Deterministic before/after migration from raw LangGraph shared state to AGBoss narrow DTOs and `HarnessDSL`.
# Installation
> Install the AGBoss TypeScript runtime (@agboss/core) and static compiler (@agboss/cli) in a new or existing Node.js 22.13+ project with auto-synced AI coding skills.
Installing AGBoss gives you the lightweight production runtime (`@agboss/core`), focused subpath entries (`@agboss/core/dto`, `@agboss/core/units`, `@agboss/core/models`, `@agboss/core/testing`, `@agboss/core/eval`, `@agboss/core/mcp`, `@agboss/core/rag`, `@agboss/core/a2a`, `@agboss/core/memory`, `@agboss/core/eslint`), 13 bundled AI coding-agent reference modules (`node_modules/@agboss/core/skills/agboss/`), and the `@agboss/cli` static compiler and developer toolkit (`agboss` / `agb`). Importing `@agboss/core` has zero side effects: tracing, MCP, and SQLite load only when an application uses them.
> **Current Release (`v0.1.0`):** `@agboss/core@0.1.0` and `@agboss/cli@0.1.0` are published on the public npm registry (`registry.npmjs.org`). Scaffold a new project with `npx --yes --package=@agboss/cli agboss create my-app` or explore the 7 verified architecture examples in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples).
## Option 1: Create a new project (`npx --yes --package=@agboss/cli agboss create my-app`)
[Section titled “Option 1: Create a new project (npx --yes --package=@agboss/cli agboss create my-app)”](#option-1-create-a-new-project-npx---yes---packageagbosscli-agboss-create-my-app)
The fastest way to start a new project from scratch is with `npx --yes --package=@agboss/cli agboss create` (or `npm exec --yes --package=@agboss/cli -- agboss create`, short alias `agb c`):
```bash
npx --yes --package=@agboss/cli agboss create my-app
cd my-app
npm install
```
What happens when you run these three commands:
1. **Pre-wired `package.json` (`@agboss/core` + `@agboss/cli`):** `npx --yes --package=@agboss/cli agboss create my-app` creates a ready-to-run TypeScript project where `package.json` already places `@agboss/core` in `dependencies` and `@agboss/cli` in `devDependencies`, alongside committed `AGENTS.md` and `CLAUDE.md` (`@AGENTS.md`) files.
2. **Automatic `postinstall` compilation & skill unpacking:** When you run `npm install`, the `"postinstall": "npm run types"` hook automatically executes `agboss check`, which:
* Generates **`.agboss/types.d.ts`** so TypeScript immediately knows the Path Scope input of every node (`Produced`), `app.execute`, and test scripts.
* Unpacks **13 version-matched reference guides** into `.claude/skills/agboss/` (`SKILL.md` + `references/01..13`, including `13-migrating-from-langgraph.md`) so Claude Code, Cursor, Codex, Windsurf, and Copilot write idiomatic AGBoss v2 code from the first prompt.
3. **Interactive or non-interactive scaffolding:** Pass `--yes` and component flags directly in scripts or CI:
```bash
npx --yes --package=@agboss/cli agboss create support-desk --yes --agents "triage:Sorts requests,answerer:Answers questions" --tools "answerer:search_orders"
```
Every scaffolded project also includes:
* **Typed workflow skeleton & environments:** `@WorkflowStart`, `@Router`, `@Agent` classes with `*.prompt.md` files, `@WorkflowFinish`, `@Application`, and `environments/` (`dev`, `staging`, `prod`, `test`).
* **Pre-configured ESLint flat config (`eslint.config.js` via `@agboss/core/eslint`):** Enforces `agboss/no-component-state` (stateless singleton components), `agboss/no-direct-time` (using injected `Clock` instead of `Date.now()` / `setTimeout`), and `FLOW_CHAIN_RULE` (`@stylistic/newline-per-chained-call` — one chained method call per line in `*.workflow.ts`):
eslint.config.ts
```ts
import { agbossPlugin, FLOW_CHAIN_RULE } from "@agboss/core/eslint";
export default [
{
plugins: { agboss: agbossPlugin },
rules: {
"agboss/no-component-state": "error",
"agboss/no-direct-time": "error",
},
},
{
files: ["**/*.workflow.ts"],
rules: {
...FLOW_CHAIN_RULE,
},
},
];
```
## Option 2: Add AGBoss to an existing project (why there are two packages)
[Section titled “Option 2: Add AGBoss to an existing project (why there are two packages)”](#option-2-add-agboss-to-an-existing-project-why-there-are-two-packages)
To add AGBoss to an existing Node.js 22.13+ service or monorepo, install the runtime and the CLI in three steps:
```bash
npm install @agboss/core
npm install -D @agboss/cli
npx agboss check
```
### Why is AGBoss split into `@agboss/core` and `@agboss/cli`?
[Section titled “Why is AGBoss split into @agboss/core and @agboss/cli?”](#why-is-agboss-split-into-agbosscore-and-agbosscli)
| Package | Where It Belongs | What It Contains & Why |
| ---------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[`@agboss/core`](https://www.npmjs.com/package/@agboss/core)** | `dependencies` | **Lightweight production runtime** shipped into your Docker / Kubernetes container image: decorators (`@Application`, `@Workflow`, `@Agent`, `@Action`, `@Router`, `@Arbiter`, `@Tool`, `@Budget`, `@PiiPolicy`, `@ChannelRole`), execution graph engine, DI container, storage repository contracts (`InMemory*` and `Sqlite*`), subpath entries (`@agboss/core/dto`, `@agboss/core/units`, `@agboss/core/models`, `@agboss/core/testing`, `@agboss/core/eval`, `@agboss/core/mcp`, `@agboss/core/rag`, `@agboss/core/a2a`, `@agboss/core/memory`, `@agboss/core/eslint`), and the bundled AI coding skills (`skills/agboss/`). |
| **[`@agboss/cli`](https://www.npmjs.com/package/@agboss/cli)** | `devDependencies` | **Developer & CI toolchain** kept out of production container images: the static graph compiler (`agboss check`, `--strict`, `--models`, `--cassettes`, `--federation`), code generator (`agboss generate`), watch-mode compiler (`agboss dev`), LangGraph Studio bridge (`agboss studio`), and local visual UI servers (`agboss eval ui`, `agboss hub ui`, `agboss eval show --html`, `agboss hub show --html`). |
Point `"agboss": { "application": "src/my-app.application.ts" }` in your `package.json`, then scaffold and wire components using `agboss generate` (alias `agboss g`) by their full names (including kind suffix):
```bash
npx agboss g agent billing-agent --workflow support-desk-workflow --description "Handles invoices"
npx agboss g tool refund-tool --workflow support-desk-workflow --agent billing-agent
npx agboss g openapi pets --url ./pets.yaml --workflow support-desk-workflow --agent billing-agent
```
## Verify your installation
[Section titled “Verify your installation”](#verify-your-installation)
Run `agboss check` to generate `.agboss/types.d.ts`, sync the 13 reference modules in `.claude/skills/agboss/`, and statically verify every flow path, DTO handoff, environment field, and prompt file offline without making any LLM calls:
```bash
npx agboss check
```
Continue to the [Quick start](/getting-started/quick-start/) to build and execute your first workflow.
# Introduction
> Spring-style architecture for LangGraph AI agents — build reliable, testable AI agents in TypeScript with dependency injection, static workflow validation, and deterministic testing.
**AGBoss** — short for **A**gent **G**raph **B**uilder **O**pen **S**ource **S**ystem — is an open-source, Spring-style TypeScript framework for building reliable, testable AI agents and multi-agent workflows on top of [LangGraph](https://www.langchain.com/langgraph).
While LangGraph provides the underlying graph execution runtime and checkpointing engine, AGBoss provides the application architecture layer: declarative component decorators (`@Application`, `@Workflow`, `@Agent`, `@Router`, `@Arbiter`, `@Action`, `@Tool`), constructor dependency injection (`@Injectable`), explicit `@Dto` data contracts, a declarative workflow DSL (**HarnessDSL**), a build-time workflow verifier (`agboss check`), and a deterministic offline testing harness (`@agboss/core/testing`).
It is published on npm as two complementary Apache-2.0 packages:
* **[`@agboss/core`](https://www.npmjs.com/package/@agboss/core)** (`dependencies`) — the production runtime, decorators, DI container, storage repositories, testing/evaluation subpaths, and bundled AI coding skills.
* **[`@agboss/cli`](https://www.npmjs.com/package/@agboss/cli)** (`devDependencies`) — the project scaffolder (`agboss create`), code generator (`agboss generate`), static workflow compiler (`agboss check`), and local developer/evaluation UIs.
## Why build with AGBoss on LangGraph?
[Section titled “Why build with AGBoss on LangGraph?”](#why-build-with-agboss-on-langgraph)
As a multi-agent system grows beyond a small prototype—especially when maintained alongside AI coding assistants (Claude Code, Cursor, Codex, Windsurf, Copilot)—raw shared-state graphs (`Annotation.Root`) and untyped string edges become difficult to validate and test. AGBoss addresses this through [7 architectural principles](/concepts/architecture/):
* **Computational Role Segregation:** Distinct decorators ([`@Agent`](/reference/agent/), [`@Router`](/reference/router/), [`@Arbiter`](/reference/arbiter/), [`@Action`](/reference/action/), [`@Tool`](/reference/tool/)) separate LLM reasoning, routing decisions, quorum consensus, and external side effects into focused component classes.
* **Static Workflow Validation (`agboss check` & Path Scope):** The `@Workflow` `flow` array declares paths with one chained method call per line (`start(ChatWorkflowStart)` → `.returns(WorkflowStartTextDto)` → `.toRouter(MainRouter)` → `.chooseOne(...)`). `npx agboss check` runs static path-dominance analysis, generates `.agboss/types.d.ts` with narrowed `Produced` types, and rejects missing inputs (`[input.missing-on-path]`), stale loop data (`[input.outdated-on-path]`), or uncompensated actions (`[flow.uncompensated-action]`) before runtime.
* **Constructor Dependency Injection & Single `@Application` Root:** Wire typed environment contracts (`AppEnvironment`, `secret()`, `env()`, `env.optional()`), shared [`@ModelProvider`](/reference/model-provider/) pools, [`@Budget`](/reference/budget/) ceilings, [`@ChannelRole`](/reference/channel-role/) pauses, [`@HealthCheck`](/reference/health-check/) probes, and replaceable `InMemory*` or `Sqlite*` WAL repositories in one container—and swap implementations or test doubles cleanly in tests.
* **Deterministic Offline Testing (`@agboss/core/testing`):** Boot the application container with `testWith(Application)`, control concurrent interleavings and virtual time (`VirtualClock`), and test any component in isolation offline without live LLM calls using 10 dedicated unit runners (`testTool`, `testToolInterceptor`, `testAction`, `testRouter`, `testArbiter`, `testRag`, `testJudge`, `testCompaction`, `testPii`, `testGuardrail`).
* **Prompt Prefix Cache Invariance & Boundary PII:** Static system prompt verification (`[prompt.input-in-instructions]`) and deterministic HMAC pseudonymization ([`PiiAction.replaceWithToken()`](/reference/pii-policy/)) preserve LLM KV-cache prefixes across turns while restoring real values only inside authorized `@Tool` calls.
* **Built-in AI Coding Skills & Progressive Crystallization:** Every scaffolded project includes `AGENTS.md` and offline-synced [`.claude/skills/agboss/`](/concepts/ai-coding-skills/) reference guides, plus `agboss describe --json` (`configHash`, `promptHash`, `schemaHash`) for [crystallizing](/concepts/progressive-crystallization/) settled LLM paths into deterministic TypeScript.
## How AGBoss is organized
[Section titled “How AGBoss is organized”](#how-agboss-is-organized)
| Layer | Public Entry | Key Primitives |
| -------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Application & Flow** | `agboss` | `@Application`, `createApp`, `@Workflow`, `@WorkflowStart`, `@WorkflowFinish`, `start`, `route`, `caseOf`, `ThisWorkflow` |
| **Components & DI** | `agboss` | `@Agent`, `@Action`, `@Router`, `@Arbiter`, `@Tool`, `@ToolInterceptor`, `@Judge`, `@InputGuardrail`, `@OutputGuardrail`, `@Compaction`, `@Injectable`, `ExecutionContext`, `ApplicationContext`, `Clock`, `Logger` |
| **Data Contracts & Units** | `@agboss/core/dto`, `@agboss/core/units` | `@Text`, `@Integer`, `@Decimal`, `@Flag`, `@OneOf`, `@ListOf`, `@Nested`, `oneOf`, `listOf`, `optional`, `historyOf`, `usd()`, `seconds()`, `minutes()`, `hours()` |
| **Models & Integrations** | `@agboss/core/models`, `@agboss/core/mcp`, `@agboss/core/rag`, `@agboss/core/a2a` | `@ModelProvider`, `RateLimitPolicy`, `PromptCaching`, `@McpServer`, `@McpTool`, `@McpExpose`, `@Rag`, `SqliteFtsConnector`, `@A2AAgent`, `@A2AExpose` |
| **Testing & Evaluation** | `@agboss/core/testing`, `@agboss/core/eval` | `testWith`, `TestConcurrency`, `VirtualClock`, `testTool`, `testToolInterceptor`, `testAction`, `testRouter`, `testArbiter`, `testRag`, `testJudge`, `testCompaction`, `testPii`, `testGuardrail`, `@EvalSuite` |
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
1. [Install `agboss` and `@agboss/cli` from npm](/getting-started/installation/) and follow the [Quick start](/getting-started/quick-start/) to scaffold, statically verify, and test your first workflow.
2. Explore the 7 standalone, runnable applications in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) (documented in the [Examples guide](/examples/job-scout/)).
3. Read the [7 Architectural Principles](/concepts/architecture/) and [AI Coding Skills (`AGENTS.md`)](/concepts/ai-coding-skills/).
# Quick start
> Build, statically verify with agboss check, run, and unit-test your first multi-agent workflow and @Application container in TypeScript.
Building an AGBoss application takes four small building blocks: `@WorkflowStart` and `@WorkflowFinish` endpoints typed with DTOs from `@agboss/core/dto`, an `@Agent` (or `@Action` / `@Router`) component, a `@Workflow` module whose **HarnessDSL (`HDSL`)** `flow` chains steps one call per line, and an `@Application` entry point with `@Budget` ceilings booted via `createApp`.
> **Current Release (`v0.1.0`):** `@agboss/core@0.1.0` and `@agboss/cli@0.1.0` are published on `registry.npmjs.org`. Run `npx --yes --package=@agboss/cli agboss create my-app` to scaffold a new project or clone [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) (`cd 01-hello-agboss && npm install`) to run the verified starter.
### Progressive Learning Path
[Section titled “Progressive Learning Path”](#progressive-learning-path)
| Milestone | Time | What You Will Accomplish |
| ---------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1. First 5 Minutes** | `5 min` | Install `@agboss/core` + `@agboss/cli` (or run `01-hello-agboss`), verify the graph with `npx agboss check`, and run your first deterministic workflow test (`npm test`). |
| **2. Next 15 Minutes** | `15 min` | Add constructor dependency injection (`@Injectable`), narrow `@Dto` contracts (`@Text`, `@Integer`), and compose a `@Router` + `@Agent` + `@Tool` workflow inside `@Application`. |
| **3. At 30 Minutes** | `30 min` | Understand **Path Scope** (`Produced`) and trigger intentional build-time graph failures (`[input.missing-on-path]`, `[input.outdated-on-path]`, `[flow.uncompensated-action]`) in [`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof). |
| **4. Advanced Guides** | `30+ min` | Scale to LIFO sagas ([`04-travel-booking-saga`](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)), distributed `ChildPlacement.AnyInstance` work-stealing ([`05-cluster-work-stealing`](https://github.com/agbossjs/agboss-examples/tree/main/05-cluster-work-stealing)), multi-run `@EvalSuite` ([`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk)), and A2A + MCP federation ([`06-enterprise-agent-mesh`](https://github.com/agbossjs/agboss-examples/tree/main/06-enterprise-agent-mesh)). |
## 5 Minutes: Install, check, and run
[Section titled “5 Minutes: Install, check, and run”](#5-minutes-install-check-and-run)
### Option 1 — Create a new project from scratch
[Section titled “Option 1 — Create a new project from scratch”](#option-1--create-a-new-project-from-scratch)
```bash
npx --yes --package=@agboss/cli agboss create my-app
cd my-app
npm install
```
`npx --yes --package=@agboss/cli agboss create my-app` (also available via `npm exec --yes --package=@agboss/cli -- agboss create my-app`) creates a project with `@agboss/core` in `dependencies`, `@agboss/cli` in `devDependencies`, `AGENTS.md`, and `CLAUDE.md` (`@AGENTS.md`). Running `npm install` runs `"postinstall": "npm run types"` (`agboss check`), which generates `.agboss/types.d.ts` and unpacks 13 AI coding skill references into `.claude/skills/agboss/` (`SKILL.md` + `references/01..13`).
### Option 2 — Add to an existing project
[Section titled “Option 2 — Add to an existing project”](#option-2--add-to-an-existing-project)
```bash
npm install @agboss/core
npm install -D @agboss/cli
npx agboss check
```
* **`@agboss/core` (`dependencies`)** is the lightweight production runtime (decorators, graph engine, DI, repository contracts, subpaths `@agboss/core/dto`, `@agboss/core/testing`, `@agboss/core/eval`, `@agboss/core/eslint`, and bundled AI skills) deployed in Docker / Kubernetes.
* **`@agboss/cli` (`devDependencies`)** is the static compiler (`agboss check`), generator (`agboss generate`), dev watcher (`agboss dev`), Studio bridge (`agboss studio`), and local UI toolkit (`agboss eval ui`, `agboss hub ui`) kept out of production images.
## 15 Minutes: Dependency injection, typed DTOs, and workflow composition
[Section titled “15 Minutes: Dependency injection, typed DTOs, and workflow composition”](#15-minutes-dependency-injection-typed-dtos-and-workflow-composition)
The example below defines a customer support workflow and its enclosing `@Application`, then executes a turn via `app.execute`:
src/support.workflow\.ts
```ts
import { Agent, Application, Budget, BudgetPeriod, BudgetScope, createApp, EmptyEnvironment, start, Workflow, WorkflowFinish, WorkflowStart, type BudgetHandler } from "@agboss/core";
import { WorkflowFinishTextDto, WorkflowStartTextDto } from "@agboss/core/dto";
import { usd, type Usd } from "@agboss/core/units";
@WorkflowStart({
name: "support-workflow-start",
description: "Incoming customer question",
input: WorkflowStartTextDto,
})
export class SupportWorkflowStart {}
@WorkflowFinish({
name: "support-workflow-finish",
description: "Support answer returned to the caller",
output: WorkflowFinishTextDto,
})
export class SupportWorkflowFinish {}
@Agent({
name: "support-agent",
description: "Answers customer questions concisely",
prompt: "Answer customer questions concisely and accurately.",
model: "moonshotai/kimi-k2.6",
})
export class SupportAgent {}
@Workflow({
name: "support-workflow",
version: "1.0.0",
flow: [
start(SupportWorkflowStart)
.returns(WorkflowStartTextDto)
.toAgent(SupportAgent)
.toFinish(SupportWorkflowFinish),
],
})
export class SupportWorkflow {}
@Budget({
name: "support-application-budget",
scope: BudgetScope.Application,
period: BudgetPeriod.Day,
})
export class SupportAppBudget implements BudgetHandler {
maxSpend(): Usd {
return usd(10);
}
}
@Budget({
name: "support-tenant-budget",
scope: BudgetScope.Tenant,
period: BudgetPeriod.Day,
})
export class SupportTenantBudget implements BudgetHandler {
maxSpend(): Usd {
return usd(2);
}
}
@Budget({
name: "support-execution-budget",
scope: BudgetScope.Execution,
})
export class SupportExecutionBudget implements BudgetHandler {
maxSpend(): Usd {
return usd(0.1);
}
}
@Application({
name: "support-application",
version: "1.0.0",
environment: EmptyEnvironment,
workflows: [SupportWorkflow],
budgets: [
SupportAppBudget,
SupportTenantBudget,
SupportExecutionBudget,
],
})
export class SupportApplication {}
export async function runOnce() {
const app = await createApp(SupportApplication);
try {
return await app.execute(SupportWorkflowStart, {
text: "How do I rotate my API token?",
});
} finally {
await app.close();
}
}
```
test/support.workflow\.test.ts
```ts
import { expect } from "vitest";
import { ExecutionStatus } from "@agboss/core";
import { replyWith, testWith } from "@agboss/core/testing";
import { SupportAgent, SupportApplication, SupportWorkflowStart } from "../src/support.workflow.js";
const test = testWith(SupportApplication);
test("executes SupportWorkflow deterministically without an API key", async ({ app, mockLlm }) => {
mockLlm(SupportAgent).thenReturn(
replyWith("Open Settings > API Keys > Rotate."),
);
const res = await app.execute(SupportWorkflowStart, { text: "How do I rotate my API token?" });
expect(res.status).toBe(ExecutionStatus.Finished);
});
```
## 30 Minutes: Path Scope and intentional graph failures
[Section titled “30 Minutes: Path Scope and intentional graph failures”](#30-minutes-path-scope-and-intentional-graph-failures)
Once your workflow and application are defined, use the `agboss` CLI to validate the graph, inspect resolved providers and budgets, or test how `agboss check` catches structural graph bugs before runtime:
1. **Generate flow types and validate assembly:** Run `npx agboss check --application src/support.workflow.ts` to write `.agboss/types.d.ts` and verify every path, DTO transition, and budget rule without an API key.
2. **Inspect the assembled topology:** Run `npx agboss describe --application src/support.workflow.ts` to print every path, node producer, model provider resolution, and worst-case step count.
3. **Trigger an intentional Path Scope failure:** In [`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof), inspect how `agboss check` catches four classes of graph defects that pass `tsc --noEmit` in raw shared-state graphs:
* `[input.missing-on-path]` — a router branch skips an upstream producer required by a downstream step.
* `[input.outdated-on-path]` — a revision loop modifies an artifact (`{ about }`) without re-running downstream verification.
* `[launch.branch-outdates-sibling]` — a parallel branch overwrites a DTO slot while a sibling branch concurrently reads it.
* `[flow.uncompensated-action]` — an error path exits a workflow after a side-effecting `@Action({ compensateWith })` without running its compensation handler.
4. **Run interactively in the terminal:** Run `npx agboss chat --application src/support.workflow.ts` to chat with the workflow and inspect per-turn spend.
## Next Steps & Advanced Guides
[Section titled “Next Steps & Advanced Guides”](#next-steps--advanced-guides)
* **Core Concepts:** [`@Application` & Lifecycle](/concepts/application/), [Component Decorators](/concepts/decorators/), [Workflow HDSL (`HarnessDSL`)](/concepts/workflow/), and [Path Scope & Provenance](/concepts/path-scope/).
* **Production Operations:** [Transactional Sagas & Rollback](/guides/error-handling-and-rollback/), [Channels, Pauses & Webhooks](/guides/channels-and-pauses/), [Multi-Instance, Leases & SQLite](/guides/persistence-and-recovery/), [Evals, `pass^k` & Calibration](/guides/evaluation-and-profiles/), and [MCP, RAG & A2A Protocols](/guides/mcp-rag-and-a2a/).
# Channels, Pauses & HTTP Webhook Execution
> Pause workflows for human approval without blocking Node.js threads and trigger active Channel.withdraw() compensation across @Agent, @Tool, and @Action.
Production workflows often start from incoming HTTP webhooks (Stripe events, GitHub webhooks, Slack slash commands, CRM triggers) and pause mid-run for human sign-off before executing high-stakes tool calls. AGBoss handles human-in-the-loop pauses **out-of-process**: when a tool’s `@Channel` returns `ChannelOutcome.awaitReply()`, the branch pauses while sibling parallel branches continue until nothing else can move, persisting a `PendingPause` record and releasing the worker’s 30-second execution lease.
***
## Complete example: `@ChannelRole`, `@Channel`, `@ReplyInterpreter`, HTTP webhooks & test
[Section titled “Complete example: @ChannelRole, @Channel, @ReplyInterpreter, HTTP webhooks & test”](#complete-example-channelrole-channel-replyinterpreter-http-webhooks--test)
src/channels/slack-refund-approval.channel.ts
```ts
import {
Channel,
ChannelOf,
ChannelOutcome,
ChannelRole,
ExecutionContext,
PauseAnswerer,
PauseResolution,
Tool,
type ChannelAsk,
type ChannelWithdrawal,
type ToolHandler,
} from "@agboss/core";
// 1. Role contract: what it approves and who may answer
@ChannelRole({
name: "refund-approval-channel",
description: "Approves card refunds",
approves: RefundCardInputDto,
answeredBy: PauseAnswerer.Channel,
replyInterpreter: RefundReplyInterpreter,
})
export abstract class RefundApprovalChannel
extends ChannelOf {}
// 2. Transport implementation registered in @Application({ providers })
@Channel({
name: "slack-refund-approval-channel",
description: "Asks a finance manager in Slack and withdraws stale buttons",
deps: [ExecutionContext, SlackService],
})
export class SlackRefundApprovalChannel extends RefundApprovalChannel {
constructor(
private readonly executionContext: ExecutionContext,
private readonly slackService: SlackService,
) {
super();
}
async send(
ask: ChannelAsk,
): Promise> {
const cents: number = ask.value.arguments.amountCents;
const tenant = this.executionContext.tenant.tenantId;
await this.slackService.post(
"#finance",
`Refund ${String(cents)}¢ (${tenant})?`,
ask.pauseId,
);
return ChannelOutcome.awaitReply();
}
override withdraw(
withdrawal: ChannelWithdrawal,
): Promise {
const statusText =
withdrawal.resolution === PauseResolution.Withdrawn
? "Withdrawn (quorum reached or sibling branch failed)"
: "Cancelled by operator";
void statusText;
return Promise.resolve();
}
}
// 3. Tool naming the approval channel role
@Tool({
name: "refund_card",
description: "Refunds a card payment",
input: RefundCardInputDto,
output: RefundCardOutputDto,
channel: RefundApprovalChannel,
})
export class RefundCardTool
implements ToolHandler
{
run({ orderId }: RefundCardInputDto): Promise {
return Promise.resolve({ status: `refunded ${orderId}` });
}
}
```
src/channels/refund-reply.interpreter.ts
```ts
import {
DecidedBy,
ReplyInterpretation,
ReplyInterpreter,
type InputOf,
type OutputOf,
type ReplyInterpreterHandler,
} from "@agboss/core";
@ReplyInterpreter({
name: "refund-reply-interpreter",
description: "Parses finance manager text replies into a typed refund approval decision",
decidedBy: DecidedBy.Code,
})
export class RefundReplyInterpreter implements ReplyInterpreterHandler {
interpret({ reply }: InputOf): ReplyInterpretation> {
const normalized = reply.trim().toLowerCase();
if (normalized === "approve" || normalized === "lgtm") {
return ReplyInterpretation.understood({ approved: true });
}
if (normalized.startsWith("reject:")) {
return ReplyInterpretation.understood({
approved: false,
reason: reply.slice("reject:".length).trim(),
});
}
return ReplyInterpretation.notUnderstood({
reason: "Reply must be 'approve', 'lgtm', or 'reject: '.",
});
}
}
```
src/webhooks/refund-webhooks.server.ts
```ts
import {
ExecutionStatus,
NotPausedError,
UninterpretableReplyError,
UnknownPauseError,
type App,
} from "@agboss/core";
import { RefundWorkflowStart } from "../refunds.js";
// 1. Trigger a workflow from an incoming HTTP webhook (e.g. POST /webhooks/refund-request)
export async function handleRefundWebhook(
app: Pick,
body: { orderId: string; tenantId: string; userId: string },
) {
const result = await app.execute(
RefundWorkflowStart,
{ text: `Refund ${body.orderId}` },
{
threadId: `order:${body.orderId}`,
ownerId: body.userId,
tenant: { tenantId: body.tenantId },
},
);
if (result.status === ExecutionStatus.Paused) {
return {
httpStatus: 202,
state: "awaiting_approval",
threadId: result.threadId,
};
}
return {
httpStatus: 200,
state: "completed",
output: result.output,
};
}
// 2. Resume a paused workflow from a Slack button or reply webhook (POST /webhooks/slack-reply)
export async function handleSlackReplyWebhook(
app: Pick,
body: { pauseId: string; replyText: string; tenantId: string; slackUserId: string },
) {
try {
const resumed = await app.resume(
{ pauseId: body.pauseId, reply: body.replyText },
{ tenantId: body.tenantId, answeredBy: body.slackUserId },
);
return { httpStatus: 200, status: resumed.status, output: resumed.output };
} catch (err) {
if (err instanceof UninterpretableReplyError) {
// Pause remains Pending! Ask the reviewer in Slack to clarify:
return { httpStatus: 422, clarify: err.reason };
}
if (err instanceof NotPausedError) {
// Duplicate button click or webhook retry — idempotent 200 OK:
return { httpStatus: 200, status: "already_resolved" };
}
if (err instanceof UnknownPauseError) {
return { httpStatus: 404, status: "unknown_or_expired_pause" };
}
throw err;
}
}
```
test/refund-pause.test.ts
```ts
import { expect } from "vitest";
import { ExecutionStatus } from "@agboss/core";
import { callTool, replyWith, testWith } from "@agboss/core/testing";
import {
RefundAgent,
RefundApplication,
RefundCardTool,
RefundWorkflowFinish,
WorkflowFinishTextDto,
} from "../src/refunds.js";
import {
handleRefundWebhook,
handleSlackReplyWebhook,
} from "../src/webhooks/refund-webhooks.server.js";
const test = testWith(RefundApplication);
test("triggers workflow via webhook, pauses for Slack approval, and resumes via webhook reply", async ({
app,
mockLlm,
}) => {
mockLlm(RefundAgent).thenReturn(
callTool(RefundCardTool, { orderId: "ord_99", amountCents: 15_000 }),
replyWith("Refunded ord_99"),
);
const triggered = await handleRefundWebhook(app, {
orderId: "ord_99",
tenantId: "acme",
userId: "customer-1",
});
expect(triggered.httpStatus).toBe(202);
expect(triggered.state).toBe("awaiting_approval");
const missingPause = await handleSlackReplyWebhook(app, {
pauseId: "non-existent-pause",
replyText: "lgtm",
tenantId: "acme",
slackUserId: "manager-17",
});
expect(missingPause.httpStatus).toBe(404);
expect(ExecutionStatus.Paused).toBeDefined();
expect(RefundWorkflowFinish).toBeDefined();
});
```
***
## How to run & resume workflows via HTTP webhooks in production
[Section titled “How to run & resume workflows via HTTP webhooks in production”](#how-to-run--resume-workflows-via-http-webhooks-in-production)
When embedding AGBoss inside an HTTP server (Fastify, Express, Hono, Next.js route handlers, or NestJS), follow these five architectural rules:
1. **Boot `createApp` once at process startup — never per request:** Call `const app = await createApp(RefundApplication, { env: "prod" })` once when your HTTP server starts. `createApp` initializes your DI container (`onInit` -> `onStart`), opens the shared SQLite/Postgres connection pool, starts the 30-second orphan recovery sweep, and validates all `@ModelProvider` and environment contracts.
2. **Use deterministic `threadId` from the webhook’s domain entity:** When starting a workflow from a webhook (`app.execute(Start, dto, { threadId: "order:ord_99", ownerId, tenantId })`), derive `threadId` from your business entity ID (or webhook event ID) so multi-turn follow-ups and `app.cancel(threadId)` address the exact conversation thread.
3. **Return `202 Accepted` immediately when `result.status === ExecutionStatus.Paused`:** When a tool requires approval, `app.execute` does **not** hold open a Promise or block a Node.js thread. It checkpoints the graph, writes a `PendingPause` row, calls your `@Channel.send(ask)` (which delivers `ask.pauseId` to Slack/email/webhook), releases the execution lease, and resolves `app.execute` with `result.status === ExecutionStatus.Paused`.
4. **Resume with two-phase `app.resume()` on the callback webhook:** Your callback endpoint (`POST /webhooks/slack-actions`) can hit *any* replica in your cluster. Calling `app.resume({ pauseId, decision: { approved: true } }, { tenantId, answeredBy })` (or `{ pauseId, reply: freeText }`) atomically flips the pause `Pending → Resolved` in Phase 1 and acquires the 30s execution-tree lease in Phase 2.
5. **Handle webhook retries & ambiguous replies explicitly:**
* **`NotPausedError`:** External webhook providers (Slack, Stripe, GitHub) retry on network timeouts, and humans double-click buttons. Catch `NotPausedError` and return `200 OK` idempotently.
* **`UninterpretableReplyError`:** When a human sends a free-text reply that `@ReplyInterpreter` cannot parse (`ReplyInterpretation.notUnderstood({ reason })`), `app.resume` throws `UninterpretableReplyError` and **keeps the pause `Pending`** so your webhook can reply in the Slack thread with `err.reason` and wait for a clearer answer.
* **`UnknownPauseError`:** The `pauseId` does not exist, belongs to another tenant, or was pruned after `pauseRecordRetention` (`hours(24)` default); return `404 Not Found`.
***
## Capability security (`PauseAnswerer`) & audit trail (`result.record.pauses`)
[Section titled “Capability security (PauseAnswerer) & audit trail (result.record.pauses)”](#capability-security-pauseanswerer--audit-trail-resultrecordpauses)
* **`PauseAnswerer.Channel` (External Reviewer):** `result.pending` intentionally omits `pauseId` so the caller of `app.execute` (e.g., an end user in chat) cannot approve their own tool call. Only your `@Channel.send(ask)` receives `ask.pauseId`, and `app.resume` requires `{ answeredBy: "" }`.
* **`PauseAnswerer.ThreadOwner` (End-User Confirmation):** `result.pending` includes `pauseId` so the user’s client UI can render an inline Confirm/Cancel prompt, and `app.resume` verifies `{ ownerId }` matches the thread owner.
* **Automatic button cleanup (`ChannelHandler.withdraw`):** If a parallel branch pause becomes obsolete (`FailurePolicy.FailFast`, quorum `BranchVerdict.Surplus`, or `app.cancel`), AGBoss calls `withdraw(withdrawal)` on your `@Channel` with `PauseResolution.Withdrawn` or `PauseResolution.Cancelled` so your Slack/UI integration can disable stale buttons immediately.
# Error Handling & Rollback (.onError)
> Handle step failures, review-loop visit limits, quorum deadlines, and transactional rollbacks in AGBoss using .onError(), .onJoinError(), and LIFO sagas.
Production workflows interact with unreliable external systems, rate-limited APIs, and bounded review loops that can fail mid-flight. AGBoss makes failure recovery explicit in **HarnessDSL (`HDSL`)** `@Workflow({ flow })` chains via `.onError(ErrorClass, sidePath)` on individual steps and `.onJoinError(ErrorClass, sidePath)` on parallel joins, keeping compensation logic type-checked and unit-tested alongside the happy path.
## How `.onError()` chains compensation steps in a workflow path
[Section titled “How .onError() chains compensation steps in a workflow path”](#how-onerror-chains-compensation-steps-in-a-workflow-path)
Instead of hidden automatic rollbacks that guess how to undo a side effect, you attach `.onError(ErrorClass, sidePath)` directly after the `@Action`, `@Agent`, or `@Router` that can throw. The compensation side path has full Path Scope access to DTOs produced by earlier steps (such as a `StockReservationDto` needed to release reserved inventory):
src/order.workflow\.ts
```ts
import { Workflow, start, toAction } from "@agboss/core";
@Workflow({
name: "order-workflow",
version: "1.0.0",
flow: [
start(OrderWorkflowStart)
.returns(OrderRequestDto)
.toAction(ReserveStockAction)
.returns(StockReservationDto)
.toAction(ShipOrderAction)
.onError(
CarrierError,
toAction(RefundCardAction)
.onError(
PaymentProviderError,
toAction(NotifyFinanceTeamAction)
.toFinish(OrderFailedWorkflowFinish),
)
.toAction(ReleaseStockAction)
.toFinish(OrderFailedWorkflowFinish),
)
.returns(OrderShippedDto)
.toFinish(OrderWorkflowFinish),
],
})
export class OrderWorkflow {}
```
src/order.components.ts
```ts
import { Action, WorkflowFinish, WorkflowStart, type ActionHandler, type InputOf, type OutputOf } from "@agboss/core";
import { Text } from "@agboss/core/dto";
export class OrderRequestDto {
@Text({ prompt: "order id" })
orderId!: string;
}
export class StockReservationDto {
@Text({ prompt: "reservation id" })
reservationId!: string;
}
export class OrderShippedDto {
@Text({ prompt: "tracking number" })
trackingNumber!: string;
}
export class CarrierError extends Error {}
export class PaymentProviderError extends Error {}
@WorkflowStart({ name: "order-workflow-start", description: "Order request", input: OrderRequestDto })
export class OrderWorkflowStart {}
@WorkflowFinish({ name: "order-workflow-finish", description: "Order shipped", output: OrderShippedDto })
export class OrderWorkflowFinish {}
@WorkflowFinish({ name: "order-failed-workflow-finish", description: "Order failed", output: OrderRequestDto })
export class OrderFailedWorkflowFinish {}
@Action({
name: "reserve-stock-action",
description: "Reserves stock",
input: { order: OrderRequestDto },
output: StockReservationDto,
})
export class ReserveStockAction implements ActionHandler {
execute(input: InputOf): Promise> {
return Promise.resolve({ reservationId: `res-${input.order.value.orderId}` });
}
}
@Action({
name: "ship-order-action",
description: "Books carrier shipment",
input: { reservation: StockReservationDto },
output: OrderShippedDto,
})
export class ShipOrderAction implements ActionHandler {
execute(input: InputOf): Promise> {
return Promise.resolve({ trackingNumber: `trk-${input.reservation.value.reservationId}` });
}
}
@Action({
name: "refund-card-action",
description: "Refunds card charge",
input: { order: OrderRequestDto },
})
export class RefundCardAction implements ActionHandler {
execute(_input: InputOf): Promise {
return Promise.resolve();
}
}
@Action({
name: "release-stock-action",
description: "Releases reserved stock",
input: { reservation: StockReservationDto },
})
export class ReleaseStockAction implements ActionHandler {
execute(_input: InputOf): Promise {
return Promise.resolve();
}
}
@Action({
name: "notify-finance-team-action",
description: "Alerts finance when refund fails",
input: { order: OrderRequestDto },
})
export class NotifyFinanceTeamAction implements ActionHandler {
execute(_input: InputOf): Promise {
return Promise.resolve();
}
}
```
test/order.workflow\.test.ts
```ts
import { Application, EmptyEnvironment } from "@agboss/core";
import { expect } from "vitest";
import { testWith } from "@agboss/core/testing";
import { OrderWorkflow } from "./order.workflow.js";
import { OrderWorkflowFinish, OrderWorkflowStart } from "./order.components.js";
@Application({
name: "order-application",
version: "1.0.0",
environment: EmptyEnvironment,
workflows: [OrderWorkflow],
budgets: [OrderAppBudget, OrderTenantBudget, OrderExecBudget],
})
export class OrderApplication {}
const test = testWith(OrderApplication);
test("reserves stock and ships order on the happy path", async ({ app }) => {
const result = await app.execute(OrderWorkflowStart, { orderId: "ORD-42" });
expect(result.finish).toBe(OrderWorkflowFinish);
expect(result.output).toEqual({ trackingNumber: "trk-res-ORD-42" });
});
```
## What error classes can `.onError()` and `.onJoinError()` catch?
[Section titled “What error classes can .onError() and .onJoinError() catch?”](#what-error-classes-can-onerror-and-onjoinerror-catch)
| Error Class | Where It Attaches | When It Fires |
| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Custom `Error` subclass (e.g. `CarrierError`) | `.onError(CarrierError, ...)` after `@Action` / `@Router` | When the step throws `CarrierError` (or an `ActionFailedError` wrapping it in `cause`) |
| `ChildExecutionFailedError` | `.onError(ChildExecutionFailedError, ...)` after `.toWorkflow(Child)` | When a child workflow execution fails without reaching one of its `@WorkflowFinish` nodes |
| `VisitsLimitExceededError` | `.onError(VisitsLimitExceededError, ...)` | When an `@Agent` or `@Router` in a review loop exceeds its `maxVisits` ceiling |
| `BudgetExceededError` / `StepsLimitExceededError` / `DepthLimitExceededError` / `LimitExceededError` | `.onError(LimitExceededError, ...)` | When an execution or step limit is exceeded. **Note (`[graph.limit-handler-uses-model]`):** side paths catching limit errors may only run deterministic `@Action` and `DecidedBy.Code` `@Router` steps — invoking an `@Agent` or `DecidedBy.Model` `@Router` inside a limit handler is rejected at assembly time. |
| `StepFailedError` | `.onJoinError(StepFailedError, ...)` after `.join()` | When one or more parallel branches fail during `.toParallelAgents()` or `.toParallelWorkflows()` |
| `QuorumNotReachedError` | `.onJoinError(QuorumNotReachedError, ...)` after `.joinQuorum()` / `.joinQuorumGroups()` | When too many branches fail or the quorum `deadline` expires before the quorum is satisfied |
## How error matching and checkpoint recovery work
[Section titled “How error matching and checkpoint recovery work”](#how-error-matching-and-checkpoint-recovery-work)
* **Most-specific class matching:** A step may declare multiple `.onError(...)` handlers; the framework matches the thrown error (and its `.cause` chain) against the most specific subclass in the prototype hierarchy. Duplicate handlers for the same error class on one step fail assembly (`[flow.duplicate-error-handler]`).
* **Durable across checkpoints:** The failure record (`lastError` with error code, message, and failed node name) is persisted in workflow state so a side path that pauses for human intervention resumes cleanly.
## How do declarative transactional sagas (`compensateWith` & `committedBy`) and automatic LIFO unwinding work?
[Section titled “How do declarative transactional sagas (compensateWith & committedBy) and automatic LIFO unwinding work?”](#how-do-declarative-transactional-sagas-compensatewith--committedby-and-automatic-lifo-unwinding-work)
Manual `.onError(...)` chains only catch step failures along normal graph edges — they do not run when an execution or parallel branch is externally aborted by `app.cancel(threadId)`, sibling `FailurePolicy.FailFast`, quorum `BranchVerdict.Surplus`, or a limit error (`BudgetExceededError`, `StepsLimitExceededError`, `DepthLimitExceededError`).
AGBoss solves both static rollback completeness and runtime external aborts with declarative compensation contracts on `@Action`:
1. **Declarative `@Action` saga options:**
* `compensateWith: WithdrawOfferAction` declares the deterministic `@Action` that reverses this action’s external side effect. Its `input` must be satisfiable from the Path Scope immediately after the compensated action completes (verified at assembly; otherwise fails with `[action.invalid-compensator-input]`).
* `committedBy: ConfirmOfferAction` (optional) marks the commit boundary: once `ConfirmOfferAction` completes, the earlier action is finalized and will no longer be rolled back on subsequent errors or cancellations.
2. **Static saga verification in `agboss check` (`[flow.uncompensated-action]`):**
* Every error path (`.onError` / `.onJoinError`) after an uncommitted compensatable `@Action` that reaches a `@WorkflowFinish` before passing through `committedBy` must execute `compensateWith` on that path. If any error branch bypasses the compensator, `agboss check` fails assembly with `[flow.uncompensated-action]`.
* `agboss describe` and `agboss describe --json` include `compensateWith` and `committedBy` metadata on action nodes.
3. **Automatic runtime LIFO unwinding on external abort:**
* Whenever an `@Action` with `compensateWith` completes, the runtime checkpoints an uncommitted compensation entry `(stepId, action, outputDto, compensator)`.
* If the execution or branch is aborted before `committedBy` (via `app.cancel(threadId)`, `FailurePolicy.FailFast`, quorum `Surplus`, or `BudgetExceededError`), the runtime automatically executes all uncommitted compensators in **reverse completion order (LIFO)** using a live cleanup signal bounded by `@Application({ compensationTimeout: seconds(15) })`.
* If a worker pod crashes mid-rollback, the recovering instance resumes the remaining uncompensated entries idempotently using deterministic `step.idempotencyKey` values.
src/actions/register-offer.action.ts
```ts
import { Action, ExecutionContext, type ActionHandler, type InputOf, type OutputOf } from "@agboss/core";
import { WithdrawOfferAction } from "./withdraw-offer.action.js";
@Action({
name: "register-offer-action",
description: "Creates a provisional offer in the external ATS; rolled back by WithdrawOfferAction",
input: { draft: OfferDraftDto },
output: RegisteredOfferDto,
compensateWith: WithdrawOfferAction,
committedBy: ConfirmOfferAction,
deps: [ExecutionContext],
})
export class RegisterOfferAction implements ActionHandler {
constructor(private readonly executionContext: ExecutionContext) {}
async execute(input: InputOf): Promise> {
return {
offerId: `ats_${this.executionContext.step.idempotencyKey}`,
role: input.draft.value.role,
};
}
}
```
src/actions/withdraw-offer.action.ts
```ts
import { Action, ExecutionContext, type ActionHandler, type InputOf } from "@agboss/core";
@Action({
name: "withdraw-offer-action",
description: "Revokes the provisional ATS offer under a live compensationTimeout cleanup signal",
input: { offer: RegisteredOfferDto },
deps: [ExecutionContext],
})
export class WithdrawOfferAction implements ActionHandler {
constructor(private readonly executionContext: ExecutionContext) {}
async execute(input: InputOf): Promise {
void input.offer.value.offerId;
void this.executionContext.signal;
}
}
```
test/offer-saga.test.ts
```ts
import { expect, it } from "vitest";
import { testAction } from "@agboss/core/testing";
import {
OfferSagaStart,
RegisterOfferAction,
} from "../src/actions/register-offer.action.js";
import { WithdrawOfferAction } from "../src/actions/withdraw-offer.action.js";
it("executes RegisterOfferAction and compensates with WithdrawOfferAction", async () => {
const runRegister = testAction(RegisterOfferAction);
const { output } = await runRegister({
draft: {
value: { role: "Principal Distributed Systems Engineer" },
producer: OfferSagaStart,
},
});
expect(output.role).toBe("Principal Distributed Systems Engineer");
const runWithdraw = testAction(WithdrawOfferAction);
await runWithdraw({
offer: {
value: output,
producer: RegisterOfferAction,
},
});
});
```
***
## Standalone Executable Proof: [`04-travel-booking-saga`](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)
[Section titled “Standalone Executable Proof: 04-travel-booking-saga”](#standalone-executable-proof-04-travel-booking-saga)
Inspect **[`04-travel-booking-saga`](https://github.com/agbossjs/agboss-examples/tree/main/04-travel-booking-saga)** and **[`02-path-scope-proof`](https://github.com/agbossjs/agboss-examples/tree/main/02-path-scope-proof)** (`cases/04-uncompensated-action`) in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) for runnable proofs of reverse-order LIFO compensation across `payment-saga`, `budget-saga`, `quorum-saga`, and `hitl-hold-saga`, idempotent external ledgers (`step.idempotencyKey`), and crash-safe SQLite WAL recovery (see [7 Verified Examples](/examples/job-scout/)).
# Loop 2–4: Live Evals, pass^k, Nightly Polygon & Judge Calibration
> Run Loops 2–4 of the AGBoss Verification Funnel — Fail-Fast @EvalSuite cascades, pass@k vs pass^k, Cohen's kappa calibration, and RFC 8785 Tern profiling.
In the AGBoss [5-Loop Cost & Determinism Verification Funnel](/concepts/verification-funnel/), once [Loop 0 (`agboss check`) and Loop 1 (`npm test`)](/guides/testing-agents/) have verified your topology and TypeScript harness for `$0`, **Loops 2–4** evaluate live model trajectories, memory compaction survival, and production telemetry. AGBoss combines in-loop [`@Judge`](/reference/judge/) gates (`DecidedBy.Code` and `DecidedBy.Model`), declarative [`@EvalSuite`](/guides/evaluation-and-profiles/#principle-5-declarative-evalsuite-passk--passk-calibration--eval-ui) benchmarks (`@agboss/core/eval`), Fail-Fast `$0 → $$$` cascades, `pass@k` vs `pass^k` baseline gates (`flaky-on-baseline` quarantine), nightly `--metamorphic` and `--chaos` polygons, Cohen’s $\kappa$ calibration (`agboss eval calibrate`), and two production feedback loops (`agboss golden` & [Progressive Crystallization](/concepts/progressive-crystallization/)).
***
## Complete example: `@Judge` quality gates, `oneOf` narrowing, YAML profile & eval test
[Section titled “Complete example: @Judge quality gates, oneOf narrowing, YAML profile & eval test”](#complete-example-judge-quality-gates-oneof-narrowing-yaml-profile--eval-test)
src/judges/answer-grounded.judge.ts
```ts
import {
Agent,
Application,
DecidedBy,
Decision,
Judge,
JudgeModel,
JudgeVerdict,
Tool,
Workflow,
WorkflowFinish,
WorkflowStart,
start,
type JudgeHandler,
type JudgeInput,
type ToolHandler,
} from "@agboss/core";
import { Integer, Text, isVariant, oneOf } from "@agboss/core/dto";
export class OpeningsDto {
@Text({ prompt: "Verified job openings with [source] citations" })
openings!: string;
}
export class OfferDto {
@Integer({ prompt: "Annual base salary offered in USD" })
salary!: number;
}
export class RefusalDto {
@Text({ prompt: "Explicit reason why the candidate was not selected" })
why!: string;
}
@Tool({
name: "job-board-search-tool",
description: "Searches verified engineering job boards",
input: OpeningsDto,
output: OpeningsDto,
})
export class JobBoardSearchTool implements ToolHandler {
run(input: OpeningsDto): Promise {
return Promise.resolve({ openings: `Results for ${input.openings} [gh-104]` });
}
}
// 1. $0 deterministic code @Judge checking bracketed citation format before any LLM judge runs:
@Judge({
name: "citation-format-judge",
description: "Checks that the reply contains at least one bracketed [source] citation",
decidedBy: DecidedBy.Code,
reply: OpeningsDto,
})
export class CitationFormatJudge implements JudgeHandler {
judge(input: JudgeInput): Promise {
const hasCitation = /\[[a-z0-9-]+\]/i.test(input.reply.openings);
return Promise.resolve(
hasCitation
? JudgeVerdict.passed({ metrics: { hasCitation: true } })
: JudgeVerdict.failed({ reason: "Every listed opening must include a [source] tag." }),
);
}
}
// 2. Decision-model @Judge using calibrated Decision.noul + Decision.score:
@Judge({
name: "answer-grounded-judge",
description: "Verifies that every listed role is grounded and complete",
model: "typesafe/jev-1.13",
reply: OpeningsDto,
deps: [JudgeModel],
})
export class AnswerGroundedJudge implements JudgeHandler {
constructor(private readonly judgeModel: JudgeModel) {}
async judge(input: JudgeInput): Promise {
const answers = await this.judgeModel.decide({
state: { task: input.task, openings: input.reply.openings },
questions: {
grounded: Decision.noul("Are all openings grounded in tool outputs?"),
usefulness: Decision.score("Overall completeness", [
"useless",
"partial",
"complete",
]),
},
});
return answers.grounded.noul >= 0.8 && answers.usefulness.score >= 1
? JudgeVerdict.passed({
metrics: {
groundedProbability: answers.grounded.noul,
usefulness: answers.usefulness.score,
},
})
: JudgeVerdict.failed({
reason: "Cite a verified [source] for every job opening.",
});
}
}
// 3. Deterministic DTO @Judge narrowing a oneOf(OfferDto, RefusalDto) union via isVariant:
@Judge({
name: "fair-decision-judge",
description: "Checks that offers have positive salary and refusals explain why",
decidedBy: DecidedBy.Code,
reply: oneOf(OfferDto, RefusalDto),
})
export class FairDecisionJudge implements JudgeHandler {
judge(input: JudgeInput): Promise {
if (isVariant(input, OfferDto)) {
return Promise.resolve(
input.reply.salary > 0
? JudgeVerdict.passed({ metrics: { salary: input.reply.salary } })
: JudgeVerdict.failed({ reason: "An offer must specify salary > 0." }),
);
}
return Promise.resolve(
input.reply.why.trim().length > 0
? JudgeVerdict.passed()
: JudgeVerdict.failed({ reason: "A refusal must explain why." }),
);
}
}
@WorkflowStart({
name: "scout-eval-workflow-start",
description: "Starts a job search evaluation turn",
input: OpeningsDto,
})
export class ScoutEvalStart {}
@WorkflowFinish({
name: "scout-eval-workflow-finish",
description: "Returns verified job openings",
output: OpeningsDto,
})
export class ScoutEvalFinish {}
@Agent({
name: "scout-eval-agent",
description: "Finds matching roles gated by CitationFormatJudge and AnswerGroundedJudge",
prompt: "List matching roles and cite [source] for each role.",
model: "moonshotai/kimi-k2.6",
input: { query: OpeningsDto },
output: OpeningsDto,
tools: [JobBoardSearchTool],
judges: [CitationFormatJudge, AnswerGroundedJudge],
maxRetries: 2,
})
export class ScoutEvalAgent {}
@Workflow({
name: "job-scout-workflow",
version: "1.0.0",
flow: [
start(ScoutEvalStart)
.returns(OpeningsDto)
.toAgent(ScoutEvalAgent)
.returns(OpeningsDto)
.toFinish(ScoutEvalFinish),
],
})
export class JobScoutEvalWorkflow {}
@Application({
name: "job-scout-eval-application",
version: "1.0.0",
environment: EmptyEnvironment,
workflows: [JobScoutEvalWorkflow],
budgets: [EvalAppBudget, EvalTenantBudget, EvalExecBudget],
})
export class JobScoutEvalApplication {}
```
profiles/job-scout-workflow/scout-low-thinking.yaml
```yaml
# yaml-language-server: $schema=../../schema/profile.schema.json
profile: scout-low-thinking
version: 1.0.0-low-thinking
agents:
scout-eval-agent:
model: moonshotai/kimi-k2.6
maxToolCalls: 4
defaults:
models:
temperature: 0
router:
model: typesafe/jev-1.13
```
test/eval-and-judges.test.ts
```ts
import { expect } from "vitest";
import { decideWith, replyWith, testJudge, testWith } from "@agboss/core/testing";
import {
AnswerGroundedJudge,
FairDecisionJudge,
JobScoutEvalApplication,
OfferDto,
OpeningsDto,
RefusalDto,
ScoutEvalAgent,
ScoutEvalStart,
} from "../src/judges/answer-grounded.judge.js";
const test = testWith(JobScoutEvalApplication);
test("retries ScoutEvalAgent when AnswerGroundedJudge rejects attempt 1, and unit-tests FairDecisionJudge", async ({
app,
mockLlm,
}) => {
// 1. Unit-test the oneOf union judge with isVariant narrowing:
const runFairJudge = testJudge(FairDecisionJudge);
const offerVerdict = await runFairJudge({
variant: OfferDto,
reply: { salary: 195_000 },
task: "Make an offer",
attempt: 0,
});
expect(offerVerdict.output.passed).toBe(true);
const emptyRefusal = await runFairJudge({
variant: RefusalDto,
reply: { why: " " },
task: "Decline candidate",
attempt: 0,
});
expect(emptyRefusal.output.passed).toBe(false);
// 2. Full workflow test: judge rejects first answer, agent retries and passes on attempt 2:
mockLlm(ScoutEvalAgent).thenReturn(
replyWith(OpeningsDto, { openings: "Uncited role" }),
replyWith(OpeningsDto, { openings: "Staff TS Engineer [gh-104]" }),
);
mockLlm(AnswerGroundedJudge).thenReturn(
decideWith({ grounded: 0.2, usefulness: 0 }),
decideWith({ grounded: 0.95, usefulness: 2 }),
);
const result = await app.execute(ScoutEvalStart, {
openings: "Find Staff TS roles",
});
expect(result.output).toEqual({ openings: "Staff TS Engineer [gh-104]" });
expect(mockLlm(ScoutEvalAgent)).toHaveBeenCalledTimes(2);
});
```
***
## Principle 1: How do Loops 2–4 fit into the 5-Loop Verification Funnel?
[Section titled “Principle 1: How do Loops 2–4 fit into the 5-Loop Verification Funnel?”](#principle-1-how-do-loops-24-fit-into-the-5-loop-verification-funnel)
A foundational rule of AGBoss is: **never verify with a paid language model what can be proven by the compiler or checked by deterministic TypeScript; and when you invoke a model, check `$0` trajectory invariants and code judges first before calling a calibrated LLM judge.** See the full [5-Loop Cost & Determinism Verification Funnel](/concepts/verification-funnel/) architecture for all four objects of verification and both production feedback loops:
| Loop | Command / Runner | Latency & Cost | What It Proves |
| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[Loop 0: Compiler](/concepts/verification-funnel/#loop-0-static-proof-agboss-check---federation--0--1-second)** | `$ agboss check --federation` | `< 1s` · **`$0`** (every save) | **Object 1 (Topology & Mesh Contracts):** Path Scope dominance, `{ about }` staleness, LIFO saga compensations, and Agent Mesh federation schemas |
| **[Loop 1: Deterministic Harness](/guides/testing-agents/)** | `npm test` (`testWith(App)`, 10 unit runners, `VirtualClock`, VCR) | `~3s` · **`$0`** (every commit; locked by `[vcr.stale-cassette]`) | **Object 2 (Harness Code):** `@Action`, `DecidedBy.Code` `@Router`/`@Arbiter`, `@Tool`, `@ToolInterceptor`, `@PiiPolicy`, retries, `ModelFailure` chaos, and `{ instances: 2 }` pod leases |
| **Loop 2: PR Smoke Gate** | `$ agboss eval run --runs 5 --baseline` | `~1 min` · **`$`** (on PR when `promptHash` / `schemaHash` changes) | **Object 3 (Model & Trajectory):** Fail-Fast cascade (`$0` `requiredTools`/`forbiddenTools`/`toolInvariant` $\to$ `$0` code `@Judge` $\to$ LLM `@Judge`), `pass^5` vs cached `--baseline` (`flaky-on-baseline` quarantine), and `eval-report.html` |
| **Loop 3: Nightly Polygon** | `$ agboss eval run --runs 20 --metamorphic --chaos` | `~15 min` · **`$$`** (nightly on `main` / pre-release) | **Objects 3 & 4 (Trajectory & Memory):** Full 30–50+ task benchmark with pinned `vendorRouting`, `--metamorphic` paraphrase invariance, multi-turn `@Compaction` survival, stale-memory override, `--chaos`, and provider drift detection |
| **Loop 4: Production & Feedback Loops A/B** | `ExecutionRecord` $\to$ `$ agboss eval ui` / `$ agboss eval calibrate` / `agboss golden` | `24/7` · Continuous | **Loop A:** Failure taxonomy tagging, Cohen’s $\kappa \ge 0.80$ judge calibration, and 1-click promotion to `golden/*.yaml`. **Loop B:** [Progressive Crystallization](/concepts/progressive-crystallization/) from paid Loop 2 back into `$0` Loop 1 |
***
## Principle 2: What every `Tern` stores & RFC 8785 `configHash`
[Section titled “Principle 2: What every Tern stores & RFC 8785 configHash”](#principle-2-what-every-tern-stores--rfc-8785-confighash)
In AGBoss, one question-to-answer execution round is called a **`Tern`**, and a `threadId` is a client’s sequence of `Tern`s. Every execution — whether `answered`, `guarded`, `failed`, or `paused` — writes a `Tern` into `ExecutionRecordRepository` containing:
* `task`, `answer`, `status`, `stopReason`, `route`, `steps` (what each agent/router answered), and `cost`.
* **`promptVersion` & `modelVersion`:** Short SHA-256 fingerprints of all agent/router prompts and resolved model settings. Editing a `*.prompt.md` file automatically produces a new `promptVersion`.
* **`configHash` (RFC 8785 Canonical JSON):** A 64-character SHA-256 fingerprint (first 8 hex characters shown in `agboss describe`) computed over RFC 8785 canonical JSON (UTF-16 code-unit key order, locale-independent) covering:
* **`elements`:** Each agent and model router’s active `@PiiPolicy` rules, `uncompactedTurns`, `@Compaction` strategy, and attached `@Judge`s.
* **`frame`:** The framework’s own system framing for routers, tool loops, judge retry prompts, and RAG citations.
* **`children`:** Every reachable child `@Workflow` recursively.
* If you modify a workflow’s prompt or flow without bumping `@Workflow({ version })`, AGBoss logs a drift warning at startup: `warning: config "job-scout" 1.0.0 changed without a version bump (hash 3f2a…)`.
***
## Principle 3: Scoring, calibrating & replaying with `agboss eval` and `agboss replay`
[Section titled “Principle 3: Scoring, calibrating & replaying with agboss eval and agboss replay”](#principle-3-scoring-calibrating--replaying-with-agboss-eval-and-agboss-replay)
1. **Calibrate your judge first:** Before trusting aggregate eval numbers, inspect and hand-label 20–30 recorded `Tern`s to verify that your decision judge agrees with human domain judgment.
2. **Score unscored `Tern`s (`agboss eval score`):**
```bash
npx agboss eval score --version 1.0.0 --limit 100
```
Scores every unscored answered `Tern` (`P(adequate)`) and prints a summary table per `promptVersion` (total `Tern`s, scored count, mean score, and USD cost).
3. **Replay historical tasks against a prompt edit (`agboss replay`):**
```bash
npx agboss replay --version --limit 100
```
Re-runs the stored tasks from `` against your **current** code and prompts as new threads (`replay_of` set), scores both sides, and prints the `before → after` delta on the exact same tasks. Both `eval` and `replay` run strictly inside your `@Application({ budgets })` and stop cleanly if a budget cap is reached.
***
## Principle 4: Golden sets & A/B profile comparisons (`agboss compare` & `compareProfiles`)
[Section titled “Principle 4: Golden sets & A/B profile comparisons (agboss compare & compareProfiles)”](#principle-4-golden-sets--ab-profile-comparisons-agboss-compare--compareprofiles)
A profile YAML file (`profiles//.yaml`) overrides `defaults`, `compactions.`, `agents.*`, or `prompts` without touching TypeScript code.
### From the CLI (`agboss golden` & `agboss compare`)
[Section titled “From the CLI (agboss golden & agboss compare)”](#from-the-cli-agboss-golden--agboss-compare)
```bash
# 1. Save the last 20 real tasks into a versioned golden set (golden//core.yaml):
npx agboss golden add --name core --from-last 20
# 2. Run side-by-side evaluation of base vs candidate YAML profile:
npx agboss compare --profiles base,scout-low-thinking --golden core
```
`agboss compare` runs each golden task as a fresh first-contact turn on every listed profile, computes **mean adequacy score**, runs **randomized-order pairwise A/B judging (`wins/losses/ties` vs `base`)**, and reports total cost, cost per task, `p50`/`p95` latency, and a diff of the exact settings that changed:
```text
profile | version | mean | vs base (w/l/t) | cost | per task | p50 s | p95 s | failed
base | 1.0.0 | 0.50 | baseline | $0.0141 | $0.0071 | 30.6 | 35.8 | 0
scout-low-thinking | 1.0.0-low-thinking | 0.62 | 1/0/1 | $0.0115 | $0.0058 | 21.7 | 33.5 | 0
```
### From TypeScript code (`compareProfiles`)
[Section titled “From TypeScript code (compareProfiles)”](#from-typescript-code-compareprofiles)
You can also invoke `compareProfiles` programmatically inside custom benchmark scripts or Vitest evaluation suites:
src/evals/compare-profiles.ts
```ts
import { compareProfiles } from "@agboss/core";
export async function runComparison() {
const comparison = await compareProfiles(JobScout, {
start: ChatWorkflowStart,
profiles: ["base", "scout-low-thinking"],
tasks: ["Find senior backend jobs in Europe", "Save the first two jobs"],
});
for (const profile of comparison.profiles) {
const pairwise = profile.pairwise
? `${String(profile.pairwise.wins)}/${String(profile.pairwise.losses)}/${String(profile.pairwise.ties)}`
: "—";
console.log(
`${profile.name}: score ${String(profile.meanScore ?? "—")}, w/l/t ${pairwise}, $${profile.costPerTaskUsd.toFixed(4)}/task`,
);
}
}
```
***
## Principle 5: Declarative `@EvalSuite`, `pass@k` / `pass^k`, Calibration & Eval UI
[Section titled “Principle 5: Declarative @EvalSuite, pass@k / pass^k, Calibration & Eval UI”](#principle-5-declarative-evalsuite-passk--passk-calibration--eval-ui)
In addition to scoring recorded `Tern`s, `@agboss/core/eval` lets you declare typed, multi-turn evaluation suites (`@EvalSuite`) with trajectory expectations (`checkTrajectory()`, `toolCall`, `toolInvariant`) and Fail-Fast cascading judges (`EvalCascade.StopAtFirstFailure`, running `$0` `DecidedBy.Code` judges before `DecidedBy.Model` judges):
src/evals/job-scout.eval-suite.ts
```ts
import {
EvalCascade,
EvalSuite,
checkTrajectory,
evalCase,
judgedBy,
pastTurn,
toolCall,
toolInvariant,
turn,
} from "@agboss/core/eval";
import {
AnswerGroundedJudge,
CitationFormatJudge,
JobBoardSearchTool,
ScoutEvalFinish,
ScoutEvalStart,
} from "../judges/answer-grounded.judge.js";
@EvalSuite({
name: "job-scout-eval-suite",
description: "Multi-turn grounding and trajectory benchmark for JobScoutEvalWorkflow",
start: ScoutEvalStart,
checks: [
checkTrajectory(), // 1. $0 deterministic trajectory invariants
judgedBy(CitationFormatJudge), // 2. $0 DecidedBy.Code judge
judgedBy(AnswerGroundedJudge), // 3. Paid DecidedBy.Model judge (runs only if 1 & 2 pass)
],
cascade: EvalCascade.StopAtFirstFailure,
cases: [
evalCase({
name: "berlin-staff-ts-roles",
history: [
pastTurn(
ScoutEvalStart,
{ openings: "Focus on remote-friendly EU hubs" },
ScoutEvalFinish,
{ openings: "Filtering to EU remote hubs [eu-hub]" },
),
],
turns: [turn({ openings: "Find Staff TypeScript roles in Berlin" })],
variants: [
turn({ openings: "List open Staff-level TypeScript engineering jobs based in Berlin" }),
turn({ openings: "Which Berlin companies are hiring Staff TS engineers?" }),
],
tools: {
required: [toolCall(JobBoardSearchTool, { openings: "Berlin" })],
invariants: [toolInvariant(JobBoardSearchTool, (input) => input.openings.trim().length > 0)],
},
}),
],
})
export class JobScoutEvalSuite {}
```
### Multi-Run Reliability (`pass@k` vs `pass^k`), Regression Gates & Calibration
[Section titled “Multi-Run Reliability (pass@k vs pass^k), Regression Gates & Calibration”](#multi-run-reliability-passk-vs-passk-regression-gates--calibration)
```bash
# 1. Run an @EvalSuite across k=5 trials per case and export JSON + static HTML report:
npx agboss eval run --suite job-scout-eval-suite --runs 5 --report .artifacts/eval/pr.json --html .artifacts/eval/report.html
# 2. Gate CI against the main branch baseline (exit code 5 on regression; flaky-on-baseline cases are quarantined):
npx agboss eval run --suite job-scout-eval-suite --runs 5 --baseline .artifacts/eval/main.json --fail-on-regression
# 3. Re-render a saved EvalRunReport artifact into a standalone HTML page without calling any models:
npx agboss eval show .artifacts/eval/pr.json --html .artifacts/eval/report.html
# 4. Verify judge agreement with human labels (pairwise Cohen's kappa & confusion matrix):
npx agboss eval calibrate --report .artifacts/eval/pr.json --labels eval/labels/job-scout-eval-suite.jsonl
# 5. Run chaos fault injection (429s, timeouts, truncated tool JSON) and metamorphic paraphrase checks:
npx agboss eval run --suite job-scout-eval-suite --chaos 0.3 --chaos-seed 42 --metamorphic
# 6. Promote a stored production ExecutionRecord into a typed @EvalSuite case (Loop 5):
npx agboss generate eval-case refund-edge-eval-case --suite job-scout-eval-suite --from-execution exec-9102
# 7. Export stored ExecutionRecords to JSONL for offline analysis or AGBoss Mesh Hub:
npx agboss records export --all-tenants --out .artifacts/records.jsonl
# 8. Open the local interactive Eval UI workbench (trajectory inspector, failure taxonomy tagging & live kappa labeling):
npx agboss eval ui --dir .artifacts/eval
```
| Capability | How It Works |
| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`pass@k` vs `pass^k` (`--runs `)** | `pass@k` measures whether a case passed **at least once** in $k$ trials (capability); `pass^k` measures whether it passed **all $k$ of $k$ trials** (production reliability). High `pass@k` (`1.00`) paired with low `pass^k` (`< 0.80`) marks the case `unstable` and flags the step as a **Progressive Crystallization candidate**. |
| **Baseline Regression Gate (`--baseline --fail-on-regression`)** | Compares every case against a baseline report JSON (`exit code 5` on new failures). Any case whose baseline had `0 < pass^k < 1` is marked `flaky-on-baseline` (`CaseComparison.FlakyOnBaseline`) and quarantined so baseline noise never blocks unrelated pull requests. |
| **Judge Calibration (`eval calibrate` & Cohen’s $\kappa$)** | Computes pairwise **Cohen’s kappa ($\kappa$)**, a confusion matrix (`TP`, `FP`, `TN`, `FN`), disagreements, and per-metric rubrics between each `@Judge` and human `EvalLabel` JSONL annotations (`eval/labels/.jsonl`). Also warns with `[judge.same-model-family]` when a model judge shares the evaluated agent’s model family. |
| **Chaos (`--chaos --chaos-seed `) & Metamorphic (`--metamorphic`)** | `--chaos` injects transient provider faults (`429`, timeouts, truncated answers) below `ProviderFetch` to verify retry and `.onError()` resilience; `--metamorphic` runs case `variants` and verifies that the tool-call trajectory (`metamorphic` check) remains invariant across prompt rewordings. |
| **Production Flywheel (`generate eval-case --from-execution` & `records export`)** | Reads an `ExecutionRecord` from `ExecutionHistoryRepository` (`SqliteExecutionHistoryRepository`), turns its conversation history, input, and required tool calls into a typed `evalCase(...)` in your `@EvalSuite`, and exports JSONL records (`agboss records export`) for offline auditing and `agboss hub`. |
| **HTML Report (`eval show --html`) & Interactive Workbench (`eval ui`)** | Generates a self-contained single-file HTML report for CI artifacts (`eval run --html` or `eval show --html`), or launches a local web workbench (`127.0.0.1`) to inspect multi-turn trajectories, tag failure modes, label runs with live $\kappa$ recalculation, and promote production traces into `@EvalSuite` cases. |
***
## Standalone Executable Proof: [`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk)
[Section titled “Standalone Executable Proof: 03-support-refund-desk”](#standalone-executable-proof-03-support-refund-desk)
Inspect **[`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk)** in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) for a complete runnable `@EvalSuite` (`src/eval-suites/refund-desk.eval-suite.ts`), compact git-diff-friendly baseline (`eval/baseline/main-baseline.json`), human label calibration (`eval/human-labels/refund-desk-eval-suite.jsonl`), and PII-safe `generate eval-case` promotion (see [7 Verified Examples](/examples/job-scout/)).
# FinOps, Budgets & Model Providers
> Control LLM costs, rate limits, tenant fairness, and circuit breakers with @Budget, @ModelProvider, RateLimitPolicy, and SpendRecorder in AGBoss.
Running multi-agent workflows in production requires strict guardrails against runaway spend, vendor rate-limit spikes, and noisy-neighbor tenants. AGBoss builds FinOps and traffic management directly into the `@Application` container via `@ModelProvider` (`@agboss/core/models`), mandatory `@Budget` components across three scopes, `SpendRecorder` for paid non-LLM APIs, and `CostLedgerRepository`.
## How `@ModelProvider` pools manage rate limits and tenant fairness
[Section titled “How @ModelProvider pools manage rate limits and tenant fairness”](#how-modelprovider-pools-manage-rate-limits-and-tenant-fairness)
Every model in an application resolves to a `@ModelProvider` listed in `@Application({ providers })` by matching its `serves` list (checked by `npx agboss check --models`). A provider’s `name` must end with `-model-provider`, and its `rateLimitPolicy` and `circuitBreakerPolicy` are shared across all executions, tenants, and instances via `RateLimitRepository`.
Out of the box, AGBoss registers two built-in catch-all providers — `OpenRouterModelProvider` (`serves: [/.*/]`) and `DecisionsModelProvider` (`serves: [/^typesafe\/jev-/]`). When you list a custom `@ModelProvider` with its own `serves` (for example `serves: [/^anthropic\//]` or `[/^bedrock\//]`) in `@Application({ providers })`, the built-in catch-all providers **automatically yield** any model matched by your custom provider (`#454`) — no subclassing of `OpenRouterModelProvider` required. To replace the default OpenRouter catch-all itself, subclass `extends OpenRouterModelProvider`:
src/model-providers/openrouter.model-provider.ts
```ts
import { ModelCost, ModelProvider, OpenRouterProvider, OPENROUTER_BASE_URL, PromptCaching, QueueBalancingStrategy, RateLimitPolicy, Reasoning, RetryPolicy, TRANSIENT_FAILURES, type OpenRouterFields } from "@agboss/core/models";
import { minutes, seconds } from "@agboss/core/units";
@ModelProvider({
name: "openrouter-model-provider",
description: "OpenRouter — shared chat model pool with tenant fairness",
serves: [/.*/],
baseUrl: OPENROUTER_BASE_URL,
baseUrlField: "openRouterBaseUrl",
apiKey: "openRouterApiKey",
timeout: minutes(2),
cost: ModelCost.fromResponse(),
reasoning: Reasoning.modelDecides(),
promptCaching: PromptCaching.off(),
retryPolicy: RetryPolicy.exponential({
maxAttempts: 3,
initialDelay: seconds(1),
maxDelay: seconds(20),
jitter: true,
retryOn: TRANSIENT_FAILURES,
}),
rateLimitPolicy: RateLimitPolicy.of({
maxConcurrentCalls: 8,
perMinute: { calls: 60 },
balancing: QueueBalancingStrategy.TenantRoundRobin,
}),
circuitBreakerPolicy: {
failureThreshold: 5,
window: minutes(1),
openFor: seconds(30),
},
})
export class OpenRouterModelProvider extends OpenRouterProvider {
override readonly requestFields: OpenRouterFields = {
provider: { ignore: ["Inceptron"] },
};
}
```
src/environments/app.environment.ts
```ts
import { secret, type Secret } from "@agboss/core";
export class AppEnvironment {
openRouterApiKey!: Secret;
sqlitePath!: string;
}
export const environment: AppEnvironment = {
openRouterApiKey: secret("OPENROUTER_API_KEY"),
sqlitePath: ".agboss/state.sqlite",
};
```
test/openrouter.model-provider.test.ts
```ts
import { expect, it } from "vitest";
import { OpenRouterModelProvider } from "../src/model-providers/openrouter.model-provider.js";
it("exports OpenRouterModelProvider with tenant round-robin queueing", () => {
expect(OpenRouterModelProvider).toBeDefined();
});
```
## How `@Budget` scopes and `SpendRecorder` enforce spend limits
[Section titled “How @Budget scopes and SpendRecorder enforce spend limits”](#how-budget-scopes-and-spendrecorder-enforce-spend-limits)
Every `@Application` requires `@Budget` coverage across three scopes (`BudgetScope.Application`, `BudgetScope.Tenant`, and `BudgetScope.Execution`). Budgets implement `BudgetHandler` (`maxSpend(): Usd`) and can inject `ExecutionContext` or a custom `TenantContext` subclass to read per-tenant limits from server-side data:
src/budgets/support.budgets.ts
```ts
import { Budget, BudgetPeriod, BudgetScope, SpendRecorder, Tool, type BudgetHandler, type ToolHandler } from "@agboss/core";
import { usd, type Usd } from "@agboss/core/units";
@Budget({
name: "app-daily-budget",
scope: BudgetScope.Application,
period: BudgetPeriod.Day,
})
export class AppDailyBudget implements BudgetHandler {
maxSpend(): Usd {
return usd(200);
}
}
@Budget({
name: "tenant-daily-budget",
scope: BudgetScope.Tenant,
period: BudgetPeriod.Day,
deps: [ShopTenantContext],
})
export class TenantDailyBudget implements BudgetHandler {
constructor(private readonly shopTenantContext: ShopTenantContext) {}
maxSpend(): Usd {
return usd(this.shopTenantContext.dailyBudgetUsd);
}
}
@Budget({
name: "execution-budget",
scope: BudgetScope.Execution,
deps: [ExecutionContext],
})
export class ExecutionBudget implements BudgetHandler {
constructor(private readonly executionContext: ExecutionContext) {}
maxSpend(): Usd {
return this.executionContext.tenant.weight > 1 ? usd(0.5) : usd(0.1);
}
}
@Tool({
name: "web_search",
description: "Searches the web (a paid API)",
input: SearchQueryDto,
output: SearchResultsDto,
deps: [SpendRecorder],
})
export class WebSearchTool
implements ToolHandler
{
constructor(private readonly spendRecorder: SpendRecorder) {}
run({ query }: SearchQueryDto): Promise {
this.spendRecorder.record({ service: "serp-api", cost: usd(0.002) });
return Promise.resolve({ text: `results for ${query}` });
}
}
```
src/support.application.ts
```ts
import { Application, SqliteCostLedgerRepository } from "@agboss/core";
import { AppEnvironment } from "./environments/app.environment.js";
import { AppDailyBudget, ExecutionBudget, TenantDailyBudget } from "./budgets/support.budgets.js";
@Application({
name: "support-application",
version: "1.0.0",
environment: AppEnvironment,
workflows: [SupportWorkflow],
providers: [SqliteCostLedgerRepository],
budgets: [AppDailyBudget, TenantDailyBudget, ExecutionBudget],
})
export class SupportApplication {}
```
test/support-finops.test.ts
```ts
import { expect, it } from "vitest";
import { testTool } from "@agboss/core/testing";
import { usd } from "@agboss/core/units";
import { AppDailyBudget, WebSearchTool } from "../src/budgets/support.budgets.js";
it("records external API spend via SpendRecorder and enforces daily budget ceiling", async () => {
const budget = new AppDailyBudget();
expect(budget.maxSpend()).toBe(usd(200));
const runSearch = testTool(WebSearchTool);
const { output, spend } = await runSearch({ query: "AGBoss FinOps" });
expect(output).toEqual({ text: "results for AGBoss FinOps" });
expect(spend).toEqual([{ service: "serp-api", cost: usd(0.002) }]);
});
```
* **Pre-flight and step checks:** Reported model usage from [OpenRouter response pricing](https://openrouter.ai/docs) and manual `SpendRecorder` entries are checked against every budget scope; if any budget is exhausted before the first step, the execution is rejected immediately (`BudgetExceededError`, firing `onExecutionRejected`) without creating a thread or calling a model.
* **Fair tenant queuing (`QueueBalancingStrategy`) & shared 429 cooldowns:** Waiting model calls share free slots via `TenantRoundRobin` (default), `TenantLeastInFlight`, `TenantWeighted` (using `tenant.weight`), or `Fifo`. When an upstream vendor returns HTTP `429` (`Retry-After`), `RateLimitRepository` propagates the cooldown window across all worker instances so sibling pods do not stampede the rate-limited endpoint.
* **OpenRouter host & quantization pinning:** When extending `OpenRouterProvider`, you can pin upstream inference hosts (`only: ["Fireworks"]`, `ignore: ["Inceptron"]`), floating-point quantizations (`quantizations: ["fp8", "bf16"]`), and `allowFallbacks: false` so open-weights models never degrade to lower-precision hosts between evaluation and production.
* **Custom SDK transport (`ModelProviderHandler`):** For AWS Bedrock (`SigV4`), Google Vertex AI, or internal mTLS gateways, omit `baseUrl` / `apiKey` on `@ModelProvider` (`[model-provider.both-modes]` if mixed) and implement `ModelProviderHandler` (`complete(request: ModelProviderRequest): Promise`) with constructor DI (`deps: [...]`). Every gateway policy (`@Budget` pre-check & ledger, `RateLimitRepository`, 429 AIMD, retries, circuit breaker, PII masking, answer tools, and `mockLlm`) wraps `complete(request)` identically.
## How Prompt Prefix Cache Invariance (`[prompt.input-in-instructions]`) cuts input token spend
[Section titled “How Prompt Prefix Cache Invariance (\[prompt.input-in-instructions\]) cuts input token spend”](#how-prompt-prefix-cache-invariance-promptinput-in-instructions-cuts-input-token-spend)
Provider KV prefix caches (Anthropic, OpenAI, Moonshot, DeepSeek) only hit when the leading bytes of a request — system instructions plus tool and output JSON schemas — remain **byte-for-byte identical** across calls:
1. **Compile-time ban on dynamic instructions (`[prompt.input-in-instructions]`):** `agboss check` rejects any `@Agent`, `@Router`, `@Arbiter`, or `@Judge` prompt that interpolates runtime `input` fields into `prompt` / `promptUrls` (only static `@Application` / `@Workflow` `promptVariables` are allowed in instructions).
2. **Deterministic schema serialization:** AGBoss serializes `@Tool` definitions and `@Dto` JSON schemas in canonical key order and passes dynamic Path Scope inputs exclusively in the trailing user message (`PromptCaching.auto()` or `PromptCaching.explicit()`).
3. **Deterministic HMAC PII tokens:** When `@PiiPolicy` uses `PiiAction.replaceWithToken()`, identical PII values produce identical tokens across turns, preventing random redaction IDs from invalidating cached conversation prefixes.
# MCP Servers, RAG & Sub-Workflows
> Connect AGBoss agents to Model Context Protocol (@McpServer, @McpTool, @McpExpose), knowledge bases (@Rag), A2A agents, and built-in AI coding skills.
Modern agent architectures rarely operate in isolation: they query internal knowledge bases via retrieval-augmented generation (`@agboss/core/rag`), consume or expose tools over the Model Context Protocol (`@agboss/core/mcp`), and delegate subtasks to child workflows or A2A peers. AGBoss models all of these integrations as typed, testable TypeScript components.
## How to consume and expose MCP tools (`@agboss/core/mcp`)
[Section titled “How to consume and expose MCP tools (@agboss/core/mcp)”](#how-to-consume-and-expose-mcp-tools-agbosscoremcp)
`@agboss/core/mcp` works in both directions: agents can call external MCP servers via `@McpServer` and `@McpTool`, and you can expose your own `@Tool` and `@WorkflowStart` components as an MCP server over stdio or HTTP using `@McpExpose` and `createMcpService`:
src/mcp/job-scout.expose.ts
```ts
import { createMcpService, McpExpose } from "@agboss/core/mcp";
@McpExpose({
name: "job-scout-mcp",
version: "1.0.0",
exports: [SearchIssues, ChatWorkflowStart],
})
export class JobScoutMcp {}
export async function serveStdio() {
const app = await createApp(JobScoutApplication);
const service = createMcpService(app, JobScoutMcp);
await service.connectStdio();
}
```
src/mcp/read-file.mcp.ts
```ts
import { type ToolHandler } from "@agboss/core";
import { McpServer, McpServerClient, McpTool, type ServerTools } from "@agboss/core/mcp";
const fsTools = {
read_file: { input: ReadFileInputDto, output: ReadFileOutputDto },
} as const satisfies ServerTools;
@McpServer({
name: "filesystem-mcp-server",
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "./workspace"],
tools: fsTools,
})
export class FilesystemMcpServer extends McpServerClient {}
@McpTool({
name: "read_file",
description: "Reads a file from the workspace via MCP",
server: FilesystemMcpServer,
input: ReadFileInputDto,
output: ReadFileOutputDto,
deps: [FilesystemMcpServer],
})
export class ReadFileMcpTool implements ToolHandler {
constructor(private readonly fs: FilesystemMcpServer) {}
run(input: ReadFileInputDto): Promise {
return this.fs.call("read_file", input);
}
}
```
## How `@Rag` knowledge bases work (`@agboss/core/rag`)
[Section titled “How @Rag knowledge bases work (@agboss/core/rag)”](#how-rag-knowledge-bases-work-agbosscorerag)
In AGBoss, a knowledge base is any `@Rag` class implementing `RagConnector` (`retrieve(query, { topK, signal })` and optional `index()`), or extending the built-in `SqliteFtsConnector`. The `topK` setting is required on every `@Rag` decorator, and reported retrieval spend (`costUsd`) is tracked automatically under `rag:`:
src/rag/company-docs.rag.ts
```ts
import { Agent, RagMode, env } from "@agboss/core";
import { Rag, SqliteFtsConnector } from "@agboss/core/rag";
export class AppEnvironment {
handbookDbPath = env.optional("HANDBOOK_DB_PATH");
}
@Rag({
name: "company_docs",
description: "Product, onboarding, and engineering runbook documentation",
topK: 4,
deps: [AppEnvironment],
})
export class CompanyDocs extends SqliteFtsConnector {
constructor(environment: AppEnvironment) {
super({
folder: "./docs/handbook",
dbFile: environment.handbookDbPath ?? ":memory:",
});
}
}
@Agent({
name: "onboarding-agent",
description: "Answers questions from the handbook with citations",
prompt: "Answer from company_docs and cite [source].",
model: "moonshotai/kimi-k2.6",
rag: [{ use: CompanyDocs, mode: RagMode.Tool }],
})
export class OnboardingAgent {}
```
test/company-docs.rag.test.ts
```ts
import { expect, it } from "vitest";
import { testRag } from "@agboss/core/testing";
import { AppEnvironment, CompanyDocs } from "../src/rag/company-docs.rag.js";
it("retrieves BM25 chunks with [source] IDs via testRag", async () => {
const runRag = testRag(CompanyDocs);
const { output, spend } = await runRag("refund timeframe");
expect(output).toBeDefined();
expect(spend.totalUsd).toBe(0);
});
```
## How child workflows (`.toWorkflow()`), `ThisWorkflow` recursion, and A2A (`@agboss/core/a2a`) work
[Section titled “How child workflows (.toWorkflow()), ThisWorkflow recursion, and A2A (@agboss/core/a2a) work”](#how-child-workflows-toworkflow-thisworkflow-recursion-and-a2a-agbosscorea2a-work)
When a workflow grows beyond a single responsibility or crosses service boundaries, compose child `@Workflow` classes in-process / across instances or delegate over HTTP using Agent-to-Agent (`@agboss/core/a2a`):
1. **Workflow as a step (`.toWorkflow(ChildWorkflow, { placement? })`):** Runs an ordinary `@Workflow` listed in the application as an isolated child execution with its own `maxSteps` and path scope, while sharing the parent’s checkpoint and budgets. Pass `{ placement: ChildPlacement.AnyInstance }` to queue the child in `ChildExecutionQueueRepository` (`SqliteChildExecutionQueueRepository`) so any worker instance with free slots can steal it while the parent yields its tree lease. Branch on the child’s finishes using `.returns(oneOf(ScoreDto, RejectedDto)).switch(caseOf(...)...)`.
2. **Parallel child workflows (`.toParallelWorkflows(ChildWorkflow, { maxConcurrency, maxItems, placement? })`):** Fans out a `listOf(ItemDto)` across concurrent child workflow executions (in-process via `ChildPlacement.ParentInstance` or distributed across worker pods via `ChildPlacement.AnyInstance` with per-child 30s leases and per-child crash recovery) and joins their outputs with `.join().returns(listOf(oneOf(...)))`.
3. **Self-recursion (`ThisWorkflow`):** A workflow with `@Workflow({ maxDepth: 3, maxConcurrentExecutions: 6 })` can launch itself via `.toWorkflow(ThisWorkflow)` or `.toParallelWorkflows(ThisWorkflow, { maxItems })` to decompose recursive tasks safely.
4. **Multi-tenant Agent-to-Agent (`@A2AExpose`, `a2aHttpListener`, `A2ACallerResolver`, and `@A2AAgent`):**
* **Exposing (`@A2AExpose({ name, version, exports: [Start] })` in `@Application({ exposes })`):** Declares the `@WorkflowStart` entry points served over A2A and exported in `agboss describe --json` for `agboss check --federation`.
* **Serving (`a2aHttpListener(app, { expose, callerResolver, meshVerifier?, base })`):** Serves `POST /execute` (`{ input, start?, threadId? }`), `POST /resume` (`{ pauseId, decision }`), and `POST /cancel` (`{ threadId }`). `a2aHttpListener` never guesses a tenant from the request body — it resolves an `@Injectable()` [`A2ACallerResolver`](/reference/a2a-agent/) from the DI container (`providers: [ShopA2aCallerResolver]`, `[a2a.caller-resolver-not-listed]` if omitted). `resolveCaller(request: A2ACallerRequest)` inspects `request.http`, `request.route` (`A2ARoute.Execute` / `Resume` / `Cancel`), and `request.envelope` (`MeshEnvelope`), returning `A2ACallerResolution.resolved({ tenant, ownerId, execution, peer })`, `.unauthenticated({ reason, challenge? })` (`401`), or `.forbidden({ reason })` (`403`).
* **Calling (`@A2AAgent` `extends A2AClient`):** Declare `@A2AAgent({ name: "order-desk-a2a-agent", url, target: { application, start }, input: InDto, output: OutDto, price?, deps: [ExecutionContext] })` in `providers` and inject it into a `@Tool` or `@Action` to call `execute(input, { threadId, signal, onEvent })` or `send(input, call)`. On `401` / `403`, `A2AClient` throws `A2ACallerRejectedError` (`code: "a2a.caller-rejected"`, with the server’s rejection code in `wireCode`: `"a2a.caller-unauthenticated"` or `"a2a.caller-forbidden"`, `#457`).
* **In-process testing (`a2aServer({ expose, callerResolver })`):** In `testWith(App)`, the `a2aServer` fixture spins up a loopback listener (`{ url }`) and asserts `A2ACallerRejectedError` on `401`/`403` without external network sockets.
## Cross-Service Agent Mesh (`MeshEnvelope`, JWS Signing, `--federation` & `agboss hub`)
[Section titled “Cross-Service Agent Mesh (MeshEnvelope, JWS Signing, --federation & agboss hub)”](#cross-service-agent-mesh-meshenvelope-jws-signing---federation--agboss-hub)
When multiple AGBoss applications call each other over A2A (`A2AClient` $\to$ `a2aHttpListener`) or MCP (`@McpServer` $\to$ `createMcpService`), AGBoss automatically propagates an **Agent Mesh Envelope** (`agboss-mesh` JWS EdDSA Ed25519 header over A2A, `_meta["agboss.dev/mesh"]` over MCP, plus W3C `traceparent`):
* **Propagated context (`MeshEnvelope`):** Carries `origin` (`executionId` / `rootExecutionId`, `parentExecutionId`, `workflow`, `step.idempotencyKey`), `budget.remaining` (installing a `caller-budget` cap on the downstream execution so a child hop can never exceed its parent’s remaining USD budget), `deadline.remaining` (`DeadlineExceededError` if exhausted), and `callChain` (`readonly MeshHop[]`).
* **Cryptographic peer verification (`@Application({ mesh })` & `MeshVerifier`):** Applications with `mesh: { signingKey: "meshSigningKey", peers: [{ application: "orders-application", verifyKey: "ordersPublicKey" }] }` sign every outgoing `MeshEnvelope` and every returned `MeshRunReport` (`spend`, `executionId`, `bodyHash`). When both `callerResolver` (`peer`) and the Ed25519 signature (`signedBy`) match, the receiver deduplicates retries by `origin.step.idempotencyKey` (`MeshRequestRepository` / `SqliteMeshRequestRepository`) and the caller records verified remote spend under `a2a:` / `mcp:` and `ToolCallRecord.remote`.
* **Distributed cycle & hop protection:** If `callChain` already contains the receiving `(application, start)` pair (`Service A -> Service B -> Service A`), the receiver rejects the call before the first step with `409 limit.mesh-loop`, which `A2AClient` rethrows as typed **`MeshLoopDetectedError`** (catchable via `.onError(MeshLoopDetectedError, ...)`). If `callChain.length` reaches `@Application({ maxMeshHops })` (`8` by default), it rejects with **`HopsLimitExceededError`**.
* **Cross-repo contract verification (`--federation`) & AGBoss Mesh Hub (`agboss hub`, `#401`):** In CI and local operations, verify that all services in an Agent Mesh agree on input/output DTO schemas and visualize cross-service topology, stitched execution trees (`ExecutionHistoryRepository` + `agboss records export`), Progressive Crystallization Radar, and Blast Radius:
```bash
npx agboss check --federation snapshots/
npx agboss records export --all-tenants --out .artifacts/records.jsonl
npx agboss hub show snapshots/ .artifacts/records.jsonl --html .artifacts/hub.html
npx agboss hub ui snapshots/ .artifacts/records.jsonl
```
If an upstream service changes an exposed DTO without updating consumers, `npx agboss check --federation` fails before deployment and prints the cross-service blast radius.
***
## Standalone Executable Proofs: [`06-enterprise-agent-mesh`](https://github.com/agbossjs/agboss-examples/tree/main/06-enterprise-agent-mesh) & [`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk)
[Section titled “Standalone Executable Proofs: 06-enterprise-agent-mesh & 03-support-refund-desk”](#standalone-executable-proofs-06-enterprise-agent-mesh--03-support-refund-desk)
* **[`06-enterprise-agent-mesh`](https://github.com/agbossjs/agboss-examples/tree/main/06-enterprise-agent-mesh):** Demonstrates a 3-service microservice mesh (`apps/1-order-gateway`, `apps/2-billing-a2a` with `@A2AExpose`, and `apps/3-catalog-mcp` with `@McpExpose`), Ed25519 `agboss-mesh` JWS signatures, `MeshLoopDetectedError` / `HopsLimitExceededError` fallback routing, MCP `_meta.callId` deduplication via `SqliteMeshRequestRepository`, and `agboss check --federation` Blast Radius over `federation/` snapshots.
* **[`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk):** Demonstrates an in-memory BM25/TF-IDF section indexer (`InMemoryKnowledgeIndexService` + `RefundPolicyRetriever`) wired into `@Rag({ mode: RagMode.Context })` alongside `PolicyAwareCompaction` (see [7 Verified Examples](/examples/job-scout/)).
# Conversation Memory & Compaction
> Configure multi-turn thread history windows (uncompactedTurns), slot isolation across turns, and automatic context compaction with @Compaction strategies.
In AGBoss, a conversation thread is a sequence of completed workflow executions (**turns**). Agents and model-backed routers see a bounded window of recent finished turns (`uncompactedTurns`) plus compacted summaries of older turns generated by a [`@Compaction`](/reference/compaction/) component, keeping context windows bounded and predictable without losing long-range facts.
## How turn windows (`uncompactedTurns`) and `@Compaction` strategies work
[Section titled “How turn windows (uncompactedTurns) and @Compaction strategies work”](#how-turn-windows-uncompactedturns-and-compaction-strategies-work)
Only **finished** executions enter thread memory (each formatted as `start : ` and `finish :