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 : `; paused or failed executions and intermediate step outputs never pollute thread history). In v2, compaction is declared as a dedicated [`@Compaction`](/reference/compaction/) class with one of four strategies (`CompactionStrategy.SummaryQueue`, `CompactionStrategy.SingleSummary`, `CompactionStrategy.Custom`, or `CompactionStrategy.None`) and bound via `compaction` and `uncompactedTurns` on `@Application`, `@Workflow`, `@Agent`, or `@Router` (the most specific level wins). A compaction summarizes every turn older than the smallest `uncompactedTurns` among the agents and routers that share it, so no reader ever loses a turn: src/workflows/compacting-support.workflow\.ts ```ts import { Agent, Compaction, CompactionStrategy, ExecutionContext, Injectable, TokenLimit, Workflow, WorkflowFinish, WorkflowStart, start, type CompactionHandler, type CompactionInput, type CompactionTurn, } from "@agboss/core"; import { WorkflowFinishTextDto, WorkflowStartTextDto } from "@agboss/core/dto"; @Compaction({ name: "support-thread-compaction", description: "Summarizes older support turns into a FIFO queue of 3 summaries", strategy: CompactionStrategy.SummaryQueue, model: "moonshotai/kimi-k2.6", turnsBetweenCompactions: 2, summaryQueueLength: 3, summaryMaxTokens: TokenLimit.MODEL_MAX, }) export class SupportThreadCompaction {} @Injectable() export class KnowledgeGraphService { saveFactsAndSummarize( threadId: string, turns: readonly CompactionTurn[], previous: readonly string[], ): Promise { const facts = turns.map((turn) => `[${threadId}] ${JSON.stringify(turn.input)}`); return Promise.resolve([...previous, ...facts]); } } @Compaction({ name: "graph-memory-compaction", description: "Extracts facts from aged-out turns into a knowledge graph via DI", strategy: CompactionStrategy.Custom, turnsBetweenCompactions: 2, deps: [KnowledgeGraphService, ExecutionContext], }) export class GraphMemoryCompaction implements CompactionHandler { constructor( private readonly knowledgeGraph: KnowledgeGraphService, private readonly executionContext: ExecutionContext, ) {} async compact(input: CompactionInput): Promise { return this.knowledgeGraph.saveFactsAndSummarize( this.executionContext.threadId, input.turnsToCompact, input.previousSummaries, ); } } @WorkflowStart({ name: "support-turn-workflow-start", description: "Customer message in a multi-turn support thread", input: WorkflowStartTextDto, }) export class SupportTurnWorkflowStart {} @WorkflowFinish({ name: "support-turn-workflow-finish", description: "Agent reply persisted in thread history", output: WorkflowFinishTextDto, }) export class SupportTurnWorkflowFinish {} @Agent({ name: "support-memory-agent", description: "Answers customer questions using recent turns and compacted summaries", model: "moonshotai/kimi-k2.6", prompt: "Answer the customer consistently with earlier turns in the thread.", uncompactedTurns: 6, }) export class SupportMemoryAgent {} @Workflow({ name: "compacting-support-workflow", version: "1.0.0", flow: [ start(SupportTurnWorkflowStart) .returns(WorkflowStartTextDto) .toAgent(SupportMemoryAgent) .toFinish(SupportTurnWorkflowFinish), ], compaction: GraphMemoryCompaction, uncompactedTurns: 1, providers: [KnowledgeGraphService], }) export class CompactingSupportWorkflow {} ``` test/compacting-support.workflow\.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { replyWith, testWith } from "@agboss/core/testing"; import { KnowledgeGraphService, SupportApplication, SupportMemoryAgent, SupportTurnWorkflowStart, } from "../src/compacting-support.workflow.js"; const test = testWith(SupportApplication); test("retains thread turns and invokes custom compaction when turns age out", async ({ app, mockLlm, mockOf, }) => { mockLlm(SupportMemoryAgent).thenReturnAlways( replyWith("Your cluster runs in eu-central-1."), ); const graph = mockOf(KnowledgeGraphService); graph.saveFactsAndSummarize.mockResolvedValue(["cluster region: eu-central-1"]); for (let i = 1; i <= 4; i++) { const res = await app.execute( SupportTurnWorkflowStart, { text: `Turn ${String(i)}: cluster is in eu-central-1.` }, { threadId: "thread-acme-1" }, ); expect(res.status).toBe(ExecutionStatus.Finished); } expect(graph.saveFactsAndSummarize).toHaveBeenCalledTimes(1); }); ``` ## Memory and `@Compaction` configuration options [Section titled “Memory and @Compaction configuration options”](#memory-and-compaction-configuration-options) | Option | Where Configured | Purpose | | ---------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `uncompactedTurns` | `@Application`, `@Workflow`, `@Agent`, `@Router` | Number of recent finished turns shown in full (`0` or higher, or `TurnLimit.ALL`; default `5`). Most specific level wins. | | `compaction` | `@Application`, `@Workflow`, `@Agent`, `@Router` | `@Compaction` class governing how turns older than `uncompactedTurns` are summarized. Most specific level wins. | | `CompactionStrategy.SummaryQueue` | `@Compaction({ strategy })` | Writes a new self-contained summary every `turnsBetweenCompactions` turns and retains the latest `summaryQueueLength` summaries in FIFO order. | | `CompactionStrategy.SingleSummary` | `@Compaction({ strategy })` | Maintains a single rolling summary rewritten from the previous summary plus the newly aged-out turns every `turnsBetweenCompactions` turns. | | `CompactionStrategy.Custom` | `@Compaction({ strategy })` | Delegates summarization to `compact(input: CompactionInput)` on your `CompactionHandler` class with full DI (`deps: [...]`) and automatic PII masking/unmasking. | | `CompactionStrategy.None` | `@Compaction({ strategy })` | Disables summarization (`model` and `turnsBetweenCompactions` are omitted); readers see only the `uncompactedTurns` window. | | `summaryMaxTokens` | `@Compaction` | Maximum tokens for each summary generation call (`number` or `TokenLimit.MODEL_MAX`). If a summary is truncated at the limit, AGBoss retries once asking for a shorter summary before throwing `CompactionError`. | # Observability, Observers & Langfuse Tracing > Monitor AGBoss workflows in production with typed ExecutionObserver hooks, PII-masked structured logs, revision snapshots, and self-hosted Langfuse. Operating multi-agent systems in production requires visibility into every workflow execution, agent visit, tool call, FinOps spend entry, and human-in-the-loop pause. AGBoss provides two complementary observability layers: compile-checked `ExecutionObserver` classes registered in `@Application({ observers })` for in-process metrics and alerts, and built-in [Langfuse](https://langfuse.com/) tracing for full execution tree inspection. ## How to subscribe to lifecycle events with `ExecutionObserver` [Section titled “How to subscribe to lifecycle events with ExecutionObserver”](#how-to-subscribe-to-lifecycle-events-with-executionobserver) Any `@Injectable()` class listed in `@Application({ observers: [...] })` can implement one or more observer hook interfaces (`OnExecutionStart`, `OnExecutionEnd`, `OnExecutionError`, `OnExecutionRejected`, `OnExecutionPause`, `OnExecutionResume`, `OnAgentStart`, `OnAgentEnd`, `OnValuesOutdated`). All event payloads are pre-masked by the application and workflow `@PiiPolicy` rules before reaching your observer: src/observers/audit.observer.ts ```ts import { Application, Budget, BudgetPeriod, BudgetScope, EmptyEnvironment, Injectable, Workflow, WorkflowFinish, WorkflowStart, start, type AgentStartEvent, type BudgetHandler, type ExecutionEndEvent, type ExecutionRejectedEvent, type OnAgentStart, type OnExecutionEnd, type OnExecutionRejected } from "@agboss/core"; import { WorkflowStartTextDto } from "@agboss/core/dto"; import { usd, type Usd } from "@agboss/core/units"; @Injectable() export class MetricsObserver implements OnExecutionEnd, OnAgentStart, OnExecutionRejected { onExecutionEnd(event: ExecutionEndEvent): void { console.log( `${event.workflow.name} ${event.executionId} cost $${String(event.spend.totalUsd)}`, ); } onAgentStart(event: AgentStartEvent): void { console.log( `${event.step.component.name} visit ${String(event.step.visitNumber)}`, ); } onExecutionRejected(event: ExecutionRejectedEvent): void { console.log( `${event.operation} refused for ${event.tenantId}: ${event.code}`, ); } } @WorkflowStart({ name: "ping-workflow-start", description: "Ping input", input: WorkflowStartTextDto, }) export class PingWorkflowStart {} @WorkflowFinish({ name: "ping-workflow-finish", description: "Ping output", output: WorkflowStartTextDto, }) export class PingWorkflowFinish {} @Workflow({ name: "ping-workflow", version: "1.0.0", flow: [ start(PingWorkflowStart) .returns(WorkflowStartTextDto) .toFinish(PingWorkflowFinish), ], }) export class PingWorkflow {} @Budget({ name: "ping-app-budget", scope: BudgetScope.Application, period: BudgetPeriod.Day, }) export class PingAppBudget implements BudgetHandler { maxSpend(): Usd { return usd(5); } } @Budget({ name: "ping-tenant-budget", scope: BudgetScope.Tenant, period: BudgetPeriod.Day, }) export class PingTenantBudget implements BudgetHandler { maxSpend(): Usd { return usd(1); } } @Budget({ name: "ping-exec-budget", scope: BudgetScope.Execution, }) export class PingExecBudget implements BudgetHandler { maxSpend(): Usd { return usd(0.1); } } @Application({ name: "ping-application", version: "1.0.0", environment: EmptyEnvironment, workflows: [PingWorkflow], budgets: [PingAppBudget, PingTenantBudget, PingExecBudget], observers: [MetricsObserver], }) export class PingApplication {} ``` test/audit.observer.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { testWith } from "@agboss/core/testing"; import { PingApplication, MetricsObserver, PingWorkflowStart } from "../src/observers/audit.observer.js"; const test = testWith(PingApplication); test("records execution and agent lifecycle events in MetricsObserver", async ({ app, mockOf }) => { const res = await app.execute(PingWorkflowStart, { text: "Check invoice #102" }); expect(res.status).toBe(ExecutionStatus.Finished); expect(mockOf(MetricsObserver).onExecutionEnd).toHaveBeenCalledTimes(1); }); ``` ## What events and spans are captured in traces and observers? [Section titled “What events and spans are captured in traces and observers?”](#what-events-and-spans-are-captured-in-traces-and-observers) Every element of the workflow graph has its own typed `On*Start` / `On*End` callback interface exported from `"@agboss/core"`: | Hook Interface(s) | Emitted When | Key Payload Fields | | ------------------------------------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `OnExecutionStart` / `OnExecutionEnd` | Root or child workflow starts or finishes | `executionId`, `parentExecutionId`, `rootExecutionId`, `workflow`, `tenantId`, `spend` | | `OnExecutionPause` / `OnExecutionResume` | Execution pauses on a `@Channel` or resumes / recovers | `pending`, `reason` (`ResumeReason.Answered` or `ResumeReason.Recovered`), `decision`, `answeredBy` | | `OnExecutionError` / `OnExecutionRejected` | Unhandled failure or pre-flight refusal before execution starts | `error`, `operation` (`Execute`, `Resume`, `Cancel`), `code`, `tenantId` | | `OnAgentStart` / `OnAgentEnd` | `@Agent` visit begins or completes | `step` (`component`, `nodeId`, `visitNumber`, `copyNumber`, `idempotencyKey`) | | `OnRouterStart` / `OnRouterEnd` | `@Router` decision begins or completes | `step`, chosen route target(s) | | `OnToolStart` / `OnToolEnd` | `@Tool` invocation begins or completes | `step`, `callId`, tool name, validated arguments / output | | `OnModelStart` / `OnModelEnd` | Model provider call begins or completes | `callerName`, `model`, `usage` (prompt, completion, cached tokens), `cost` | | `OnRagStart` / `OnRagEnd` | `@Rag` retrieval begins or completes | `step`, retrieved chunk count and query | | `OnActionStart` / `OnActionEnd` | `@Action` step begins or completes | `step`, input and output metadata | | `OnGuardrailStart` / `OnGuardrailEnd` | `@Guardrail` check begins or completes | `step`, stage (`Input` / `Output`), pass or block verdict | | `OnChannelStart` / `OnChannelEnd` | `@Channel` outbound ask begins or completes | `step`, channel role, `pauseId` | | `OnJudgeStart` / `OnJudgeEnd` | `@Judge` verification begins or completes | `step`, verdict (`passed`, `reason`) | | `OnValuesOutdated` | Loop or branch write cascades stale dependents | `step`, `trigger` (`valueId`, `slot`, `producer`), `outdated` (`OutdatedReason`) | | `OnLaunchItemsTruncated` | Parallel launch trims input list to `maxItems` | `step`, `launch`, `items`, `maxItems` | | `OnQuorumDecided` | `.joinQuorum` / `.joinQuorumGroups` reaches a decision | `step`, `isReached`, `branches`, winning `group` | ## How to run self-hosted Langfuse locally and correlate traces with `agboss describe --json` [Section titled “How to run self-hosted Langfuse locally and correlate traces with agboss describe --json”](#how-to-run-self-hosted-langfuse-locally-and-correlate-traces-with-agboss-describe---json) Start the local Langfuse stack (`./scripts/langfuse.sh up`) and set `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, and `LANGFUSE_BASE_URL` in your environment. Every workflow execution, child workflow, parallel branch, and RAG retrieval is exported automatically without sending data to a third-party SaaS. To analyze real production coverage of your graph branches, tools, prompts, and DTO fields, export the declarative revision snapshot (`{ application, workflows[], schemas }`) in CI with `agboss describe --json --application `: * **Revision & prompt fingerprinting (`configHash`, `promptHash`, `schemaHash`):** Runtime traces and `ExecutionRecord` entries carry `WorkflowRecord.configHash`, `ComponentRecord` (`{ name, kind, promptHash, schemaHash }`), and `StepRecord` (`{ toolLoopIteration, validationAttempts, branch: { verdict, status } }`) so observability pipelines can join live Langfuse / OpenTelemetry spans directly against the `agboss describe --json` snapshot to see which `@Router` branches, `@Tool` definitions, and `@Action` `compensateWith` saga edges were exercised in production. * **Versioned child workflow pause compatibility (`[ChildWorkflowV1, ChildWorkflowV2]`):** When a parent workflow registers multiple versions of a child workflow for rolling upgrades, the revision snapshot records each version’s `configHash` so long-sleeping pauses (`PendingPause`) always resume on their matching workflow definition. # Parallel, Quorums & Child Workflows > Fan out parallel agents and child workflows, evaluate quorum consensus with @Arbiter, and distribute child workflows across workers with AnyInstance. AGBoss provides first-class **HarnessDSL (`HDSL`)** flow primitives for deterministic fan-out/fan-in (`.toParallelAgents()`, `.toParallelWorkflows()`), deadline-bounded quorum consensus (`.acceptedBy()`, `.joinQuorum()`, `.joinQuorumGroups(group(...).priority(1))`, [`@Arbiter`](/reference/arbiter/)), modular child workflows (`.toWorkflow(Child)`), and depth-bounded recursion (`ThisWorkflow`). ## How parallel fan-out, `.join()`, and quorum consensus work [Section titled “How parallel fan-out, .join(), and quorum consensus work”](#how-parallel-fan-out-join-and-quorum-consensus-work) When a step fans out across multiple specialists or a dynamic list of items (`maxItems`, `maxConcurrency`), every branch receives an isolated copy of the Path Scope. You then merge results in one of three ways: * **Wait for all (`.join()`):** Waits for every branch to complete and binds all outputs via `.returns(...)`. * **Count-based quorum (`.acceptedBy(Arbiter).joinQuorum({ min, max, deadline })`):** Evaluates each arriving branch output with one or more [`@Arbiter`](/reference/arbiter/) classes (`anyOf(A, B)` accepts if either passes; chaining `.acceptedBy(A).acceptedBy(B)` requires both). As soon as `max` (default `min`) accepted answers arrive, the join proceeds immediately and aborts slower in-flight branches (`BranchVerdict.Surplus`). Unless `min` equals the total number of launched agents, single-agent outputs after `.joinQuorum()` are typed as `optional(Dto)` in `.returns(...)`. * **Priority group quorum (`.acceptedBy(Arbiter).joinQuorumGroups(group(...).priority(1)..., { deadline })`):** Defines prioritized combinations of agents (`priority(1)` is highest), where each `group(...)` declares its own `.returns(...)` and continuation path: src/workflows/parallel-review\.workflow\.ts ```ts import { QuorumNotReachedError, Workflow, group, start, toAction } from "@agboss/core"; import { seconds } from "@agboss/core/units"; @Workflow({ name: "parallel-review-workflow", version: "1.0.0", flow: [ start(ProposalStart) .returns(ProposalDto) .toParallelAgents(SecurityAgent, LegalAgent, BackupAuditAgent) .acceptedBy(ReviewQuorumArbiter) .joinQuorumGroups( group(SecurityAgent, LegalAgent) .priority(1) .returns(SecurityVerdictDto) .returns(LegalVerdictDto) .toAction(MergePrimaryVerdictsAction) .returns(ApprovedProposalDto) .toFinish(ProposalApprovedFinish), group(SecurityAgent, BackupAuditAgent) .priority(2) .returns(SecurityVerdictDto) .returns(BackupVerdictDto) .toAction(MergeFallbackVerdictsAction) .returns(ApprovedProposalDto) .toFinish(ProposalApprovedFinish), { deadline: seconds(10) }, ) .onJoinError( QuorumNotReachedError, toAction(RecordQuorumFailureAction) .toFinish(ProposalRejectedFinish), ), ], }) export class ParallelReviewWorkflow {} ``` test/parallel-review\.workflow\.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { TestConcurrency, replyWith, step, testWith } from "@agboss/core/testing"; import { ReviewApplication, ProposalStart, SecurityAgent, BackupAuditAgent, LegalAgent, SecurityVerdictDto, BackupVerdictDto, } from "../src/parallel-review.workflow.js"; const test = testWith(ReviewApplication, { concurrency: TestConcurrency.inOrder([ step(BackupAuditAgent), step(SecurityAgent), step(LegalAgent), ]), }); test("reaches priority-2 quorum when SecurityAgent and BackupAuditAgent finish first", async ({ app, mockLlm }) => { mockLlm(BackupAuditAgent).thenReturn(replyWith(BackupVerdictDto, { passed: true })); mockLlm(SecurityAgent).thenReturn(replyWith(SecurityVerdictDto, { passed: true, confidence: 95 })); const res = await app.execute(ProposalStart, { title: "SOC2 Policy Update" }); expect(res.status).toBe(ExecutionStatus.Finished); }); ``` ## Recursive `ThisWorkflow` decomposition, soft stop at `maxDepth`, and KV cache prefixes [Section titled “Recursive ThisWorkflow decomposition, soft stop at maxDepth, and KV cache prefixes”](#recursive-thisworkflow-decomposition-soft-stop-at-maxdepth-and-kv-cache-prefixes) A workflow can invoke a modular child workflow (`.toWorkflow(ChildWorkflow)`) or recursively invoke itself (`ThisWorkflow`) either deterministically or as a model-driven tool call: src/workflows/recursive-research.workflow\.ts ```ts import { ChildPlacement, ThisWorkflow, Workflow, caseOf, listOf, start } from "@agboss/core"; @Workflow({ name: "recursive-research-workflow", version: "1.0.0", flow: [ start(ResearchStart) .returns(TopicDto) .toAgent(DecomposeTopicAgent) .returns(oneOf(ResolvedDto, SubTopicsDto)) .switch( caseOf(ResolvedDto) .toWorkflow(FactCheckChildWorkflow, { placement: ChildPlacement.AnyInstance, }) .returns(VerifiedSummaryDto) .toFinish(ResearchFinish), caseOf(SubTopicsDto) .toAction(ExtractSubTopicsAction) .returns(listOf(TopicDto)) .toParallelWorkflows(ThisWorkflow, { maxItems: 5, maxConcurrency: 2, placement: ChildPlacement.AnyInstance, }) .join() .returns(listOf(VerifiedSummaryDto)) .toAction(MergeFindingsAction) .returns(VerifiedSummaryDto) .toFinish(ResearchFinish), ), ], maxDepth: 3, maxConcurrentExecutions: 6, }) export class RecursiveResearchWorkflow {} ``` test/recursive-research.workflow\.test.ts ```ts import { expect, it } from "vitest"; import { testAction } from "@agboss/core/testing"; import { DecomposeTopicAgent, ExtractSubTopicsAction, RecursiveResearchWorkflow, } from "../src/recursive-research.workflow.js"; it("extracts sub-topic items for recursive ThisWorkflow fan-out", async () => { expect(RecursiveResearchWorkflow).toBeDefined(); const runExtract = testAction(ExtractSubTopicsAction); const { output } = await runExtract({ subTopics: { value: { items: [{ topic: "Raft" }, { topic: "Paxos" }] }, producer: DecomposeTopicAgent, }, }); expect(output).toEqual([{ topic: "Raft" }, { topic: "Paxos" }]); }); ``` ### How `maxDepth` enforces a soft stop (and warms at most two KV cache prefixes) [Section titled “How maxDepth enforces a soft stop (and warms at most two KV cache prefixes)”](#how-maxdepth-enforces-a-soft-stop-and-warms-at-most-two-kv-cache-prefixes) Every recursive workflow requires `maxDepth` on `@Workflow` (`[graph.missing-max-depth]`). How `maxDepth` behaves at runtime depends on whether `ThisWorkflow` is launched by the model or by deterministic code: * **Model-chosen recursion (soft stop at `depth === maxDepth`):** When an `@Agent` or `DecidedBy.Model` `@Router` can launch `ThisWorkflow`, reaching `depth === maxDepth` **does not crash** the execution. Instead, AGBoss simply omits the `ThisWorkflow` tool from the model’s tool definitions on that step, forcing the model to synthesize a base-case answer instead of recursing further. Because the tool list is identical across all `depth < maxDepth` calls and differs only at `depth === maxDepth`, the recursive agent warms at most **two KV prefix cache entries** across the entire recursion tree. * **Deterministic recursion (`.toWorkflow(ThisWorkflow)`):** If a deterministic flow path unconditionally launches `ThisWorkflow` past `maxDepth`, the runtime throws `DepthLimitExceededError` (catchable via `.onError(DepthLimitExceededError, ...)`). ## Key safeguards for parallel and recursive execution [Section titled “Key safeguards for parallel and recursive execution”](#key-safeguards-for-parallel-and-recursive-execution) | Safeguard | Option / Error | How it protects production | | -------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Fan-out truncation** | `maxItems` + `OnLaunchItemsTruncated` | Caps dynamic list fan-out and emits a typed observer event when excess items are truncated. | | **Bounded branch concurrency** | `maxConcurrency` | Limits how many parallel branches execute simultaneously within a fan-out step. | | **Tree-wide execution throttle** | `maxConcurrentExecutions` | Caps how many executions across the root and all child workflow trees run a step simultaneously (default `10`; enforced across instances via `RateLimitRepository` when `ChildPlacement.AnyInstance` is used). | | **Distributed child placement** | `placement: ChildPlacement.AnyInstance` | Enqueues `.toWorkflow()`, `.toParallelWorkflows()`, or `route(Workflow)` children into `ChildExecutionQueueRepository` (`SqliteChildExecutionQueueRepository`), yielding the parent’s lease while waiting and recovering each child independently under its own 30s lease (`[flow.conflicting-placement]` if two paths to the same child disagree). | | **Quorum cancellation** | `deadline: seconds(10)` | Automatically aborts slower losing branches (`BranchVerdict.Surplus`) via `AbortSignal` the instant quorum is decided. | | **Recursion ceiling** | `maxDepth` | Required on every `ThisWorkflow` graph (`[graph.missing-max-depth]`); omits the recursion tool at `depth === maxDepth` for model launches (soft stop) and throws `DepthLimitExceededError` if exceeded by deterministic launches. | # Multi-Instance Distribution, Leases, SQLite & Custom Stores > Scale AGBoss from in-memory unit tests to SQLite WAL and Kubernetes pods using shared repositories, 30s execution leases, fencing, and @HealthCheck probes. An AGBoss `@Application` can run as **multiple instances (processes, containers, or Kubernetes pods)** over shared storage: any instance can resume a pause created on another instance, recover an execution whose worker process died mid-step, or cancel a running thread across the cluster — while 30-second execution-tree leases guarantee that two instances never advance the same execution tree simultaneously. *** ## Multi-instance application & failover test [Section titled “Multi-instance application & failover test”](#multi-instance-application--failover-test) Below is a complete multi-instance application wired with the 7 `Sqlite*` WAL repositories, a custom `TableExecutionLeaseRepository extends ExecutionLeaseRepository` implementation, and a multi-instance `testWith(App, { instances: 2 })` test suite: src/environments/prod.environment.ts ```ts import { SqliteCheckpointSaver, SqliteChildExecutionQueueRepository, SqliteCostLedgerRepository, SqliteExecutionHistoryRepository, SqliteExecutionLeaseRepository, SqliteExecutionRecordRepository, SqliteExecutionTreeRepository, SqliteMeshRequestRepository, SqlitePausedExecutionRepository, SqliteRateLimitRepository, env, } from "@agboss/core"; export class StoreEnvironment { sqlitePath!: string; } export const environment: StoreEnvironment = { sqlitePath: env("AGBOSS_SQLITE_PATH", { default: ".agboss/job-scout.sqlite", }), }; export const durableStores = [ SqliteExecutionRecordRepository, SqliteExecutionHistoryRepository, SqliteCostLedgerRepository, SqlitePausedExecutionRepository, SqliteCheckpointSaver, SqliteExecutionLeaseRepository, SqliteExecutionTreeRepository, SqliteChildExecutionQueueRepository, SqliteRateLimitRepository, SqliteMeshRequestRepository, ]; ``` src/support.application.ts ```ts import { Application, SqliteCheckpointSaver, SqliteCostLedgerRepository, SqliteExecutionLeaseRepository, SqliteExecutionRecordRepository, SqliteExecutionTreeRepository, SqlitePausedExecutionRepository, SqliteRateLimitRepository, } from "@agboss/core"; import { seconds } from "@agboss/core/units"; @Application({ name: "support-application", version: "1.0.0", environment: StoreEnvironment, workflows: [SupportWorkflow], budgets: [SupportAppBudget, SupportTenantBudget, SupportExecBudget], providers: [ SqliteExecutionRecordRepository, SqliteCostLedgerRepository, SqlitePausedExecutionRepository, SqliteCheckpointSaver, SqliteExecutionLeaseRepository, SqliteExecutionTreeRepository, SqliteRateLimitRepository, ], maxConcurrentSteps: 32, compensationTimeout: seconds(15), shutdownTimeout: seconds(25), }) export class SupportApplication {} ``` src/stores/table-lease.repository.ts ```ts import { ExecutionLeaseRepository, Injectable, type ExecutionLease, } from "@agboss/core"; import type { Milliseconds } from "@agboss/core/units"; @Injectable() export class LeaseTableService { insertIfFree( _tenantId: string, _rootExecutionId: string, _lease: number, ): Promise { return Promise.resolve(undefined); } extendIfHeld( _executionLease: ExecutionLease, _lease: number, ): Promise { return Promise.resolve(false); } deleteIfHeld(_executionLease: ExecutionLease): Promise { return Promise.resolve(); } selectOne(): Promise { return Promise.resolve(); } } @Injectable({ deps: [LeaseTableService] }) export class TableExecutionLeaseRepository extends ExecutionLeaseRepository { constructor(private readonly leaseTableService: LeaseTableService) { super(); } override acquire( tenantId: string, rootExecutionId: string, lease: Milliseconds, ): Promise { return this.leaseTableService.insertIfFree( tenantId, rootExecutionId, lease, ); } override renew( executionLease: ExecutionLease, lease: Milliseconds, ): Promise { return this.leaseTableService.extendIfHeld(executionLease, lease); } override release(executionLease: ExecutionLease): Promise { return this.leaseTableService.deleteIfHeld(executionLease); } override ping(): Promise { return this.leaseTableService.selectOne(); } } ``` test/multi-instance.test.ts ```ts import { Application, EmptyEnvironment } from "@agboss/core"; import { expect } from "vitest"; import { testWith } from "@agboss/core/testing"; import { seconds } from "@agboss/core/units"; import { SupportFinish, SupportStart, SupportWorkflow } from "../src/support.application.js"; @Application({ name: "cluster-test-application", version: "1.0.0", environment: EmptyEnvironment, workflows: [SupportWorkflow], }) export class ClusterTestApplication {} const test = testWith(ClusterTestApplication, { instances: 2 }); test("runs two application instances over shared stores and a shared virtual clock", async ({ apps, clock, }) => { const [instanceA, instanceB] = apps; if (!instanceA || !instanceB) throw new Error("Expected 2 instances"); const first = await instanceA.execute( SupportStart, { text: "Turn 1 on instance A" }, { threadId: "shared-thread-1" }, ); expect(first.finish).toBe(SupportFinish); await clock.advance(seconds(31)); const second = await instanceB.execute( SupportStart, { text: "Turn 2 on instance B" }, { threadId: "shared-thread-1" }, ); expect(second.output).toEqual({ text: "Turn 2 on instance B" }); }); ``` *** ## 4-stage progressive infrastructure scaling [Section titled “4-stage progressive infrastructure scaling”](#4-stage-progressive-infrastructure-scaling) Because `@Agent`, `@Router`, `@Arbiter`, `@Action`, and `@Tool` classes never import storage drivers directly, your application scales across four infrastructure stages purely by updating `@Application({ providers })`: | Stage | `@Application({ providers })` Configuration | Use Case | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | **Stage 1: In-Memory (Default)** | `providers: []` (built-in `InMemory*` stores) | Local prototyping (`npm run chat`), unit tests (`testWith`), and deterministic CI suites | | **Stage 2: Single-Node Durable** | Built-in `Sqlite*` WAL repositories (`sqlitePath`) | Single-VM / single-container production with durable checkpoints, pauses, and spend ledgers | | **Stage 3: Multi-Instance on One Machine** | Shared `Sqlite*` WAL file + `SqliteExecutionLeaseRepository` | Multiple worker processes on one host sharing WAL storage, 30s leases, and recovery sweeps | | **Stage 4: Multi-Machine Cluster Stores** | Custom `@Injectable()` classes extending `ExecutionRecordRepository`, `ExecutionLeaseRepository`, etc. | Distributed Postgres / Redis / DynamoDB backends across Kubernetes nodes | *** ## What the 10 shared stores coordinate across instances [Section titled “What the 10 shared stores coordinate across instances”](#what-the-10-shared-stores-coordinate-across-instances) Every store is an infrastructure contract with a built-in in-memory default (`InMemory*`) and a built-in zero-dependency `node:sqlite` WAL implementation (`Sqlite*`): | Base Repository Contract | Built-in SQLite Implementation | What It Coordinates Across Instances | | ------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BaseCheckpointSaver` | `SqliteCheckpointSaver` | Saves LangGraph state checkpoints so a pause or crash on Instance A resumes on Instance B | | `PausedExecutionRepository` | `SqlitePausedExecutionRepository` | Transitions pauses `Pending → Resolved` atomically (CAS) once, whichever instance handles `app.resume()` | | `ExecutionRecordRepository` | `SqliteExecutionRecordRepository` | Persists tenant-isolated threads, owners, `Tern` records, and atomic CAS status transitions | | `ExecutionHistoryRepository` | `SqliteExecutionHistoryRepository` | Persists completed `ExecutionRecord` snapshots (`steps[].decision`, `inputHash`, `toolCalls`, `origin`, retained for `executionRecordRetention`) for `agboss records export`, `agboss generate eval-case --from-execution`, and `agboss hub` | | `CostLedgerRepository` | `SqliteCostLedgerRepository` | Aggregates token and tool spend across all instances for `Execution`, `Tenant`, and `Application` `@Budget` scopes | | `ExecutionLeaseRepository` | `SqliteExecutionLeaseRepository` | Guarantees a single active holder (`seconds(30)` lease) per `(tenantId, rootExecutionId)` tree and per remote child (`ChildPlacement.AnyInstance`) | | `ExecutionTreeRepository` | `SqliteExecutionTreeRepository` | Shares request-scoped keys (`saveIfAbsent`), `@agboss/cancel-request`, and `@agboss/unwinding` across the execution tree | | `ChildExecutionQueueRepository` | `SqliteChildExecutionQueueRepository` | Distributes `ChildPlacement.AnyInstance` child workflow tasks (`Queued → Running → Finished`) across worker instances with work-stealing and per-child crash recovery | | `RateLimitRepository` | `SqliteRateLimitRepository` | Shares `@ModelProvider` concurrency slots (`:slots`), shared tree concurrency slots (`SharedTreeSlots`), per-minute call/token windows, 429 cooldowns, and open circuit breakers | | `MeshRequestRepository` | `SqliteMeshRequestRepository` | Deduplicates incoming signed Agent Mesh (`@A2AExpose` / `@McpExpose`) requests by `origin.step.idempotencyKey` | Multi-instance testing with `InMemory*` vs SQLite `:memory:` (`#458`) `testWith(App, { instances: 2 })` shares the built-in `InMemory*` stores across all instances in the test automatically. However, if your application lists `Sqlite*` stores and sets `sqlitePath: ":memory:"`, SQLite creates a **separate** in-memory database per connection (`SqliteConnection`), so two instances will not see each other’s rows. To test multiple instances against a shared SQLite database, point `sqlitePath` in a test environment (e.g., `testWith(App, { instances: 2, env: "instances" })`) at a temporary file path created with `fs.mkdtempSync`. *** ## How 30-second execution leases & local fencing work [Section titled “How 30-second execution leases & local fencing work”](#how-30-second-execution-leases--local-fencing-work) One lease exists per execution tree, keyed by `(tenantId, rootExecutionId)`, held by `ApplicationContext.instanceId`: 1. **Background renewal & pre-checkpoint renewal:** The active holder renews its `seconds(30)` lease every 10 seconds on the application’s `Clock` and immediately before every checkpoint write. Paused or finished trees hold no lease. 2. **Immediate mid-tool-loop fencing (`ExecutionLeaseLostError`):** If a renewal returns `false` (the instance froze for >30s and another instance took the lease with `leaseEpoch + 1`), or if the lease store fails to confirm a renewal before `confirmedUntil` (local self-fencing), the holder immediately aborts `ExecutionContext.signal` with `ExecutionLeaseLostError` (`execution.lease-lost`). An `@Agent` tool loop starts no further model or tool calls, and the error bypasses `.onError` and `onExecutionError` because the new lease holder now owns the execution. 3. **Concurrent `app.resume()` calls:** `app.resume` runs in two phases: first it transitions the pause `Pending → Resolved` atomically (a duplicate click gets `NotPausedError`), then it acquires or waits for the tree’s lease. When two parallel branch pauses are answered simultaneously on two different instances, both decisions are recorded and drained in a single pass so each branch runs once. *** ## Crash recovery, cross-instance `app.cancel`, and mid-undo recovery [Section titled “Crash recovery, cross-instance app.cancel, and mid-undo recovery”](#crash-recovery-cross-instance-appcancel-and-mid-undo-recovery) * **Automatic recovery sweep (every 30 s):** At startup and every 30 seconds, each instance scans `ExecutionRecordStatus.Running` records (`findByStatus`), claims any tree whose holder died or released its lease on shutdown, and resumes from the **last committed checkpoint** (`onExecutionResume` with `ResumeReason.Recovered`). Because every finished step is checkpointed before the next starts, only the interrupted step runs again, and external side effects deduplicate via `executionContext.step.idempotencyKey`. The sweep also purges `Taken` / `Cancelled` pause records older than `@Application({ pauseRecordRetention })` (default `hours(24)`). * **Cross-instance `app.cancel(threadId)`:** Behind a load balancer, `app.cancel` can land on any instance. If the target thread is running on another instance, `app.cancel` writes the reserved key `@agboss/cancel-request` into the shared `ExecutionTreeRepository`. The holding instance checks this key on every lease renewal (every 10s and before each checkpoint write) and aborts the run with `ExecutionCancelledError`. * **Durable LIFO saga unwinding (`@agboss/unwinding`):** Before automatic LIFO rollback of [`@Action({ compensateWith })`](/guides/error-handling-and-rollback/) starts, the holder writes `@agboss/unwinding` and marks each completed compensator under `@agboss/compensated/`. If the instance crashes or times out mid-rollback, the next instance taking the lease reads `@agboss/unwinding`, skips already-compensated steps, and finishes the remaining compensators in LIFO order. *** ## Instance step caps (`maxConcurrentSteps`) & distributed child work-stealing (`ChildPlacement.AnyInstance`) [Section titled “Instance step caps (maxConcurrentSteps) & distributed child work-stealing (ChildPlacement.AnyInstance)”](#instance-step-caps-maxconcurrentsteps--distributed-child-work-stealing-childplacementanyinstance) * **Local pod ceiling (`@Application({ maxConcurrentSteps: 32 })`):** Caps how many `@Agent`, `@Router`, and `@Action` steps run concurrently inside one process across all tenants and trees. When all slots are busy, additional steps wait in a deadlock-free priority queue: 1. **Priority 1:** Steps that close a branch (right after a child workflow or `.join()` / `.joinQuorum()`). 2. **Priority 2:** Deeper in-flight executions (`depth`, with +1 depth aging per 10s waited on the `Clock`). 3. **Priority 3:** Newly arriving root `app.execute` calls. * **Load-balancer readiness shedding (`step-slots`):** While all `maxConcurrentSteps` slots are busy, `await app.checkReadiness()` reports component `step-slots` as `HealthStatus.Degraded` so Kubernetes / load balancers route new requests to idle pods, and the recovery sweep claims at most as many orphans as the instance has free slots. * **Distributed child workflow queue (`ChildPlacement.AnyInstance` & `ChildExecutionQueueRepository`, `#333`):** By default, `.toWorkflow(Child)` and `.toParallelWorkflows(Child, ...)` run on the parent’s instance (`ChildPlacement.ParentInstance`). Passing `{ placement: ChildPlacement.AnyInstance }` (on `.toWorkflow`, `.toParallelWorkflows`, or `route(Child, { placement: ChildPlacement.AnyInstance })`) dispatches child workflow executions through `ChildExecutionQueueRepository` (`SqliteChildExecutionQueueRepository`) so any instance with free `maxConcurrentSteps` slots can claim and run them: * **Shared tree concurrency slots (`RateLimitRepository`):** The root workflow’s `maxConcurrentExecutions` cap (`SharedTreeSlots`) is enforced across all worker instances via `RateLimitRepository`. * **Parent lease yielding while waiting:** While the parent workflow waits for remote `AnyInstance` children to finish, it **releases its own tree lease** (`ExecutionLeaseRepository`) and local step slot so the parent pod is never blocked. Whichever instance finishes the last child wakes the parent from its checkpoint. * **Per-child 30s lease & per-child crash recovery:** Each `AnyInstance` child holds its own 30-second lease (`ExecutionLeaseRepository`) renewed every 10s. If the worker running Child #3 crashes mid-wave, another instance reclaims **only Child #3** (`Running` row with expired child lease) from its last checkpoint while sibling children continue uninterrupted. (If `ChildPlacement.AnyInstance` is used without a shared `ChildExecutionQueueRepository` in `@Application({ providers })`, `agboss check` warns with `[application.in-memory-child-queue]`.) * **Shared `@ModelProvider` rate limits (`RateLimitRepository`):** Across all instances, `RateLimitRepository` enforces a provider’s `maxConcurrentCalls` slots (`acquire` / `renew` / `release`), per-minute call and token windows (`addToWindow`), and 429 `Retry-After` pauses / open circuit breakers (`pauseUntil` / `pausedUntil`). *** ## Extending SQLite stores or writing custom Postgres/Redis stores [Section titled “Extending SQLite stores or writing custom Postgres/Redis stores”](#extending-sqlite-stores-or-writing-custom-postgresredis-stores) You can either subclass a built-in `Sqlite*` store (injecting `SqliteConnection`, `Clock`, `ApplicationContext`, `IdGenerator`) to add custom audit logging, or subclass any of the 7 base repository contracts (`extends ExecutionLeaseRepository`, `extends PausedExecutionRepository`, `extends RateLimitRepository`, etc.) to back AGBoss with Postgres, Redis, or DynamoDB. Every store contract includes `ping(): Promise`, which `await app.checkReadiness()` calls under a 2-second timeout to verify database connectivity. *** ## Standalone Executable Proof: [`05-cluster-work-stealing`](https://github.com/agbossjs/agboss-examples/tree/main/05-cluster-work-stealing) [Section titled “Standalone Executable Proof: 05-cluster-work-stealing”](#standalone-executable-proof-05-cluster-work-stealing) Inspect **[`05-cluster-work-stealing`](https://github.com/agbossjs/agboss-examples/tree/main/05-cluster-work-stealing)** in [`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples) for a multi-instance cluster harness proving `ChildPlacement.AnyInstance` over `SqliteChildExecutionQueueRepository`, bounded recursive `ThisWorkflow` (`maxDepth: 2`), 30s SQLite leases & fencing (`ExecutionLeaseLostError`), `maxConcurrentSteps: 2` backpressure (`HealthComponentKind.StepSlots`), `@HealthCheck` liveness/readiness probes, and cross-instance pause/resume/cancel (see [7 Verified Examples](/examples/job-scout/)). # Security, Guardrails & PII Policies > Protect prompts, tool calls, and logs in AGBoss using @PiiPolicy HMAC tokenization, @InputGuardrail, @OutputGuardrail, and @ToolInterceptor policies. When enterprise workflows process customer tickets, financial records, or medical notes, personal data and credentials must never leak into external LLM prompts or observability logs. AGBoss provides three defense-in-depth mechanisms: [`@PiiPolicy`](/reference/pii-policy/) for declarative or custom PII masking and reversible pseudonymization at the model boundary, [`@InputGuardrail` and `@OutputGuardrail`](/reference/guardrail/) for pre-model and post-model safety enforcement, and `secret()` for environment credential masking. ## How `@PiiPolicy` masks and reveals personal data at the model boundary (without breaking KV caches) [Section titled “How @PiiPolicy masks and reveals personal data at the model boundary (without breaking KV caches)”](#how-piipolicy-masks-and-reveals-personal-data-at-the-model-boundary-without-breaking-kv-caches) A `@PiiPolicy` class (whose `name` must end with `-pii-policy`) can declare built-in `rules: [pii(Kind | Group, Action)]` or implement `PiiPolicyHandler.findMatches(text)` for custom project detectors. Masking happens at the model boundary: models only see masked tokens, while `PiiAction.replaceWithToken()` computes a **deterministic HMAC pseudonym** with `pseudonymKey` so the exact same email or badge ID maps to the exact same token across every turn and step — preserving multi-turn LLM KV prefix cache hits while restoring real values before tools, actions, and finishes run: src/policies/customer.pii-policy.ts ```ts import { ExecutionContext, Injectable, InputGuardrail, OutputGuardrail, PiiAction, PiiGroup, PiiKind, PiiPolicy, pii, secret, type GuardrailRefusal, type PiiMatch, type PiiPolicyHandler, type PiiRefusal } from "@agboss/core"; import { OneOf, Text } from "@agboss/core/dto"; // 1. Declarative PII policy with reversible token replacement and card blocking @PiiPolicy({ name: "customer-pii-policy", description: "Contact details hidden, cards refused, names found by a model", rules: [ pii(PiiGroup.Contact, PiiAction.replaceWithKind()), pii(PiiKind.Email, PiiAction.replaceWithToken()), pii(PiiKind.PersonName, PiiAction.replaceWithKind()), pii(PiiKind.CardNumber, PiiAction.blockWithRefusal()), ], detectWithModel: "moonshotai/kimi-k2.6", pseudonymKey: secret("PII_PSEUDONYM_KEY"), }) export class CustomerPiiPolicy implements PiiRefusal { refusal(kind: PiiKind): string { return `Please do not send a ${kind} here.`; } } @Injectable() export class EmployeeDirectory { readonly ids: readonly string[] = ["EMP-42"]; } // 2. Custom project detector implementing PiiPolicyHandler.findMatches(text) @PiiPolicy({ name: "employee-id-pii-policy", action: PiiAction.replaceWithToken(), deps: [EmployeeDirectory], }) export class EmployeeIdPiiPolicy implements PiiPolicyHandler { constructor(private readonly employeeDirectory: EmployeeDirectory) {} findMatches(text: string): readonly PiiMatch[] { return this.employeeDirectory.ids.flatMap((id) => { const start = text.indexOf(id); return start < 0 ? [] : [{ start, end: start + id.length }]; }); } } // 3. Field-level sensitive masking on DTO properties export class TicketDto { @Text({ prompt: "the customer's account", sensitive: PiiAction.replacePart({ keepLast: 4 }), }) account!: string; } // 4. Input and output guardrails (DecidedBy.Model and DecidedBy.Code) export class ClinicExecutionContext extends ExecutionContext { @OneOf({ values: ["en", "he"] }) locale!: "en" | "he"; } @Injectable() export class RefusalTextService { legalAdvice(locale: "en" | "he"): Promise { return Promise.resolve(locale === "he" ? "אין ייעוץ משפטי." : "No legal advice."); } } @InputGuardrail({ name: "legal-advice-guardrail", description: "Blocks requests for legal advice", prompt: "Does the message ask for legal advice?", model: "typesafe/jev-1.13", threshold: 0.8, deps: [ClinicExecutionContext, RefusalTextService], }) export class LegalAdviceGuardrail implements GuardrailRefusal { constructor( private readonly clinicExecutionContext: ClinicExecutionContext, private readonly refusalTextService: RefusalTextService, ) {} refusal(): Promise { return this.refusalTextService.legalAdvice(this.clinicExecutionContext.locale); } } @OutputGuardrail({ name: "no-legal-advice-guardrail", description: "Blocks answers that give legal advice", prompt: "Does the answer give legal advice?", threshold: 0.8, refusal: "I can't give legal advice. Please contact a lawyer.", }) export class NoLegalAdviceGuardrail {} ``` test/customer.pii-policy.test.ts ```ts import { expect, it } from "vitest"; import { testPii } from "@agboss/core/testing"; import { EmployeeIdPiiPolicy } from "../src/policies/customer.pii-policy.js"; it("redacts EMP-42 employee badges before model calls and restores them on reveal", async () => { const pii = testPii([EmployeeIdPiiPolicy]); const { output: masked } = await pii.mask("Badge EMP-42 requested access."); expect(masked).not.toContain("EMP-42"); expect(pii.reveal(masked)).toBe("Badge EMP-42 requested access."); }); ``` ## Deterministic Code Guardrails (`DecidedBy.Code`) [Section titled “Deterministic Code Guardrails (DecidedBy.Code)”](#deterministic-code-guardrails-decidedbycode) When a safety rule is deterministic — such as a tenant-specific blocklist from a database, a regex/checksum check, or an external moderation microservice — declare `decidedBy: DecidedBy.Code` on [`@InputGuardrail` or `@OutputGuardrail`](/reference/guardrail/) and implement `GuardrailHandler.check(text)`. Code guardrails receive the PII-masked text, support full DI (`deps: [...]`), emit `GuardrailStartEvent` / `GuardrailEndEvent` with `$0.00` cost, and require no LLM call: src/policies/tenant-blocklist.guardrail.ts ```ts import { DecidedBy, GuardrailVerdict, Injectable, OutputGuardrail, TenantContext, type GuardrailHandler, } from "@agboss/core"; import { OneOf } from "@agboss/core/dto"; export class ShopTenantContext extends TenantContext { @OneOf({ values: ["toys", "tools"] }) shop!: "toys" | "tools"; } @Injectable() export class BlocklistService { containsBlockedTerms(shop: "toys" | "tools", text: string): Promise { const blocked = shop === "toys" ? ["knife"] : ["toy"]; return Promise.resolve(blocked.some((term) => text.toLowerCase().includes(term))); } } @OutputGuardrail({ name: "tenant-blocklist-guardrail", description: "Blocks replies containing terms forbidden for the active tenant", decidedBy: DecidedBy.Code, refusal: "The response contained restricted terms.", deps: [ShopTenantContext, BlocklistService], }) export class TenantBlocklistGuardrail implements GuardrailHandler { constructor( private readonly tenant: ShopTenantContext, private readonly blocklist: BlocklistService, ) {} async check(text: string): Promise { const hasForbidden = await this.blocklist.containsBlockedTerms(this.tenant.shop, text); return hasForbidden ? GuardrailVerdict.blocked() : GuardrailVerdict.passed(); } } ``` test/tenant-blocklist.guardrail.test.ts ```ts import { expect, it } from "vitest"; import { testGuardrail } from "@agboss/core/testing"; import { ShopTenantContext, TenantBlocklistGuardrail, } from "../src/policies/tenant-blocklist.guardrail.js"; it("blocks restricted tenant terms deterministically without a model stub", async () => { const check = testGuardrail(TenantBlocklistGuardrail, { tenantContext: ShopTenantContext, tenant: { tenantId: "t-1", shop: "toys" }, }); const blocked = await check("We recommend a pocket knife."); expect(blocked.output.isBlocked).toBe(true); expect(blocked.output.refusal).toBe("The response contained restricted terms."); }); ``` ## How PII inheritance and unit testing work [Section titled “How PII inheritance and unit testing work”](#how-pii-inheritance-and-unit-testing-work) * **Hierarchical policy lists (`piiPolicies` & `excludePiiPolicies`):** Policies listed on `@Application`, `@Workflow`, `@Agent`, and `@Router` add up automatically. An agent can opt out of an inherited policy for its own model calls via `excludePiiPolicies`, while logs, traces, and `result.record` remain masked by the application and workflow policies. * **Cross-instance token consistency:** Set `@Application({ pseudonymKey: "piiPseudonymKey" })` pointing to a `Secret` field in your environment contract so every replica generates identical HMAC tokens for logs and traces. * **Unit testing with `testPii` and `testGuardrail`:** Validate `mask(text)` and `reveal(text)` offline with `testPii([CustomerPiiPolicy])` and verify both `DecidedBy.Model` and `DecidedBy.Code` guardrails with `testGuardrail(TenantBlocklistGuardrail)` from `@agboss/core/testing`. * **Standalone executable proof:** 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 end-to-end HMAC token roundtrips (`` $\to$ `revealPii: true` in `IssueRefundTool`), `PiiRefusedError` boundary refusal, `@ToolInterceptor` policy enforcement, and `DecidedBy.Code` / `DecidedBy.Model` guardrails (see [7 Verified Examples](/examples/job-scout/)). # Loop 0–1: Deterministic Harness, 10 Unit Runners & VCR Lock > Verify graph and mesh contracts in Loop 0 (agboss check --federation) and harness logic in Loop 1 (npm test) with testWith, 10 unit runners, and VCR locks. In the AGBoss [5-Loop Cost & Determinism Verification Funnel](/concepts/verification-funnel/), **Loop 0 (`$ agboss check --federation`, `< 1s`, `$0`)** proves graph topology and cross-service mesh contracts at compile time, while **Loop 1 (`npm test`, `~3s`, `$0`)** tests your entire TypeScript harness (`@Action`, `@Tool`, `@ToolInterceptor`, `DecidedBy.Code` `@Router` and `@Arbiter`, `@PiiPolicy`, `@Compaction`, queues, retries, and LIFO sagas) without calling a live LLM. Together, `@agboss/core/testing` provides `testWith(Application)` on a deterministic `VirtualClock`, ten isolated component runners, `ModelFailure` chaos injection, and VCR cassettes locked to [Loop 2 live PR evals](/guides/evaluation-and-profiles/) via `promptHash` and `schemaHash` (`[vcr.stale-cassette]`). *** ## How do Loop 0 (`agboss check`) and Loop 1 (`npm test`) fit into the 5-Loop Funnel? [Section titled “How do Loop 0 (agboss check) and Loop 1 (npm test) fit into the 5-Loop Funnel?”](#how-do-loop-0-agboss-check-and-loop-1-npm-test-fit-into-the-5-loop-funnel) | Loop | Command | Latency & Cost | Objects Verified | Lock to Next Loop | | ----------------------------------------- | ----------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Loop 0: Static Proof** | `$ agboss check --federation` | `< 1s` · **`$0`** (every save & first CI step) | **Object 1 (Topology & Mesh Contracts):** Path Scope dominance (`[input.missing-on-path]`), `{ about }` staleness (`[input.outdated-on-path]`), LIFO saga compensations (`[flow.uncompensated-action]`), and Agent Mesh schemas | Emits `.agboss/types.d.ts` required for Loop 1 | | **Loop 1: Deterministic Harness & Chaos** | `npm test` (`@agboss/core/testing` + VCR) | `~3s` · **`$0`** (every commit; no live LLM) | **Object 2 (Harness Code):** 10 unit runners, full-graph `testWith(App)` with `mockLlm`, `ModelFailure` chaos (`429`, timeouts, truncated JSON), `TestConcurrency.inOrder`, and multi-pod `{ instances: 2 }` leases | **`[vcr.stale-cassette]`** (`promptHash` + `schemaHash`) triggers [Loop 2 PR Smoke Gate](/guides/evaluation-and-profiles/) whenever a prompt or DTO schema changes | *** ## Complete example: `testWith(Application)`, `testTool` & `testJudge` [Section titled “Complete example: testWith(Application), testTool & testJudge”](#complete-example-testwithapplication-testtool--testjudge) `testWith(Application)` boots your real `@Application` container and infrastructure in each test while scripting model turns via `mockLlm`, action calls via `mockAction`, tool failures via `mockTool`, and service methods via `mockOf`: src/support.workflow\.ts ```ts import { Agent, Workflow, start } from "@agboss/core"; @Agent({ name: "support-agent", description: "Answers support questions using LookupOrderTool", model: "moonshotai/kimi-k2.6", prompt: "Look up the order and answer the customer.", tools: [LookupOrderTool], }) export class SupportAgent {} @Workflow({ name: "support-workflow", version: "1.0.0", flow: [ start(SupportWorkflowStart) .returns(WorkflowStartTextDto) .toAgent(SupportAgent) .toFinish(SupportWorkflowFinish), ], }) export class SupportWorkflow {} ``` test/support.workflow\.test.ts ```ts import { expect, it } from "vitest"; import { decideWith, override, replyWith, testJudge, testTool, testWith, waitFor, } from "@agboss/core/testing"; import { seconds, usd } from "@agboss/core/units"; const test = testWith(OfferApplication); test("reviewer asks for changes, then approves", async ({ app, mockLlm, mockAction, }) => { mockLlm(ReviewerAgent) .when({ draft: { text: "v1" } }) .thenReturn(replyWith(ChangesRequestedDto, { notes: "shorter" })); mockLlm(ReviewerAgent) .when({ draft: { text: "v2" } }) .thenReturn( waitFor(seconds(1)), replyWith(DraftApprovedDto, {}, { cost: usd(0.01) }), ); mockLlm(EditorAgent).thenReturn(replyWith("v2")); await app.execute(OfferWorkflowStart, shortlist); expect(mockLlm(ReviewerAgent)).toHaveBeenCalledTimes(2); expect(mockLlm(ReviewerAgent)).toHaveReturned(ChangesRequestedDto, { notes: "shorter", }); expect(mockAction(SendOfferAction)).toHaveBeenCalledWith({ draft: { text: "v2" }, }); }); it("searches the board it is given in isolation", async () => { const runSearch = testTool(SearchJobsTool, { overrides: [override(JobBoardService, FakeJobBoardService)], }); const { output, spend } = await runSearch( { query: "TS" }, { timeout: seconds(1) }, ); expect(output.jobs).toContain("TS"); expect(spend.totalUsd).toBe(0); }); it("a grounded reply passes the decision judge", async () => { const runJudge = testJudge(GroundedJudge, { model: decideWith({ grounded: 0.9 }), }); const reply = { text: "Acme hires TS engineers [acme.example]" }; const { output } = await runJudge({ variant: PlainTextDto, reply, task: "Find TS jobs", attempt: 0, }); expect(output.passed).toBe(true); }); ``` *** ## Core principles of testing AGBoss applications [Section titled “Core principles of testing AGBoss applications”](#core-principles-of-testing-agboss-applications) 1. **Test your graph code in `npm test`, and test prompt behavior in Evals:** A scripted `mockLlm(Agent)` reply proves that **your workflow graph, DTO contracts, error edges, and compensators** behave deterministically given any sequence of model decisions. Never assert what a real LLM “would answer” inside a unit test; instead, assert what your step **sent** to the model (`mockLlm(Agent).requests`, `toHaveBeenAskedWith`) and use [Evals & Golden Sets](/guides/evaluation-and-profiles/) (`agboss eval`, `agboss compare`) to evaluate prompt changes on real models. 2. **Deterministic parallel ordering (`TestConcurrency`):** Parallel branches (`.toParallelAgents()`, `.toParallelWorkflows()`) run one at a time in declaration order by default (`TestConcurrency.Sequential`). To reproduce a specific parallel race or quorum arrival order without flaky `setTimeout` sleeps, pass `concurrency: TestConcurrency.inOrder([step(FastAgent), step(SlowAgent)])` to `testWith`. 3. **Zero wall-clock waits (`VirtualClock`):** Every timeout (`seconds(10)`), quorum `deadline`, retry backoff, and 30-second execution lease runs on the test’s shared `clock` (`VirtualClock`). Calling `await clock.advance(seconds(31))` lets started steps reach their next wait point and advances virtual time instantaneously. 4. **Multi-instance cluster testing (`{ instances: 2 }`):** Passing `testWith(App, { instances: 2 })` boots two independent application containers (`apps[0]` and `apps[1]`) sharing the same stores and `VirtualClock` so you can test cross-pod `app.resume()`, `app.cancel()`, and 30-second lease recovery in milliseconds. *** ## All 10 isolated unit runners in `@agboss/core/testing` [Section titled “All 10 isolated unit runners in @agboss/core/testing”](#all-10-isolated-unit-runners-in-agbosscoretesting) When testing a single component’s logic — such as a PII policy, a tool interceptor, a custom compaction handler, or an arbiter’s quorum voting — use the dedicated unit runner for that component type. Each runner builds a minimal container with `overrides: [override(Base, Subclass)]` and returns `{ output, spend }`: | Unit Runner | Target Component | What It Validates | | ---------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `testTool(ToolClass, opts?)` | [`@Tool`](/reference/tool/) | Input/output DTO validation, `deps` injection, timeouts (`clock.advance`), and `run(input)` result | | `testToolInterceptor(InterceptorClass, opts?)` | [`@ToolInterceptor`](/reference/tool-interceptor/) | `beforeToolCall(ToolClass, input)` (`ToolCallDecision`) and `afterToolCall(ToolClass, input, output)` DTO-validated transformation | | `testAction(ActionClass, opts?)` | [`@Action`](/reference/action/) | Deterministic step logic, `SpendRecorder` entries, and `execute(input)` output | | `testRouter(RouterClass, opts?)` | [`@Router`](/reference/router/) | Model or code route selection (`DecidedBy.Model` with `routeTo` / `DecidedBy.Code`) | | `testArbiter(ArbiterClass, opts?)` | [`@Arbiter`](/reference/arbiter/) | Quorum answer acceptance across `@OnAnswer(Agent)` or `decideWith` model criteria | | `testRag(RagClass, opts?)` | [`@Rag`](/reference/rag/) | `retrieve(query, { topK, signal })` results and reported `rag:` spend | | `testJudge(JudgeClass, opts?)` | [`@Judge`](/reference/judge/) | Quality gate `judge({ reply, task, attempt, toolCalls })` with `decideWith`, `replyWith`, or `DecidedBy.Code` | | `testCompaction(CompactionClass, opts?)` | [`@Compaction`](/reference/compaction/) | Single-step memory compaction (`CompactionInput` $\to$ `{ summaries, requests }`) across `SummaryQueue`, `SingleSummary`, or `CompactionStrategy.Custom`, verifying that critical constraints are passed and preserved | | `testPii(Policies, opts?)` | [`@PiiPolicy`](/reference/pii-policy/) | Assembly rule checks, `mask(text)` token generation, and `reveal(text)` restoration | | `testGuardrail(GuardrailClass, opts?)` | [`@InputGuardrail` / `@OutputGuardrail`](/reference/guardrail/) | Pre-model and post-model safety refusal checks (`DecidedBy.Model` with `decideWith` or `DecidedBy.Code` with `GuardrailHandler`) | *** ## Tool-Call Trajectories (`ToolCallRecord`), Order-Independent Matchers & Silent Failure Detection [Section titled “Tool-Call Trajectories (ToolCallRecord), Order-Independent Matchers & Silent Failure Detection”](#tool-call-trajectories-toolcallrecord-order-independent-matchers--silent-failure-detection) ### Why you should never test a rigid sequence of tool calls [Section titled “Why you should never test a rigid sequence of tool calls”](#why-you-should-never-test-a-rigid-sequence-of-tool-calls) In an agentic tool loop, an LLM often has two or more independent lookup tools (for example, `GetOrderTool` and `CheckPolicyTool`). Whether the model calls `GetOrderTool` before `CheckPolicyTool`, calls `CheckPolicyTool` first, or calls both in the same turn is an irrelevant execution detail. Asserting `calls[0] === "get_order" && calls[1] === "check_policy"` creates brittle tests that break whenever a model update reorders independent lookups. Instead, AGBoss verifies **trajectory invariants** over sets and argument predicates: * **`expect(result).toHaveCalledTools([GetOrderTool, CheckPolicyTool])`:** Verifies the **required tool set** — every listed tool was called at least once, regardless of call order. * **`expect(result).not.toHaveCalledTools([DeleteOrderTool])`:** Verifies the **forbidden tool set** — none of the listed tools were invoked at any point in the execution. * **`expect(result).toHaveToolCallMatching(GetOrderTool, (input) => input.orderId === "555")`:** Verifies **argument invariants** (accepts either a predicate `(input) => boolean` or a partial object `{ orderId: "555" }`). * **`StepRecord.toolCalls` & `JudgeInput.toolCalls` (`ReadonlyArray`):** Every `ExecutionOutput`, `StepRecord`, and `JudgeInput` carries the full `readonly toolCalls: readonly ToolCallRecord[]` trajectory (narrowable by tool via `toolCallsOf(GetOrderTool, input.toolCalls)`). This allows a `@Judge` to catch **“silent failures”** inside the agent loop — such as an agent calling `GetOrderTool` on another customer’s order (`orderId: "999"`), reading their private data into context, and then answering the current customer (`orderId: "555"`) politely. *** ## Chaos & Invalid-Turn Scripting (`callToolWithInvalidArguments`, `replyWithInvalidAnswer`, `ModelFailure`) [Section titled “Chaos & Invalid-Turn Scripting (callToolWithInvalidArguments, replyWithInvalidAnswer, ModelFailure)”](#chaos--invalid-turn-scripting-calltoolwithinvalidarguments-replywithinvalidanswer-modelfailure) Real language models occasionally emit malformed JSON, truncated tool arguments, schema-violating answers, or HTTP `429` / timeout failures. `@agboss/core/testing` lets you script these exact faults in `Loop 1` (`$0`) to prove your harness self-corrects: * **`callToolWithInvalidArguments(ToolClass, invalidFields)`:** Scripts a tool call whose arguments violate the tool’s input `@Dto` (`ToolCallStatus.InvalidArguments`), verifying that the agent loop feeds the validation error back to the model and succeeds on the next turn. * **`callToolWithTruncatedArguments(ToolClass, partialArgs)`:** Simulates a tool call cut off mid-stream (`maxOutputTokens` truncation) so you can verify retry recovery. * **`replyWithInvalidAnswer(DtoClass, invalidFields)`:** Scripts an invalid structured final answer, incrementing `step.validationAttempts` on the `StepRecord` and throwing `AgentAnswerInvalidError` once retries are exhausted. * **`failWith(ModelFailure.RateLimited)` / `Timeout` / `ServerError` / `NetworkError`:** Simulates transport-level provider failures to test retry backoff on `VirtualClock` and `.onError()` workflow recovery edges. * **`a2aServer({ expose, callerResolver })`:** Boots an in-process loopback HTTP server for your `@Application` inside `testWith` so you can test `A2AClient` calls, `A2ACallerResolver` `401`/`403` rejections (`A2ACallerRejectedError`, where `code` is `"a2a.caller-rejected"` and `wireCode` holds the server’s code `"a2a.caller-unauthenticated"` or `"a2a.caller-forbidden"`), and signed `MeshEnvelope` propagation without opening external sockets. * **Multi-instance tests (`testWith(App, { instances: 2 })`, `#458`):** When `instances: 2` is passed, `testWith` shares the default `InMemory*` stores and `VirtualClock` across all instances automatically. If your application lists `Sqlite*` stores, note that `sqlitePath: ":memory:"` opens a **separate** SQLite in-memory database per instance connection; to test multiple instances sharing one SQLite database, point `sqlitePath` in a test environment (`testWith(App, { instances: 2, env: "instances" })`) at a temporary file path (`mkdtempSync`). test/trajectory-and-chaos.test.ts ```ts import { expect } from "vitest"; import { callTool, callToolWithInvalidArguments, replyWith, replyWithInvalidAnswer, testWith, } from "@agboss/core/testing"; // Code @Judge inspecting JudgeInput.toolCalls to catch silent cross-order reads: @Judge({ name: "no-cross-order-read-judge", description: "Fails if the agent inspected any order other than 555 during its tool loop", decidedBy: DecidedBy.Code, reply: OrderIdDto, }) export class NoCrossOrderReadJudge implements JudgeHandler { judge(input: JudgeInput): Promise { const orderCalls = toolCallsOf(GetOrderTool, input.toolCalls); const leaked = orderCalls.some( (call) => call.status === ToolCallStatus.Succeeded && call.input.orderId !== "555", ); return Promise.resolve( leaked ? JudgeVerdict.failed({ reason: "Agent read an unauthorized order ID during tool loop." }) : JudgeVerdict.passed(), ); } } const test = testWith(SupportTestApp); test("verifies trajectory invariants and self-correction after invalid tool args & answer", async ({ app, mockLlm, }) => { mockLlm(OrderSupportAgent).thenReturn( callToolWithInvalidArguments(GetOrderTool, { orderId: 12345 }), callTool(CheckPolicyTool, { orderId: "555" }), callTool(GetOrderTool, { orderId: "555" }), replyWithInvalidAnswer(OrderIdDto, { orderId: null }), replyWith(OrderIdDto, { orderId: "555" }), ); const result = await app.execute(OrderSupportStart, orderStartInput); expect(result).toHaveCalledTools([GetOrderTool, CheckPolicyTool]); expect(result).not.toHaveCalledTools([DeleteOrderTool]); expect(result).toHaveToolCallMatching(GetOrderTool, (input) => input.orderId === "555"); }); ``` *** ## Recorded VCR Cassettes & Stale-Cassette Guard (`[vcr.stale-cassette]`) [Section titled “Recorded VCR Cassettes & Stale-Cassette Guard (\[vcr.stale-cassette\])”](#recorded-vcr-cassettes--stale-cassette-guard-vcrstale-cassette) Between scripted unit tests and live evaluation sits **VCR recording**: `testWith(Application, { vcr: { cassetteName: "onboarding", mode: process.env.CI ? VCRMode.REPLAY : VCRMode.AUTO } })` records real model calls once to `__snapshots__/onboarding.cassette.json` and replays them deterministically in milliseconds in CI (`VCRMode.REPLAY`). Each recorded cassette (version 2) embeds the component’s **`promptHash`** and **`schemaHash`**. If an engineer or coding agent edits an `@Agent`’s prompt or changes a `@Dto` / `@Tool` schema: 1. **`npx agboss check --cassettes` (in Loop 0) and `VCRMode.REPLAY` (in Loop 1)** fail immediately with `StaleCassetteError` (**`[vcr.stale-cassette]`**, reporting `CassetteChange.Prompt`, `Schema`, or `Removed`) instead of silently passing against an outdated prompt snapshot. 2. **Surgical re-recording (`VCRMode.RECORD_STALE`):** Run `AGBOSS_VCR_MODE=RECORD_STALE npm test` to replay all unchanged steps from the cassette while re-recording **only** the components whose `promptHash` or `schemaHash` changed, then trigger [Loop 2 (`agboss eval run`)](/guides/evaluation-and-profiles/) to verify the updated behavior. *** ## Symmetric Spring-Style `tests/` Layout & Reference Examples [Section titled “Symmetric Spring-Style tests/ Layout & Reference Examples”](#symmetric-spring-style-tests-layout--reference-examples) All 7 reference applications in **[`agbossjs/agboss-examples`](https://github.com/agbossjs/agboss-examples)** (`01-hello-agboss` through `07-langgraph-migration`) organize `tests/` as a 1-to-1 mirror of `src/` (`tests/services/`, `tests/tools/`, `tests/actions/`, `tests/routers/`, `tests/guardrails/`, `tests/interceptors/`, `tests/http/`, `tests/workflows//`). Inspect [`01-hello-agboss`](https://github.com/agbossjs/agboss-examples/tree/main/01-hello-agboss) and [`03-support-refund-desk`](https://github.com/agbossjs/agboss-examples/tree/main/03-support-refund-desk) for complete unit runner and `testWith` suites (see [7 Verified Examples](/examples/job-scout/)). # @A2AAgent, @A2AExpose, a2aHttpListener & A2ACallerResolver > Complete API reference for @A2AAgent, @A2AExpose, A2AClient, a2aHttpListener, A2ACallerResolver, and wireCode error handling in the AGBoss Agent Mesh. The `@A2AAgent` and `@A2AExpose` decorators, `a2aHttpListener`, and `A2ACallerResolver` (`"@agboss/core/a2a"`) provide both the client and server sides of the multi-tenant Agent-to-Agent (A2A) and federated Agent Mesh protocol in AGBoss, enabling strongly typed cross-service workflow calls with signed `MeshEnvelope` budget, deadline, and call-chain propagation: * **`@A2AAgent`** (`a2a/*.a2a-agent.ts`) defines a typed contract client (`extends A2AClient`) for a remote A2A workflow (`target: { application, start }`, `input`, `output`, `price?`), registered in `@Workflow({ providers: [RemoteA2AAgent] })` and injected into any `@Tool` or `@Action` via `deps: [RemoteA2AAgent]`. * **`@A2AExpose`** (`a2a/*.a2a-expose.ts`) declares the `@WorkflowStart` entry points (`exports: [Start]`) that an `@Application({ exposes: [AppA2AExpose] })` serves over A2A and exports in `agboss describe --json` for `agboss check --federation`. * **`a2aHttpListener(app, { expose, callerResolver, meshVerifier?, base })`** and **`A2ACallerResolver`** (`a2a/*.a2a-caller-resolver.ts`) serve an `@A2AExpose` contract over HTTP (`POST /execute`, `/resume`, `/cancel`), resolving the authenticated `tenant`, `ownerId`, `execution`, and `peer` from an `@Injectable()` resolver in the DI container before reading any request body. ## Complete `@A2AAgent`, `@A2AExpose`, `A2ACallerResolver`, `a2aHttpListener` & `a2aServer` example [Section titled “Complete @A2AAgent, @A2AExpose, A2ACallerResolver, a2aHttpListener & a2aServer example”](#complete-a2aagent-a2aexpose-a2acallerresolver-a2ahttplistener--a2aserver-example) src/a2a/billing-specialist.a2a-agent.ts ```ts import { Action, ExecutionContext, type ActionHandler, type InputOf, type OutputOf, } from "@agboss/core"; import { A2AAgent, A2AClient, type A2ACallOptions } from "@agboss/core/a2a"; import { Text } from "@agboss/core/dto"; import { usd } from "@agboss/core/units"; export class TaxAuditInputDto { @Text({ prompt: "Invoice ID to audit" }) invoiceId!: string; } export class TaxAuditOutputDto { @Text({ prompt: "Remote tax compliance verdict" }) verdict!: string; } // 1. Declare the remote @A2AAgent contract (registered in @Workflow({ providers: [RemoteTaxAuditorA2AAgent] })): @A2AAgent({ name: "remote-tax-auditor-a2a-agent", url: "https://tax-agent.internal.acme.com/a2a", target: { application: "support-escalation-application", start: "tax-audit-workflow-start", }, input: TaxAuditInputDto, output: TaxAuditOutputDto, price: usd(0.002), deps: [ExecutionContext], }) export class RemoteTaxAuditorA2AAgent extends A2AClient { constructor(private readonly executionContext: ExecutionContext) { super(); } protected override getHeaders(): Promise> { return Promise.resolve({ "idempotency-key": this.executionContext.step.idempotencyKey, "x-tenant-id": this.executionContext.tenant.tenantId, }); } } // 2. Inject RemoteTaxAuditorA2AAgent into an @Action or @Tool: @Action({ name: "delegate-tax-audit-action", description: "Delegates invoice tax verification to the remote A2A tax auditor service", input: { auditRequest: TaxAuditInputDto, }, output: TaxAuditOutputDto, deps: [RemoteTaxAuditorA2AAgent, ExecutionContext], }) export class DelegateTaxAuditAction implements ActionHandler { constructor( private readonly remoteTaxAuditor: RemoteTaxAuditorA2AAgent, private readonly executionContext: ExecutionContext, ) {} async execute(input: InputOf): Promise> { const call: A2ACallOptions = { signal: this.executionContext.signal }; return this.remoteTaxAuditor.execute(input.auditRequest.value, call); } } ``` src/a2a/tax-auditor.a2a-caller-resolver.ts ```ts import { Injectable } from "@agboss/core"; import { A2ACallerResolution, A2ARoute, type A2ACallerRequest, type A2ACallerResolver, } from "@agboss/core/a2a"; import type { TaxAuditWorkflowStart } from "../workflows/support-escalation.workflow.js"; // 3. @Injectable() caller resolver registered in @Workflow({ providers: [ShopA2aCallerResolver] }): @Injectable() export class ShopA2aCallerResolver implements A2ACallerResolver { resolveCaller( request: A2ACallerRequest, ): A2ACallerResolution { const auth = request.http.headers.authorization ?? ""; if (!auth.startsWith("Bearer ")) { return A2ACallerResolution.unauthenticated({ reason: "Missing Bearer token", challenge: 'Bearer realm="a2a"', }); } const tenantId = request.http.headers["x-tenant-id"]; if (typeof tenantId !== "string" || tenantId.length === 0) { return A2ACallerResolution.unauthenticated({ reason: "Missing x-tenant-id header" }); } if (request.route === A2ARoute.Resume && auth === "Bearer read-only-key") { return A2ACallerResolution.forbidden({ reason: "Read-only key cannot answer workflow pauses", }); } return A2ACallerResolution.resolved({ tenant: { tenantId }, ownerId: `client-${tenantId}`, peer: request.signedBy, }); } } ``` src/workflows/support-escalation.workflow\.ts ```ts import { Workflow, WorkflowFinish, WorkflowStart, start } from "@agboss/core"; import { A2AExpose, a2aHttpListener, type A2AAdapter, type A2AApp } from "@agboss/core/a2a"; import { DelegateTaxAuditAction, RemoteTaxAuditorA2AAgent, TaxAuditInputDto, TaxAuditOutputDto, } from "../a2a/billing-specialist.a2a-agent.js"; import { ShopA2aCallerResolver } from "../a2a/tax-auditor.a2a-caller-resolver.js"; @WorkflowStart({ name: "tax-audit-workflow-start", description: "Starts remote tax audit delegation", input: TaxAuditInputDto, }) export class TaxAuditWorkflowStart {} @WorkflowFinish({ name: "tax-audit-workflow-finish", description: "Returns remote tax compliance verdict", output: TaxAuditOutputDto, }) export class TaxAuditWorkflowFinish {} @Workflow({ name: "support-escalation-workflow", version: "1.0.0", providers: [RemoteTaxAuditorA2AAgent, ShopA2aCallerResolver], flow: [ start(TaxAuditWorkflowStart) .returns(TaxAuditInputDto) .toAction(DelegateTaxAuditAction) .returns(TaxAuditOutputDto) .toFinish(TaxAuditWorkflowFinish), ], }) export class SupportEscalationWorkflow {} // 4. Declare the @A2AExpose contract (listed in @Application({ exposes: [TaxAuditA2AExpose] })): @A2AExpose({ name: "tax-audit-a2a-expose", version: "1.0.0", exports: [TaxAuditWorkflowStart], }) export class TaxAuditA2AExpose {} // 5. Multi-tenant HTTP listener resolving ShopA2aCallerResolver from the app container: export function createTaxA2AListener(app: A2AApp, _adapter?: A2AAdapter) { return a2aHttpListener(app, { expose: TaxAuditA2AExpose, callerResolver: ShopA2aCallerResolver, base: "/a2a", }); } ``` test/billing-specialist.a2a.test.ts ```ts import { expect } from "vitest"; import { A2ACallerRejectedError, A2AClient } from "@agboss/core/a2a"; import { override, testAction, testWith } from "@agboss/core/testing"; import { DelegateTaxAuditAction, RemoteTaxAuditorA2AAgent, type TaxAuditInputDto, type TaxAuditOutputDto, } from "../src/a2a/billing-specialist.a2a-agent.js"; import { ShopA2aCallerResolver } from "../src/a2a/tax-auditor.a2a-caller-resolver.js"; import { SupportEscalationApplication } from "../src/escalation.application.js"; import { TaxAuditA2AExpose, TaxAuditWorkflowStart, } from "../src/workflows/support-escalation.workflow.js"; class FakeTaxAuditorA2AAgent extends RemoteTaxAuditorA2AAgent { override execute(_input: TaxAuditInputDto): Promise { return Promise.resolve({ verdict: "Invoice #INV-9102 compliant." }); } } const test = testWith(SupportEscalationApplication, { overrides: [override(RemoteTaxAuditorA2AAgent, FakeTaxAuditorA2AAgent)], }); test("rejects unauthenticated A2A requests via a2aServer and delegates when authenticated", async ({ a2aServer, }) => { const { url } = await a2aServer({ expose: TaxAuditA2AExpose, callerResolver: ShopA2aCallerResolver, }); // 1. Unauthenticated caller receives 401 A2ACallerRejectedError before any workflow step runs: const unauthed = new A2AClient({ url }).send({ invoiceId: "INV-9102" }); await expect(unauthed).rejects.toFailWith(A2ACallerRejectedError, { httpStatus: 401, code: "a2a.caller-rejected", wireCode: "a2a.caller-unauthenticated", }); // 2. Unit-test the delegating @Action in isolation: const runAction = testAction(DelegateTaxAuditAction, { overrides: [override(RemoteTaxAuditorA2AAgent, FakeTaxAuditorA2AAgent)], }); const { output } = await runAction({ auditRequest: { producer: TaxAuditWorkflowStart, value: { invoiceId: "INV-9102" }, }, }); expect(output.verdict).toContain("INV-9102"); }); ``` ## What options do `@A2AAgent`, `@A2AExpose`, and `a2aHttpListener` accept? [Section titled “What options do @A2AAgent, @A2AExpose, and a2aHttpListener accept?”](#what-options-do-a2aagent-a2aexpose-and-a2ahttplistener-accept) | API / Decorator | Option | Type | Required | Description | | ------------------------------- | ------------------------------ | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@A2AAgent` | `name` | `string` | **Yes** | Unique kebab-case identifier ending with `-a2a-agent` (`[name.kind-suffix]`). | | `@A2AAgent` | `url` | `string` | **Yes** | Base URL of the remote A2A endpoint (`"https://billing.internal.example.com/a2a"`), serving `POST /execute`, `/resume`, and `/cancel`. | | `@A2AAgent` | `target` | `{ application: string; start: string }` | **Yes** | Declared contract target (`[a2a.target-required]`), verified against peer snapshots by `agboss check --federation`. | | `@A2AAgent` | `input` / `output` | `DtoClass` | **Yes** | Request and response DTO contracts validated on `execute(input, call?)` and exported with `schemaHash` in `agboss describe --json`. | | `@A2AAgent` | `price` | `Usd` | No | Declared fallback cost per remote call (`usd(0.002)`); the step records the larger of `price` and a trusted peer’s signed `MeshRunReport`. | | `@A2AAgent` | `deps` | `readonly Token[]` | No | Constructor dependencies (`ExecutionContext`, `TenantContext`, or `@Injectable` auth services) used inside `protected override getHeaders()`. | | `@A2AExpose` | `name` / `version` / `exports` | `string` / `readonly StartClass[]` | **Yes** | Declares the `@WorkflowStart` classes (`-a2a-expose`) that `@Application({ exposes })` serves over A2A (`[application.expose-unknown-start]`, `[application.expose-unlisted]`). | | `a2aHttpListener(app, options)` | `expose` | `Class` | **Yes** | The `@A2AExpose` class listed in `@Application({ exposes })` served at this A2A endpoint. | | `a2aHttpListener(app, options)` | `callerResolver` | `Class>` | **Yes** | An `@Injectable()` class listed in `@Application({ providers })` or `@Workflow({ providers })` (`[a2a.caller-resolver-not-listed]` if omitted). | | `a2aHttpListener(app, options)` | `meshVerifier` | `Class` | No | Optional `@Injectable()` service implementing `verifyKeyOf(issuer)` to resolve peer Ed25519 SPKI public keys dynamically alongside `@Application({ mesh: { peers } })`. | | `a2aHttpListener(app, options)` | `base` | `string` | No | URL path prefix (`"/a2a"` by default). | ## `A2ACallerResolution`, `A2ACallerRejectedError`, `a2aServer` & the Signed Agent Mesh Envelope [Section titled “A2ACallerResolution, A2ACallerRejectedError, a2aServer & the Signed Agent Mesh Envelope”](#a2acallerresolution-a2acallerrejectederror-a2aserver--the-signed-agent-mesh-envelope) | Related Entity | Import Path | Purpose | | ---------------------------------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `A2ACallerResolution.resolved({ tenant, ownerId, execution, peer })` | `"@agboss/core/a2a"` | Returned by `resolveCaller(request: A2ACallerRequest)` to bind the verified `tenant`, `ownerId` (thread ownership), `execution` context values, and trusted `peer`. | | `A2ACallerResolution.unauthenticated({ reason, challenge? })` / `.forbidden({ reason })` | `"@agboss/core/a2a"` | Rejects the request before reading the body or starting an execution with HTTP `401` (`a2a.caller-unauthenticated`) or HTTP `403` (`a2a.caller-forbidden`), thrown on the client as `A2ACallerRejectedError`. | | `A2ACallerRejectedError` | `"@agboss/core/a2a"` | Thrown by `A2AClient` on `401` / `403`: `err.code` is always `"a2a.caller-rejected"`, `err.httpStatus` is `401` or `403`, and `err.wireCode` carries the server’s specific rejection code (`"a2a.caller-unauthenticated"` or `"a2a.caller-forbidden"`). | | `a2aServer({ expose, callerResolver, meshVerifier? })` | `"@agboss/core/testing"` | `testWith` fixture that serves the test’s `app` on `127.0.0.1:0` and returns `{ url }` for `new A2AClient({ url, headers })`. | | **Agent Mesh Envelope (`MeshEnvelope`, `agboss-mesh`)** | `"@agboss/core"` | JWS EdDSA (Ed25519) envelope header (`_meta["agboss.dev/mesh"]` over MCP, plus W3C `traceparent`) carrying `origin` (`executionId`, `parentExecutionId`, `workflow`, `step.idempotencyKey`), `budget.remaining`, `deadline.remaining`, and `callChain` (`MeshHop[]`), plus signed `MeshRunReport` spend attribution (`ToolCallRecord.remote`). | | `MeshLoopDetectedError` & `HopsLimitExceededError` | `"@agboss/core"` | Thrown before executing a step when `callChain` contains the current `(application, start)` pair (`limit.mesh-loop`) or reaches `@Application({ maxMeshHops })` (`8` by default, `limit.hops`). | | `$ agboss check --federation ` & `$ agboss hub` | CLI | Verifies cross-repository `@A2AAgent` / `@McpServer` contracts against `agboss describe --json` snapshots (`--federation`) and renders the global Agent Mesh graph, cross-service execution trees, Progressive Crystallization Radar, and Blast Radius (`agboss hub show --html` / `agboss hub ui`). | # @Action > Complete API reference for the @Action decorator in AGBoss — deterministic workflow graph nodes, ActionHandler, DI, idempotency, and testAction(). The `@Action` decorator defines a deterministic code node (`actions/*.action.ts`) in a `@Workflow` graph that runs without calling an LLM. Actions read typed DTOs from the workflow’s Path Scope, inject `@Injectable` services and `ExecutionContext` through their constructor, and either return a new DTO to the path (`.returns(OutputDto)`) or perform a side effect (`Promise`) before transitioning to the next node. ## Complete `@Action` examples (value-producing and side-effect-only) [Section titled “Complete @Action examples (value-producing and side-effect-only)”](#complete-action-examples-value-producing-and-side-effect-only) src/actions/send-offer.action.ts ```ts import { Action, ExecutionContext, SpendRecorder, optional, type ActionHandler, type InputOf, type OutputOf, } from "@agboss/core"; import { Text } from "@agboss/core/dto"; import { usd } from "@agboss/core/units"; import { MailerService } from "../services/mailer.service.js"; import { DraftApprovedDto, OfferDraftDto } from "../dto/offer.dto.js"; export class DeliveryReceiptDto { @Text({ prompt: "External message provider receipt ID" }) receiptId!: string; } // 1. Value-producing @Action (declares output -> wired with .toAction(SendOfferAction).returns(DeliveryReceiptDto)): @Action({ name: "send-offer-action", description: "Sends the approved offer letter via the mailer service with an idempotency key", input: { draft: OfferDraftDto, approval: DraftApprovedDto, }, output: DeliveryReceiptDto, deps: [ExecutionContext, SpendRecorder, MailerService], }) export class SendOfferAction implements ActionHandler { constructor( private readonly executionContext: ExecutionContext, private readonly spendRecorder: SpendRecorder, private readonly mailerService: MailerService, ) {} async execute(input: InputOf): Promise> { // Single-value inputs in InputOf are wrapped in Produced ({ value, producer }): const receiptId = await this.mailerService.sendDocument({ body: input.draft.value.letterText, idempotencyKey: this.executionContext.step.idempotencyKey, signal: this.executionContext.signal, }); this.spendRecorder.record({ service: "docusign-api", cost: usd(0.05) }); return { receiptId }; } } // 2. Side-effect-only @Action (no output -> wired directly with .toAction(NotifyRecruiterAction).toFinish(...)): @Action({ name: "notify-recruiter-action", description: "Alerts the recruiting channel when an offer loop fails", input: { draft: optional(OfferDraftDto), }, deps: [MailerService], }) export class NotifyRecruiterAction implements ActionHandler { constructor(private readonly mailerService: MailerService) {} async execute(input: InputOf): Promise { await this.mailerService.notifySlack( `Manual review needed for draft: ${input.draft?.value.letterText ?? "none"}`, ); } } ``` src/services/mailer.service.ts ```ts import { Injectable } from "@agboss/core"; @Injectable() export class MailerService { sendDocument(opts: { body: string; idempotencyKey: string; signal: AbortSignal; }): Promise { return Promise.resolve(`msg_${opts.idempotencyKey}`); } notifySlack(_message: string): Promise { return Promise.resolve(); } } ``` src/dto/offer.dto.ts ```ts import { Text } from "@agboss/core/dto"; export class OfferDraftDto { @Text({ prompt: "Offer letter text" }) letterText!: string; } export class DraftApprovedDto { @Text({ prompt: "Approval summary" }) summary!: string; } ``` src/workflows/offer-delivery.workflow\.ts ```ts import { StepFailedError, Workflow, WorkflowFinish, WorkflowStart, start, toAction, } from "@agboss/core"; import { DeliveryReceiptDto, NotifyRecruiterAction, SendOfferAction, } from "../actions/send-offer.action.js"; import { DraftApprovedDto, OfferDraftDto } from "../dto/offer.dto.js"; import { MailerService } from "../services/mailer.service.js"; @WorkflowStart({ name: "offer-delivery-workflow-start", description: "Starts offer delivery", input: { draft: OfferDraftDto, approval: DraftApprovedDto, }, }) export class OfferDeliveryStart {} @WorkflowFinish({ name: "offer-delivery-workflow-finish", description: "Returns delivery receipt", output: DeliveryReceiptDto, }) export class OfferDeliveryFinish {} @WorkflowFinish({ name: "offer-alerted-workflow-finish", description: "Finishes after recruiter alert", output: OfferDraftDto, }) export class OfferAlertedFinish {} @Workflow({ name: "offer-delivery-workflow", version: "1.0.0", providers: [MailerService], flow: [ start(OfferDeliveryStart) .returns(OfferDraftDto) .returns(DraftApprovedDto) .toAction(SendOfferAction) .onError( StepFailedError, toAction(NotifyRecruiterAction) .toFinish(OfferAlertedFinish), ) .returns(DeliveryReceiptDto) .toFinish(OfferDeliveryFinish), ], }) export class OfferDeliveryWorkflow {} ``` test/send-offer.action.test.ts ```ts import { expect, it } from "vitest"; import { override, testAction } from "@agboss/core/testing"; import { SendOfferAction } from "../src/actions/send-offer.action.js"; import { OfferDeliveryStart } from "../src/workflows/offer-delivery.workflow.js"; import { MailerService } from "../src/services/mailer.service.js"; class FakeMailerService extends MailerService { override sendDocument(): Promise { return Promise.resolve("msg-9001"); } } it("sends the approved draft and returns the receipt DTO", async () => { const runAction = testAction(SendOfferAction, { overrides: [override(MailerService, FakeMailerService)], }); const { output, spend } = await runAction({ draft: { producer: OfferDeliveryStart, value: { letterText: "Welcome to Acme!" } }, approval: { producer: OfferDeliveryStart, value: { summary: "Approved by VP" } }, }); expect(output).toEqual({ receiptId: "msg-9001" }); expect(spend.totalUsd).toBeCloseTo(0.05); }); ``` ## What options does `@Action` accept? [Section titled “What options does @Action accept?”](#what-options-does-action-accept) Every `@Action` class name ends with `Action` and its `name` option ends with `-action`: | Option | Type | Required | Description | | ---------------- | --------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Yes | Unique kebab-case identifier ending with `-action` (e.g., `"send-offer-action"`). | | `description` | `string` | Yes | Human-readable summary of what the action does for Studio graphs and logs. | | `input` | `InputFields` | No | Map of DTOs read from the workflow’s Path Scope (`{ draft: OfferDraftDto, approval: optional(DraftApprovedDto) }`). | | `output` | `OutputShape` | No | DTO class, `listOf(Dto)`, or `oneOf(A, B)` returned by `execute()`. Omit `output` for side-effect-only actions returning `Promise`. | | `compensateWith` | `Type` | No | Deterministic `@Action` class that reverses this action’s external side effect on error paths (verified by `agboss check` via `[flow.uncompensated-action]`) and automatically in LIFO order on `app.cancel`, `FailFast`, quorum `Surplus`, or `@Budget` abort. | | `committedBy` | `Type` | No | Optional downstream `@Action` class that finalizes the side effect so subsequent errors or cancellations no longer trigger `compensateWith`. | | `deps` | `readonly Token[]` | No | Constructor dependencies (`ExecutionContext`, `SpendRecorder`, `Clock`, `@Injectable` services) verified against constructor parameters at compile time. | ## What related entities work with `@Action`? [Section titled “What related entities work with @Action?”](#what-related-entities-work-with-action) | Related Entity | Import Path | Purpose | | ------------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `ActionHandler` | `"@agboss/core"` | Interface implemented by every `@Action` class (`execute(input: InputOf): Promise>`). | | `InputOf` & `OutputOf` | `"@agboss/core"` | Type helpers generated by `agboss check` (`.agboss/types.d.ts`) that type `execute(input)` from the decorator’s `input` and `output`. | | `.toAction(ActionClass)` & `action(ActionClass)` | `"@agboss/core"` | HarnessDSL (HDSL) flow methods for chaining an action on a path or starting a shared continuation path from an action. | | `testAction(ActionClass, options?)` | `"@agboss/core/testing"` | Isolated unit runner that executes an `@Action` directly with mocked `overrides` and custom `executionContext` without booting the workflow. | | `mockAction(ActionClass)` | `"@agboss/core/testing"` | Spies on or overrides an `@Action` inside a `testWith(Application)` workflow test. | # @Agent > Complete API reference for the @Agent decorator in AGBoss — all options, prompts, input/output shapes, tools, RAG, @Judge gates, reasoning, and testing. The `@Agent` decorator defines an LLM-powered workflow node (`agents/*.agent.ts`) in AGBoss. An agent is a pure declarative class that binds a model, static instructions (`prompt` or `promptUrls`), typed Path Scope `input` fields, structured `output` DTOs (including `oneOf(...)` union variants), `@Tool` and `@McpTool` capabilities, `@Rag` knowledge bases, `@Judge` quality gates, and loop/token limits. ## Complete `@Agent` example with all capabilities [Section titled “Complete @Agent example with all capabilities”](#complete-agent-example-with-all-capabilities) src/agents/offer-reviewer.agent.ts ```ts import { Agent, RagMode, TokenLimit, historyOf, oneOf } from "@agboss/core"; import { CacheRetention, CachedPart, PromptCaching, Reasoning, ReasoningEffort, } from "@agboss/core/models"; import { CandidatePiiPolicy, CompensationHandbookRag, FetchSalaryBandMcpTool, GroundedOfferJudge, OfferComplianceGuardrail, SearchJobsTool, } from "./offer-deps.js"; import { CandidateInputDto, ChangesRequestedDto, DraftApprovedDto, OfferDraftDto, } from "../dto/offer.dto.js"; @Agent({ name: "offer-reviewer-agent", description: "Audits offer drafts against compensation bands and either approves or requests changes", model: "moonshotai/kimi-k2.6", promptUrls: ["./offer-reviewer.prompt.md"], promptVariables: { department: "Core Engineering", }, input: { candidate: CandidateInputDto, drafts: historyOf(OfferDraftDto, { limit: 3 }), }, output: oneOf(DraftApprovedDto, ChangesRequestedDto), tools: [SearchJobsTool, FetchSalaryBandMcpTool], rag: [ { use: CompensationHandbookRag, mode: RagMode.Tool, }, ], judges: [GroundedOfferJudge], maxRetries: 2, maxToolCalls: 6, maxVisits: 4, temperature: 0.1, maxTokens: TokenLimit.MODEL_MAX, reasoning: Reasoning.on({ effort: ReasoningEffort.Medium }), cache: PromptCaching.whereSupported({ retention: CacheRetention.FiveMinutes, cachedParts: [CachedPart.SystemPrompt, CachedPart.Tools, CachedPart.History], }), uncompactedTurns: 8, piiPolicies: [CandidatePiiPolicy], guardrails: [OfferComplianceGuardrail], }) export class OfferReviewerAgent {} ``` src/agents/offer-reviewer.prompt.md ```md # Role: Principal Compensation Reviewer ({{department}}) Audit the candidate offer draft against the {{department}} salary bands. - Cite every handbook rule using its [source] identifier. - Output `DraftApprovedDto` only if base salary and equity are within band. - Otherwise output `ChangesRequestedDto` with concrete revision instructions. ``` src/dto/offer.dto.ts ```ts import { Integer, Text } from "@agboss/core/dto"; export class CandidateInputDto { @Text() level!: string; @Text() location!: string; } export class OfferDraftDto { @Text() text!: string; @Integer() baseSalaryUsd!: number; } export class DraftApprovedDto { @Text() summary!: string; } export class ChangesRequestedDto { @Text() notes!: string; } ``` src/workflows/offer-review\.workflow\.ts ```ts import { Workflow, agent, caseOf, oneOf, start } from "@agboss/core"; import { OfferReviewerAgent } from "../agents/offer-reviewer.agent.js"; import { CompensationMcpServer, OfferReviewFinish, OfferReviewStart, OfferWriterAgent, } from "../agents/offer-deps.js"; import { CandidateInputDto, ChangesRequestedDto, DraftApprovedDto, OfferDraftDto, } from "../dto/offer.dto.js"; @Workflow({ name: "offer-review-workflow", version: "1.0.0", mcp: [CompensationMcpServer], flow: [ start(OfferReviewStart) .returns(CandidateInputDto) .toAgent(OfferWriterAgent), agent(OfferWriterAgent) .returns(OfferDraftDto) .toAgent(OfferReviewerAgent) .returns(oneOf(DraftApprovedDto, ChangesRequestedDto)) .switch( caseOf(DraftApprovedDto) .toFinish(OfferReviewFinish), caseOf(ChangesRequestedDto, { about: OfferDraftDto }) .toAgent(OfferWriterAgent), ), ], }) export class OfferReviewWorkflow {} ``` src/offer.application.ts ```ts import { Application, EmptyEnvironment } from "@agboss/core"; import { AgAppBudget, AgExecBudget, AgTenantBudget } from "./agents/offer-deps.js"; import { OfferReviewWorkflow } from "./workflows/offer-review.workflow.js"; @Application({ name: "offer-application", version: "1.0.0", environment: EmptyEnvironment, workflows: [OfferReviewWorkflow], budgets: [AgAppBudget, AgTenantBudget, AgExecBudget], }) export class OfferApplication {} ``` test/offer-reviewer.agent.test.ts ```ts import { callTool, replyWith, testWith } from "@agboss/core/testing"; import { usd } from "@agboss/core/units"; import { OfferApplication } from "../src/offer.application.js"; import { OfferReviewerAgent } from "../src/agents/offer-reviewer.agent.js"; import { SearchJobsTool } from "../src/agents/offer-deps.js"; import { ChangesRequestedDto, DraftApprovedDto } from "../src/dto/offer.dto.js"; const test = testWith(OfferApplication); test("scripts tool calls and union output variants on OfferReviewerAgent", ({ mockLlm }) => { mockLlm(OfferReviewerAgent) .thenReturn( callTool(SearchJobsTool, { level: "L6", location: "Berlin" }), replyWith(ChangesRequestedDto, { notes: "Add equity vesting schedule" }), ) .thenReturn( replyWith(DraftApprovedDto, { summary: "Meets all bands" }, { cost: usd(0.012) }), ); }); ``` ## What options does `@Agent` accept? [Section titled “What options does @Agent accept?”](#what-options-does-agent-accept) Every `@Agent` class name ends with `Agent` and its `name` option ends with `-agent`: | Option | Type | Required | Description | | ------------------------- | ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Yes | Unique kebab-case identifier ending with `-agent` (e.g., `"draft-reviewer-agent"`). | | `description` | `string` | Yes | Describes the agent’s responsibility for traces, Studio graphs, and CLI inspection. | | `model` | `string` | Yes | Model identifier (e.g., `"moonshotai/kimi-k2.6"`); must match a `@ModelProvider({ serves })` pattern. | | `prompt` | `string` | No\* | Inline system prompt instructions (\*either `prompt` or `promptUrls` is required). | | `promptUrls` | `readonly string[]` | No\* | Relative paths to Markdown prompt files (`["./reviewer.prompt.md"]`) verified at assembly. | | `promptVariables` | `Readonly>` | No | Static `{{key}}` template values substituted into the agent’s prompt at assembly time. | | `input` | `InputFields` | No | Map of DTOs read from the workflow’s Path Scope (`{ draft: OfferDraftDto, history: historyOf(CritiqueDto, { limit: 3 }) }`). | | `output` | `OutputShape` | No | Structured output DTO class, `listOf(Dto)`, or `oneOf(ApprovedDto, ChangesRequestedDto)`. Defaults to `WorkflowFinishTextDto` when omitted. | | `tools` | `readonly Class[]` | No | `@Tool` and `@McpTool` classes the agent can invoke during its tool-calling loop. | | `rag` | `readonly AgentRag[]` | No | Knowledge bases attached as `[{ use: DocsRag, mode: RagMode.Tool \| RagMode.Context }]`. | | `judges` | `readonly Class[]` | No | `@Judge` classes that verify the agent’s reply and trigger self-correction retries on failure. | | `maxRetries` | `number` | No | Maximum self-correction retries when a `@Judge` rejects the agent’s answer. | | `maxToolCalls` | `number` | No | Maximum tool calls allowed within a single visit to this agent. | | `maxVisits` | `number` | No | Maximum times a workflow execution may visit this agent (`VisitsLimitExceededError`). | | `reasoning` | `Reasoning` | No | Reasoning effort: `Reasoning.off()`, `Reasoning.on({ effort: ReasoningEffort.Low \| Medium \| High })`, or `Reasoning.modelDecides()`. | | `cache` | `PromptCaching` | No | Prompt caching policy (`PromptCaching.off()` or `PromptCaching.whereSupported({ retention, cachedParts })`). | | `temperature` | `number` | No | Sampling temperature (`0` by default). | | `maxTokens` | `number \| TokenLimit` | No | Maximum completion tokens (`8192` by default, or `TokenLimit.MODEL_MAX`). | | `uncompactedTurns` | `number \| TurnLimit` | No | Number of recent conversation turns shown in full to this agent (overrides workflow/app `uncompactedTurns`). | | `compaction` | `Class` | No | [`@Compaction`](/reference/compaction/) class summarizing older turns for this agent. | | `piiPolicies` | `readonly Class[]` | No | Additional `@PiiPolicy` classes applied to this agent’s model and tool calls. | | `excludePiiPolicies` | `readonly Class[]` | No | Application or workflow `@PiiPolicy` classes explicitly exempted for this agent. | | `guardrails` | `readonly Class[]` | No | Additional [`@InputGuardrail` / `@OutputGuardrail`](/reference/guardrail/) classes applied to this agent. | | `excludeGuardrails` | `readonly Class[]` | No | Inherited application or workflow guardrails exempted for this agent. | | `toolInterceptors` | `readonly Class[]` | No | Additional [`@ToolInterceptor`](/reference/tool-interceptor/) classes applied to every `@Tool` and `@McpTool` invoked by this agent. | | `excludeToolInterceptors` | `readonly Class[]` | No | Inherited application or workflow `@ToolInterceptor` classes exempted for this agent. | | `price` | `{ input: number; output: number }` | No | Custom USD price per 1M input/output tokens overriding the provider’s cost table. | ## What related entities work with `@Agent`? [Section titled “What related entities work with @Agent?”](#what-related-entities-work-with-agent) | Related Entity | Import Path | Purpose | | -------------------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | `oneOf(DtoA, DtoB)`, `listOf(Dto)`, `optional(Dto)`, `historyOf(Dto, { limit })` | `"@agboss/core"` | Declares union output variants, list outputs, optional inputs, and bounded loop histories. | | `Reasoning` & `PromptCaching` | `"@agboss/core/models"` | Configures reasoning effort (`Reasoning.on({ effort })`) and prompt caching (`PromptCaching.whereSupported()`). | | `RagMode` (`Tool`, `Context`) | `"@agboss/core"` | Controls whether a `@Rag` knowledge base is exposed as a callable search tool or injected into context automatically. | | `VisitsLimitExceededError` | `"@agboss/core"` | Thrown before the model call when an agent exceeds `maxVisits` in a review loop; catchable via `.onError()`. | | `mockLlm(AgentClass)` & `replyWith(Dto, value)` | `"@agboss/core/testing"` | Scripts deterministic agent responses, tool calls (`callTool`), and simulated latency (`waitFor`) in `testWith()` tests. | # @Application > Complete API reference for the @Application decorator in AGBoss — configuration options, createApp(), App runtime methods, contexts, and testWith(). The `@Application` decorator defines the single assembly root (`*.application.ts`) of an AGBoss service. It wires your top-level `@Workflow` classes, typed `environment` contract, `@ModelProvider` gateways, mandatory `@Budget` caps, `@PiiPolicy` rules, `ExecutionObserver` hooks, and multi-instance storage repositories into one container verified at startup by `createApp(Application)` and in tests by `testWith(Application)`. ## Complete `@Application` example with all capabilities [Section titled “Complete @Application example with all capabilities”](#complete-application-example-with-all-capabilities) src/support.application.ts ```ts import { Application, SqliteCheckpointSaver, SqliteCostLedgerRepository, SqliteExecutionLeaseRepository, SqliteExecutionRecordRepository, SqliteExecutionTreeRepository, SqlitePausedExecutionRepository, SqliteRateLimitRepository, } from "@agboss/core"; import { Reasoning, ReasoningEffort } from "@agboss/core/models"; import { hours, seconds } from "@agboss/core/units"; import { AppDailyBudget, ExecutionRunBudget, TenantMonthlyBudget, } from "./budgets/support.budgets.js"; import { AppEnvironment, SupportExecutionContext, SupportTenantContext, } from "./support.contracts.js"; import { ContactPiiPolicy, DecisionsModelProvider, MetricsObserver, OpenRouterModelProvider, SlackRefundApprovalChannel, SupportWorkflow, } from "./support.components.js"; @Application({ name: "support-application", version: "0.1.0", workflows: [SupportWorkflow], environment: AppEnvironment, tenantContext: SupportTenantContext, executionContext: SupportExecutionContext, providers: [ OpenRouterModelProvider, DecisionsModelProvider, SlackRefundApprovalChannel, SqliteCheckpointSaver, SqliteCostLedgerRepository, SqliteExecutionRecordRepository, SqlitePausedExecutionRepository, SqliteExecutionLeaseRepository, SqliteExecutionTreeRepository, SqliteRateLimitRepository, ], budgets: [ AppDailyBudget, TenantMonthlyBudget, ExecutionRunBudget, ], piiPolicies: [ContactPiiPolicy], pseudonymKey: "piiPseudonymKey", observers: [MetricsObserver], uncompactedTurns: 6, defaults: { models: { temperature: 0, reasoning: Reasoning.on({ effort: ReasoningEffort.Low }), }, router: { model: "typesafe/jev-1.13", }, tools: { maxToolCalls: 8, }, }, pauseRecordRetention: hours(48), shutdownTimeout: seconds(25), }) export class SupportApplication {} export { SupportWorkflow }; ``` src/environments/prod.environment.ts ```ts import { env, secret } from "@agboss/core"; import type { AppEnvironment } from "../support.contracts.js"; export const environment: AppEnvironment = { openRouterApiKey: secret("OPENROUTER_API_KEY"), piiPseudonymKey: secret("PII_PSEUDONYM_KEY"), sqlitePath: env("AGBOSS_SQLITE_PATH", { default: ".agboss/state.sqlite" }), webhookBaseUrl: env.optional("WEBHOOK_BASE_URL"), }; ``` src/budgets/support.budgets.ts ```ts import { Budget, BudgetPeriod, BudgetScope, type BudgetHandler } 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-monthly-budget", scope: BudgetScope.Tenant, period: BudgetPeriod.Month }) export class TenantMonthlyBudget implements BudgetHandler { maxSpend(): Usd { return usd(50); } } @Budget({ name: "execution-run-budget", scope: BudgetScope.Execution }) export class ExecutionRunBudget implements BudgetHandler { maxSpend(): Usd { return usd(0.5); } } ``` src/main.ts ```ts import { ExecutionStatus, createApp } from "@agboss/core"; import { seconds } from "@agboss/core/units"; import { SupportApplication } from "./support.application.js"; import { ChatWorkflowStart } from "./support.components.js"; export async function runSupportServer() { const app = await createApp(SupportApplication, { env: "prod" }); const result = await app.execute( ChatWorkflowStart, { text: "Please refund order #A-1042" }, { threadId: "thread-1042", ownerId: "user-42", tenant: { tenantId: "acme", weight: 3, planTier: "enterprise" }, execution: { accountId: "acc-99" }, record: true, }, ); if (result.status === ExecutionStatus.Paused) { const pause = result.pending[0]; if (pause?.pauseId !== undefined) { await app.resume( { pauseId: pause.pauseId, decision: { approved: true } }, { tenantId: "acme", answeredBy: "finance-lead" }, ); } } process.once("SIGTERM", () => { void app.close({ timeout: seconds(25) }); }); } ``` test/support.application.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { replyWith, testWith } from "@agboss/core/testing"; import { SupportApplication } from "../src/support.application.js"; import { ChatWorkflowStart, SupportAgent } from "../src/support.components.js"; const test = testWith(SupportApplication, { instances: 2 }); test("boots the application container and executes ChatWorkflowStart", async ({ app, mockLlm }) => { mockLlm(SupportAgent).thenReturn(replyWith("Refund processed.")); const res = await app.execute( ChatWorkflowStart, { text: "Refund order #194" }, { tenant: { tenantId: "acme", weight: 2, planTier: "pro" }, execution: { accountId: "acc-1" }, }, ); expect(res.status).toBe(ExecutionStatus.Finished); }); ``` ## What options does `@Application` accept? [Section titled “What options does @Application accept?”](#what-options-does-application-accept) Every `@Application` class name ends with `Application` and its `name` option ends with `-application`: | Option | Type | Required | Description | | -------------------------- | -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `name` | `string` | Yes | Unique kebab-case identifier ending with `-application` (e.g., `"job-scout-application"`). | | `version` | `string` | Yes | Semantic release version of the application (`"0.1.0"`), recorded in logs and traces. | | `workflows` | `readonly Class[]` | Yes | Top-level `@Workflow` classes registered in the application; child workflows are discovered automatically. | | `environment` | `Class` | Yes | Environment contract class (`AppEnvironment` or `EmptyEnvironment`) validated at startup. | | `providers` | `readonly Provider[]` | No | Infrastructure repository implementations (`Sqlite*Repository`), `@ModelProvider` classes, `@Channel` implementations, and shared `@Injectable` services. | | `budgets` | `readonly Class[]` | No | `@Budget` classes enforcing spend caps across `Application`, `Tenant`, and `Execution` scopes. | | `observers` | `readonly ObserverClass[]` | No | `@Injectable` classes implementing `OnExecutionEnd`, `OnToolEnd`, `OnModelEnd`, `OnValuesOutdated`, and other observer hooks. | | `healthChecks` | `readonly Class[]` | No | `@HealthCheck` classes executed by `app.checkReadiness()` and `app.checkLiveness()` alongside built-in repository and concurrency checks. | | `piiPolicies` | `readonly Class[]` | No | Application-wide `@PiiPolicy` classes applied to every agent, router, log entry, and trace. | | `guardrails` | `readonly Class[]` | No | Application-wide `@InputGuardrail` and `@OutputGuardrail` classes applied to every agent and model router. | | `toolInterceptors` | `readonly Class[]` | No | Application-wide [`@ToolInterceptor`](/reference/tool-interceptor/) classes applied to every `@Tool` and `@McpTool` call. | | `maxMeshHops` | `number` | No | Maximum cross-service A2A/MCP federation hops allowed (`8` by default; throws `HopsLimitExceededError` when exceeded). | | `mesh` | `MeshOptions` | No | Agent Mesh Envelope signing and verification settings (`{ signingKey, peers, idempotencyRetention }`) for multi-service A2A/MCP deployments. | | `compaction` | `Class` | No | Default [`@Compaction`](/reference/compaction/) class summarizing turns older than `uncompactedTurns`. | | `uncompactedTurns` | `number \| TurnLimit` | No | Default number of recent turns shown in full to every agent and model router (`5` by default, or `TurnLimit.ALL`). | | `pseudonymKey` | `string` | No | Name of a `Secret` field on the `environment` class used for HMAC token pseudonymization across all pods. | | `tenantContext` | `TenantContextClass` | No | Custom subclass of `TenantContext` whose decorated DTO fields (without `prompt`, `[context.prompt-unused]`) are required on `app.execute(..., { tenant })`. | | `executionContext` | `ExecutionContextClass` | No | Custom subclass of `ExecutionContext` carrying per-execution identity fields passed via `app.execute(..., { execution })`. | | `defaults` | `WorkflowDefaults` | No | Default `models`, `router`, `guardrail`, `tools`, and `flow` settings inherited by all workflows. | | `exposes` | `readonly Class[]` | No | `@A2AExpose` and `@McpExpose` contract classes served over A2A/MCP and exported in `agboss describe --json`. | | `maxConcurrentSteps` | `number` | No | Maximum `@Agent`, `@Router`, and `@Action` steps running concurrently in this process before queueing by priority and reporting `step-slots` as `HealthStatus.Degraded`. | | `compensationTimeout` | `Milliseconds` | No | Dedicated cleanup window for automatic LIFO saga compensation when an execution is cancelled or hits a limit (`seconds(30)` by default). | | `pauseRecordRetention` | `Milliseconds` | No | How long resolved or cancelled pause records remain stored (`hours(24)` by default). | | `executionRecordRetention` | `Milliseconds` | No | How long completed `ExecutionRecord` snapshots remain in `ExecutionHistoryRepository` (`days(7)` by default). | | `shutdownTimeout` | `Milliseconds` | No | Grace period for `app.close()` and `SIGTERM` / `SIGINT` drain before releasing active execution leases (`seconds(25)` by default). | ## What related entities work with `@Application`? [Section titled “What related entities work with @Application?”](#what-related-entities-work-with-application) | Related Entity | Import Path | Purpose | | ----------------------------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `createApp(AppClass, options?)` | `"@agboss/core"` | Boots the container, runs `onInit()` and `onStart()` on all components, starts recovery sweeps, and returns the `App` handle. | | `App` (`execute`, `resume`, `cancel`, `checkReadiness`, `checkLiveness`, `close`, `tool`) | `"@agboss/core"` | Runtime handle used to start workflows, resume pauses, cancel runs (`app.cancel(threadId)`), report pod health (`HealthStatus.Healthy` / `HealthStatus.Degraded` / `HealthStatus.Unhealthy`), invoke isolated tools, and drain gracefully on shutdown. | | `EmptyEnvironment` | `"@agboss/core"` | Built-in empty contract class for applications that do not read environment variables. | | `env()`, `env.optional()`, `secret()` | `"@agboss/core"` | Readers used in `environments/*.environment.ts` files to bind and validate `process.env` variables at startup. | | `ApplicationContext`, `Clock`, `Logger` | `"@agboss/core"` | Built-in container singletons injectable into any service, action, tool, or observer via `deps`. | | `Sqlite*Repository` (10 classes) | `"@agboss/core"` | Zero-dependency `node:sqlite` WAL implementations of the 10 multi-instance durability, child work-stealing (`SqliteChildExecutionQueueRepository`), history (`SqliteExecutionHistoryRepository`), and mesh deduplication (`SqliteMeshRequestRepository`) contracts. | | `testWith(AppClass, options?)` | `"@agboss/core/testing"` | Deterministic test harness that boots one or more application instances with DI `overrides` and a `VirtualClock`. | # @Arbiter & @OnAnswer > Complete API reference for the @Arbiter and @OnAnswer decorators in AGBoss — quorum voting, DecidedBy.Code vs DecidedBy.Model, and testArbiter(). The `@Arbiter` class decorator and `@OnAnswer(AgentClass)` method decorator (`arbiters/*.arbiter.ts`) evaluate individual answers arriving from a parallel agent launch (`.toParallelAgents(...)`) to decide whether each answer counts toward a `.joinQuorum()` or `.joinQuorumGroups()` consensus. Unlike a `@Router`, an `@Arbiter` never chooses where the graph goes next — it votes `true` (accept) or `false` (reject) as each parallel branch finishes so the quorum can resolve immediately and cancel slower branches. ## Complete `@Arbiter` and `@OnAnswer` examples (`DecidedBy.Code` and `DecidedBy.Model`) [Section titled “Complete @Arbiter and @OnAnswer examples (DecidedBy.Code and DecidedBy.Model)”](#complete-arbiter-and-onanswer-examples-decidedbycode-and-decidedbymodel) src/arbiters/risk-quorum.arbiter.ts ```ts import { Arbiter, DecidedBy, ExecutionContext, OnAnswer } from "@agboss/core"; import { BackupRiskAgent, FastRiskAgent, PrimaryRiskAgent, RiskThresholdService, } from "../agents/risk.agents.js"; import { RiskScoreDto } from "../dto/risk.dto.js"; // 1. Code Arbiter (DecidedBy.Code — uses @OnAnswer(AgentClass) methods + constructor DI): @Arbiter({ name: "confidence-threshold-arbiter", decidedBy: DecidedBy.Code, deps: [ExecutionContext, RiskThresholdService], }) export class ConfidenceThresholdArbiter { constructor( private readonly executionContext: ExecutionContext, private readonly thresholds: RiskThresholdService, ) {} @OnAnswer(PrimaryRiskAgent) fromPrimaryRisk(answer: RiskScoreDto): boolean { void this.executionContext.executionId; return answer.confidence >= this.thresholds.primaryMinConfidence(); } @OnAnswer(FastRiskAgent) fromFastRisk(answer: RiskScoreDto): boolean { return answer.confidence >= 0.85 && answer.score >= 0; } @OnAnswer(BackupRiskAgent) fromBackupRisk(answer: RiskScoreDto): boolean { return answer.confidence >= 0.8; } } // 2. Model Arbiter (DecidedBy.Model — declarative per-agent criteria, no methods): @Arbiter({ name: "rationale-quality-arbiter", decidedBy: DecidedBy.Model, model: "typesafe/jev-1.13", prompt: "Accept the risk assessment only if its rationale cites specific financial metrics.", criteria: [ { agent: PrimaryRiskAgent, prompt: "The rationale cites debt-to-income and cash-flow figures from the input", }, { agent: FastRiskAgent, prompt: "The rationale names at least one concrete risk factor and a numeric score", }, { agent: BackupRiskAgent, prompt: "The rationale is complete and non-contradictory", }, ], }) export class RationaleQualityArbiter {} ``` src/dto/risk.dto.ts ```ts import { Decimal, Integer, Text } from "@agboss/core/dto"; export class RiskScoreDto { @Integer({ min: 0, max: 100 }) score!: number; @Decimal({ min: 0, max: 1 }) confidence!: number; @Text() rationale!: string; } ``` src/agents/risk.agents.ts ```ts import { Agent, Injectable } from "@agboss/core"; import { WorkflowStartTextDto } from "@agboss/core/dto"; import { RiskScoreDto } from "../dto/risk.dto.js"; @Injectable() export class RiskThresholdService { primaryMinConfidence(): number { return 0.9; } } @Agent({ name: "primary-risk-agent", description: "Deep credit risk evaluation", model: "moonshotai/kimi-k2.6", prompt: "Score credit risk from 0 to 100.", input: { req: WorkflowStartTextDto }, output: RiskScoreDto, }) export class PrimaryRiskAgent {} @Agent({ name: "fast-risk-agent", description: "Fast heuristic credit risk evaluation", model: "moonshotai/kimi-k2.6", prompt: "Score credit risk quickly from 0 to 100.", input: { req: WorkflowStartTextDto }, output: RiskScoreDto, }) export class FastRiskAgent {} @Agent({ name: "backup-risk-agent", description: "Backup credit risk evaluation", model: "moonshotai/kimi-k2.6", prompt: "Provide backup credit risk score from 0 to 100.", input: { req: WorkflowStartTextDto }, output: RiskScoreDto, }) export class BackupRiskAgent {} ``` src/workflows/risk-quorum.workflow\.ts ```ts import { Action, QuorumNotReachedError, Workflow, WorkflowFinish, WorkflowStart, action, group, listOf, start, toAction, type ActionHandler, type InputOf, } from "@agboss/core"; import { WorkflowStartTextDto } from "@agboss/core/dto"; import { seconds } from "@agboss/core/units"; import { ConfidenceThresholdArbiter } from "../arbiters/risk-quorum.arbiter.js"; import { BackupRiskAgent, FastRiskAgent, PrimaryRiskAgent, RiskThresholdService, } from "../agents/risk.agents.js"; import { RiskScoreDto } from "../dto/risk.dto.js"; @WorkflowStart({ name: "risk-workflow-start", description: "Start", input: WorkflowStartTextDto }) export class RiskStart {} @WorkflowFinish({ name: "risk-workflow-finish", description: "Finish", output: RiskScoreDto }) export class RiskFinish {} @Action({ name: "merge-risk-scores-action", description: "Combines quorum risk scores", input: { scores: listOf(RiskScoreDto) }, output: RiskScoreDto, }) export class MergeRiskScoresAction implements ActionHandler { execute(input: InputOf): Promise { return Promise.resolve(input.scores[0]!.value); } } @Action({ name: "fallback-risk-action", description: "Returns conservative fallback score when quorum fails", input: { req: WorkflowStartTextDto }, output: RiskScoreDto, }) export class FallbackRiskAction implements ActionHandler { execute(_input: InputOf): Promise { return Promise.resolve({ score: 50, confidence: 0.5, rationale: "Fallback assessment" }); } } @Workflow({ name: "risk-quorum-workflow", version: "1.0.0", providers: [RiskThresholdService], flow: [ start(RiskStart) .returns(WorkflowStartTextDto) .toParallelAgents(PrimaryRiskAgent, FastRiskAgent, BackupRiskAgent) .acceptedBy(ConfidenceThresholdArbiter) .joinQuorumGroups( group(PrimaryRiskAgent, FastRiskAgent) .priority(1) .returns(listOf(RiskScoreDto)) .toAction(MergeRiskScoresAction), group(PrimaryRiskAgent, BackupRiskAgent) .priority(2) .returns(listOf(RiskScoreDto)) .toAction(MergeRiskScoresAction), { deadline: seconds(6) }, ) .onJoinError( QuorumNotReachedError, toAction(FallbackRiskAction) .returns(RiskScoreDto) .toFinish(RiskFinish), ), action(MergeRiskScoresAction) .returns(RiskScoreDto) .toFinish(RiskFinish), ], }) export class RiskQuorumWorkflow {} ``` test/risk-quorum.arbiter.test.ts ```ts import { expect, it } from "vitest"; import { testArbiter } from "@agboss/core/testing"; import { ConfidenceThresholdArbiter } from "../src/arbiters/risk-quorum.arbiter.js"; import { FastRiskAgent } from "../src/agents/risk.agents.js"; it("unit-tests @OnAnswer handlers on ConfidenceThresholdArbiter", async () => { const runArbiter = testArbiter(ConfidenceThresholdArbiter); const { output } = await runArbiter(FastRiskAgent, { score: 72, confidence: 0.91, rationale: "Healthy cash reserve", }); expect(output).toBe(true); }); ``` ## What options do `@Arbiter` and `@OnAnswer` accept? [Section titled “What options do @Arbiter and @OnAnswer accept?”](#what-options-do-arbiter-and-onanswer-accept) Every `@Arbiter` class name ends with `Arbiter` and its `name` option ends with `-arbiter`: | Option / Decorator | Applies To | Type | Required | Description | | ----------------------- | ----------------- | ----------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Both | `string` | Yes | Unique kebab-case identifier ending with `-arbiter` (e.g., `"valid-score-arbiter"`). | | `decidedBy` | Both | `DecidedBy.Code \| DecidedBy.Model` | Yes | Whether TypeScript `@OnAnswer` methods (`Code`) or a decision/chat model (`Model`) judges each arriving answer. | | `deps` | `DecidedBy.Code` | `readonly Token[]` | No | Constructor dependencies (`ExecutionContext`, `@Injectable` services) for a code arbiter. | | `@OnAnswer(AgentClass)` | `DecidedBy.Code` | `MethodDecorator` | Yes | Decorates one method per launched `@Agent` (`(answer: OutputDto) => boolean \| Promise`); missing an agent fails compilation with `[arbiter.unhandled-agent]`. | | `model` | `DecidedBy.Model` | `string` | Yes | Decision model (`"typesafe/jev-1.13"`) or chat model used to judge each arriving answer. | | `prompt` | `DecidedBy.Model` | `string` | Yes | General instructions explaining how the model arbiter evaluates answers. | | `criteria` | `DecidedBy.Model` | `readonly [ArbiterCriterion, ...]` | Yes | Array of `{ agent: AgentClass, prompt: string }` entries covering every agent in the parallel launch. | ## What related entities work with `@Arbiter`? [Section titled “What related entities work with @Arbiter?”](#what-related-entities-work-with-arbiter) | Related Entity | Import Path | Purpose | | -------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `.acceptedBy(Arbiter)` & `.acceptedBy(anyOf(A, B))` | `"@agboss/core"` | Chains arbiter gates after `.toParallelAgents(...)`; multiple `.acceptedBy()` calls form a logical `AND`, while `anyOf(A, B)` forms a logical `OR`. | | `.joinQuorum({ min, max?, deadline? })` | Flow chain | Collects accepted answers as soon as `max` (default `min`) answers pass all arbiters, cancelling remaining branches (`BranchVerdict.Surplus`). | | `.joinQuorumGroups(group(A, B).priority(1)..., { deadline? })` | `"@agboss/core"` | Enforces prioritized group quorum rules where each `group(...)` declares its own `.returns(...)` and continuation path. | | `QuorumNotReachedError` | `"@agboss/core"` | Thrown when fewer than `min` answers are accepted or the `deadline` expires; catchable via `.onJoinError(QuorumNotReachedError, sidePath)`. | | `testArbiter(ArbiterClass, options?)` | `"@agboss/core/testing"` | Isolated unit runner that tests an arbiter’s verdict for any agent and answer payload. | # @BindTool > Complete API reference for the @BindTool method decorator in AGBoss — binding service methods to @Tool or @McpTool invocations. The `@BindTool(ToolClass, options?)` method decorator (`"@agboss/core"`) binds a method on a workflow provider class to a [`@Tool`](/reference/tool/) or [`@McpTool`](/reference/mcp-tool/) (optionally scoped to a specific [`@Agent`](/reference/agent/) via `{ agent: MyAgent }`). ## `@BindTool` example [Section titled “@BindTool example”](#bindtool-example) src/tools/bound-search.service.ts ```ts import { Agent, BindTool, Injectable, Tool, type ToolHandler } from "@agboss/core"; import { ListOf, Text } from "@agboss/core/dto"; export class RegionalSearchInputDto { @Text({ prompt: "Role keywords, e.g. Staff TypeScript" }) query!: string; } export class RegionalSearchOutputDto { @ListOf(Text, { prompt: "Matching role titles" }) openings!: string[]; } @Tool({ name: "search_regional_jobs", description: "Searches openings in a target region", input: RegionalSearchInputDto, output: RegionalSearchOutputDto, }) export class SearchRegionalJobsTool implements ToolHandler { run(input: RegionalSearchInputDto): Promise { return Promise.resolve({ openings: [`Global: ${input.query}`] }); } } @Agent({ name: "eu-scout-agent", description: "Finds EU roles using SearchRegionalJobsTool", model: "moonshotai/kimi-k2.6", prompt: "Call search_regional_jobs to find matching roles in Europe.", tools: [SearchRegionalJobsTool], }) export class EuScoutAgent {} @Injectable() export class EuJobSearchBindings { @BindTool(SearchRegionalJobsTool, { agent: EuScoutAgent }) searchForEuAgent(input: RegionalSearchInputDto): Promise { return Promise.resolve({ openings: [`EU: ${input.query}`] }); } } ``` test/bound-search.service.test.ts ```ts import { expect, it } from "vitest"; import { testTool } from "@agboss/core/testing"; import { EuJobSearchBindings, SearchRegionalJobsTool, } from "../src/tools/bound-search.service.js"; it("executes default tool handler and bound EU override method", async () => { const runTool = testTool(SearchRegionalJobsTool); const { output: globalOut } = await runTool({ query: "Staff TypeScript" }); expect(globalOut.openings).toEqual(["Global: Staff TypeScript"]); const bindings = new EuJobSearchBindings(); const euOut = await bindings.searchForEuAgent({ query: "Staff TypeScript" }); expect(euOut.openings).toEqual(["EU: Staff TypeScript"]); }); ``` ## How to configure `@BindTool` options and signatures [Section titled “How to configure @BindTool options and signatures”](#how-to-configure-bindtool-options-and-signatures) | Parameter | Type | Required | Description | | --------------- | ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `tool` | `Class \| string` | **Yes** | The `@Tool` / `@McpTool` class (or snake_case tool name string) handled or intercepted by this method. | | `options.agent` | `Class \| string` | No | Optional `@Agent` class (or kebab-case agent name) restricting the binding to calls made by that specific agent. | # @Budget > Complete API reference for the @Budget decorator in AGBoss — Application, Tenant, and Execution spend scopes, BudgetPeriod, and CostLedgerRepository. The `@Budget` decorator (`config/budget.config.ts`) defines a FinOps spend ceiling registered in `@Application({ budgets })`. Budgets are checked before the first step of an execution and reconciled after every model call and `SpendRecorder.record()` entry across three hierarchical scopes: **`BudgetScope.Application`**, **`BudgetScope.Tenant`**, and **`BudgetScope.Execution`**. ## Complete `@Budget` example across all 3 scopes [Section titled “Complete @Budget example across all 3 scopes”](#complete-budget-example-across-all-3-scopes) src/budgets/support.budgets.ts ```ts import { Budget, BudgetPeriod, BudgetScope, EmptyEnvironment, ExecutionContext, TenantContext, type BudgetHandler, } from "@agboss/core"; import { Text } from "@agboss/core/dto"; import { usd, type Usd } from "@agboss/core/units"; import { DeepResearchWorkflow } from "../workflows/support.workflow.js"; export class SupportTenantContext extends TenantContext { @Text() planTier!: string; } // 1. Application-wide daily cap: @Budget({ name: "application-daily-budget", scope: BudgetScope.Application, period: BudgetPeriod.Day, deps: [EmptyEnvironment], }) export class ApplicationDailyBudget implements BudgetHandler { constructor(private readonly _env: EmptyEnvironment) {} maxSpend(): Usd { return usd(100); } } // 2. Per-tenant daily cap (reads dynamic limit from TenantContext or a billing service): @Budget({ name: "tenant-daily-budget", scope: BudgetScope.Tenant, period: BudgetPeriod.Day, deps: [SupportTenantContext], }) export class TenantDailyBudget implements BudgetHandler { constructor(private readonly tenant: SupportTenantContext) {} maxSpend(): Usd { return this.tenant.weight > 1 ? usd(25) : usd(5); } } // 3. Default per-execution cap (plus a higher cap for a specific heavy workflow): @Budget({ name: "default-execution-budget", scope: BudgetScope.Execution, deps: [ExecutionContext], }) export class DefaultExecutionBudget implements BudgetHandler { constructor(private readonly executionContext: ExecutionContext) {} maxSpend(): Usd { void this.executionContext.executionId; return usd(0.15); } } @Budget({ name: "deep-research-execution-budget", scope: BudgetScope.Execution, workflow: DeepResearchWorkflow, }) export class DeepResearchExecutionBudget implements BudgetHandler { maxSpend(): Usd { return usd(1.5); } } ``` src/support.application.ts ```ts import { Application, EmptyEnvironment, SqliteCostLedgerRepository } from "@agboss/core"; import { ApplicationDailyBudget, DeepResearchExecutionBudget, DefaultExecutionBudget, SupportTenantContext, TenantDailyBudget, } from "./budgets/support.budgets.js"; import { DeepResearchWorkflow, SupportWorkflow } from "./workflows/support.workflow.js"; @Application({ name: "support-application", version: "1.0.0", environment: EmptyEnvironment, tenantContext: SupportTenantContext, workflows: [SupportWorkflow, DeepResearchWorkflow], providers: [SqliteCostLedgerRepository], budgets: [ ApplicationDailyBudget, TenantDailyBudget, DefaultExecutionBudget, DeepResearchExecutionBudget, ], }) export class SupportApplication {} ``` test/budget-cap.test.ts ```ts import { expect } from "vitest"; import { BudgetExceededError } from "@agboss/core"; import { replyWith, testWith } from "@agboss/core/testing"; import { usd } from "@agboss/core/units"; import { SupportApplication } from "../src/support.application.js"; import { SupportAgent, SupportStart } from "../src/workflows/support.workflow.js"; const test = testWith(SupportApplication); test("halts execution when simulated spend exceeds DefaultExecutionBudget", async ({ app, mockLlm }) => { mockLlm(SupportAgent).thenReturn( replyWith("Expensive turn", { cost: usd(1.25) }), ); await expect( app.execute( SupportStart, { text: "Analyze all logs" }, { tenant: { tenantId: "acme", weight: 1, planTier: "pro" } }, ), ).rejects.toThrow(BudgetExceededError); }); ``` ## What options does `@Budget` accept? [Section titled “What options does @Budget accept?”](#what-options-does-budget-accept) Every `@Budget` class implements `BudgetHandler` (`maxSpend(): Usd | Promise`) and its `name` ends with `-budget`: | Option | Applies To | Type | Required | Description | | ---------- | ------------------------ | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | `name` | All scopes | `string` | Yes | Unique kebab-case identifier ending with `-budget` (e.g., `"tenant-daily-budget"`). | | `scope` | All scopes | `BudgetScope` | Yes | `BudgetScope.Application`, `BudgetScope.Tenant`, or `BudgetScope.Execution`. | | `period` | `Application` & `Tenant` | `BudgetPeriod` | Yes | UTC accounting window: `BudgetPeriod.Day`, `BudgetPeriod.Week`, or `BudgetPeriod.Month`. Forbidden on `BudgetScope.Execution`. | | `workflow` | `Execution` | `Class` | No | Optional `@Workflow` class this execution cap applies to; omit to set the default cap for all root executions. | | `deps` | All scopes | `readonly Token[]` | No | Constructor dependencies (`AppEnvironment`, `TenantContext`, `ExecutionContext`, or `@Injectable` billing services). | ## What related entities work with `@Budget`? [Section titled “What related entities work with @Budget?”](#what-related-entities-work-with-budget) | Related Entity | Import Path | Purpose | | ----------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `BudgetHandler` | `"@agboss/core"` | Interface implemented by every `@Budget` class (`maxSpend(): Usd \| Promise`). | | `BudgetScope` & `BudgetPeriod` | `"@agboss/core"` | Enums specifying what is capped (`Application`, `Tenant`, `Execution`) and over which UTC period (`Day`, `Week`, `Month`). | | `usd(amount)` & `Usd` | `"@agboss/core/units"` | Exact monetary unit factory required for all budget caps and spend reporting. | | `SpendRecorder` | `"@agboss/core"` | Injected into `@Tool` and `@Action` classes via `deps: [SpendRecorder]` to count paid external API calls toward `@Budget` caps. | | `CostLedgerRepository` & `SqliteCostLedgerRepository` | `"@agboss/core"` | Atomic multi-instance ledger storing period and execution spend counters; required in production (`agboss check --strict`). | | `BudgetExceededError` | `"@agboss/core"` | Thrown when a pre-call reservation or post-call spend exceeds any active `@Budget` ceiling. | # @ChannelRole & @Channel > Complete API reference for @ChannelRole and @Channel in AGBoss — out-of-process pauses, ChannelOutcome, withdraw(), and two-phase app.resume(). The `@ChannelRole` and `@Channel` decorators (`channels/*.channel.ts`) power human-in-the-loop approvals and external questions in AGBoss. A `@Tool` declares which abstract `@ChannelRole` gates its execution (`@Tool({ channel: RefundApprovalChannel })`), while `@Application({ providers })` binds the concrete `@Channel` implementation (such as Slack, PagerDuty, or email) by class inheritance — and tests can swap in `AutoApproveChannel` or `AutoRejectChannel` without touching tool code. ## Complete `@ChannelRole` and `@Channel` example with two-phase `app.resume()` [Section titled “Complete @ChannelRole and @Channel example with two-phase app.resume()”](#complete-channelrole-and-channel-example-with-two-phase-appresume) src/channels/slack-refund-approval.channel.ts ```ts import { Channel, ChannelOf, ChannelOutcome, ChannelRole, ExecutionContext, Injectable, PauseAnswerer, PauseResolution, type ChannelAsk, type ChannelWithdrawal, } from "@agboss/core"; import { Integer, Text } from "@agboss/core/dto"; export class RefundInputDto { @Text({ prompt: "Order ID to refund" }) orderId!: string; @Integer({ min: 1, prompt: "Refund amount in cents" }) amountCents!: number; } export class RefundReceiptDto { @Text({ prompt: "Refund reference ID" }) refundId!: string; @Text({ prompt: "Settlement status" }) status!: string; } @Injectable() export class SlackService { postApprovalButtons(req: { channel: string; pauseId: string; executionId: string; orderId: string; amountCents: number; }): Promise { void req; return Promise.resolve(); } disableApprovalButtons(req: { pauseId: string; reason: string }): Promise { void req; return Promise.resolve(); } } // 1. Abstract @ChannelRole (referenced by @Tool({ channel: RefundApprovalChannel })): @ChannelRole({ name: "refund-approval-channel", description: "Approves customer card refunds over $100", approves: RefundInputDto, answeredBy: PauseAnswerer.Channel, }) export abstract class RefundApprovalChannel extends ChannelOf {} // 2. Concrete @Channel implementation (registered in @Application({ providers: [SlackRefundApprovalChannel] })): @Channel({ name: "slack-refund-approval-channel", description: "Posts an interactive approval button to #finance-approvals in Slack", deps: [ExecutionContext, SlackService], }) export class SlackRefundApprovalChannel extends RefundApprovalChannel { constructor( private readonly executionContext: ExecutionContext, private readonly slackService: SlackService, ) { super(); } async send( ask: ChannelAsk, ): Promise> { // Immediate auto-approval for tiny amounts without pausing the workflow: if (ask.value.arguments.amountCents <= 5_00) { return ChannelOutcome.decided({ approved: true, reason: "Auto-approved under $5" }); } // Otherwise persist PendingPause, release worker lease, and await external webhook: await this.slackService.postApprovalButtons({ channel: "#finance-approvals", pauseId: ask.pauseId, executionId: this.executionContext.executionId, orderId: ask.value.arguments.orderId, amountCents: ask.value.arguments.amountCents, }); return ChannelOutcome.awaitReply(); } override async withdraw(withdrawal: ChannelWithdrawal): Promise { // Called automatically when a paused branch is cancelled by FailFast, quorum Surplus, or app.cancel(threadId): await this.slackService.disableApprovalButtons({ pauseId: withdrawal.pauseId, reason: withdrawal.resolution === PauseResolution.Withdrawn ? "Quorum reached / branch withdrawn" : "Cancelled", }); } } ``` src/tools/refund-card-payment.tool.ts ```ts import { Tool, type ToolHandler } from "@agboss/core"; import { RefundApprovalChannel, RefundInputDto, RefundReceiptDto, } from "../channels/slack-refund-approval.channel.js"; @Tool({ name: "refund_card_payment", description: "Issues a customer card refund after Slack approval", input: RefundInputDto, output: RefundReceiptDto, channel: RefundApprovalChannel, }) export class RefundCardPaymentTool implements ToolHandler { run(input: RefundInputDto): Promise { return Promise.resolve({ refundId: `re_${input.orderId}`, status: "succeeded" }); } } ``` test/slack-refund-approval.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { callTool, testWith } from "@agboss/core/testing"; import { SupportApplication, RefundAgent, SupportStart } from "../src/support.application.js"; import { RefundCardPaymentTool } from "../src/tools/refund-card-payment.tool.js"; const test = testWith(SupportApplication); test("pauses on RefundCardPaymentTool and resumes with human approval", async ({ app, mockLlm }) => { mockLlm(RefundAgent).thenReturn( callTool(RefundCardPaymentTool, { orderId: "ORD-42", amountCents: 25_000 }), ); const paused = await app.execute(SupportStart, { text: "Refund ORD-42" }); expect(paused.status).toBe(ExecutionStatus.Paused); }); ``` ## What options do `@ChannelRole` and `@Channel` accept? [Section titled “What options do @ChannelRole and @Channel accept?”](#what-options-do-channelrole-and-channel-accept) | Decorator | Option | Type | Required | Description | | -------------- | ------------------ | ---------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | `@ChannelRole` | `name` | `string` | Yes | Unique kebab-case identifier ending with `-channel` (`"refund-approval-channel"`). | | `@ChannelRole` | `description` | `string` | Yes | Describes what this channel role approves or asks. | | `@ChannelRole` | `answeredBy` | `PauseAnswerer.Channel \| PauseAnswerer.ThreadOwner` | Yes | Who is authorized to call `app.resume()`: the external channel (`Channel`) or the thread’s original `ownerId` (`ThreadOwner`). | | `@ChannelRole` | `approves` | `DtoClass` | Yes\* | Input DTO class of the `@Tool`s this role approves (\*mutually exclusive with `ask` / `answer`). | | `@ChannelRole` | `ask` & `answer` | `DtoClass` | Yes\* | Question and answer DTO classes for workflow-level questions (\*mutually exclusive with `approves`). | | `@ChannelRole` | `replyInterpreter` | `Class` | No | Optional interpreter class that parses free-text user replies into the typed `answer` DTO. | | `@Channel` | `name` | `string` | Yes | Unique kebab-case identifier for the concrete channel implementation (`"slack-refund-approval-channel"`). | | `@Channel` | `description` | `string` | Yes | Describes how this implementation delivers the prompt (e.g., Slack webhook, CLI terminal). | | `@Channel` | `deps` | `readonly Token[]` | No | Constructor dependencies (`ExecutionContext`, `@Injectable` services) injected into the channel. | ## What related entities work with `@ChannelRole` and `@Channel`? [Section titled “What related entities work with @ChannelRole and @Channel?”](#what-related-entities-work-with-channelrole-and-channel) | Related Entity | Import Path | Purpose | | ---------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ChannelOf` & `ChannelAsk` | `"@agboss/core"` | Base class extended by `@ChannelRole` and typed payload passed to `send(ask)` (`ask.pauseId`, `ask.value.arguments`). | | `ChannelWithdrawal` & `PauseResolution` | `"@agboss/core"` | Payload passed to optional `withdraw(withdrawal)` when a paused branch is cancelled due to `FailFast` / quorum `Surplus` (`PauseResolution.Withdrawn`) or `app.cancel` (`PauseResolution.Cancelled`). | | `ChannelOutcome` (`awaitReply()`, `decided(...)`) | `"@agboss/core"` | Return value of `send(ask)`: either suspends the branch out-of-process (`awaitReply()`) or resolves inline (`decided({ approved })`). | | `PauseAnswerer` (`Channel`, `ThreadOwner`) | `"@agboss/core"` | Enforces who is allowed to resume the pause (`answeredBy` vs `ownerId`). | | `app.resume({ pauseId, decision }, options)` | `"@agboss/core"` | Two-phase atomic resume: transitions `Pending → Resolved` via CAS in `PausedExecutionRepository` before acquiring the execution-tree lease. | | `AutoApproveChannel`, `AutoRejectChannel`, `TerminalUserChannel` | `"@agboss/core"` | Built-in channel implementations for automated tests and interactive CLI sessions. | | `PausedExecutionRepository` & `SqlitePausedExecutionRepository` | `"@agboss/core"` | Durable store persisting `PendingPause` records across restarts and worker pods. | # @ChannelRole > Complete API reference for the @ChannelRole decorator in AGBoss — declaring tool-approval and question/answer pause contracts with PauseAnswerer. The `@ChannelRole` decorator (`"@agboss/core"`) defines a transport-independent **human-in-the-loop pause contract**. Tools (`@Tool({ channel: RefundApprovalChannel })`) reference the abstract `@ChannelRole` class, while [`@Application({ providers })`](/reference/application/) binds the concrete [`@Channel`](/reference/channel/) implementation (such as Slack, webhook, or `AutoApproveChannel` in tests) by class inheritance. ## Complete `@ChannelRole` example [Section titled “Complete @ChannelRole example”](#complete-channelrole-example) src/channels/refund-approval.channel-role.ts ```ts import { Channel, ChannelOf, ChannelOutcome, ChannelRole, Injectable, PauseAnswerer, Tool, type ChannelAsk, type ToolHandler, } from "@agboss/core"; import { Flag, Integer, Text } from "@agboss/core/dto"; export class RefundCardInputDto { @Text({ prompt: "charge id" }) chargeId!: string; @Integer({ prompt: "refund amount in cents" }) amountCents!: number; } export class RefundCardOutputDto { @Text({ prompt: "refund status" }) status!: string; } // 1. Tool approval role (approves: RefundCardInputDto): @ChannelRole({ name: "refund-approval-channel", description: "Finance approval required before executing card refunds", approves: RefundCardInputDto, answeredBy: PauseAnswerer.Channel, }) export abstract class RefundApprovalChannel extends ChannelOf {} // 2. Custom question/answer role (ask + answer DTOs): export class LegalSignOffQuestionDto { @Text({ prompt: "contract id" }) contractId!: string; @Integer({ prompt: "liability cap in USD" }) liabilityCapUsd!: number; } export class LegalSignOffDecisionDto { @Flag({ prompt: "whether approved" }) approved!: boolean; @Text({ prompt: "counsel notes" }) counselNotes!: string; } @ChannelRole({ name: "legal-signoff-channel", description: "Requests legal counsel sign-off on non-standard liability caps", ask: LegalSignOffQuestionDto, answer: LegalSignOffDecisionDto, answeredBy: PauseAnswerer.Channel, }) export abstract class LegalSignOffChannel extends ChannelOf {} @Injectable() export class SlackWebhookClient { postApproval(_pauseId: string, _chargeId: string): Promise { return Promise.resolve(); } } // 3. Concrete @Channel implementation extending the @ChannelRole class: @Channel({ name: "slack-refund-approval-channel", description: "Delivers RefundApprovalChannel requests to #finance-approvals in Slack", deps: [SlackWebhookClient], }) export class SlackRefundApprovalChannel extends RefundApprovalChannel { constructor(private readonly slack: SlackWebhookClient) { super(); } async send( ask: ChannelAsk, ): Promise> { await this.slack.postApproval(ask.pauseId, ask.value.arguments.chargeId); return ChannelOutcome.awaitReply(); } } // 4. @Tool referencing the abstract @ChannelRole: @Tool({ name: "refund_card_payment", description: "Refunds a card payment after finance approval", input: RefundCardInputDto, output: RefundCardOutputDto, channel: RefundApprovalChannel, }) export class RefundCardPaymentTool implements ToolHandler { run({ chargeId }: RefundCardInputDto): Promise { return Promise.resolve({ status: `refunded:${chargeId}` }); } } ``` test/refund-approval.channel-role.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { callTool, testWith } from "@agboss/core/testing"; import { RefundCardPaymentTool } from "../src/channels/refund-approval.channel-role.js"; import { RefundSupportAgent, RefundSupportApplication, RefundWorkflowStart, } from "../src/refund-support.application.js"; const test = testWith(RefundSupportApplication); test("pauses tool execution when RefundApprovalChannel returns awaitReply()", async ({ app, mockLlm, }) => { mockLlm(RefundSupportAgent).thenReturn( callTool(RefundCardPaymentTool, { chargeId: "ch_901", amountCents: 15_000 }), ); const res = await app.execute(RefundWorkflowStart, { text: "Refund ch_901" }); expect(res.status).toBe(ExecutionStatus.Paused); }); ``` ## What options does `@ChannelRole` accept? [Section titled “What options does @ChannelRole accept?”](#what-options-does-channelrole-accept) Every `@ChannelRole` class extends `ChannelOf`, has a `name` ending with `-channel`, requires `answeredBy` (`PauseAnswerer.Channel` or `PauseAnswerer.ThreadOwner`), and uses one of two forms — **tool approval** (`approves`) or **custom question/answer** (`ask` + `answer`): | Option | Type | Required | Description | | ------------------ | ---------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | **Yes** | Unique kebab-case identifier ending with `-channel` (`"refund-approval-channel"`). | | `description` | `string` | **Yes** | Explains what human decision this pause role requests. | | `answeredBy` | `PauseAnswerer.Channel \| PauseAnswerer.ThreadOwner` | **Yes** | Who may call `app.resume()`: the external channel (`PauseAnswerer.Channel`, requiring `{ answeredBy }`) or the thread’s owner (`PauseAnswerer.ThreadOwner`, checked against `ownerId`). | | `approves` | `DtoClass` | Form 1\* | Input DTO class of the `@Tool`s this role approves (`[channel.wrong-tool]` if a tool with another input DTO names this role). \*Mutually exclusive with `ask`/`answer`. | | `ask` & `answer` | `DtoClass` | Form 2\* | Custom question and answer DTO classes (\*mutually exclusive with `approves`). | | `replyInterpreter` | `Class` | No | Optional [`@ReplyInterpreter`](/reference/reply-interpreter/) class that turns free-text `app.resume({ pauseId, reply })` messages into the role’s typed decision. | ## What related entities work with `@ChannelRole`? [Section titled “What related entities work with @ChannelRole?”](#what-related-entities-work-with-channelrole) | Related Entity | Import Path | Purpose | | -------------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------- | | [`@Channel`](/reference/channel/) & `ChannelOf` | `"@agboss/core"` | Concrete transport implementation bound in `@Application({ providers })` by extending the `@ChannelRole` class. | | [`@ReplyInterpreter`](/reference/reply-interpreter/) | `"@agboss/core"` | Parses free-text human replies (`app.resume({ pauseId, reply })`) into the role’s typed decision. | | `AutoApproveChannel` & `AutoRejectChannel` | `"@agboss/core"` | Built-in test channels for automated approval or rejection. | | `ToolCallApprovalAskDto` & `ToolCallApprovalDecisionDto` | `"@agboss/core/dto"` | Standard DTOs used by `@ChannelRole({ approves })`. | # @Compaction > API reference for the @Compaction decorator, CompactionStrategy (SummaryQueue, SingleSummary, Custom, None), and CompactionHandler in AGBoss. `@Compaction` (`"@agboss/core"`) declares how completed conversation turns older than a reader’s `uncompactedTurns` window are summarized in multi-turn threads. A `@Compaction` class is bound via `compaction: MyCompaction` on [`@Application`](/reference/application/), [`@Workflow`](/reference/workflow/), [`@Agent`](/reference/agent/), or [`@Router`](/reference/router/) (the most specific level that sets `compaction` wins). ## Complete `@Compaction` example [Section titled “Complete @Compaction example”](#complete-compaction-example) A `@Compaction` `name` must end with `-compaction` (and its TypeScript class name must end with `Compaction`). Choose one of four strategies via `CompactionStrategy` (`SummaryQueue`, `SingleSummary`, `Custom`, or `None`). With `CompactionStrategy.Custom`, your class implements `CompactionHandler` (`compact(input: CompactionInput): Promise | readonly string[]`) and can inject DI services, `TenantContext`, or `ExecutionContext` via `deps: [...]`: src/compactions/support.compaction.ts ```ts import { Agent, Application, Budget, BudgetPeriod, BudgetScope, Compaction, CompactionStrategy, EmptyEnvironment, ExecutionContext, Injectable, TokenLimit, TurnLimit, Workflow, WorkflowFinish, WorkflowStart, start, type BudgetHandler, type CompactionHandler, type CompactionInput, type CompactionTurn, } from "@agboss/core"; import { WorkflowFinishTextDto, WorkflowStartTextDto } from "@agboss/core/dto"; import { usd, type Usd } from "@agboss/core/units"; // 1. FIFO SummaryQueue strategy (model-backed) @Compaction({ name: "support-queue-compaction", description: "Keeps the latest 3 FIFO summaries of older support turns", strategy: CompactionStrategy.SummaryQueue, model: "moonshotai/kimi-k2.6", turnsBetweenCompactions: 2, summaryQueueLength: 3, summaryMaxTokens: TokenLimit.MODEL_MAX, }) export class SupportQueueCompaction {} // 2. Rolling SingleSummary strategy (model-backed) @Compaction({ name: "support-rolling-compaction", description: "Rewrites a single rolling summary every 3 turns", strategy: CompactionStrategy.SingleSummary, model: "moonshotai/kimi-k2.6", turnsBetweenCompactions: 3, summaryMaxTokens: 512, prompt: "Summarize key customer facts, region settings, and unresolved questions.", }) export class SupportRollingCompaction {} // 3. Custom strategy (CompactionHandler + DI services / knowledge graph) @Injectable() export class KnowledgeGraphService { saveFactsAndSummarize( threadId: string, turns: readonly CompactionTurn[], previous: readonly string[], ): Promise { const facts = turns.map((turn) => `[${threadId}] ${JSON.stringify(turn.input)}`); return Promise.resolve([...previous, ...facts]); } } @Compaction({ name: "graph-memory-compaction", description: "Stores aged-out turn facts in a knowledge graph and returns updated summaries", strategy: CompactionStrategy.Custom, turnsBetweenCompactions: 2, deps: [KnowledgeGraphService, ExecutionContext], }) export class GraphMemoryCompaction implements CompactionHandler { constructor( private readonly knowledgeGraph: KnowledgeGraphService, private readonly executionContext: ExecutionContext, ) {} async compact(input: CompactionInput): Promise { return this.knowledgeGraph.saveFactsAndSummarize( this.executionContext.threadId, input.turnsToCompact, input.previousSummaries, ); } } // 4. Window-only None strategy (no summary calls) @Compaction({ name: "window-only-compaction", description: "Shows only recent uncompactedTurns without generating summaries", strategy: CompactionStrategy.None, }) export class WindowOnlyCompaction {} @WorkflowStart({ name: "support-chat-workflow-start", description: "Customer turn input", input: WorkflowStartTextDto, }) export class SupportChatWorkflowStart {} @WorkflowFinish({ name: "support-chat-workflow-finish", description: "Agent turn reply", output: WorkflowFinishTextDto, }) export class SupportChatWorkflowFinish {} @Agent({ name: "support-specialist-agent", description: "Answers support questions with 6 uncompacted turns", model: "moonshotai/kimi-k2.6", prompt: "Help the customer using the conversation context.", uncompactedTurns: 6, }) export class SupportSpecialistAgent {} @Agent({ name: "audit-transcript-agent", description: "Sees every turn in full without compaction", model: "moonshotai/kimi-k2.6", prompt: "Review the full thread.", compaction: WindowOnlyCompaction, uncompactedTurns: TurnLimit.ALL, }) export class AuditTranscriptAgent {} @Workflow({ name: "support-chat-workflow", version: "1.0.0", flow: [ start(SupportChatWorkflowStart) .returns(WorkflowStartTextDto) .toAgent(SupportSpecialistAgent) .toFinish(SupportChatWorkflowFinish), ], compaction: GraphMemoryCompaction, uncompactedTurns: 1, providers: [KnowledgeGraphService], }) export class SupportChatWorkflow {} @Budget({ name: "cmp-app-budget", scope: BudgetScope.Application, period: BudgetPeriod.Day }) export class CmpAppBudget implements BudgetHandler { maxSpend(): Usd { return usd(20); } } @Budget({ name: "cmp-tenant-budget", scope: BudgetScope.Tenant, period: BudgetPeriod.Day }) export class CmpTenantBudget implements BudgetHandler { maxSpend(): Usd { return usd(5); } } @Budget({ name: "cmp-exec-budget", scope: BudgetScope.Execution }) export class CmpExecBudget implements BudgetHandler { maxSpend(): Usd { return usd(0.5); } } @Application({ name: "support-chat-application", version: "1.0.0", environment: EmptyEnvironment, workflows: [SupportChatWorkflow], budgets: [CmpAppBudget, CmpTenantBudget, CmpExecBudget], }) export class SupportChatApplication {} ``` test/support.compaction.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { replyWith, testWith } from "@agboss/core/testing"; import { KnowledgeGraphService, SupportChatApplication, SupportChatWorkflowStart, SupportSpecialistAgent, } from "../src/compactions/support.compaction.js"; const test = testWith(SupportChatApplication); test("compacts aged-out turns using GraphMemoryCompaction across a multi-turn thread", async ({ app, mockLlm, mockOf, }) => { mockLlm(SupportSpecialistAgent).thenReturnAlways(replyWith("Updated your EU VAT ID.")); const graph = mockOf(KnowledgeGraphService); graph.saveFactsAndSummarize.mockResolvedValue(["EU VAT ID updated"]); for (let turn = 1; turn <= 4; turn++) { const res = await app.execute( SupportChatWorkflowStart, { text: `Turn ${String(turn)}: update EU VAT ID` }, { threadId: "thread-eu-1" }, ); expect(res.status).toBe(ExecutionStatus.Finished); } expect(graph.saveFactsAndSummarize).toHaveBeenCalledTimes(1); }); ``` ## `@Compaction` options reference [Section titled “@Compaction options reference”](#compaction-options-reference) | Option | Type | Required | Description | | ------------------------- | -------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | **Yes** | Kebab-case identifier ending with `-compaction`; the class name must end with `Compaction` (`[name.kind-suffix]`). | | `description` | `string` | **Yes** | Non-empty summary of the compaction policy. | | `strategy` | `CompactionStrategy` | **Yes** | `CompactionStrategy.SummaryQueue`, `CompactionStrategy.SingleSummary`, `CompactionStrategy.Custom`, or `CompactionStrategy.None`. | | `turnsBetweenCompactions` | `number` | For `SummaryQueue`, `SingleSummary`, and `Custom` | Positive integer (`>= 1`) specifying how many aged-out turns accumulate before compaction runs. | | `deps` | `readonly Class[]` | No (`Custom` only) | Injectable dependencies (`@Injectable()`, `TenantContext`, `ExecutionContext`) passed to the `CompactionHandler` constructor. | | `model` | `string` | For `SummaryQueue` and `SingleSummary` | Chat model identifier used to write summaries (resolved against `@ModelProvider({ serves })`). Forbidden on `Custom` and `None` (`[compaction.invalid-setting]`). | | `summaryQueueLength` | `number` | For `SummaryQueue` only | Positive integer (`>= 1`) specifying how many summaries are retained in the FIFO queue. Forbidden on `Custom`, `SingleSummary`, and `None`. | | `summaryMaxTokens` | `number \| TokenLimit.MODEL_MAX` | For `SummaryQueue` and `SingleSummary` | `max_tokens` limit for each summary call. Forbidden on `Custom` and `None`. | | `prompt` / `promptUrls` | `string` / `readonly string[]` | No (`SummaryQueue` / `SingleSummary`) | Custom summarization instructions (defaults to the built-in factual summarization prompt). Forbidden on `Custom` and `None`. | ## How `CompactionHandler` & `CompactionInput` work [Section titled “How CompactionHandler & CompactionInput work”](#how-compactionhandler--compactioninput-work) When `strategy: CompactionStrategy.Custom` is selected, the decorated class must implement `CompactionHandler` (enforced by TypeScript and at `agboss check` / assembly time via `[compaction.missing-handler]`): | Interface / Type | Signature / Fields | Description | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CompactionHandler` | `compact(input: CompactionInput): Promise \| readonly string[]` | Receives the aged-out turns and existing summaries (both masked by active `@PiiPolicy` rules) and returns the complete list of summaries to store for the thread. Throws `CompactionError` if `compact()` rejects or returns a non-string / blank item. | | `CompactionInput` | `{ turnsToCompact: readonly CompactionTurn[]; previousSummaries: readonly string[] }` | Frozen input passed to `compact()`. `previousSummaries` is `[]` on the first compaction of a thread and the previously returned `readonly string[]` on subsequent runs. | | `CompactionTurn` | `{ startName: string; input: Readonly>; finishName: string; output: Readonly> }` | One completed workflow turn older than the `uncompactedTurns` window, with its `input` and `output` masked by the application’s and workflow’s `@PiiPolicy` rules. | # DTO Field Decorators (@Text, @ListOf, @Nested…) > Complete API reference for DTO classes and field decorators in agboss/dto — @Text, @Integer, @Floating, @Flag, @Choice, @ListOf, @OneOf, and @RecordOf. In AGBoss, every payload crossing a workflow path, `@Agent`, `@Action`, `@Router`, `@Tool`, or `@ChannelRole` boundary is a **DTO class** (`*Dto`) whose properties each carry a single field decorator from `@agboss/core/dto` (`[dto.missing-decorator]`, `[dto.multiple-decorators]`). A single DTO class serves simultaneously as the compile-time TypeScript type, the runtime validator, the deterministic JSON Schema generator for LLM structured outputs, and the field-level PII masking contract. Runtime values are always plain object literals `{ ... }` — never instantiate DTOs with `new`. ## Complete DTO example with nested objects, lists, formats, and `sensitive: PiiAction.*` [Section titled “Complete DTO example with nested objects, lists, formats, and sensitive: PiiAction.\*”](#complete-dto-example-with-nested-objects-lists-formats-and-sensitive-piiaction) Every scalar field decorator accepts `sensitive?: MaskingPiiAction` (`PiiAction.replaceWithKind()`, `PiiAction.replaceWithToken()`, or `PiiAction.replacePart({ keepFirst?, keepLast?, maskChar? })`): src/dto/candidate-offer.dto.ts ```ts import { Agent, PiiAction } from "@agboss/core"; import { CountryCode, CurrencyCode, Date, DateTime, DecimalString, Email, Flag, Integer, ListOf, Nested, OneOf, PhoneNumber, Text, Url, Uuid, } from "@agboss/core/dto"; export class CompensationLineItemDto { @OneOf({ values: ["base", "bonus", "equity"], prompt: "Compensation component type" }) kind!: "base" | "bonus" | "equity"; @DecimalString({ scale: 2, prompt: "Exact amount as a decimal string, e.g. 185000.00" }) amount!: string; @CurrencyCode({ prompt: "ISO 4217 currency code" }) currency!: string; } export class CandidateContactDto { @Email({ prompt: "Candidate personal email address", sensitive: PiiAction.replaceWithToken(), }) email!: string; @PhoneNumber({ optional: true, prompt: "Candidate E.164 phone number", sensitive: PiiAction.replacePart({ keepLast: 4 }), }) phone?: string; @CountryCode({ prompt: "Candidate residence country code" }) country!: string; } export class OfferPackageDto { @Uuid({ prompt: "Unique offer package identifier" }) offerId!: string; @Text({ minLength: 2, maxLength: 120, prompt: "Candidate full legal name", sensitive: PiiAction.replaceWithKind(), }) candidateName!: string; @Nested(CandidateContactDto, { prompt: "Candidate contact details" }) contact!: CandidateContactDto; @ListOf(CompensationLineItemDto, { minItems: 1, maxItems: 5, prompt: "Breakdown of compensation items" }) items!: CompensationLineItemDto[]; @ListOf(Text, { optional: true, prompt: "Optional onboarding tags" }) tags?: string[]; @Date({ prompt: "Proposed start date (YYYY-MM-DD)" }) startDate!: string; @DateTime({ prompt: "Offer expiration timestamp (ISO 8601)" }) expiresAt!: string; @Url({ prompt: "Link to benefits summary PDF" }) benefitsUrl!: string; @Integer({ min: 1, max: 10, prompt: "Engineering level (1-10)" }) level!: number; @Flag({ prompt: "Whether relocation assistance is included" }) relocationIncluded!: boolean; } @Agent({ name: "offer-drafter-agent", description: "Produces a validated OfferPackageDto structured output", model: "moonshotai/kimi-k2.6", prompt: "Draft a structured offer package matching OfferPackageDto.", output: OfferPackageDto, }) export class OfferDrafterAgent {} ``` test/candidate-offer.dto.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { replyWith, testWith } from "@agboss/core/testing"; import { OfferDrafterAgent, OfferPackageDto } from "../src/dto/candidate-offer.dto.js"; import { OfferDraftApplication, OfferDraftStart } from "../src/offer-drafter.application.js"; const test = testWith(OfferDraftApplication); test("validates nested OfferPackageDto structured output at runtime", async ({ app, mockLlm }) => { mockLlm(OfferDrafterAgent).thenReturn( replyWith(OfferPackageDto, { offerId: "550e8400-e29b-41d4-a716-446655440000", candidateName: "Ada Lovelace", contact: { email: "ada@example.com", phone: "+14155550199", country: "US", }, items: [{ kind: "base", amount: "225000.00", currency: "USD" }], tags: ["principal", "distributed-systems"], startDate: "2026-11-01", expiresAt: "2026-10-31T23:59:59Z", benefitsUrl: "https://example.com/benefits.pdf", level: 7, relocationIncluded: true, }), ); const res = await app.execute(OfferDraftStart, { text: "Prepare Principal Engineer offer" }); expect(res.status).toBe(ExecutionStatus.Finished); }); ``` ## What field decorators does `@agboss/core/dto` provide? [Section titled “What field decorators does @agboss/core/dto provide?”](#what-field-decorators-does-agbosscoredto-provide) | Decorator | TypeScript Field Type | Options | Description | | ----------------------------- | --------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `@Text(options?)` | `string` | `minLength`, `maxLength`, `pattern`, `prompt`, `optional`, `sensitive` | UTF-8 string with optional length bounds and regex pattern. | | `@Integer(options?)` | `number` | `min`, `max`, `prompt`, `optional`, `sensitive` | Whole integer number (`Number.isInteger`). | | `@Decimal(options?)` | `number` | `min`, `max`, `prompt`, `optional`, `sensitive` | Finite floating-point number. | | `@DecimalString(options?)` | `string` | `scale`, `prompt`, `optional`, `sensitive` | Exact base-10 decimal formatted as a string (`"19.99"`) with at most `scale` fractional digits. | | `@Flag(options?)` | `boolean` | `prompt`, `optional`, `sensitive` | Boolean `true` or `false`. | | `@OneOf({ values, ... })` | `V[number]` | `values: readonly [string, ...]`, `prompt`, `optional`, `sensitive` | String literal union; TypeScript enforces that the property type matches `values` exactly. | | `@ListOf(Item, options?)` | `T[]` | `minItems`, `maxItems`, `prompt`, `optional`, `sensitive` | Array of scalar field factories (`@ListOf(Text)`) or nested DTO classes (`@ListOf(LineItemDto)`). | | `@Nested(DtoClass, options?)` | `DtoClass` | `prompt`, `optional`, `sensitive` | Embeds a child DTO object inside a parent DTO. | | `@Email(options?)` | `string` | `prompt`, `optional`, `sensitive` | Validated email address string. | | `@PhoneNumber(options?)` | `string` | `prompt`, `optional`, `sensitive` | E.164 phone number (`"+14155550199"`). | | `@Url(options?)` | `string` | `prompt`, `optional`, `sensitive` | Validated `http://` or `https://` URL string. | | `@Uuid(options?)` | `string` | `prompt`, `optional`, `sensitive` | RFC 4122 UUID string. | | `@Date(options?)` | `string` | `prompt`, `optional`, `sensitive` | ISO 8601 calendar date (`"2026-10-20"`). | | `@DateTime(options?)` | `string` | `prompt`, `optional`, `sensitive` | ISO 8601 timestamp with timezone offset or `Z` (`"2026-10-20T09:00:00Z"`). | | `@Duration(options?)` | `string` | `prompt`, `optional`, `sensitive` | ISO 8601 duration string (`"PT30M"`). | | `@CurrencyCode(options?)` | `string` | `prompt`, `optional`, `sensitive` | ISO 4217 3-letter currency code (`"USD"`). | | `@CountryCode(options?)` | `string` | `prompt`, `optional`, `sensitive` | ISO 3166-1 alpha-2 country code (`"US"`). | | `@TimeZone(options?)` | `string` | `prompt`, `optional`, `sensitive` | IANA time zone identifier (`"America/New_York"`). | | `@MediaType(options?)` | `string` | `prompt`, `optional`, `sensitive` | MIME type string (`"application/pdf"`). | ## Built-in standard DTOs and Path Scope wrappers [Section titled “Built-in standard DTOs and Path Scope wrappers”](#built-in-standard-dtos-and-path-scope-wrappers) | Entity | Import Path | Purpose | | -------------------------------------------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `PlainTextDto`, `WorkflowStartTextDto`, `WorkflowFinishTextDto` | `"@agboss/core/dto"` | Built-in standard text DTOs (`{ text: string }`) for plain-text agent judges, workflow starts, and workflow finishes. | | `ToolCallApprovalAskDto` & `ToolCallApprovalDecisionDto` | `"@agboss/core/dto"` | Built-in standard DTOs used by `@ChannelRole({ approves })` tool approval pauses (`{ approved: boolean, reason?: string, overrideArguments?: ... }`). | | `oneOf(DtoA, DtoB)`, `listOf(Dto)`, `optional(Dto)`, `historyOf(Dto, { limit })` | `"@agboss/core"` | Path Scope type wrappers used in `.returns(...)` and `@Agent` / `@Action` / `@Router` `input` and `output` contracts. | | `variant(DtoClass, plainObject)` & `isVariant(value, DtoClass)` | `"@agboss/core"` | Tags and narrows union variants when returning or inspecting `oneOf(DtoA, DtoB)` values in deterministic code. | # @InputGuardrail & @OutputGuardrail > Complete API reference for @InputGuardrail, @OutputGuardrail, DecidedBy.Model vs DecidedBy.Code handlers, GuardrailVerdict, and testGuardrail in AGBoss. `@InputGuardrail` and `@OutputGuardrail` (`"@agboss/core"`) define boundary safety checks on [`@Application`](/reference/application/), [`@Workflow`](/reference/workflow/), [`@Agent`](/reference/agent/), and [`@Router`](/reference/router/). Unlike [`@Judge`](/reference/judge/) (which retries an agent with feedback), a guardrail **blocks** execution immediately (`GuardFailedError`) when a message or reply violates the guardrail. Both `@InputGuardrail` and `@OutputGuardrail` support two execution modes via `decidedBy`: * **`DecidedBy.Model` (default):** Evaluates a yes/no `prompt` against `model` (such as `"typesafe/jev-1.13"`) and blocks when $P(\text{yes}) \ge \text{threshold}$. * **`DecidedBy.Code`:** Runs a deterministic `check(text: string): Promise | GuardrailVerdict` method on your `GuardrailHandler` class with full DI (`deps: [...]`) at `$0.00` LLM cost and `0` tokens (`model`, `prompt`, `promptUrls`, and `threshold` are forbidden). ## Complete `@InputGuardrail` and `@OutputGuardrail` example [Section titled “Complete @InputGuardrail and @OutputGuardrail example”](#complete-inputguardrail-and-outputguardrail-example) src/guardrails/clinic.guardrails.ts ```ts import { Agent, DecidedBy, ExecutionContext, GuardrailVerdict, Injectable, InputGuardrail, OutputGuardrail, TenantContext, type GuardrailHandler, type GuardrailRefusal, } from "@agboss/core"; import { OneOf } from "@agboss/core/dto"; export class ClinicTenantContext extends TenantContext { @OneOf({ values: ["pediatrics", "general"] }) department!: "pediatrics" | "general"; } export class ClinicExecutionContext extends ExecutionContext { @OneOf({ values: ["en", "de"] }) locale!: "en" | "de"; } @Injectable() export class LocalizedRefusalService { medicalPrescription(locale: "en" | "de"): Promise { return Promise.resolve( locale === "de" ? "Ich darf keine verschreibungspflichtigen Medikamente verordnen." : "I cannot prescribe medication. Please consult a licensed physician.", ); } } @Injectable() export class RestrictedTermsService { containsForbiddenTerm(department: "pediatrics" | "general", text: string): Promise { const terms = department === "pediatrics" ? ["adult-dosage", "aspirin-500mg"] : ["unapproved-compound"]; return Promise.resolve(terms.some((term) => text.toLowerCase().includes(term))); } } // 1. Model-backed @InputGuardrail with dynamic refusal via GuardrailRefusal + DI @InputGuardrail({ name: "prescription-request-guardrail", description: "Blocks prompts asking for a medical prescription", decidedBy: DecidedBy.Model, prompt: "Does the message ask the assistant to prescribe medication?", model: "typesafe/jev-1.13", threshold: 0.8, deps: [ClinicExecutionContext, LocalizedRefusalService], }) export class PrescriptionRequestGuardrail implements GuardrailRefusal { constructor( private readonly ctx: ClinicExecutionContext, private readonly refusals: LocalizedRefusalService, ) {} refusal(): Promise { return this.refusals.medicalPrescription(this.ctx.locale); } } // 2. Model-backed @OutputGuardrail with fixed refusal string @OutputGuardrail({ name: "no-diagnosis-output-guardrail", description: "Blocks answers that state a definitive medical diagnosis", prompt: "Does the answer provide a definitive medical diagnosis?", model: "typesafe/jev-1.13", threshold: 0.75, refusal: "I can only share general wellness information, not a medical diagnosis.", }) export class NoDiagnosisOutputGuardrail {} // 3. Deterministic DecidedBy.Code @OutputGuardrail with GuardrailHandler + DI ($0 cost) @OutputGuardrail({ name: "tenant-blocklist-guardrail", description: "Blocks replies containing department-restricted drug terms", decidedBy: DecidedBy.Code, refusal: "The response contained restricted clinical terms.", deps: [ClinicTenantContext, RestrictedTermsService], }) export class TenantBlocklistGuardrail implements GuardrailHandler { constructor( private readonly tenant: ClinicTenantContext, private readonly blocklist: RestrictedTermsService, ) {} async check(text: string): Promise { const blocked = await this.blocklist.containsForbiddenTerm(this.tenant.department, text); return blocked ? GuardrailVerdict.blocked() : GuardrailVerdict.passed(); } } // 4. Attach via guardrails: [...] (lists add up across @Application -> @Workflow -> @Agent; remove with excludeGuardrails) @Agent({ name: "triage-nurse-agent", description: "Collects symptom summaries under model and code guardrails", model: "moonshotai/kimi-k2.6", prompt: "Ask clarifying questions about the patient's symptoms.", guardrails: [ PrescriptionRequestGuardrail, NoDiagnosisOutputGuardrail, TenantBlocklistGuardrail, ], }) export class TriageNurseAgent {} ``` test/clinic.guardrails.test.ts ```ts import { expect, it } from "vitest"; import { decideWith, testGuardrail } from "@agboss/core/testing"; import { ClinicTenantContext, NoDiagnosisOutputGuardrail, TenantBlocklistGuardrail, } from "../src/guardrails/clinic.guardrails.js"; it("blocks answers when the model guardrail score meets the threshold", async () => { const check = testGuardrail(NoDiagnosisOutputGuardrail, { model: decideWith({ blocks: 0.92 }), }); const { output } = await check("You have acute bronchitis."); expect(output.isBlocked).toBe(true); expect(output.refusal).toContain("not a medical diagnosis"); }); it("runs DecidedBy.Code guardrails with zero LLM calls via GuardrailHandler", async () => { const check = testGuardrail(TenantBlocklistGuardrail, { tenantContext: ClinicTenantContext, tenant: { tenantId: "clinic-1", department: "pediatrics" }, }); const blocked = await check("Take aspirin-500mg twice daily."); expect(blocked.output.isBlocked).toBe(true); expect(blocked.output.refusal).toBe("The response contained restricted clinical terms."); const passed = await check("Rest and drink plenty of fluids."); expect(passed.output.isBlocked).toBe(false); }); ``` ## What options do `@InputGuardrail` and `@OutputGuardrail` accept? [Section titled “What options do @InputGuardrail and @OutputGuardrail accept?”](#what-options-do-inputguardrail-and-outputguardrail-accept) | Option / Interface | Type | Required | Description | | ----------------------- | ----------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | **Yes** | Unique kebab-case identifier ending with `-guardrail`; the class name must end with `Guardrail` (`[name.kind-suffix]`). | | `description` | `string` | **Yes** | Non-empty summary of the safety rule. | | `decidedBy` | `DecidedBy.Model \| DecidedBy.Code` | No | Execution mode (defaults to `DecidedBy.Model`). | | `prompt` / `promptUrls` | `string` / `readonly string[]` | For `DecidedBy.Model` | Yes/no safety question evaluated by the guardrail model (`"yes"` blocks). Forbidden on `DecidedBy.Code`. | | `threshold` | `number` | For `DecidedBy.Model` | Probability ceiling (`0 < threshold <= 1`); blocks when $P(\text{yes}) \ge \text{threshold}$. Forbidden on `DecidedBy.Code`. | | `model` | `string` | For `DecidedBy.Model`\* | Decision model (`"typesafe/jev-1.13"`) or chat model (\*uses `defaults.guardrail.model` when omitted; never inherits `defaults.router.model`). Forbidden on `DecidedBy.Code`. | | `refusal` | `string` | Either `refusal` or `implements GuardrailRefusal` | Fixed refusal text returned when the guardrail blocks (`[guardrail.refusal-twice]` if both `refusal` and `GuardrailRefusal` are provided). | | `deps` | `readonly Token[]` | No | Constructor dependencies injected into `GuardrailHandler` and/or `GuardrailRefusal`. | ## `GuardrailHandler` & `GuardrailVerdict` (`DecidedBy.Code`) [Section titled “GuardrailHandler & GuardrailVerdict (DecidedBy.Code)”](#guardrailhandler--guardrailverdict-decidedbycode) When `decidedBy: DecidedBy.Code` is set, the guardrail class must implement `GuardrailHandler` (`[guardrail.missing-handler]`): * **`check(text: string): Promise | GuardrailVerdict`:** Receives the PII-masked input or output string and returns `GuardrailVerdict.passed()` or `GuardrailVerdict.blocked()`. * **Nominal `GuardrailVerdict`:** Plain `{ isBlocked: boolean }` objects are rejected by TypeScript and at runtime; always construct verdicts via `GuardrailVerdict.passed()` (`isBlocked: false`) or `GuardrailVerdict.blocked()` (`isBlocked: true`). * **Fail-closed & `$0` cost:** If `check()` throws, the runtime fails closed with `GuardFailedError` without calling the downstream LLM or leaking internal error strings into `result.refusal`. Observer events (`GuardrailStartEvent` / `GuardrailEndEvent`) fire identically for both modes, with `cost: usd(0)` on code guardrails. ## Hierarchical inheritance (`guardrails` & `excludeGuardrails`) [Section titled “Hierarchical inheritance (guardrails & excludeGuardrails)”](#hierarchical-inheritance-guardrails--excludeguardrails) * **Additive inheritance:** Guardrails declared on `@Application({ guardrails })`, `@Workflow({ guardrails })`, and `@Agent({ guardrails })` / `@Router({ guardrails })` accumulate automatically. * **Selective opt-out (`excludeGuardrails`):** An individual `@Agent` or `@Router` can opt out of an inherited application or workflow guardrail via `excludeGuardrails: [PrescriptionRequestGuardrail]`. # @HealthCheck & Pod Lifecycle > Reference for @HealthCheck, app.checkLiveness(), app.checkReadiness(), graceful SIGTERM pod shutdown, and 4-stage lifecycle hooks in AGBoss. AGBoss applications running in Kubernetes or multi-instance worker clusters expose liveness and readiness health probes via `await app.checkLiveness()` and `await app.checkReadiness()`, register custom dependency probes with `@HealthCheck`, and coordinate graceful `SIGTERM` / `SIGINT` lease handoff through the 4-stage container lifecycle (`OnInit`, `OnStart`, `OnStop`, `OnDestroy`). ## Complete `@HealthCheck`, Liveness, and Readiness example [Section titled “Complete @HealthCheck, Liveness, and Readiness example”](#complete-healthcheck-liveness-and-readiness-example) src/health/claims-api.health-check.ts ```ts import { HealthCheck, HealthCheckResult, Injectable, type HealthCheckHandler, type OnDestroy, type OnInit, type OnStart, type OnStop, } from "@agboss/core"; import { seconds } from "@agboss/core/units"; @Injectable() export class ClaimsApiService implements OnInit, OnStart, OnStop, OnDestroy { private ready = false; onInit(): void { // 1. Runs in topological dependency order during createApp() } onStart(): void { // 2. Opens connections after all onInit() hooks complete this.ready = true; } onStop(): void { // 3. Runs in reverse topological order on app.close() / SIGTERM this.ready = false; } onDestroy(): void { // 4. Final cleanup after active execution leases are released } ping(): Promise { return Promise.resolve(this.ready); } } @HealthCheck({ name: "claims-api-health-check", timeout: seconds(2), deps: [ClaimsApiService], }) export class ClaimsApiHealthCheck implements HealthCheckHandler { constructor(private readonly claimsApiService: ClaimsApiService) {} async check(): Promise { return (await this.claimsApiService.ping()) ? HealthCheckResult.healthy() : HealthCheckResult.degraded({ reason: "claims API warming up" }); } } ``` src/worker-cluster.application.ts ```ts import { Application, SqliteCheckpointSaver, SqliteCostLedgerRepository, SqliteExecutionLeaseRepository, SqliteExecutionRecordRepository, SqliteExecutionTreeRepository, SqlitePausedExecutionRepository, SqliteRateLimitRepository, } from "@agboss/core"; import { hours, seconds } from "@agboss/core/units"; import { ClusterEnvironment } from "./environments/app.environment.js"; import { ClaimsApiHealthCheck, ClaimsApiService } from "./health/claims-api.health-check.js"; import { WorkerAppBudget, WorkerExecBudget, WorkerPingWorkflow, WorkerTenantBudget, } from "./worker-ping.workflow.js"; @Application({ name: "worker-cluster-application", version: "1.0.0", environment: ClusterEnvironment, workflows: [WorkerPingWorkflow], providers: [ ClaimsApiService, SqliteExecutionRecordRepository, SqliteCostLedgerRepository, SqlitePausedExecutionRepository, SqliteCheckpointSaver, SqliteExecutionLeaseRepository, SqliteExecutionTreeRepository, SqliteRateLimitRepository, ], healthChecks: [ClaimsApiHealthCheck], budgets: [WorkerAppBudget, WorkerTenantBudget, WorkerExecBudget], maxConcurrentSteps: 32, compensationTimeout: seconds(15), shutdownTimeout: seconds(25), pauseRecordRetention: hours(24), }) export class WorkerClusterApplication {} ``` src/main.ts ```ts import { createServer } from "node:http"; import { HealthStatus, createApp, type HealthReport } from "@agboss/core"; import { WorkerClusterApplication } from "./worker-cluster.application.js"; export async function startProbeServer(): Promise { const app = await createApp(WorkerClusterApplication, { env: "prod" }); createServer((req, res) => { const probe: Promise | undefined = req.url === "/livez" ? app.checkLiveness() : req.url === "/readyz" ? app.checkReadiness() : undefined; if (probe !== undefined) { void probe.then((report) => { res.writeHead(report.status === HealthStatus.Unhealthy ? 503 : 200, { "content-type": "application/json", }); res.end(JSON.stringify(report)); }); } }); } ``` test/claims-api.health-check.test.ts ```ts import { expect } from "vitest"; import { HealthStatus } from "@agboss/core"; import { testWith } from "@agboss/core/testing"; import { WorkerClusterApplication } from "../src/worker-cluster.application.js"; const test = testWith(WorkerClusterApplication); test("reports Healthy on checkLiveness and checkReadiness after startup", async ({ app }) => { const liveness = await app.checkLiveness(); const readiness = await app.checkReadiness(); expect(liveness.status).toBe(HealthStatus.Healthy); expect(readiness.status).toBe(HealthStatus.Healthy); }); ``` ## What options does `@HealthCheck` accept? [Section titled “What options does @HealthCheck accept?”](#what-options-does-healthcheck-accept) Every `@HealthCheck` class implements `HealthCheckHandler`, has a `name` ending with `-health-check` (`[name.kind-suffix]`), and is registered in `@Application({ healthChecks })` (`[health.not-a-health-check]`, `[health.duplicate-name]`): | Option | Type | Required | Description | | --------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | **Yes** | Unique kebab-case identifier ending with `-health-check` (e.g., `"claims-api-health-check"`). | | `timeout` | `Milliseconds` | **Yes** | Maximum duration `check()` may take (`seconds(2)`); if exceeded or thrown, the component reports `HealthStatus.Unhealthy`. | | `deps` | `readonly Token[]` | No | Constructor dependencies injected by the application container and verified against the constructor signature. | ## How `app.checkLiveness()` and `app.checkReadiness()` work [Section titled “How app.checkLiveness() and app.checkReadiness() work”](#how-appcheckliveness-and-appcheckreadiness-work) Both `app.checkLiveness()` and `app.checkReadiness()` resolve to a `HealthReport { status, components }` and never reject. The top-level `status` is the worst status across all checked components (`HealthStatus.Healthy`, `HealthStatus.Degraded`, or `HealthStatus.Unhealthy`): | Probe | Checked Components (`HealthComponentKind`) | Behavior | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`app.checkLiveness()`** | `Application` (`"application"`), `RecoveryLoop` (`"recovery-loop"`) | Performs zero external I/O. Reports `Unhealthy` once `app.close()` begins (`stopping`) or if the 30-second recovery sweep has not completed within 90 seconds. | | **`app.checkReadiness()`** | Liveness components + `Store` (`"store"`), `ModelProvider` (`"model-provider"`), `StepSlots` (`"step-slots"`), `Project` (`"project"`) | Calls `ping()` on every configured repository (`seconds(2)` timeout), checks every `@ModelProvider` circuit breaker (open breaker -> `Degraded`), checks `@Application({ maxConcurrentSteps })` saturation (all slots busy -> `Degraded`), and runs every `@HealthCheck` under its own `timeout`. | Custom `@HealthCheck` classes return branded results built exclusively with `HealthCheckResult.healthy()`, `HealthCheckResult.degraded({ reason })`, or `HealthCheckResult.unhealthy({ reason })` (a `Degraded` instance continues serving traffic while signaling reduced capacity). ## Pod lifecycle and concurrency settings reference [Section titled “Pod lifecycle and concurrency settings reference”](#pod-lifecycle-and-concurrency-settings-reference) | Setting / Hook | Where Configured | Default | Purpose | | --------------------------------------------- | ------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `healthChecks` | `@Application` | `[]` | `@HealthCheck` classes executed by `await app.checkReadiness()` under their configured `timeout`. | | `maxConcurrentSteps` | `@Application` | Unbounded | Maximum concurrent agent, router, and action steps on this pod (`[limits.max-concurrent-steps-invalid]`). When all slots are busy, `checkReadiness()` reports `HealthStatus.Degraded` (`step-slots`). | | `shutdownTimeout` | `@Application` | `seconds(25)` | Grace period on `app.close()` / `SIGTERM` before interrupting active executions and releasing their 30s leases (`< kubernetes terminationGracePeriodSeconds`). | | `compensationTimeout` | `@Application` | `seconds(15)` | Clean-up deadline per automatic `@Action({ compensateWith })` rollback step. | | `pauseRecordRetention` | `@Application` | `hours(24)` | How long resolved (`Taken`, `Cancelled`) pause records are retained in `PausedExecutionRepository` so duplicate webhook clicks receive `NotPausedError`. | | `OnInit` / `OnStart` / `OnStop` / `OnDestroy` | `@Injectable()` providers | — | Deterministic 4-stage startup and reverse-topological shutdown hooks. | # @Injectable > Complete API reference for @Injectable in AGBoss — constructor dependency injection, OnInit/OnStart/OnStop/OnDestroy lifecycle hooks, and DI overrides. The `@Injectable` decorator marks a TypeScript class (`services/*.service.ts` or `observers/*.observer.ts`) as a managed singleton in the AGBoss dependency injection container. Injectable classes receive their constructor dependencies via `deps: [...]`, participate in the four application lifecycle phases (`OnInit`, `OnStart`, `OnStop`, `OnDestroy`), can implement `ExecutionObserver` hooks (`OnExecutionEnd`, `OnToolEnd`, `OnModelEnd`, `OnValuesOutdated`), and are replaced in tests by class inheritance via `override(BaseService, FakeService)`. ## Complete `@Injectable` example with lifecycle hooks and DI [Section titled “Complete @Injectable example with lifecycle hooks and DI”](#complete-injectable-example-with-lifecycle-hooks-and-di) src/services/job-index.service.ts ```ts import { ApplicationContext, Clock, EmptyEnvironment, ExecutionContext, Injectable, LogLevel, Logger, type OnDestroy, type OnInit, type OnStart, type OnStop, } from "@agboss/core"; @Injectable({ deps: [EmptyEnvironment, ApplicationContext, ExecutionContext, Clock, Logger], }) export class JobIndexService implements OnInit, OnStart, OnStop, OnDestroy { constructor( private readonly _env: EmptyEnvironment, private readonly appContext: ApplicationContext, private readonly executionContext: ExecutionContext, private readonly clock: Clock, private readonly logger: Logger, ) {} async onInit(): Promise { this.logger.write({ level: LogLevel.Info, code: "job-index.init", message: `Initializing index on host ${this.appContext.host.hostname}`, time: this.clock.now(), }); } async onStart(): Promise { this.logger.write({ level: LogLevel.Info, code: "job-index.ready", message: `Ready to accept traffic at ${this.clock.now().toISOString()}`, time: this.clock.now(), }); } async searchByTenant(query: string): Promise { const idempotencyKey = this.executionContext.step.idempotencyKey; return [`Senior TypeScript Engineer — ${query} (${idempotencyKey})`]; } async onStop(): Promise { this.logger.write({ level: LogLevel.Info, code: "job-index.stop", message: "Draining in-flight index queries before lease release", time: this.clock.now(), }); } async onDestroy(): Promise { this.logger.write({ level: LogLevel.Info, code: "job-index.destroy", message: "Closing underlying index file descriptors", time: this.clock.now(), }); } } ``` src/tools/search-jobs.tool.ts ```ts import { Tool, type ToolHandler } from "@agboss/core"; import { ListOf, Text } from "@agboss/core/dto"; import { JobIndexService } from "../services/job-index.service.js"; export class JobSearchInputDto { @Text({ prompt: "Role search query" }) query!: string; } export class JobSearchOutputDto { @ListOf(Text, { prompt: "Matching roles" }) jobs!: string[]; } @Tool({ name: "search_jobs", description: "Queries the warmed JobIndexService", input: JobSearchInputDto, output: JobSearchOutputDto, deps: [JobIndexService], }) export class SearchJobsTool implements ToolHandler { constructor(private readonly index: JobIndexService) {} async run(input: JobSearchInputDto): Promise { return { jobs: await this.index.searchByTenant(input.query) }; } } ``` src/job-scout.application.ts ```ts import { Application, Budget, BudgetPeriod, BudgetScope, EmptyEnvironment, type BudgetHandler, } from "@agboss/core"; import { usd, type Usd } from "@agboss/core/units"; import { JobIndexService } from "./services/job-index.service.js"; import { JobScoutWorkflow } from "./workflows/job-scout.workflow.js"; @Budget({ name: "js-app-budget", scope: BudgetScope.Application, period: BudgetPeriod.Day }) export class JsAppBudget implements BudgetHandler { maxSpend(): Usd { return usd(20); } } @Budget({ name: "js-tenant-budget", scope: BudgetScope.Tenant, period: BudgetPeriod.Day }) export class JsTenantBudget implements BudgetHandler { maxSpend(): Usd { return usd(5); } } @Budget({ name: "js-exec-budget", scope: BudgetScope.Execution }) export class JsExecBudget implements BudgetHandler { maxSpend(): Usd { return usd(0.5); } } @Application({ name: "job-scout-application", version: "1.0.0", environment: EmptyEnvironment, workflows: [JobScoutWorkflow], providers: [JobIndexService], budgets: [JsAppBudget, JsTenantBudget, JsExecBudget], }) export class JobScoutApplication {} ``` test/job-index.service.test.ts ```ts import { expect } from "vitest"; import { override, testWith } from "@agboss/core/testing"; import { JobScoutApplication } from "../src/job-scout.application.js"; import { JobIndexService } from "../src/services/job-index.service.js"; import { ChatWorkflowStart } from "../src/workflows/job-scout.workflow.js"; class FakeJobIndexService extends JobIndexService { override searchByTenant(): Promise { return Promise.resolve(["Stubbed Staff Engineer Role"]); } } const test = testWith(JobScoutApplication, { overrides: [override(JobIndexService, FakeJobIndexService)], }); test("uses FakeJobIndexService and spies on calls via mockOf", async ({ app, mockOf }) => { await app.execute(ChatWorkflowStart, { text: "Find TypeScript roles" }); expect(mockOf(JobIndexService).searchByTenant).toHaveBeenCalledOnce(); }); ``` ## What options does `@Injectable` accept? [Section titled “What options does @Injectable accept?”](#what-options-does-injectable-accept) | Option | Type | Required | Description | | ------ | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `deps` | `readonly Token[]` | No | Ordered list of constructor dependency tokens (`AppEnvironment`, `ExecutionContext`, `TenantContext`, `ApplicationContext`, `Clock`, `Logger`, `ROUTER_FACTORY`, or other `@Injectable` classes) verified against constructor parameters by TypeScript. | ## What related entities work with `@Injectable`? [Section titled “What related entities work with @Injectable?”](#what-related-entities-work-with-injectable) | Related Entity | Import Path | Purpose | | --------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `OnInit`, `OnStart`, `OnStop`, `OnDestroy` | `"@agboss/core"` | Four deterministic container lifecycle hooks executed during `createApp()` startup and `app.close()` shutdown on every container element. | | `ExecutionContext`, `TenantContext`, `ApplicationContext` | `"@agboss/core"` | Built-in context classes injectable into any service constructor (`ExecutionContext` reads current step metadata via `AsyncLocalStorage`). | | `Clock` & `Logger` | `"@agboss/core"` | Built-in time and PII-redacted structured logging singletons (`Clock` is replaced by `VirtualClock` in `testWith()`). | | `InjectionToken` & `provide(TOKEN, value)` | `"@agboss/core"` | Binds non-class configuration values or factory tokens (`ROUTER_FACTORY`) in `@Workflow({ providers })`. | | `override(BaseClass, SubClass)` & `mockOf(ServiceClass)` | `"@agboss/core/testing"` | Replaces a service with a subclass or spy in `testWith()`, `testTool()`, `testAction()`, or `testRouter()`. | # @Judge > Complete API reference for the @Judge decorator in AGBoss — DecidedBy.Model vs DecidedBy.Code, JudgeVerdict, toolCalls inspection, and testJudge runner. The `@Judge` decorator (`"@agboss/core"`) defines an independent quality gate attached to an `@Agent` via `@Agent({ judges: [MyJudge], maxRetries: 2 })` or to an `@EvalSuite` case via `judgedBy: [MyJudge]`. Every time the agent produces a reply, its judges inspect `{ reply, variant, task, attempt, toolCalls }` and return a `JudgeVerdict` built via `JudgeVerdict.passed({ metrics? })` or `JudgeVerdict.failed({ reason, metrics? })`. When an in-loop judge fails, AGBoss feeds `reason` back into the agent and retries up to `maxRetries` times before letting the output leave the step. `@Judge` supports two execution modes via `decidedBy`: * **`DecidedBy.Model` (default):** Injects `JudgeModel` (`judgeModel.decide()` for decision models like `"typesafe/jev-1.13"` or `judgeModel.invoke()` for chat models). Emits a `[judge.same-model-family]` warning if the judge uses the same model family as the judged `@Agent` (preventing self-preference bias). * **`DecidedBy.Code`:** Runs deterministic DTO, state, and tool-trajectory checks (`input.toolCalls` / `toolCallsOf(ToolClass, input.toolCalls)`) at `$0.00` LLM cost without requiring a `model` option. ## Complete `@Judge` example with `JudgeHandler`, `JudgeModel`, and `JudgeVerdict` [Section titled “Complete @Judge example with JudgeHandler\, JudgeModel, and JudgeVerdict”](#complete-judge-example-with-judgehandlertreply-judgemodel-and-judgeverdict) src/judges/grounded-citation.judge.ts ```ts import { Agent, Decision, ExecutionContext, Judge, JudgeModel, JudgeVerdict, RagMode, type JudgeHandler, type JudgeInput, } from "@agboss/core"; import { ListOf, Text } from "@agboss/core/dto"; import { Rag, type RagConnector, type RagRetrieval } from "@agboss/core/rag"; export class PolicyAnswerDto { @Text({ prompt: "Policy answer citing [source] identifiers" }) answer!: string; @ListOf(Text, { prompt: "Cited source IDs" }) citations!: string[]; } @Rag({ name: "handbook_rag", description: "Company policy handbook", topK: 3, }) export class HandbookRag implements RagConnector { retrieve( _query: string, _options: { readonly topK: number; readonly signal: AbortSignal }, ): Promise { return Promise.resolve({ results: [{ text: "16 weeks paid parental leave", source: "hr-leave" }], }); } } @Judge({ name: "grounded-citation-judge", description: "Verifies that every factual claim cites a retrieved [source] and answers the question", model: "typesafe/jev-1.13", reply: PolicyAnswerDto, deps: [JudgeModel, ExecutionContext], }) export class GroundedCitationJudge implements JudgeHandler { constructor( private readonly judgeModel: JudgeModel, private readonly executionContext: ExecutionContext, ) {} async judge(input: JudgeInput): Promise { void this.executionContext.executionId; const answers = await this.judgeModel.decide({ state: `Task:\n${input.task}\n\nReply:\n${input.reply.answer}\nCitations: ${input.reply.citations.join(", ")}`, questions: { hasCitations: Decision.noul("Does every factual claim cite a [source] identifier?"), accuracyScore: Decision.score("Rate how directly the reply answers the task", [ "inaccurate or off-topic", "partially answers the question", "directly and accurately answers the question", ]), }, }); const passed = input.reply.citations.length > 0 && answers.hasCitations.noul >= 0.8 && answers.accuracyScore.score >= 2; const metrics = { hasCitationsProbability: answers.hasCitations.noul, accuracyScore: answers.accuracyScore.score, attempt: input.attempt, }; return passed ? JudgeVerdict.passed({ metrics }) : JudgeVerdict.failed({ reason: "Cite the exact [source] for every claim and directly answer the user's question.", metrics, }); } } @Agent({ name: "policy-advisor-agent", description: "Answers policy questions verified by GroundedCitationJudge", model: "moonshotai/kimi-k2.6", prompt: "Answer using handbook_rag and cite [source] on every claim.", rag: [{ use: HandbookRag, mode: RagMode.Tool }], output: PolicyAnswerDto, judges: [GroundedCitationJudge], maxRetries: 2, }) export class PolicyAdvisorAgent {} ``` test/grounded-citation.judge.test.ts ```ts import { expect, it } from "vitest"; import { decideWith, testJudge } from "@agboss/core/testing"; import { GroundedCitationJudge, PolicyAnswerDto } from "../src/judges/grounded-citation.judge.js"; it("fails an uncited reply and returns actionable retry reason", async () => { const runJudge = testJudge(GroundedCitationJudge, { model: decideWith({ hasCitations: "no", accuracyScore: 4, }), }); const { output } = await runJudge({ variant: PolicyAnswerDto, task: "What is our parental leave policy?", reply: { answer: "Employees receive 16 weeks of paid leave.", citations: [] }, attempt: 0, }); expect(output.passed).toBe(false); if (!output.passed) { expect(output.reason).toContain("[source]"); } }); ``` ## What options does `@Judge` accept? [Section titled “What options does @Judge accept?”](#what-options-does-judge-accept) | Option | Type | Required | Description | | ------------- | ----------------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | **Yes** | Unique kebab-case identifier ending with `-judge` (class name ending with `Judge`, `[name.kind-suffix]`); keys profile overrides (`judges.`) and cost attribution (`judge:`). | | `decidedBy` | `DecidedBy.Model \| DecidedBy.Code` | No | Execution mode (defaults to `DecidedBy.Model`). With `DecidedBy.Code`, `model` is forbidden and the judge runs deterministically at `$0.00` cost. | | `model` | `string` | For `DecidedBy.Model` | Model used by the judge (`[judge.no-model]` if omitted on `DecidedBy.Model`): a decision model (`"typesafe/jev-1.13"`, called via `judgeModel.decide()`) or a chat model (`judgeModel.invoke()`). Checked by `[judge.same-model-family]`. | | `reply` | `DtoClass \| OneOfWrapper \| ListWrapper` | **Yes** | The DTO shape the judge inspects (`PolicyAnswerDto`, `oneOf(A, B)`, `listOf(Dto)`, or `PlainTextDto` for text agents). Must match the `@Agent`’s `output` shape (`[judge.wrong-reply]`). | | `description` | `string` | No | Human-readable summary of what quality criteria this judge enforces. | | `deps` | `readonly Token[]` | No | Constructor dependencies (`JudgeModel`, `ExecutionContext`, `@Injectable` services) verified at compile time. | ## What related entities work with `@Judge`? [Section titled “What related entities work with @Judge?”](#what-related-entities-work-with-judge) | Related Entity | Import Path | Purpose | | ------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `JudgeHandler` | `"@agboss/core"` | Interface implemented by every `@Judge` class (`judge(input: JudgeInput): Promise \| JudgeVerdict`). | | `JudgeInput` | `"@agboss/core"` | Typed input passed to `judge()`: `{ reply: TReply, variant: DtoClass, task: string, attempt: number, toolCalls: readonly ToolCallRecord[] }` (use `isVariant(input, VariantDto)` when `reply: oneOf(A, B)`). | | `toolCallsOf(ToolClass, input.toolCalls)` | `"@agboss/core"` | Filters `input.toolCalls` to calls of `ToolClass` with `input` and `output` narrowed to that tool’s DTO types. | | `JudgeVerdict` (`passed()`, `failed({ reason })`) | `"@agboss/core"` | Branded builders for pass/fail verdicts (`{ passed: true, metrics? }` or `{ passed: false, reason, metrics? }`). | | `JudgeModel` | `"@agboss/core"` | Injected via `deps: [JudgeModel]` on `DecidedBy.Model` judges; provides `judgeModel.decide({ state, questions })` for decision models and `judgeModel.invoke(messages)` for chat models, with automatic FinOps cost tracking under `judge:`. | | `Decision` (`choice`, `noul`, `score`) | `"@agboss/core"` | Builders for typed multi-question evaluation requests sent to `judgeModel.decide()`. | | `testJudge(JudgeClass, options?)` | `"@agboss/core/testing"` | Isolated unit runner that tests a `@Judge` class offline with scripted `decideWith` / `replyWith` turns or `DecidedBy.Code` logic. | # @McpServer, @McpTool & @McpExpose > Complete API reference for @McpServer, @McpTool, and @McpExpose in agboss/mcp — consuming external MCP servers with typed DTOs and exposing tools over MCP. `@agboss/core/mcp` provides three decorators for the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/): **`@McpServer`** (declares and supervises a connection to an external stdio or HTTP/SSE MCP server), **`@McpTool`** (wraps an external server’s tool in compile-checked TypeScript input/output DTOs and `ToolHandler`), and **`@McpExpose`** (exposes your own `@Tool` and `@WorkflowStart` classes as an MCP server via `createMcpService(app, ConfigClass)`). ## Complete `@McpServer`, `@McpTool`, and `@McpExpose` example [Section titled “Complete @McpServer, @McpTool, and @McpExpose example”](#complete-mcpserver-mcptool-and-mcpexpose-example) src/mcp/workspace-fs.mcp.ts ```ts import { Agent, ExecutionContext, Tool, type ToolHandler } from "@agboss/core"; import { Text } from "@agboss/core/dto"; import { McpExpose, McpServer, McpServerClient, McpTool, type ServerTools, } from "@agboss/core/mcp"; export class ReadFileArgsDto { @Text({ prompt: "Relative path inside the workspace" }) path!: string; } export class ReadFileResultDto { @Text({ prompt: "UTF-8 file contents" }) content!: string; } const serverTools = { read_file: { input: ReadFileArgsDto, output: ReadFileResultDto, }, } as const satisfies ServerTools; // 1. @McpServer: connects to an external MCP server and types its tools @McpServer({ name: "workspace-fs-mcp", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "./workspace"], tools: serverTools, }) export class WorkspaceFsMcpServer extends McpServerClient {} // 2. @McpTool: wraps the MCP server call with constructor DI and ToolHandler @McpTool({ name: "read_workspace_file", description: "Reads a file from the sandboxed workspace MCP server", server: WorkspaceFsMcpServer, input: ReadFileArgsDto, output: ReadFileResultDto, deps: [WorkspaceFsMcpServer, ExecutionContext], }) export class ReadWorkspaceFileMcpTool implements ToolHandler { constructor( private readonly server: WorkspaceFsMcpServer, private readonly executionContext: ExecutionContext, ) {} async run(input: ReadFileArgsDto): Promise { void this.executionContext.signal; return this.server.call("read_file", input); } } @Tool({ name: "ping_workspace", description: "Checks workspace status", input: ReadFileArgsDto, output: ReadFileResultDto, }) export class PingWorkspaceTool implements ToolHandler { run(input: ReadFileArgsDto): Promise { return Promise.resolve({ content: `ok:${input.path}` }); } } // 3. @McpExpose: publishes @Tool and @WorkflowStart classes as an MCP server @McpExpose({ name: "acme-workspace-mcp-server", version: "1.0.0", exports: [PingWorkspaceTool], }) export class AcmeWorkspaceMcpConfig {} @Agent({ name: "workspace-reader-agent", description: "Reads workspace files via MCP", model: "moonshotai/kimi-k2.6", prompt: "Use read_workspace_file to inspect requested files.", tools: [ReadWorkspaceFileMcpTool], }) export class WorkspaceReaderAgent {} ``` test/workspace-fs.mcp.test.ts ```ts import { expect, it } from "vitest"; import { mcpServerStub, testTool } from "@agboss/core/testing"; import { PingWorkspaceTool, WorkspaceFsMcpServer, } from "../src/mcp/workspace-fs.mcp.js"; it("stubs @McpServer calls and unit-tests exposed @Tool offline", async () => { const fsStub = mcpServerStub(WorkspaceFsMcpServer, { read_file: ({ path }) => Promise.resolve({ content: `file:${path}` }), }); const readRes = await fsStub.call("read_file", { path: "README.md" }); expect(readRes.content).toBe("file:README.md"); const runPing = testTool(PingWorkspaceTool); const { output } = await runPing({ path: "src/index.ts" }); expect(output.content).toBe("ok:src/index.ts"); }); ``` ## What options do `@McpServer`, `@McpTool`, and `@McpExpose` accept? [Section titled “What options do @McpServer, @McpTool, and @McpExpose accept?”](#what-options-do-mcpserver-mcptool-and-mcpexpose-accept) | Decorator | Option | Type | Required | Description | | ------------ | ---------------------------------------- | ----------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `@McpServer` | `name` | `string` | **Yes** | Unique identifier for the external MCP server (`"workspace-fs-mcp"`). | | `@McpServer` | `command` / `args` or `url` | `string` / `readonly string[]` | **Yes** | Launch command for stdio servers (`command`, `args`, `env`) or endpoint `url` for HTTP/SSE servers. | | `@McpServer` | `tools` | `ServerTools` (`Record`) | **Yes** | Map of upstream MCP tool names to their input and output DTO classes, typing `this.server.call(toolName, args)`. | | `@McpTool` | `name`, `description`, `input`, `output` | Same as [`@Tool`](/reference/tool/) | **Yes** | Exposes the MCP capability to `@Agent({ tools: [...] })` with full DTO validation. | | `@McpTool` | `server` | `Class` | **Yes** | The `@McpServer` class this tool calls; also listed in `deps` for constructor injection. | | `@McpTool` | `deps` | `readonly Token[]` | No | Constructor dependencies including the `@McpServer` client class and `ExecutionContext`. | | `@McpExpose` | `name` | `string` | **Yes** | Server name advertised to connecting MCP clients (`"acme-support-mcp"`). | | `@McpExpose` | `version` | `string` | **Yes** | Server version advertised over MCP (`"1.0.0"`). | | `@McpExpose` | `exports` | `readonly Class[]` | **Yes** | Array of `@Tool` and `@WorkflowStart` classes exposed over MCP. | ## What related entities work with `@McpServer`, `@McpTool`, and `@McpExpose`? [Section titled “What related entities work with @McpServer, @McpTool, and @McpExpose?”](#what-related-entities-work-with-mcpserver-mcptool-and-mcpexpose) | Related Entity | Import Path | Purpose | | ------------------------------------------ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `McpServerClient` | `"@agboss/core/mcp"` | Base class extended by `@McpServer` classes; provides `this.call(toolName, input)` with compile-checked arguments and return types. | | `@Workflow({ mcp: [ServerClass] })` | `"@agboss/core"` | Registers `@McpServer` classes on the workflow so the container connects them on startup and closes them on shutdown. | | `createMcpService(app, ExposeConfigClass)` | `"@agboss/core/mcp"` | Creates a stdio (`service.connectStdio()`) or session-based MCP server from a booted `App` and an `@McpExpose` class. | | `mcpServerStub(ServerClass, handlers)` | `"@agboss/core/testing"` | Stubs an `@McpServer` in unit tests (`testTool`) without spawning an external process. | # @McpExpose > Complete API reference for @McpExpose and createMcpService in agboss/mcp — exposing AGBoss @WorkflowStart and @Tool classes as an MCP server. The `@McpExpose` decorator and `createMcpService(app, ConfigClass)` (`"@agboss/core/mcp"`) turn an AGBoss `@Application` into a standards-compliant **Model Context Protocol (MCP) server**. External MCP clients (Codex, Cursor, Windsurf, Claude Code, or another AGBoss application) can discover and call your exposed [`@WorkflowStart`](/reference/workflow-start/) entry points and [`@Tool`](/reference/tool/) classes with full `@Budget`, `@PiiPolicy`, and `@InputGuardrail` / `@OutputGuardrail` enforcement. ## Complete `@McpExpose` and `createMcpService` example [Section titled “Complete @McpExpose and createMcpService example”](#complete-mcpexpose-and-createmcpservice-example) src/mcp/job-scout.expose.ts ```ts import { Tool, WorkflowStart, createApp, type ToolHandler } from "@agboss/core"; import { Text, WorkflowStartTextDto } from "@agboss/core/dto"; import { McpExpose, createMcpService } from "@agboss/core/mcp"; import { JobScoutApplication } from "../job-scout.application.js"; export class JobQueryDto { @Text({ prompt: "Role keywords" }) query!: string; } export class JobResultsDto { @Text({ prompt: "Formatted job matches" }) text!: string; } @Tool({ name: "search_jobs", description: "Searches open engineering roles", input: JobQueryDto, output: JobResultsDto, }) export class SearchJobsTool implements ToolHandler { run({ query }: JobQueryDto): Promise { return Promise.resolve({ text: `Matches for ${query}` }); } } @WorkflowStart({ name: "chat-workflow-start", description: "Starts a Job Scout recruiting turn from an MCP client", input: WorkflowStartTextDto, }) export class ChatWorkflowStart {} @McpExpose({ name: "job-scout-mcp", version: "1.0.0", exports: [SearchJobsTool, ChatWorkflowStart], }) export class JobScoutMcpExpose {} export async function serveMcpStdio() { const app = await createApp(JobScoutApplication); const service = createMcpService(app, JobScoutMcpExpose); await service.connectStdio(); } ``` test/job-scout.expose.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { replyWith, testTool, testWith } from "@agboss/core/testing"; import { JobScoutApplication, ScoutAgent } from "../src/job-scout.application.js"; import { ChatWorkflowStart, JobScoutMcpExpose, SearchJobsTool, } from "../src/mcp/job-scout.expose.js"; const test = testWith(JobScoutApplication); test("executes the MCP-exposed tool and workflow start entry point", async ({ app, mockLlm }) => { expect(JobScoutMcpExpose).toBeDefined(); const runSearchTool = testTool(SearchJobsTool); const { output: toolOut } = await runSearchTool({ query: "Staff TypeScript" }); expect(toolOut.text).toContain("Staff TypeScript"); mockLlm(ScoutAgent).thenReturn(replyWith("Found 3 Staff TypeScript roles.")); const wfOut = await app.execute(ChatWorkflowStart, { text: "Find Staff TypeScript roles" }); expect(wfOut.status).toBe(ExecutionStatus.Finished); }); ``` ## What options does `@McpExpose` accept? [Section titled “What options does @McpExpose accept?”](#what-options-does-mcpexpose-accept) | Option | Type | Required | Description | | --------- | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | `name` | `string` | **Yes** | MCP server identifier advertised during protocol initialization (e.g., `"job-scout-mcp"`). | | `version` | `string` | **Yes** | Semantic version of the exposed MCP surface (e.g., `"1.0.0"`). | | `exports` | `readonly Class[]` | **Yes** | Array of [`@Tool`](/reference/tool/) and [`@WorkflowStart`](/reference/workflow-start/) classes exposed as callable MCP tools. | # @McpTool > Complete API reference for the @McpTool decorator in agboss/mcp — wrapping external Model Context Protocol server tools with typed DTO schemas, DI, and channels. The `@McpTool` decorator (`"@agboss/core/mcp"`) wraps an external [`@McpServer`](/reference/mcp/) tool inside a typed AGBoss `ToolHandler` class. Because an `@McpTool` is a first-class class with constructor dependency injection (`deps`), it can inject its `@McpServer` client, `ExecutionContext`, `SpendRecorder`, or custom services, and can require human approval via `channel: MyChannelRole`. ## Complete `@McpTool` example [Section titled “Complete @McpTool example”](#complete-mcptool-example) src/mcp/create-pull-request.mcp.ts ```ts import { ChannelRole, ChannelOf, PauseAnswerer, type ToolHandler, } from "@agboss/core"; import { Integer, Text, Url } from "@agboss/core/dto"; import { McpServer, McpServerClient, McpTool, type ServerTools, } from "@agboss/core/mcp"; import { seconds } from "@agboss/core/units"; export class CreatePrInputDto { @Text({ prompt: "Repository in owner/name format" }) repo!: string; @Text({ prompt: "Pull request title" }) title!: string; @Text({ prompt: "Source branch name" }) head!: string; @Text({ prompt: "Target base branch name" }) base!: string; } export class CreatePrOutputDto { @Integer({ prompt: "PR number" }) number!: number; @Url({ prompt: "PR HTML URL" }) htmlUrl!: string; } const githubTools = { create_pull_request: { input: CreatePrInputDto, output: CreatePrOutputDto, }, } as const satisfies ServerTools; @McpServer({ name: "github-mcp-server", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], tools: githubTools, }) export class GithubMcpServer extends McpServerClient {} @ChannelRole({ name: "release-approval-channel", description: "Approves opening release pull requests", approves: CreatePrInputDto, answeredBy: PauseAnswerer.Channel, }) export abstract class ReleaseApprovalChannel extends ChannelOf {} @McpTool({ name: "create_pull_request", description: "Opens a GitHub pull request via the GitHub MCP server after human approval", server: GithubMcpServer, input: CreatePrInputDto, output: CreatePrOutputDto, timeout: seconds(20), channel: ReleaseApprovalChannel, deps: [GithubMcpServer], }) export class CreatePullRequestMcpTool implements ToolHandler { constructor(private readonly github: GithubMcpServer) {} run(input: CreatePrInputDto): Promise { return this.github.call("create_pull_request", input); } } ``` test/create-pull-request.mcp.test.ts ```ts import { expect, it } from "vitest"; import { mcpServerStub } from "@agboss/core/testing"; import { CreatePullRequestMcpTool, GithubMcpServer, } from "../src/mcp/create-pull-request.mcp.js"; it("unit-tests @McpTool offline using mcpServerStub", async () => { const github = mcpServerStub(GithubMcpServer, { create_pull_request: () => Promise.resolve({ number: 42, htmlUrl: "https://github.com/agbossjs/agboss/pull/42", }), }); const tool = new CreatePullRequestMcpTool(github); const output = await tool.run({ repo: "agbossjs/agboss", title: "feat: quorum priority groups", head: "feat/quorum", base: "main", }); expect(output.number).toBe(42); }); ``` ## What options does `@McpTool` accept? [Section titled “What options does @McpTool accept?”](#what-options-does-mcptool-accept) | Option | Type | Required | Description | | ------------------------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | **Yes** | Unique snake_case tool identifier exposed to `@Agent({ tools })` (e.g., `"create_pull_request"`). | | `description` | `string` | **Yes** | Instructions telling the LLM when and how to call this MCP tool. | | `server` | `Class` | **Yes** | The [`@McpServer`](/reference/mcp/) class that backs this tool (`extends McpServerClient`). | | `input` | `DtoClass` | **Yes** | DTO class validating arguments before `run(input)` executes. | | `output` | `DtoClass` | **Yes** | DTO class validating the structured output returned by `run(input)`. | | `deps` | `readonly Token[]` | No | Constructor dependencies (typically `[MyMcpServer]` plus optional `ExecutionContext` or services). | | `channel` | `Class` | No | [`@ChannelRole`](/reference/channel-role/) or [`@Channel`](/reference/channel/) requiring human approval before invoking the MCP tool. | | `toolInterceptors` | `readonly Class[]` | No | [`@ToolInterceptor`](/reference/tool-interceptor/) classes attached directly to this MCP tool (`BeforeToolCall` / `AfterToolCall`). | | `excludeToolInterceptors` | `readonly Class[]` | No | Inherited application, workflow, or agent `@ToolInterceptor` classes exempted for this MCP tool. | | `timeout` | `Milliseconds` | No | Per-call timeout (`seconds(20)`); defaults to `seconds(30)`. | | `maxResultSize` | `Bytes` | No | Maximum UTF-8 result size (`kilobytes(256)`) shown to the model before truncation. | # @ModelProvider > Complete API reference for @ModelProvider in agboss/models — OpenRouterModelProvider, custom chat/decision/embedding adapters, and automatic yield rules. The `@ModelProvider` decorator (`"@agboss/core/models"`) defines an application-level LLM gateway registered in [`@Application({ providers })`](/reference/application/). Every model string referenced by an `@Agent`, `@Router`, `@Arbiter`, `@Judge`, `@Compaction`, `@InputGuardrail`, `@OutputGuardrail`, or `@PiiPolicy` is matched at startup against the `serves` regular expressions of your registered `@ModelProvider` classes (`[model.unserved]` or `[model.ambiguous-provider]` if zero or multiple custom providers match). Built-in providers automatically yield claimed models (`#454`) Every application includes the built-in catch-all `OpenRouterModelProvider` (`serves: [/.*/]`) and `DecisionsModelProvider` (`serves: [/^typesafe\/jev-/]`). You do **not** need to subclass `OpenRouterModelProvider` just to add a custom `@ModelProvider` (such as an Anthropic, Bedrock, or local Ollama provider) — the built-in catch-all providers automatically yield any model matched by another `@ModelProvider` listed in `@Application({ providers })`. Subclass `OpenRouterModelProvider` (`extends OpenRouterModelProvider`) only when you want to replace the default OpenRouter catch-all itself. `@ModelProvider` supports two transport modes (`[model-provider.both-modes]` if mixed): 1. **HTTP base URL mode (`OpenRouterProvider` / `OpenAiCompatibleProvider`):** Configured with `baseUrl` / `baseUrlField` and optional `apiKey` (plus OpenRouter host and quantization pinning `only`, `ignore`, `quantizations`, `allowFallbacks`). 2. **Custom transport mode (`implements ModelProviderHandler`):** Implements `complete(request: ModelProviderRequest): Promise` with constructor DI (`deps: [...]`) for AWS Bedrock SDK (`SigV4`), Google Vertex AI, or corporate mTLS gateways while retaining every AGBoss gateway policy (`@Budget` pre-check & ledger, `RateLimitRepository`, 429 AIMD, retries, circuit breaker, PII masking, structured output answer tools, and `mockLlm` interception). ## Complete `@ModelProvider` example [Section titled “Complete @ModelProvider example”](#complete-modelprovider-example) src/model-providers/openrouter.model-provider.ts ```ts import { DecisionsModelProvider, ModelCost, ModelProvider, OPENROUTER_BASE_URL, OpenRouterProvider, PromptCaching, QueueBalancingStrategy, RateLimitPolicy, Reasoning, RetryPolicy, TRANSIENT_FAILURES, type OpenRouterFields, } from "@agboss/core/models"; import { minutes, seconds } from "@agboss/core/units"; export { DecisionsModelProvider }; @ModelProvider({ name: "openrouter-model-provider", description: "Serves primary chat and reasoning models via OpenRouter with tenant-fair queueing", serves: [/^moonshotai\//, /^anthropic\//, /^google\//], 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: 16, perMinute: { calls: 240 }, balancing: QueueBalancingStrategy.TenantRoundRobin, }), circuitBreakerPolicy: { failureThreshold: 5, window: minutes(1), openFor: seconds(30), }, }) export class CustomOpenRouterModelProvider extends OpenRouterProvider { override readonly requestFields: OpenRouterFields = { provider: { ignore: ["Inceptron"], }, }; } ``` src/model-providers/bedrock.model-provider.ts ```ts // Custom transport (@ModelProvider + ModelProviderHandler + DI) for AWS Bedrock / Vertex AI @Injectable() export class BedrockSdkClient { converse(model: string, promptCount: number): Promise<{ text: string; inTokens: number; outTokens: number }> { return Promise.resolve({ text: `Bedrock(${model}) reply for ${String(promptCount)} messages`, inTokens: 120, outTokens: 45, }); } } @ModelProvider({ name: "bedrock-model-provider", description: "Serves bedrock/* models over the AWS Bedrock Converse SDK with SigV4 auth", serves: [/^bedrock\//], timeout: minutes(2), cost: ModelCost.fromPrices({ "bedrock/claude-sonnet-4-5": { inputPerMillion: usd(3), outputPerMillion: usd(15), }, }), reasoning: Reasoning.off(), promptCaching: PromptCaching.off(), retryPolicy: RetryPolicy.exponential({ maxAttempts: 3, initialDelay: seconds(1), maxDelay: seconds(10), jitter: true, retryOn: TRANSIENT_FAILURES, }), deps: [BedrockSdkClient], }) export class BedrockModelProvider implements ModelProviderHandler { constructor(private readonly bedrock: BedrockSdkClient) {} async complete(request: ModelProviderRequest): Promise { const res = await this.bedrock.converse(request.model, request.messages.length); return { text: res.text, usage: { inputTokens: res.inTokens, outputTokens: res.outTokens, }, }; } } ``` test/openrouter.model-provider.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { replyWith, testWith } from "@agboss/core/testing"; import { ProviderDemoAgent, ProviderDemoApplication, ProviderDemoWorkflowStart, } from "../src/provider-demo.application.js"; const test = testWith(ProviderDemoApplication); test("matches moonshotai/kimi-k2.6 against CustomOpenRouterModelProvider serves regex", async ({ app, mockLlm, }) => { mockLlm(ProviderDemoAgent).thenReturn(replyWith("Served by CustomOpenRouterModelProvider")); const res = await app.execute(ProviderDemoWorkflowStart, { text: "Ping provider" }); expect(res.status).toBe(ExecutionStatus.Finished); }); ``` ## What options does `@ModelProvider` accept? [Section titled “What options does @ModelProvider accept?”](#what-options-does-modelprovider-accept) Every `@ModelProvider` class extends `OpenRouterProvider`, `OpenAiCompatibleProvider`, or implements `ModelProviderHandler`, and its `name` ends with `-model-provider` (class name ending with `ModelProvider`, `[name.kind-suffix]`): | Option | Type | Required | Description | | ------------------------------------------------------ | ------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `name` | `string` | **Yes** | Unique kebab-case identifier ending with `-model-provider` (e.g., `"openrouter-model-provider"`). | | `description` | `string` | **Yes** | Human-readable summary of which models and upstream endpoint this provider serves. | | `serves` | `readonly (string \| RegExp)[]` | **Yes** | Non-empty array of model names or regex patterns matching model IDs served by this provider (`[/^moonshotai\//, /^anthropic\//]`). | | `baseUrl` | `string` | For HTTP providers | Default upstream base URL (`OPENROUTER_BASE_URL`). Forbidden when implementing `ModelProviderHandler` (`[model-provider.both-modes]`). | | `baseUrlField` | `string` | No (HTTP only) | Optional field name on `AppEnvironment` (`env.optional("OPENROUTER_BASE_URL")`) that overrides `baseUrl` when set. | | `apiKey` | `string` | No (HTTP only) | Field name on `AppEnvironment` holding the `secret("...")` API key; omit for unauthenticated local model servers. | | `deps` | `readonly Token[]` | No (`ModelProviderHandler`) | Constructor dependencies (`@Injectable()` SDK clients, `ApplicationContext`, `TenantContext`, `ExecutionContext`) injected into custom transport providers. | | `timeout` | `Milliseconds` | **Yes** | Per-attempt request timeout (`minutes(2)`), passed via `request.signal`. | | `cost` | `ModelCost` | **Yes** | Cost calculation policy (`ModelCost.fromResponse()`, `ModelCost.fromPrices(...)`, or `ModelCost.free()`). | | `reasoning` | `Reasoning` | **Yes** | Default reasoning mode (`Reasoning.off()`, `Reasoning.on({ effort })`, or `Reasoning.modelDecides()`). | | `promptCaching` | `PromptCaching` | **Yes** | Default prompt caching policy (`PromptCaching.off()`, `PromptCaching.whereSupported(...)`). | | `retryPolicy` | `RetryPolicy` | **Yes** | Per-call retry backoff (`RetryPolicy.exponential({ maxAttempts, initialDelay, maxDelay, jitter, retryOn })` or `RetryPolicy.none()`). | | `rateLimitPolicy` | `RateLimitPolicy` | No | Cluster-wide concurrency (`maxConcurrentCalls`), RPM (`perMinute`), and tenant fairness (`QueueBalancingStrategy.TenantRoundRobin`) coordinated via `RateLimitRepository`. | | `circuitBreakerPolicy` | `CircuitBreakerPolicy` | No | Trips open across all instances when upstream fails (`{ failureThreshold, window, openFor }`), shifting `app.checkReadiness()` to `HealthStatus.Degraded` and activating `fallbackModels`. | | `only` / `ignore` / `quantizations` / `allowFallbacks` | `OpenRouterProvider` options | No | OpenRouter host and quantization pinning (`only: ["Fireworks"]`, `ignore: ["Inceptron"]`, `quantizations: ["fp8", "bf16"]`, `allowFallbacks: false`) to prevent silent quantization degradation in production and evals. | ## What related entities work with `@ModelProvider`? [Section titled “What related entities work with @ModelProvider?”](#what-related-entities-work-with-modelprovider) | Related Entity | Import Path | Purpose | | ---------------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OpenRouterProvider` & `OpenAiCompatibleProvider` | `"@agboss/core/models"` | Base classes for OpenRouter and OpenAI-compatible chat/decision model servers (OpenAI, vLLM, Ollama, LiteLLM). | | `ModelProviderHandler` (`complete(request)`) | `"@agboss/core/models"` | Interface for custom SDK/transport gateways (AWS Bedrock, Vertex AI, corporate proxies) wrapped by all AGBoss FinOps, rate-limit, circuit-breaker, and PII policies. | | `ModelProviderFailureError` | `"@agboss/core/models"` | Thrown inside `complete(request)` to report classified failures (`ModelFailure.RateLimit` with `retryAfter`, `ModelFailure.Overloaded`, `ModelFailure.InvalidOutput`, `ModelFailure.InvalidToolArguments`) to the retry and AIMD engines. | | `OpenRouterModelProvider` & `DecisionsModelProvider` | `"@agboss/core/models"` | Built-in catch-all providers (`serves: [/.*/]` and `[/^typesafe\/jev-/]`) that automatically yield any model claimed by another `@ModelProvider` in `@Application({ providers })` (unless explicitly replaced by subclassing `extends OpenRouterModelProvider` or `extends DecisionsModelProvider`). | | `RateLimitPolicy` & `QueueBalancingStrategy` | `"@agboss/core/models"` | Configures `maxConcurrentCalls`, `perMinute`, and `QueueBalancingStrategy` (`TenantRoundRobin`, `TenantLeastInFlight`, `TenantWeighted`, `Fifo`). | | `RetryPolicy` & `CircuitBreakerPolicy` | `"@agboss/core/models"` | Configures per-call exponential backoff and shared multi-instance circuit breaking. | | `ModelCost`, `Reasoning`, `PromptCaching` | `"@agboss/core/models"` | Configures token pricing, thinking effort (`ReasoningEffort`), and KV prefix cache policies. | | `RateLimitRepository` & `SqliteRateLimitRepository` | `"@agboss/core"` | Storage contract that coordinates concurrency slots, `429` cooldown windows, and circuit breaker state across worker processes. | # @OnAnswer > Complete API reference for the @OnAnswer(AgentClass) method decorator in AGBoss — evaluating arriving parallel branch answers inside a DecidedBy.Code @Arbiter. The `@OnAnswer(AgentClass)` method decorator (`"@agboss/core"`) marks the per-agent evaluation method inside a `DecidedBy.Code` [`@Arbiter`](/reference/arbiter/) class. During a `.joinQuorum()` or `.joinQuorumGroups()` step, AGBoss invokes the matching `@OnAnswer(AgentClass)` method as each parallel branch finishes so your arbiter can return `true` (count toward quorum) or `false` (reject the branch answer) in real time. ## Complete `@OnAnswer` example [Section titled “Complete @OnAnswer example”](#complete-onanswer-example) src/arbiters/margin-threshold.arbiter.ts ```ts import { Agent, Arbiter, DecidedBy, ExecutionContext, Injectable, OnAnswer, } from "@agboss/core"; import { Integer, Text } from "@agboss/core/dto"; export class PricingQuoteDto { @Text({ prompt: "Pricing tier name" }) tierName!: string; @Integer({ min: 0, max: 100, prompt: "Gross margin percentage (0-100)" }) marginPercent!: number; } @Agent({ name: "enterprise-pricing-agent", description: "Generates enterprise tier quote", model: "moonshotai/kimi-k2.6", prompt: "Propose an enterprise pricing quote.", output: PricingQuoteDto, }) export class EnterprisePricingAgent {} @Agent({ name: "self-serve-pricing-agent", description: "Generates self-serve tier quote", model: "moonshotai/kimi-k2.6", prompt: "Propose a self-serve pricing quote.", output: PricingQuoteDto, }) export class SelfServePricingAgent {} @Injectable() export class MarginPolicyService { minMarginForTenant(_tenantId: string): number { return 30; } } @Arbiter({ name: "margin-threshold-arbiter", decidedBy: DecidedBy.Code, deps: [ExecutionContext, MarginPolicyService], }) export class MarginThresholdArbiter { constructor( private readonly ctx: ExecutionContext, private readonly policy: MarginPolicyService, ) {} @OnAnswer(EnterprisePricingAgent) fromEnterprisePricing(answer: PricingQuoteDto): boolean { const minMargin = this.policy.minMarginForTenant(this.ctx.tenant.tenantId); return answer.marginPercent >= minMargin; } @OnAnswer(SelfServePricingAgent) fromSelfServePricing(answer: PricingQuoteDto): boolean { return answer.marginPercent >= 20; } } ``` test/margin-threshold.arbiter.test.ts ```ts import { expect, it } from "vitest"; import { testArbiter } from "@agboss/core/testing"; import { EnterprisePricingAgent, MarginThresholdArbiter, } from "../src/margin-threshold.js"; it("accepts quotes meeting the margin threshold via @OnAnswer", async () => { const runArbiter = testArbiter(MarginThresholdArbiter); const { output } = await runArbiter(EnterprisePricingAgent, { tierName: "Enterprise", marginPercent: 42, }); expect(output).toBe(true); }); ``` ## How `@OnAnswer(AgentClass)` works inside `@Arbiter` [Section titled “How @OnAnswer(AgentClass) works inside @Arbiter”](#how-onansweragentclass-works-inside-arbiter) | Signature Part | Type | Description | | ----------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@OnAnswer(AgentClass)` | Method decorator | Binds the method on a `DecidedBy.Code` `@Arbiter` to a specific `@Agent` class launched by `.toParallelAgents(...)`. Every agent in the launch must have a corresponding `@OnAnswer` method (`[arbiter.unhandled-agent]`). | | `answer` parameter | `ArbiterAnswerOf` | The plain validated output DTO produced by that `@Agent` branch. TypeScript rejects mismatched DTO types at compile time. | | Return value | `boolean \| Promise` | Returns `true` if the branch’s answer counts toward the quorum, or `false` to reject it. | # @PiiPolicy > Complete API reference for @PiiPolicy in AGBoss — declarative pii() rules, reversible replaceWithToken() pseudonymization, custom handlers, and testPii(). The `@PiiPolicy` decorator (`policies/*.pii-policy.ts`) defines personal-data protection rules applied at the LLM and tool boundaries as well as across structured logs and traces. `@PiiPolicy` supports two forms: **declarative policies** (mapping built-in `PiiKind` / `PiiGroup` targets to `PiiAction` transformations like reversible HMAC tokens ``) and **custom detectors** implementing `PiiPolicyHandler.findMatches(text)`. ## Complete `@PiiPolicy` examples (declarative and custom detector) [Section titled “Complete @PiiPolicy examples (declarative and custom detector)”](#complete-piipolicy-examples-declarative-and-custom-detector) src/policies/customer.pii-policy.ts ```ts import { ExecutionContext, PiiAction, PiiGroup, PiiKind, PiiPolicy, pii, secret, type PiiMatch, type PiiPolicyHandler, type PiiRefusal, } from "@agboss/core"; // 1. Declarative @PiiPolicy with reversible token masking and custom refusal message: @PiiPolicy({ name: "customer-contact-pii-policy", description: "Pseudonymizes emails and phones with reversible tokens and blocks payment card numbers", rules: [ pii(PiiKind.Email, PiiAction.replaceWithToken()), pii(PiiKind.Phone, PiiAction.replacePart({ keepLast: 2 })), pii(PiiKind.CardNumber, PiiAction.blockWithRefusal()), pii(PiiGroup.Government, PiiAction.replaceWithKind()), ], detectWithModel: "moonshotai/kimi-k2.6", pseudonymKey: secret("PII_PSEUDONYM_KEY"), deps: [ExecutionContext], }) export class CustomerContactPiiPolicy implements PiiRefusal { constructor(private readonly executionContext: ExecutionContext) {} refusal(kind: PiiKind): string { return `For your security, please do not send a ${kind} in chat (ref: ${this.executionContext.executionId}).`; } } // 2. Custom detector @PiiPolicy implementing PiiPolicyHandler: @PiiPolicy({ name: "employee-badge-pii-policy", action: PiiAction.replaceWithToken(), }) export class EmployeeBadgePiiPolicy implements PiiPolicyHandler { findMatches(text: string): readonly PiiMatch[] { const matches: PiiMatch[] = []; for (const match of text.matchAll(/\bEMP-\d{6}\b/g)) { if (match.index !== undefined) { matches.push({ start: match.index, end: match.index + match[0].length }); } } return matches; } } ``` src/tools/lookup-customer.tool.ts ```ts import { Tool, type ToolHandler } from "@agboss/core"; import { Email, Text } from "@agboss/core/dto"; export class CustomerLookupDto { @Email({ prompt: "Customer email address" }) email!: string; } export class CustomerProfileDto { @Email({ prompt: "Customer email address" }) email!: string; @Text({ prompt: "Subscription tier" }) tier!: string; } @Tool({ name: "lookup_customer", description: "Receives de-tokenized email automatically when invoked by the LLM", input: CustomerLookupDto, output: CustomerProfileDto, }) export class LookupCustomerTool implements ToolHandler { run(input: CustomerLookupDto): Promise { // input.email is transparently restored from to the real address return Promise.resolve({ email: input.email, tier: "enterprise" }); } } ``` src/support.application.ts ```ts import { Application, Budget, BudgetPeriod, BudgetScope, EmptyEnvironment, type BudgetHandler, } from "@agboss/core"; import { usd, type Usd } from "@agboss/core/units"; import { CustomerContactPiiPolicy, EmployeeBadgePiiPolicy, } from "./policies/customer.pii-policy.js"; import { SupportWorkflow } from "./workflows/support.workflow.js"; @Budget({ name: "pii-app-budget", scope: BudgetScope.Application, period: BudgetPeriod.Day }) export class PiiAppBudget implements BudgetHandler { maxSpend(): Usd { return usd(20); } } @Budget({ name: "pii-tenant-budget", scope: BudgetScope.Tenant, period: BudgetPeriod.Day }) export class PiiTenantBudget implements BudgetHandler { maxSpend(): Usd { return usd(5); } } @Budget({ name: "pii-exec-budget", scope: BudgetScope.Execution }) export class PiiExecBudget implements BudgetHandler { maxSpend(): Usd { return usd(0.5); } } @Application({ name: "support-application", version: "1.0.0", environment: EmptyEnvironment, workflows: [SupportWorkflow], piiPolicies: [CustomerContactPiiPolicy, EmployeeBadgePiiPolicy], budgets: [PiiAppBudget, PiiTenantBudget, PiiExecBudget], }) export class SupportApplication {} ``` test/employee-badge.pii-policy.test.ts ```ts import { expect, it } from "vitest"; import { testPii } from "@agboss/core/testing"; import { EmployeeBadgePiiPolicy } from "../src/policies/customer.pii-policy.js"; it("pseudonymizes employee badge numbers and restores them for tools", async () => { const pii = testPii([EmployeeBadgePiiPolicy]); const { output: masked } = await pii.mask("Assign ticket to EMP-482910 immediately."); expect(masked).not.toContain("EMP-482910"); expect(pii.reveal(masked)).toBe("Assign ticket to EMP-482910 immediately."); }); ``` ## What options does `@PiiPolicy` accept? [Section titled “What options does @PiiPolicy accept?”](#what-options-does-piipolicy-accept) Every `@PiiPolicy` class name ends with `PiiPolicy` and its `name` option ends with `-pii-policy`: | Option | Applies To | Type | Required | Description | | ----------------- | --------------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `name` | Both | `string` | Yes | Unique kebab-case identifier ending with `-pii-policy` (e.g., `"customer-pii-policy"`). | | `description` | Declarative | `string` | Yes | Explains which PII categories this policy governs. | | `rules` | Declarative | `readonly [PiiRule, ...]` | Yes | Non-empty list of `pii(PiiKind \| PiiGroup, PiiAction.*)` rules; specific `PiiKind` rules override broader `PiiGroup` rules. | | `detectWithModel` | Declarative | `string` | No | Chat model used to detect unstructured PII (such as person names and street addresses) in free text. | | `pseudonymKey` | Declarative | `Secret` | No | `secret("PII_PSEUDONYM_KEY")` used for `PiiAction.replaceWithToken()`; falls back to `@Application({ pseudonymKey })`. | | `action` | Custom Detector | `MaskingPiiAction` | Yes | `PiiAction.replaceWithToken()`, `replaceWithKind()`, or `replacePart()` applied to every `[start, end)` match returned by `findMatches()`. | | `deps` | Both | `readonly Token[]` | No | Constructor dependencies injected into a `PiiRefusal` or `PiiPolicyHandler` class. | ## What related entities work with `@PiiPolicy`? [Section titled “What related entities work with @PiiPolicy?”](#what-related-entities-work-with-piipolicy) | Related Entity | Import Path | Purpose | | --------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pii(target, action)` | `"@agboss/core"` | Binds a `PiiKind` or `PiiGroup` to a `PiiAction` inside `@PiiPolicy({ rules })`. | | `PiiKind` & `PiiGroup` | `"@agboss/core"` | Built-in PII categories (`Email`, `Phone`, `CardNumber`, `Iban`, `IpAddress`, `PersonName`, `StreetAddress`, `Government`, `Financial`, `Contact`). | | `PiiAction` | `"@agboss/core"` | Action factories: `replaceWithToken()` (reversible HMAC `` restored on tool calls), `replaceWithKind()`, `replacePart({ keepLast, keepDomain })`, and `blockWithRefusal()`. | | `PiiPolicyHandler` & `PiiRefusal` | `"@agboss/core"` | Interfaces implemented by custom range detectors (`findMatches(text)`) and localized refusal builders (`refusal(kind)`). | | `testPii(Policies, options?)` | `"@agboss/core/testing"` | Isolated unit runner that tests masking, reversible token restoration, and refusal triggers offline. | # @Rag > Complete API reference for the @Rag decorator in AGBoss — RagConnector, SqliteFtsConnector, RagMode.Tool vs RagMode.Context, citations, and testRag(). The `@Rag` decorator (`rag/*.rag.ts`) defines a knowledge base component implementing `RagConnector` (or extending the built-in zero-dependency `SqliteFtsConnector`). Agents attach knowledge bases via `@Agent({ rag: [{ use: DocsRag, mode: RagMode.Tool | RagMode.Context }] })`, and AGBoss automatically formats retrieved passages with `[source]` citations and tracks retrieval spend under `rag:`. ## Complete `@Rag` examples (`SqliteFtsConnector` and custom `RagConnector`) [Section titled “Complete @Rag examples (SqliteFtsConnector and custom RagConnector)”](#complete-rag-examples-sqliteftsconnector-and-custom-ragconnector) src/rag/job-postings.rag.ts ```ts import { ExecutionContext } from "@agboss/core"; import { Rag, SqliteFtsConnector, type RagConnector, type RagRetrieval, } from "@agboss/core/rag"; import { AppEnvironment, VectorSearchService } from "../services/vector-search.service.js"; // 1. Built-in SQLite FTS5 BM25 knowledge base (extends SqliteFtsConnector): @Rag({ name: "job_postings_fts", description: "Full-text BM25 index of open engineering job descriptions and salary bands", topK: 5, prompt: "Cite the posting ID in brackets like [job-104] for every compensation figure.", deps: [AppEnvironment], }) export class JobPostingsFtsRag extends SqliteFtsConnector { constructor(env: AppEnvironment) { super({ folder: "./docs/jobs", dbFile: env.sqlitePath ?? ":memory:", }); } } // 2. Custom external vector/hybrid database (implements RagConnector): @Rag({ name: "engineering_runbooks", description: "Production architecture runbooks and incident postmortems", topK: 4, deps: [ExecutionContext, VectorSearchService], }) export class EngineeringRunbooksRag implements RagConnector { constructor( private readonly executionContext: ExecutionContext, private readonly vectorSearch: VectorSearchService, ) {} async retrieve( query: string, options: { readonly topK: number; readonly signal: AbortSignal }, ): Promise { void this.executionContext.signal; const hits = await this.vectorSearch.query(query, { topK: options.topK, }); return { results: hits.map((hit) => ({ text: hit.text, source: hit.id, score: hit.score, })), costUsd: 0.0004, }; } } ``` src/services/vector-search.service.ts ```ts import { Injectable } from "@agboss/core"; export class AppEnvironment { sqlitePath?: string; } @Injectable() export class VectorSearchService { query(_query: string, opts: { topK: number; tenantId?: string }) { return Promise.resolve( [ { id: "band-l6", text: "L6 Staff Engineer band: $210k-$250k", score: 0.94 }, ].slice(0, opts.topK), ); } } ``` src/agents/recruiter-rag.agent.ts ```ts import { Agent, RagMode } from "@agboss/core"; import { EngineeringRunbooksRag, JobPostingsFtsRag } from "../rag/job-postings.rag.js"; @Agent({ name: "recruiter-rag-agent", description: "Combines BM25 FTS search and custom vector retrieval with [source] citations", model: "moonshotai/kimi-k2.6", prompt: "Answer candidate questions using retrieved chunks and cite [source].", rag: [ { use: JobPostingsFtsRag, mode: RagMode.Tool }, { use: EngineeringRunbooksRag, mode: RagMode.Context }, ], }) export class RecruiterRagAgent {} ``` test/job-postings.rag.test.ts ```ts import { expect, it } from "vitest"; import { testRag } from "@agboss/core/testing"; import { EngineeringRunbooksRag } from "../src/rag/job-postings.rag.js"; it("retrieves ranked passages with source IDs and tracks rag spend", async () => { const runRag = testRag(EngineeringRunbooksRag); const { output, spend } = await runRag("Staff Engineer salary band", { topK: 3 }); expect(output[0]?.source).toBe("band-l6"); expect(spend.totalUsd).toBeCloseTo(0.0004); }); ``` ## What options does `@Rag` accept? [Section titled “What options does @Rag accept?”](#what-options-does-rag-accept) | Option | Type | Required | Description | | ----------------------- | ------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `name` | `string` | Yes | Unique identifier for the knowledge base (`"company_handbook"`); keys cost attribution (`rag:company_handbook`) and generated search tool names. | | `description` | `string` | Yes | Explains what documents this knowledge base holds so the agent knows when to query it in `RagMode.Tool`. | | `topK` | `number` | Yes | Maximum number of passages returned per retrieval call (required, no implicit default). | | `prompt` / `promptUrls` | `string` / `readonly string[]` | No | Additional instructions guiding how the agent should query or cite passages from this knowledge base. | | `deps` | `readonly Token[]` | No | Constructor dependencies (`AppEnvironment`, `ExecutionContext`, `@Injectable` vector clients) verified against constructor parameters. | ## What related entities work with `@Rag`? [Section titled “What related entities work with @Rag?”](#what-related-entities-work-with-rag) | Related Entity | Import Path | Purpose | | ----------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RagConnector` | `"@agboss/core/rag"` | Interface implemented by `@Rag` classes (`retrieve(query, { topK, signal }): Promise` and optional `index(docs)`). | | `SqliteFtsConnector` | `"@agboss/core/rag"` | Built-in zero-dependency SQLite FTS5 BM25 connector with automatic tokenization and ranking. | | `RagMode` (`Tool`, `Context`) | `"@agboss/core"` | Used in `@Agent({ rag: [{ use: MyRag, mode }] })`: `RagMode.Tool` exposes a search tool the model calls on demand; `RagMode.Context` pre-fetches top-K passages before the model call. | | `testRag(RagClass, options?)` | `"@agboss/core/testing"` | Isolated unit runner that tests `retrieve()` and `rag:` cost tracking with DI overrides without booting an agent. | # @ReplyInterpreter > Complete API reference for the @ReplyInterpreter decorator, ReplyInterpreterHandler, and testReplyInterpreter() for structured model output extraction. `@ReplyInterpreter` (`"@agboss/core"`) declares the component that converts a free-text human reply (`app.resume({ pauseId, reply })`) into the typed decision required by a [`@ChannelRole`](/reference/channel-role/). Each `@ReplyInterpreter` belongs to a single `@ChannelRole({ replyInterpreter })` and can be driven either by a model (`DecidedBy.Model`) or by deterministic TypeScript (`DecidedBy.Code`). ## How to declare a `@ReplyInterpreter` (`DecidedBy.Model` & `DecidedBy.Code`) [Section titled “How to declare a @ReplyInterpreter (DecidedBy.Model & DecidedBy.Code)”](#how-to-declare-a-replyinterpreter-decidedbymodel--decidedbycode) A `@ReplyInterpreter` `name` must end with `-reply-interpreter`. Downstream code always consumes validated types — never raw reply strings: src/channels/refund-reply.interpreter.ts ```ts import { ChannelRole, DecidedBy, PauseAnswerer, ReplyInterpretation, ReplyInterpreter, type InputOf, type OutputOf, type ReplyInterpreterHandler, } from "@agboss/core"; // 1. Deterministic code interpreter (implements ReplyInterpreterHandler) @ReplyInterpreter({ name: "refund-code-reply-interpreter", description: "Parses short keyword replies ('approve', 'lgtm', 'reject: ') in code", decidedBy: DecidedBy.Code, }) export class RefundCodeReplyInterpreter implements ReplyInterpreterHandler { interpret( input: InputOf, ): ReplyInterpretation> { const text = input.reply.trim().toLowerCase(); if (text === "approve" || text === "lgtm" || text === "yes") { return ReplyInterpretation.understood({ approved: true }); } if (text.startsWith("reject:")) { return ReplyInterpretation.understood({ approved: false, reason: input.reply.slice("reject:".length).trim(), }); } return ReplyInterpretation.notUnderstood({ reason: "Reply with 'approve' or 'reject: '.", }); } } // 2. Attach the interpreter to @ChannelRole({ replyInterpreter }) @ChannelRole({ name: "refund-approval-channel", description: "Approves card refunds from Slack buttons or thread replies", approves: RefundInputDto, answeredBy: PauseAnswerer.Channel, replyInterpreter: RefundCodeReplyInterpreter, }) export abstract class RefundApprovalChannel extends ChannelOf {} ``` src/channels/model-reply.interpreter.ts ```ts import { DecidedBy, ReplyInterpreter } from "@agboss/core"; @ReplyInterpreter({ name: "refund-model-reply-interpreter", description: "Extracts approval and optional argument overrides from natural-language Slack replies", decidedBy: DecidedBy.Model, model: "moonshotai/kimi-k2.6", prompt: "Read the original refund request and the reviewer's reply. Approve only when the reviewer clearly agrees.", }) export class RefundModelReplyInterpreter {} ``` test/refund-reply.interpreter.test.ts ```ts import { expect, it } from "vitest"; import { ReplyInterpretation } from "@agboss/core"; import { RefundCodeReplyInterpreter } from "../src/refund-reply.js"; it("interprets free-text approval and rejection replies deterministically", () => { const interpreter = new RefundCodeReplyInterpreter(); const approved = interpreter.interpret({ ask: { callId: "call_1", tool: "issue_refund", summary: "Refund $40.00 to ORD-12", arguments: { orderId: "ORD-12", amountCents: 4000 }, }, reply: "LGTM", }); expect(approved).toEqual(ReplyInterpretation.understood({ approved: true })); const unclear = interpreter.interpret({ ask: { callId: "call_2", tool: "issue_refund", summary: "Refund $40.00 to ORD-12", arguments: { orderId: "ORD-12", amountCents: 4000 }, }, reply: "maybe tomorrow", }); expect(unclear).toEqual( ReplyInterpretation.notUnderstood({ reason: "Reply must be 'approve', 'lgtm', 'reject: ', or 'approve amount='.", }), ); }); ``` ## `@ReplyInterpreter` options & `ReplyInterpretation` reference [Section titled “@ReplyInterpreter options & ReplyInterpretation reference”](#replyinterpreter-options--replyinterpretation-reference) | Option / Helper | Applies To | Description | | ----------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Both | Kebab-case identifier ending with `-reply-interpreter`. | | `description` | Both | Non-empty description of how the reply is interpreted. | | `decidedBy` | Both | `DecidedBy.Model` or `DecidedBy.Code`. | | `model` | `DecidedBy.Model` | Chat model (uses a single structured answer tool) or — for an approval role only — a decision model (`Decision.noul("approved")`; using a decision model on a question role fails with `[reply-interpreter.decision-model]`). | | `prompt` / `promptUrls` | `DecidedBy.Model` | Static instructions for interpreting the reply (the pause’s `ask` and `reply` arrive in the user message). | | `deps` | `DecidedBy.Code` | Optional constructor dependency tokens injected into the `ReplyInterpreterHandler` class. | | `ReplyInterpretation.understood(decision)` | `DecidedBy.Code` | Returns a parsed decision to be validated against the channel role’s answer DTO. | | `ReplyInterpretation.notUnderstood({ reason })` | `DecidedBy.Code` | Rejects `app.resume({ pauseId, reply })` with `UninterpretableReplyError` (carrying `.reason`) while keeping the pause `Pending`. | # @Router > Complete API reference for @Router in AGBoss — DecidedBy.Model vs DecidedBy.Code, SpawnPolicy, FailurePolicy, RouterHandler, and testRouter(). The `@Router` decorator defines a control-flow decision node (`routers/*.router.ts`) in AGBoss. Routers decouple orchestration from domain agents and come in two forms discriminated by `decidedBy`: **`DecidedBy.Model`** (where a fast classifier model like `typesafe/jev-1.13` picks the target route from prompt criteria) and **`DecidedBy.Code`** (where deterministic TypeScript in `route(input)` with constructor DI picks the target at zero LLM cost and zero latency). ## Complete `@Router` examples (`DecidedBy.Model` and `DecidedBy.Code`) [Section titled “Complete @Router examples (DecidedBy.Model and DecidedBy.Code)”](#complete-router-examples-decidedbymodel-and-decidedbycode) src/routers/support.routers.ts ```ts import { DecidedBy, FailurePolicy, Router, SpawnPolicy, type InputOf, type RouterAnswerOf, type RouterHandler, } from "@agboss/core"; import { WorkflowStartTextDto } from "@agboss/core/dto"; import { AuditLogAgent, BillingAgent, ClaimBillingAgent, ClaimRefundAgent, OrderPolicyService, RefundAgent, TechnicalSupportAgent, } from "../support.components.js"; import { OrderClaimDto } from "../dto/order.dto.js"; // 1. Model-decided router (DecidedBy.Model — empty class, no route() method): @Router({ name: "intent-triage-router", description: "Routes incoming customer messages to the matching specialist", decidedBy: DecidedBy.Model, model: "typesafe/jev-1.13", input: { message: WorkflowStartTextDto, }, prompt: "Classify the user's support request and choose the most specific handler.", maxVisits: 3, routes: [ { target: RefundAgent, prompt: "The customer is asking for a refund, chargeback, or order cancellation", }, { target: BillingAgent, prompt: "The customer is asking about an invoice, receipt, or subscription charge", }, { target: TechnicalSupportAgent, prompt: "The customer is reporting a bug, outage, or API integration problem", }, ], }) export class IntentTriageRouter {} // 2. Code-decided router (DecidedBy.Code — implements RouterHandler with constructor DI): @Router({ name: "claim-tier-router", description: "Selects which specialist agents to launch based on deterministic order policy rules", decidedBy: DecidedBy.Code, input: { claim: OrderClaimDto, }, deps: [OrderPolicyService], routes: [ { target: AuditLogAgent, spawn: SpawnPolicy.Always, failure: FailurePolicy.Skip }, { target: ClaimRefundAgent, spawn: SpawnPolicy.OnDemand }, { target: ClaimBillingAgent, spawn: SpawnPolicy.OnDemand }, ], }) export class ClaimTierRouter implements RouterHandler { constructor(private readonly orderPolicy: OrderPolicyService) {} async route(input: InputOf): Promise> { // Single-value inputs in InputOf are wrapped in Produced ({ value, producer }): const requiresBillingReview = await this.orderPolicy.requiresFinanceSignOff( input.claim.value.amountCents, ); return requiresBillingReview ? [ClaimRefundAgent, ClaimBillingAgent] : [ClaimRefundAgent]; } } ``` src/dto/order.dto.ts ```ts import { Integer, Text } from "@agboss/core/dto"; export class OrderClaimDto { @Text() orderId!: string; @Integer() amountCents!: number; } ``` src/workflows/support-routing.workflow\.ts ```ts import { Workflow, listOf, route, start } from "@agboss/core"; import { WorkflowFinishTextDto, WorkflowStartTextDto } from "@agboss/core/dto"; import { ClaimTierRouter, IntentTriageRouter } from "../routers/support.routers.js"; import { AuditLogAgent, BillingAgent, ClaimBillingAgent, ClaimRefundAgent, ClaimStart, CombineClaimRepliesAction, OrderPolicyService, RefundAgent, SupportFinish, SupportStart, TechnicalSupportAgent, } from "../support.components.js"; import { OrderClaimDto } from "../dto/order.dto.js"; @Workflow({ name: "support-routing-workflow", version: "1.0.0", providers: [OrderPolicyService], flow: [ start(SupportStart) .returns(WorkflowStartTextDto) .toRouter(IntentTriageRouter) .chooseOne( route(BillingAgent) .toFinish(SupportFinish), route(RefundAgent) .toFinish(SupportFinish), route(TechnicalSupportAgent) .toFinish(SupportFinish), ), start(ClaimStart) .returns(OrderClaimDto) .toRouter(ClaimTierRouter) .toParallelAgents(AuditLogAgent, ClaimRefundAgent, ClaimBillingAgent) .join() .returns(listOf(WorkflowFinishTextDto)) .toAction(CombineClaimRepliesAction) .returns(WorkflowFinishTextDto) .toFinish(SupportFinish), ], }) export class SupportRoutingWorkflow {} ``` test/support.routers.test.ts ```ts import { expect, it } from "vitest"; import { routeTo, testRouter } from "@agboss/core/testing"; import { ClaimTierRouter, IntentTriageRouter } from "../src/routers/support.routers.js"; import { ClaimBillingAgent, ClaimRefundAgent, ClaimStart, RefundAgent, SupportStart, } from "../src/support.components.js"; it("tests both a model router and a code router in isolation", async () => { const runModelRouter = testRouter(IntentTriageRouter, { model: routeTo(RefundAgent), }); const modelPicked = await runModelRouter({ message: { producer: SupportStart, value: { text: "Please cancel my order and refund my card" }, }, }); expect(modelPicked.output).toBe(RefundAgent); const runCodeRouter = testRouter(ClaimTierRouter); const codePicked = await runCodeRouter({ claim: { producer: ClaimStart, value: { orderId: "A-1042", amountCents: 250_00 }, }, }); expect(codePicked.output).toEqual([ClaimRefundAgent, ClaimBillingAgent]); }); ``` ## What options does `@Router` accept? [Section titled “What options does @Router accept?”](#what-options-does-router-accept) Every `@Router` class name ends with `Router` and its `name` option ends with `-router`: | Option | Applies To | Type | Required | Description | | ------------------------------------ | ----------------- | ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Both | `string` | Yes | Unique kebab-case identifier ending with `-router` (e.g., `"main-router"`). | | `description` | Both | `string` | Yes | Describes what decision this router makes. | | `decidedBy` | Both | `DecidedBy.Model \| DecidedBy.Code` | Yes | Selects whether an LLM/decision model or a TypeScript `route(input)` method chooses the branch. | | `input` | Both | `InputFields` | Yes | Non-empty map of DTOs read from Path Scope (`[router.empty-input]` if empty). | | `routes` | Both | `readonly [RouterRoute, ...]` | Yes | List of allowed `target` components; must match the targets in `.chooseOne(route(...))` or `.toParallelAgents(...)` (`[router.routes-mismatch]`). | | `maxVisits` | Both | `number` | No | Maximum visits per execution before throwing `VisitsLimitExceededError`. | | `model` | `DecidedBy.Model` | `string` | No | Decision model (`"typesafe/jev-1.13"`) or chat model; inherits `defaults.router.model` when omitted. | | `prompt` / `promptUrls` | `DecidedBy.Model` | `string` / `readonly string[]` | No | Instructions telling the model how to evaluate the input and select a route. | | `promptVariables` | `DecidedBy.Model` | `Readonly>` | No | Static `{{key}}` variables interpolated into the router’s prompt. | | `piiPolicies` / `excludePiiPolicies` | `DecidedBy.Model` | `readonly Class[]` | No | `@PiiPolicy` classes masking inputs before they reach the router model. | | `guardrails` / `excludeGuardrails` | `DecidedBy.Model` | `readonly Class[]` | No | `@InputGuardrail` classes evaluated before the router model runs. | | `compaction` / `uncompactedTurns` | `DecidedBy.Model` | `Class` / `number \| TurnLimit` | No | Overrides thread compaction or recent turn window for this router. | | `deps` | `DecidedBy.Code` | `readonly Token[]` | No | Constructor dependencies (`ExecutionContext`, `@Injectable` services) injected into the code router class. | ### Route entry options (`routes: [...]`) [Section titled “Route entry options (routes: \[...\])”](#route-entry-options-routes-) Each item in `routes` configures a branch target for `.chooseOne(route(...))` or `.toParallelAgents(...)`: | Route Field | Type | Description | | ----------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `target` | `ComponentClass` | Target `@Agent`, `@Action`, `@Router`, or `@WorkflowFinish` class. | | `prompt` | `string` | Required on `DecidedBy.Model` routes with `SpawnPolicy.OnDemand`; describes when the model should select `target`. Forbidden on `DecidedBy.Code` routes. | | `spawn` | `SpawnPolicy.Always \| SpawnPolicy.OnDemand` | Used before `.toParallelAgents(...)`: `Always` launches unconditionally alongside chosen branches; `OnDemand` launches only when chosen by the router. | | `failure` | `FailurePolicy.FailFast \| FailurePolicy.Skip` | Used before `.toParallelAgents(...)`: whether a failure in this target fails the parallel join (`FailFast`, default) or drops out (`Skip`). | ## What related entities work with `@Router`? [Section titled “What related entities work with @Router?”](#what-related-entities-work-with-router) | Related Entity | Import Path | Purpose | | ------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `DecidedBy`, `SpawnPolicy`, `FailurePolicy` | `"@agboss/core"` | Enums controlling decision mode (`Model` vs `Code`), parallel launch selection (`Always` vs `OnDemand`), and branch failure handling (`FailFast` vs `Skip`). | | `RouterHandler` & `Route` | `"@agboss/core"` | Contract and return type for `DecidedBy.Code` routers (`route(input)` returns one target for `.chooseOne()` or one/many targets for `.toParallelAgents()`). | | `.toRouter(R).chooseOne(route(Target)...)` | `"@agboss/core"` | Connects a router to exclusive branch paths in `@Workflow({ flow })`. | | `.toRouter(R).toParallelAgents(...)` | `"@agboss/core"` | Uses a router to dynamically select which subset of `SpawnPolicy.OnDemand` agents to launch in parallel alongside `SpawnPolicy.Always` agents. | | `RouterFailedError` & `RouterDecisionError` | `"@agboss/core"` | Errors thrown when a router cannot select a valid route; catchable in the flow via `.onError(RouterFailedError, sidePath)`. | | `testRouter(RouterClass, options?)` | `"@agboss/core/testing"` | Unit runner that tests a model or code router in isolation without booting the workflow. | # @Tool & @BindTool > Complete API reference for @Tool and @BindTool in AGBoss — ToolHandler, channel approvals, ExecutionContext, SpendRecorder, and testTool(). The `@Tool` class decorator (`tools/*.tool.ts`) defines a typed capability that an `@Agent` can invoke during its tool-calling loop, or that can be called directly via `app.tool(ToolClass).invoke(input)` or exposed over MCP via `@McpExpose`. Every `@Tool` declares its input and output DTO classes from `@agboss/core/dto` and implements `ToolHandler`. The companion `@BindTool` method decorator binds tool callbacks onto component methods. ## Complete `@Tool` example with all capabilities [Section titled “Complete @Tool example with all capabilities”](#complete-tool-example-with-all-capabilities) src/tools/refund-card-payment.tool.ts ```ts import { ExecutionContext, SpendRecorder, Tool, type ToolHandler } from "@agboss/core"; import { Integer, OneOf, Text } from "@agboss/core/dto"; import { kilobytes, seconds, usd } from "@agboss/core/units"; import { RefundApprovalChannel } from "../channels/refund-approval.channel.js"; import { PaymentsService } from "../services/payments.service.js"; export class RefundInputDto { @Text({ minLength: 1, prompt: "Order ID to refund, e.g. ORD-1042" }) orderId!: string; @Integer({ min: 1, max: 500_000, prompt: "Refund amount in cents" }) amountCents!: number; } export class RefundOutputDto { @OneOf({ values: ["refunded", "queued"], prompt: "Settlement status" }) status!: "refunded" | "queued"; @Text({ prompt: "Payment gateway refund reference ID" }) refundId!: string; } @Tool({ name: "refund_card_payment", description: "Issues a card refund for an order after channel approval", prompt: "Call this tool only after verifying the order ID and exact amount in cents.", input: RefundInputDto, output: RefundOutputDto, channel: RefundApprovalChannel, timeout: seconds(15), maxResultSize: kilobytes(64), deps: [ExecutionContext, SpendRecorder, PaymentsService], }) export class RefundCardPaymentTool implements ToolHandler { constructor( private readonly executionContext: ExecutionContext, private readonly spendRecorder: SpendRecorder, private readonly paymentsService: PaymentsService, ) {} async run(input: RefundInputDto): Promise { const refundId = await this.paymentsService.refund({ orderId: input.orderId, amountCents: input.amountCents, idempotencyKey: this.executionContext.step.idempotencyKey, signal: this.executionContext.signal, }); this.spendRecorder.record({ service: "stripe-refund-fee", cost: usd(0.02) }); return { status: "refunded", refundId, }; } } ``` src/services/payments.service.ts ```ts import { Injectable } from "@agboss/core"; @Injectable() export class PaymentsService { refund(req: { orderId: string; amountCents: number; idempotencyKey: string; signal: AbortSignal; }): Promise { return Promise.resolve(`re_${req.orderId}_${req.idempotencyKey.slice(0, 8)}`); } } ``` src/channels/refund-approval.channel.ts ```ts import { ChannelOf, ChannelRole, PauseAnswerer } from "@agboss/core"; import { RefundInputDto } from "../tools/refund-card-payment.tool.js"; @ChannelRole({ name: "refund-approval-channel", description: "Requests human sign-off before executing high-value card refunds", approves: RefundInputDto, answeredBy: PauseAnswerer.Channel, }) export abstract class RefundApprovalChannel extends ChannelOf {} ``` src/agents/refund-specialist.agent.ts ```ts import { Agent } from "@agboss/core"; import { RefundOutputDto, RefundCardPaymentTool } from "../tools/refund-card-payment.tool.js"; @Agent({ name: "refund-specialist-agent", description: "Issues idempotent Stripe refunds via RefundCardPaymentTool", model: "moonshotai/kimi-k2.6", prompt: "Verify the order ID and call refund_card_payment when eligible.", tools: [RefundCardPaymentTool], output: RefundOutputDto, maxToolCalls: 2, }) export class RefundSpecialistAgent {} ``` test/refund-card-payment.tool.test.ts ```ts import { expect, it } from "vitest"; import { override, testTool } from "@agboss/core/testing"; import { RefundCardPaymentTool } from "../src/tools/refund-card-payment.tool.js"; import { PaymentsService } from "../src/services/payments.service.js"; class FakePaymentsService extends PaymentsService { override refund(): Promise { return Promise.resolve("re_test_1042"); } } it("executes RefundCardPaymentTool with a fake PaymentsService", async () => { const runTool = testTool(RefundCardPaymentTool, { overrides: [override(PaymentsService, FakePaymentsService)], }); const { output } = await runTool({ orderId: "ORD-1042", amountCents: 49_00, }); expect(output).toEqual({ status: "refunded", refundId: "re_test_1042", }); }); ``` ## What options do `@Tool` and `@BindTool` accept? [Section titled “What options do @Tool and @BindTool accept?”](#what-options-do-tool-and-bindtool-accept) | Option | Type | Required | Description | | ------------------------- | ------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Yes | Snake_case or kebab-case tool name exposed to the model (`"refund_card"`). | | `description` | `string` | Yes | Concise description telling the model what the tool does. | | `input` | `DtoClass` | Yes | DTO class (`@agboss/core/dto`) validating the tool’s input arguments and generating its JSON Schema. | | `output` | `DtoClass` | Yes | DTO class validating the plain object returned by `run(input)`. | | `deps` | `readonly Token[]` | No | Constructor dependencies (`ExecutionContext`, `SpendRecorder`, `@Injectable` services) verified against constructor parameters. | | `prompt` / `promptUrls` | `string` / `readonly string[]` | No | Detailed usage instructions for the tool appended to the agent’s tool guidance. | | `channel` | `Class` | No | [`@ChannelRole`](/reference/channel-role/) or [`@Channel`](/reference/channel/) class that gates tool execution on human or external approval. | | `toolInterceptors` | `readonly Class[]` | No | [`@ToolInterceptor`](/reference/tool-interceptor/) classes attached directly to this tool (`BeforeToolCall` / `AfterToolCall`). | | `excludeToolInterceptors` | `readonly Class[]` | No | Inherited application, workflow, or agent `@ToolInterceptor` classes exempted for this tool. | | `timeout` | `Milliseconds` | No | Per-call timeout (`seconds(15)`); defaults to `seconds(30)` and aborts `executionContext.signal` when exceeded. | | `maxResultSize` | `Bytes` | No | Maximum UTF-8 result size (`kilobytes(256)`) shown to the model before truncation (`kilobytes(128)` by default). | ## What related entities work with `@Tool`? [Section titled “What related entities work with @Tool?”](#what-related-entities-work-with-tool) | Related Entity | Import Path | Purpose | | ----------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `ToolHandler` | `"@agboss/core"` | Contract implemented by `@Tool` and `@McpTool` classes (`run(input: TInput): Promise`). | | `ExecutionContext` | `"@agboss/core"` | Injected via `deps: [ExecutionContext]`; provides `step.idempotencyKey`, `step.callId`, `signal`, `executionId`, and `ownerId`. | | `SpendRecorder` | `"@agboss/core"` | Injected via `deps: [SpendRecorder]`; records external API spend (`spendRecorder.record({ service, cost })`) against `@Budget` limits. | | `app.tool(ToolClass).invoke(input)` | `"@agboss/core"` | Executes a registered tool directly inside its own isolated execution context. | | `testTool(ToolClass, options?)` | `"@agboss/core/testing"` | Isolated unit runner that executes `run(input)` with DI `overrides` and returns `{ output, spend }`. | | `mockTool(ToolClass)` | `"@agboss/core/testing"` | Mocks or spies on a tool inside a `testWith(Application)` workflow test. | # @ToolInterceptor > Complete API reference for @ToolInterceptor, BeforeToolCall, AfterToolCall, ToolCallDecision, and testToolInterceptor() for @Tool and @McpTool in AGBoss. `@ToolInterceptor` (`"@agboss/core"`) declares a reusable pre-call and/or post-call interceptor around [`@Tool`](/reference/tool/) and [`@McpTool`](/reference/mcp-tool/) invocations. While [`@InputGuardrail` and `@OutputGuardrail`](/reference/guardrail/) guard the outer text boundary of an `@Agent` or `@Router`, `@ToolInterceptor` guards every individual tool call inside the agent’s tool loop (`app.execute` / `app.stream`) as well as direct `app.tool(ToolClass).invoke(input)` calls. A `@ToolInterceptor` class implements `BeforeToolCall`, `AfterToolCall`, or both (`[tool-interceptor.missing-handler]` if neither is implemented), and cascades across `@Application` $\to$ `@Workflow` $\to$ `@Agent` $\to$ `@Tool` / `@McpTool` via `toolInterceptors: [...]` and `excludeToolInterceptors: [...]`. ## Complete `@ToolInterceptor` example [Section titled “Complete @ToolInterceptor example”](#complete-toolinterceptor-example) src/interceptors/tenant-order.tool-interceptor.ts ```ts import { Injectable, TenantContext, Tool, ToolCallDecision, ToolInterceptor, type AfterToolCall, type BeforeToolCall, type ToolCallContext, type ToolCallResultContext, type ToolHandler, } from "@agboss/core"; import { Integer, Text } from "@agboss/core/dto"; export class OrderInputDto { @Text({ prompt: "Order ID to inspect or refund" }) orderId!: string; } export class OrderRecordDto { @Text({ prompt: "Order ID" }) orderId!: string; @Integer({ prompt: "Total in cents" }) amountCents!: number; @Text({ prompt: "Internal merchant notes" }) internalNote!: string; } @Injectable() export class OrderAclService { belongsToTenant(tenantId: string, orderId: string): Promise { return Promise.resolve(orderId.startsWith(`${tenantId.toUpperCase()}-`)); } } @Tool({ name: "get-order-tool", description: "Loads an order record by ID", input: OrderInputDto, output: OrderRecordDto, }) export class GetOrderTool implements ToolHandler { run(input: OrderInputDto): Promise { return Promise.resolve({ orderId: input.orderId, amountCents: 12_500, internalNote: "VIP margin tier A", }); } } @ToolInterceptor({ name: "tenant-order-tool-interceptor", description: "Verifies order ownership before running a tool and strips internal notes after", deps: [TenantContext, OrderAclService], }) export class TenantOrderToolInterceptor implements BeforeToolCall, AfterToolCall { constructor( private readonly tenantContext: TenantContext, private readonly orderAcl: OrderAclService, ) {} async beforeToolCall(call: ToolCallContext): Promise { const orderId = typeof call.input === "object" && call.input !== null && "orderId" in call.input && typeof call.input.orderId === "string" ? call.input.orderId : ""; const allowed = await this.orderAcl.belongsToTenant(this.tenantContext.tenantId, orderId); if (!allowed) { return ToolCallDecision.replyToModel({ message: `Order ${orderId} does not belong to tenant ${this.tenantContext.tenantId}. Ask the user to confirm their order ID.`, }); } return ToolCallDecision.allow(); } afterToolCall(call: ToolCallResultContext): OrderRecordDto { const record = call.output as OrderRecordDto; return { ...record, internalNote: "[REDACTED]", }; } } ``` test/tenant-order.tool-interceptor.test.ts ```ts import { expect, it } from "vitest"; import { ToolCallDecision } from "@agboss/core"; import { testToolInterceptor } from "@agboss/core/testing"; import { GetOrderTool, TenantOrderToolInterceptor, } from "../src/interceptors/tenant-order.tool-interceptor.js"; it("replies to the model when an order belongs to another tenant and scrubs internalNote on allow", async () => { const interceptor = testToolInterceptor(TenantOrderToolInterceptor, { tenant: { tenantId: "acme" }, }); const { output: denied } = await interceptor.beforeToolCall(GetOrderTool, { orderId: "OTHER-99", }); expect(denied).toEqual( ToolCallDecision.replyToModel({ message: "Order OTHER-99 does not belong to tenant acme. Ask the user to confirm their order ID.", }), ); const { output: allowed } = await interceptor.beforeToolCall(GetOrderTool, { orderId: "ACME-42", }); expect(allowed).toEqual(ToolCallDecision.allow()); const { output: redacted } = await interceptor.afterToolCall( GetOrderTool, { orderId: "ACME-42" }, { orderId: "ACME-42", amountCents: 12_500, internalNote: "VIP margin tier A" }, ); expect(redacted.internalNote).toBe("[REDACTED]"); }); ``` ## `@ToolInterceptor` options & handler interfaces [Section titled “@ToolInterceptor options & handler interfaces”](#toolinterceptor-options--handler-interfaces) | Option / Interface | Type | Required | Description | | ------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | **Yes** | Unique kebab-case identifier ending with `-tool-interceptor`; the class name must end with `ToolInterceptor` (`[name.kind-suffix]`). | | `description` | `string` | **Yes** | Non-empty summary of the interceptor’s responsibility. | | `deps` | `readonly Token[]` | No | Constructor dependencies (`TenantContext`, `ExecutionContext`, `@Injectable()` services) injected per invocation. | | `BeforeToolCall` | `beforeToolCall(call: ToolCallContext)` | At least one of `BeforeToolCall` or `AfterToolCall` (`[tool-interceptor.missing-handler]`) | Inspects `{ tool, toolName, callId, input }` after DTO validation and returns a `ToolCallDecision`. | | `AfterToolCall` | `afterToolCall(call: ToolCallResultContext)` | At least one of `BeforeToolCall` or `AfterToolCall` | Inspects `{ tool, toolName, callId, input, output }` after `tool.run(input)` and returns the (optionally transformed) output validated against the tool’s `output` DTO before `maxResultSize` truncation and PII masking. | ## `ToolCallDecision` outcomes [Section titled “ToolCallDecision outcomes”](#toolcalldecision-outcomes) | Factory Method | Runtime Behavior | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ToolCallDecision.allow()` | Proceeds to the next `BeforeToolCall` interceptor (or `tool.run(input)` once all interceptors allow). | | `ToolCallDecision.replyToModel({ message })` | **Self-correcting tool feedback:** `tool.run()` and `channel` approval do **not** execute; inside an `@Agent` loop, `message` is returned to the model as the tool call’s observation so the agent can self-correct its arguments. On direct `app.tool(T).invoke()`, throws `ToolBlockedError`. | | `ToolCallDecision.block({ reason })` | **Hard abort:** `tool.run()` does **not** execute and the step fails immediately with `ToolBlockedError` (a subclass of `StepFailedError`, catchable by `.onError()` in workflow flows). | ## How the cascading interceptor hierarchy works [Section titled “How the cascading interceptor hierarchy works”](#how-the-cascading-interceptor-hierarchy-works) Interceptors attach via `toolInterceptors: [...]` on [`@Application`](/reference/application/), [`@Workflow`](/reference/workflow/), [`@Agent`](/reference/agent/), [`@Tool`](/reference/tool/), and [`@McpTool`](/reference/mcp-tool/), and can be opted out at any inner scope via `excludeToolInterceptors: [...]`: 1. **`beforeToolCall` runs outer-to-inner:** `@Application` $\to$ `@Workflow` $\to$ `@Agent` $\to$ `@Tool` / `@McpTool`, **before** any human approval `@Channel` on the tool is triggered (so unauthorized or malformed calls never wake a human reviewer). 2. **`afterToolCall` runs inner-to-outer (onion order):** `@Tool` / `@McpTool` $\to$ `@Agent` $\to$ `@Workflow` $\to$ `@Application`, before output DTO re-validation, `maxResultSize` truncation, and `@PiiPolicy` masking. # @Workflow > Complete API reference for @Workflow in AGBoss — options, HarnessDSL (HDSL) Path Scope flow methods, parallel waves, quorums, child workflows, and review loops. The `@Workflow` decorator defines a typed state graph (`*.workflow.ts`) in AGBoss using **HarnessDSL (`HDSL`)** — our declarative graph syntax. Every workflow declares its `name`, `version`, and `flow` array of paths built with `start()`, `agent()`, `router()`, `action()`, `workflow()`, `route()`, and `caseOf()`, enforcing strictly one chained method call per line (`@stylistic/newline-per-chained-call`) and compile-time Path Scope verification. ## Complete `@Workflow` example with all capabilities [Section titled “Complete @Workflow example with all capabilities”](#complete-workflow-example-with-all-capabilities) src/workflows/offer-negotiation.workflow\.ts ```ts import { StepFailedError, VisitsLimitExceededError, Workflow, action, agent, caseOf, listOf, oneOf, route, start, toAction, } from "@agboss/core"; import { Reasoning, ReasoningEffort } from "@agboss/core/models"; import { seconds } from "@agboss/core/units"; import { AuditArbiter, AuditTrailGuardrail, ComplianceCheckWorkflow, DirectoryMcpServer, DrafterAgent, EditorAgent, LegalAuditAgent, NotifyRecruiterAction, OfferCompaction, OfferFailedWorkflowFinish, OfferFormattingService, OfferPiiPolicy, OfferWorkflowFinish, OfferWorkflowStart, ReviewerAgent, SecurityAuditAgent, SendOfferAction, TriageRouter, } from "../offer.components.js"; import { AuditVerdictDto, CandidateInputDto, ChangesRequestedDto, ComplianceReportDto, DraftApprovedDto, OfferDraftDto, } from "../dto/offer-negotiation.dto.js"; @Workflow({ name: "offer-workflow", version: "1.0.0", maxSteps: 18, maxDepth: 3, maxConcurrentExecutions: 8, providers: [OfferFormattingService], mcp: [DirectoryMcpServer], piiPolicies: [OfferPiiPolicy], guardrails: [AuditTrailGuardrail], compaction: OfferCompaction, uncompactedTurns: 8, promptVariables: { companyName: "Acme Corp", }, defaults: { models: { temperature: 0.2, reasoning: Reasoning.on({ effort: ReasoningEffort.Low }), }, router: { model: "typesafe/jev-1.13", }, tools: { maxToolCalls: 6, }, }, flow: [ start(OfferWorkflowStart) .returns(CandidateInputDto) .toRouter(TriageRouter) .chooseOne( route(DrafterAgent) .returns(OfferDraftDto) .toAgent(ReviewerAgent), route(OfferFailedWorkflowFinish), ), agent(ReviewerAgent) .onError( VisitsLimitExceededError, toAction(NotifyRecruiterAction), ) .returns(oneOf(DraftApprovedDto, ChangesRequestedDto)) .switch( caseOf(DraftApprovedDto) .toWorkflow(ComplianceCheckWorkflow) .returns(ComplianceReportDto) .toParallelAgents(SecurityAuditAgent, LegalAuditAgent) .acceptedBy(AuditArbiter) .joinQuorum({ min: 2, deadline: seconds(15) }) .onJoinError( StepFailedError, toAction(NotifyRecruiterAction), ) .returns(listOf(AuditVerdictDto)) .toAction(SendOfferAction) .toFinish(OfferWorkflowFinish), caseOf(ChangesRequestedDto, { about: OfferDraftDto }) .toAgent(EditorAgent) .returns(OfferDraftDto) .toAgent(ReviewerAgent), ), action(NotifyRecruiterAction) .toFinish(OfferFailedWorkflowFinish), ], }) export class OfferWorkflow {} ``` src/dto/offer-negotiation.dto.ts ```ts import { Integer, Text } from "@agboss/core/dto"; export class CandidateInputDto { @Text() role!: string; @Text() region!: string; } export class OfferDraftDto { @Text() summary!: string; @Integer() baseUsd!: number; } export class DraftApprovedDto { @Text() signedBy!: string; } export class ChangesRequestedDto { @Text() critique!: string; } export class ComplianceReportDto { @Text() status!: string; } export class AuditVerdictDto { @Text() auditor!: string; } ``` test/offer-negotiation.workflow\.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { replyWith, routeTo, testWith } from "@agboss/core/testing"; import { OfferApplication } from "../src/offer.application.js"; import { ComplianceAgent, DrafterAgent, LegalAuditAgent, OfferWorkflowStart, ReviewerAgent, SecurityAuditAgent, TriageRouter, } from "../src/offer.components.js"; import { AuditVerdictDto, ComplianceReportDto, DraftApprovedDto, OfferDraftDto, } from "../src/dto/offer-negotiation.dto.js"; const test = testWith(OfferApplication); test("runs OfferWorkflow through review loop and quorum to approval", async ({ app, mockLlm }) => { mockLlm(TriageRouter).thenReturn(routeTo(DrafterAgent)); mockLlm(DrafterAgent).thenReturn( replyWith(OfferDraftDto, { summary: "Staff TS Engineer", baseUsd: 215_000 }), ); mockLlm(ReviewerAgent).thenReturn( replyWith(DraftApprovedDto, { signedBy: "comp-committee" }), ); mockLlm(ComplianceAgent).thenReturn( replyWith(ComplianceReportDto, { status: "passed" }), ); mockLlm(SecurityAuditAgent).thenReturn( replyWith(AuditVerdictDto, { auditor: "sec-1" }), ); mockLlm(LegalAuditAgent).thenReturn( replyWith(AuditVerdictDto, { auditor: "legal-1" }), ); const res = await app.execute(OfferWorkflowStart, { role: "Staff TS", region: "EU" }); expect(res.status).toBe(ExecutionStatus.Finished); }); ``` ## What options does `@Workflow` accept? [Section titled “What options does @Workflow accept?”](#what-options-does-workflow-accept) Every `@Workflow` class name ends with `Workflow` and its `name` option ends with `-workflow`: | Option | Type | Required | Description | | ------------------------- | ---------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Yes | Unique kebab-case identifier ending with `-workflow` (e.g., `"offer-workflow"`). | | `version` | `string` | Yes | Behavior version of the workflow (`"1.0.0"`), checked when resuming paused executions across deploys. | | `flow` | `readonly FlowPath[]` | Yes | List of paths starting with `start()`, `agent()`, `router()`, `action()`, or `workflow()`. | | `maxSteps` | `number` | No | Maximum agent and router visits per execution; defaults to `(agents + routers) * 3`. | | `maxDepth` | `number` | No | Maximum nesting depth for child workflows and `ThisWorkflow` recursive calls (soft stop omitting the recursion tool at `depth === maxDepth` for model launches; throws `DepthLimitExceededError` for deterministic launches). | | `maxConcurrentExecutions` | `number` | No | Maximum concurrent executions across the root workflow and all child/recursive subtrees (`10` by default). | | `defaults` | `WorkflowDefaults` | No | Workflow-level defaults for `models`, `router`, `guardrail`, `tools`, and `flow.maxPathDepth`. | | `providers` | `readonly Provider[]` | No | Workflow-scoped `@Injectable` services, `@A2AAgent` clients, or `provide(TOKEN, value)` bindings. | | `mcp` | `readonly Class[]` | No | `@McpServer` classes whose tools are used by agents in this workflow. | | `piiPolicies` | `readonly Class[]` | No | Additional `@PiiPolicy` classes applied to all agents and routers in this workflow. | | `guardrails` | `readonly Class[]` | No | [`@InputGuardrail` and `@OutputGuardrail`](/reference/guardrail/) classes applied across all agents and model routers in this workflow. | | `excludeGuardrails` | `readonly Class[]` | No | Inherited application guardrails exempted for this workflow. | | `toolInterceptors` | `readonly Class[]` | No | [`@ToolInterceptor`](/reference/tool-interceptor/) classes applied across all `@Tool` and `@McpTool` invocations in this workflow. | | `excludeToolInterceptors` | `readonly Class[]` | No | Inherited application `@ToolInterceptor` classes exempted for this workflow. | | `compaction` | `Class` | No | [`@Compaction`](/reference/compaction/) class summarizing conversation turns older than `uncompactedTurns`. | | `uncompactedTurns` | `number \| TurnLimit` | No | Number of recent finished turns shown in full to agents and model routers in this workflow (`5` by default, or `TurnLimit.ALL`). | | `promptVariables` | `Readonly>` | No | Static `{{variable}}` substitutions available to every prompt in the workflow. | ## What HarnessDSL (HDSL) flow functions and related entities work with `@Workflow`? [Section titled “What HarnessDSL (HDSL) flow functions and related entities work with @Workflow?”](#what-harnessdsl-hdsl-flow-functions-and-related-entities-work-with-workflow) | Builder / Entity | Import Path | Description | | ----------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `start(StartClass)` | `"@agboss/core"` | Begins a root path from a `@WorkflowStart` class. | | `agent(AgentClass)`, `router(RouterClass)`, `action(ActionClass)`, `workflow(ChildWorkflow)` | `"@agboss/core"` | Begins a shared continuation path from a node reached by multiple upstream paths. | | `.returns(Dto, { about? })` | Flow chain | Declares one output DTO added to Path Scope; `{ about: TargetDto }` binds review verdicts to the artifact they judge. | | `.toAgent(A)`, `.toAction(A)`, `.toRouter(R)`, `.toWorkflow(W, { placement? })`, `.toFinish(F)` | Flow chain | Transitions execution to the next node after verifying its `input` fields exist in Path Scope; `.toWorkflow` and `route(W, { placement? })` accept `placement: ChildPlacement.ParentInstance \| ChildPlacement.AnyInstance`. | | `.chooseOne(route(Target, { placement? })...)` | Flow chain | Branches after `.toRouter(R)` to exactly one chosen route target. | | `.switch(caseOf(Dto, { about? })...)` | Flow chain | Branches deterministically based on which union output variant (`oneOf(...)`) the preceding node produced. | | `.toParallelAgents(...)`, `.toParallelWorkflows(W, { maxItems?, maxConcurrency?, placement? })` | Flow chain | Fans out across multiple agents, `{ agent, count }` copies, or `{ agent, each: ListDto }` items with `{ maxItems, maxConcurrency, placement: ChildPlacement.AnyInstance }`. | | `.join()`, `.joinQuorum({ min, max?, deadline? })`, `.joinQuorumGroups(...)` | Flow chain | Joins all parallel branches or resolves early as soon as a quorum verified by `.acceptedBy(Arbiter)` is reached. | | `.onError(ErrorClass, sidePath)`, `.onJoinError(ErrorClass, sidePath)` | Flow chain | Catches step or parallel-join failures (`StepFailedError`, `VisitsLimitExceededError`, `QuorumNotReachedError`) onto a typed recovery path. | | `ThisWorkflow` | `"@agboss/core"` | Sentinel target passed to `.toWorkflow(ThisWorkflow)` or `.toParallelWorkflows(ThisWorkflow)` for bounded self-recursion. | | `oneOf(A, B)`, `listOf(Dto)`, `optional(Dto)`, `historyOf(Dto, { limit })` | `"@agboss/core"` | Path Scope input/output shape wrappers used on component `input` and `output` declarations. | # @WorkflowFinish > Complete API reference for the @WorkflowFinish decorator in AGBoss — pass-through finishes, FinishHandler transformations, and DI support. The `@WorkflowFinish` decorator defines a terminal node (`workflows//.workflow-finish.ts`) where a `@Workflow` path completes and returns a typed `output` DTO to the caller of `app.execute()` (or to a parent workflow calling `.toWorkflow(Child)`). A finish can be a **pass-through** (returning the latest `output` DTO already in Path Scope) or a **handled finish** implementing `FinishHandler` to assemble its `output` from multiple scoped DTOs and injected services. ## Complete `@WorkflowFinish` examples (pass-through and `FinishHandler`) [Section titled “Complete @WorkflowFinish examples (pass-through and FinishHandler)”](#complete-workflowfinish-examples-pass-through-and-finishhandler) src/workflows/offer-package/offer-package.workflow-finish.ts ```ts import { Clock, ExecutionContext, WorkflowFinish, listOf, type FinishHandler, type InputOf, type OutputOf, } from "@agboss/core"; import { Integer, Text, WorkflowFinishTextDto } from "@agboss/core/dto"; export class OfferDraftDto { @Text({ prompt: "Final approved offer letter text" }) letterText!: string; } export class AuditVerdictDto { @Text({ prompt: "Auditor name" }) auditor!: string; } export class OfferSummaryDto { @Text({ prompt: "Execution ID for audit trail" }) executionId!: string; @Text({ prompt: "Approved offer letter" }) letterText!: string; @Integer({ prompt: "Number of audit sign-offs collected" }) signOffCount!: number; @Text({ prompt: "ISO timestamp when the workflow finished" }) completedAt!: string; } // 1. Pass-through finish (no input option, no finish() method — passes WorkflowFinishTextDto through): @WorkflowFinish({ name: "chat-reply-workflow-finish", description: "Returns the agent's text reply directly to the caller", output: WorkflowFinishTextDto, }) export class ChatReplyWorkflowFinish {} // 2. Handled finish (declares input + deps and implements FinishHandler): @WorkflowFinish({ name: "offer-summary-workflow-finish", description: "Assembles the final offer summary from the approved draft and quorum verdicts", input: { draft: OfferDraftDto, verdicts: listOf(AuditVerdictDto), }, output: OfferSummaryDto, deps: [ExecutionContext, Clock], }) export class OfferSummaryWorkflowFinish implements FinishHandler { constructor( private readonly executionContext: ExecutionContext, private readonly clock: Clock, ) {} finish( input: InputOf, ): OutputOf { // Single-value inputs in InputOf are wrapped in Produced ({ value, producer }): return { executionId: this.executionContext.executionId, letterText: input.draft.value.letterText, signOffCount: input.verdicts.length, completedAt: this.clock.now().toISOString(), }; } } ``` src/workflows/offer-package/offer-package.workflow\.ts ```ts import { Agent, Workflow, WorkflowStart, listOf, start } from "@agboss/core"; import { WorkflowStartTextDto } from "@agboss/core/dto"; import { AuditVerdictDto, OfferDraftDto, OfferSummaryWorkflowFinish, } from "./offer-package.workflow-finish.js"; @WorkflowStart({ name: "offer-workflow-start", description: "Starts offer workflow", input: WorkflowStartTextDto, }) export class OfferStart {} @Agent({ name: "offer-drafter-agent", description: "Drafts offer letter", model: "moonshotai/kimi-k2.6", prompt: "Draft the offer letter.", input: { req: WorkflowStartTextDto }, output: OfferDraftDto, }) export class OfferDrafterAgent {} @Agent({ name: "offer-auditor-agent", description: "Audits offer letter", model: "moonshotai/kimi-k2.6", prompt: "Audit the offer letter.", input: { draft: OfferDraftDto }, output: listOf(AuditVerdictDto), }) export class OfferAuditorAgent {} @Workflow({ name: "offer-package-workflow", version: "1.0.0", flow: [ start(OfferStart) .returns(WorkflowStartTextDto) .toAgent(OfferDrafterAgent) .returns(OfferDraftDto) .toAgent(OfferAuditorAgent) .returns(listOf(AuditVerdictDto)) .toFinish(OfferSummaryWorkflowFinish), ], }) export class OfferPackageWorkflow {} ``` test/offer-package.workflow-finish.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { replyWith, testWith } from "@agboss/core/testing"; import { OfferApplication } from "../src/offer.application.js"; import { AuditVerdictDto, OfferDraftDto, } from "../src/workflows/offer-package/offer-package.workflow-finish.js"; import { OfferAuditorAgent, OfferDrafterAgent, OfferStart, } from "../src/workflows/offer-package/offer-package.workflow.js"; const test = testWith(OfferApplication); test("assembles final OfferSummaryDto in OfferSummaryWorkflowFinish", async ({ app, mockLlm }) => { mockLlm(OfferDrafterAgent).thenReturn( replyWith(OfferDraftDto, { letterText: "Welcome to Acme as Staff Engineer!" }), ); mockLlm(OfferAuditorAgent).thenReturn( replyWith(AuditVerdictDto, [{ auditor: "comp-committee" }]), ); const res = await app.execute(OfferStart, { text: "Prepare Staff Engineer offer" }); expect(res.status).toBe(ExecutionStatus.Finished); }); ``` ## What options does `@WorkflowFinish` accept? [Section titled “What options does @WorkflowFinish accept?”](#what-options-does-workflowfinish-accept) Every `@WorkflowFinish` class name ends with `WorkflowFinish` and its `name` option ends with `-workflow-finish`: | Option | Type | Required | Description | | ------------- | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Yes | Unique kebab-case identifier ending with `-workflow-finish` (e.g., `"offer-sent-workflow-finish"`). | | `description` | `string` | Yes | Description of this terminal outcome; also used by `agboss describe` and studio visualizations. | | `output` | `DtoClass` | Yes | The DTO class returned in `result.output` when execution ends at this finish node. | | `input` | `InputFields` | No | Map of DTOs read from Path Scope. When omitted, the finish is a pass-through; when set, the class must implement `FinishHandler`. | | `deps` | `readonly Token[]` | No | Constructor dependencies (`ExecutionContext`, `Clock`, `@Injectable` services) injected into a handled finish. | ## What related entities work with `@WorkflowFinish`? [Section titled “What related entities work with @WorkflowFinish?”](#what-related-entities-work-with-workflowfinish) | Related Entity | Import Path | Purpose | | --------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `FinishHandler` | `"@agboss/core"` | Interface implemented by `@WorkflowFinish` classes that declare `input` (`finish(input): OutputOf \| Promise>`). | | `WorkflowFinishTextDto` | `"@agboss/core/dto"` | Standard built-in output DTO (`{ text: string }`) for text-returning workflows. | | `.toFinish(FinishClass)` | Flow chain | Terminates a path in `@Workflow({ flow })` after the compiler verifies `FinishClass`’s required DTOs exist on the path. | | `result.finish` & `result.output` | `"@agboss/core"` | On `ExecutionStatus.Finished`, `result.finish.name` identifies which `@WorkflowFinish` ended the run and `result.output` holds the validated DTO. | # @WorkflowStart > Complete API reference for the @WorkflowStart decorator in AGBoss — input DTO shapes, start() flow chains, and app.execute() options. The `@WorkflowStart` decorator defines a typed entry point (`workflows//.workflow-start.ts`) for starting a `@Workflow`. A workflow can declare multiple `@WorkflowStart` classes (for example, an interactive chat start and a batch webhook start), each seeding its own initial DTOs into the workflow’s Path Scope via `start(StartClass).returns(InputDto)`. ## Complete `@WorkflowStart` examples (single DTO, list input, and multi-field input) [Section titled “Complete @WorkflowStart examples (single DTO, list input, and multi-field input)”](#complete-workflowstart-examples-single-dto-list-input-and-multi-field-input) src/workflows/role-match/role-match.workflow-start.ts ```ts import { WorkflowStart, listOf, optional } from "@agboss/core"; import { Text, Url } from "@agboss/core/dto"; export class CandidateProfileDto { @Text({ minLength: 1, prompt: "Candidate full name" }) fullName!: string; @Url({ prompt: "Link to candidate resume or portfolio" }) resumeUrl!: string; } export class JobRoleDto { @Text({ minLength: 1, prompt: "Target engineering role title" }) roleTitle!: string; } // 1. Single DTO input: @WorkflowStart({ name: "single-candidate-workflow-start", description: "Evaluates a single candidate profile", input: CandidateProfileDto, }) export class SingleCandidateWorkflowStart {} // 2. List of DTOs input (for .toParallelAgents({ each })): @WorkflowStart({ name: "batch-candidates-workflow-start", description: "Screens a batch of candidates in parallel", input: listOf(CandidateProfileDto), }) export class BatchCandidatesWorkflowStart {} // 3. Multi-field map input with optional fields: @WorkflowStart({ name: "role-match-workflow-start", description: "Matches a candidate against a specific job role", input: { candidate: CandidateProfileDto, role: optional(JobRoleDto), }, }) export class RoleMatchWorkflowStart {} ``` src/agents/matcher/matcher.agent.ts ```ts import { Agent, optional } from "@agboss/core"; import { CandidateProfileDto, JobRoleDto, } from "../../workflows/role-match/role-match.workflow-start.js"; @Agent({ name: "matcher-agent", description: "Scores candidate profile against open roles", model: "moonshotai/kimi-k2.6", prompt: "Rank the candidate profile against open roles.", input: { profile: CandidateProfileDto, role: optional(JobRoleDto), }, }) export class MatcherAgent {} ``` src/workflows/role-match/role-match.workflow\.ts ```ts import { Workflow, WorkflowFinish, optional, start } from "@agboss/core"; import { WorkflowFinishTextDto } from "@agboss/core/dto"; import { CandidateProfileDto, JobRoleDto, RoleMatchWorkflowStart, } from "./role-match.workflow-start.js"; import { MatcherAgent } from "../../agents/matcher/matcher.agent.js"; @WorkflowFinish({ name: "match-workflow-finish", description: "Returns candidate match report", output: WorkflowFinishTextDto, }) export class MatchWorkflowFinish {} @Workflow({ name: "role-match-workflow", version: "1.0.0", flow: [ start(RoleMatchWorkflowStart) .returns(CandidateProfileDto) .returns(optional(JobRoleDto)) .toAgent(MatcherAgent) .toFinish(MatchWorkflowFinish), ], }) export class RoleMatchWorkflow {} ``` test/role-match.workflow-start.test.ts ```ts import { expect } from "vitest"; import { ExecutionStatus } from "@agboss/core"; import { replyWith, testWith } from "@agboss/core/testing"; import { RoleMatchApplication } from "../src/role-match.application.js"; import { MatcherAgent } from "../src/agents/matcher/matcher.agent.js"; import { RoleMatchWorkflowStart } from "../src/workflows/role-match/role-match.workflow-start.js"; const test = testWith(RoleMatchApplication); test("validates RoleMatchWorkflowStart input before entering the graph", async ({ app, mockLlm }) => { mockLlm(MatcherAgent).thenReturn(replyWith("Strong match for Principal Systems Engineer")); const res = await app.execute( RoleMatchWorkflowStart, { candidate: { fullName: "Ada Lovelace", resumeUrl: "https://example.com/ada.pdf" }, role: { roleTitle: "Principal Systems Engineer" }, }, { threadId: "candidate-ada-01", ownerId: "recruiter-7", tenant: { tenantId: "acme" }, record: true, }, ); expect(res.status).toBe(ExecutionStatus.Finished); }); ``` ## What options does `@WorkflowStart` accept? [Section titled “What options does @WorkflowStart accept?”](#what-options-does-workflowstart-accept) Every `@WorkflowStart` class name ends with `WorkflowStart` and its `name` option ends with `-workflow-start`: | Option | Type | Required | Description | | ------------- | --------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- | | `name` | `string` | Yes | Unique kebab-case identifier ending with `-workflow-start` (e.g., `"chat-workflow-start"`). | | `description` | `string` | Yes | Human- and CLI-readable description of what triggers this entry point. | | `input` | `DtoClass \| ListWrapper \| Readonly>` | Yes | The DTO class, `listOf(DtoClass)`, or map of DTO fields required by `app.execute(StartClass, payload)`. | ## What related entities work with `@WorkflowStart`? [Section titled “What related entities work with @WorkflowStart?”](#what-related-entities-work-with-workflowstart) | Related Entity | Import Path | Purpose | | ------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `start(StartClass)` | `"@agboss/core"` | Starts a flow path in `@Workflow({ flow })` and chains `.returns(...)` to place the start’s input DTOs into Path Scope. | | `WorkflowStartTextDto` | `"@agboss/core/dto"` | Standard built-in DTO (`{ text: string }`) for chat and text-first workflow entry points. | | `app.execute(StartClass, input, options?)` | `"@agboss/core"` | Executes the workflow owning `StartClass` with compile-checked `InputOf` payload. | | `ExecutionOptions` | `"@agboss/core"` | Optional third argument to `app.execute`: `{ threadId, ownerId, tenant, execution, signal, record }`. | | `ExecutionOutput` | `"@agboss/core"` | Return value of `app.execute`: discriminated by `status` (`ExecutionStatus.Finished` or `ExecutionStatus.Paused`). | # Roadmap > Explore the AGBoss engineering roadmap — shipped v2 architecture, 13 AI coding skills, Kubernetes probes, progressive crystallization, and Mesh Hub. **A**gent **G**raph **B**uilder **O**pen **S**ource **S**ystem ## Why AGBoss is a compiler-verified framework, not a hosted platform AGBoss is an open-source TypeScript application framework for LangGraph agents that runs inside your own Node.js infrastructure under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. Instead of routing prompts, checkpoints, and traces through a third-party SaaS platform, AGBoss compiles your workflows, dependency injection container, and FinOps budgets directly into your service. As multi-agent systems grow to hundreds of nodes and AI coding agents generate the implementation, teams need deterministic compiler guardrails — not another cloud dashboard. AGBoss enforces [7 distributed-systems invariants](/concepts/architecture/) at build time so coding agents and engineers ship reliable systems inside your existing perimeter. ## Free for everyone. Enterprise support for teams that need it. Open source · [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) ### Everything in the framework * Every decorator, Path Scope flow builder, repository contract, and integration * Offline workflow testing, 10 unit runners, trajectory matchers, `@EvalSuite` (`pass@k` / `pass^k`, Cohen's κ), and FinOps budgets * Deploy anywhere: your cloud, your Kubernetes cluster, your SQLite or Postgres store * Offline-bundled AI coding skills (`AGENTS.md`, `CLAUDE.md`, and `.claude/skills/agboss/`) Take it, run it, change it. No vendor lock-in and nothing to pay. Enterprise Coming ### Guarantees from the people who build it * A dedicated engineering team that answers within an SLA * Long-term support (LTS) release lines and Day-0 security patches * Architecture reviews, compliance reports, and custom connectors * Automated codemods and upgrade assistance across major versions This is how AGBoss is funded. [See what’s planned →](/enterprise/) ## Where we are going No guesswork — milestones backed by GitHub issues and verified by CI. 1. Shipped ### Production application architecture & AI-native DX Single `@Application` assembly root, compile-time Path Scope `HarnessDSL (HDSL)`, transactional LIFO sagas, 5-loop verification & evals, Agent Mesh Hub, Kubernetes health probes, and built-in coding agent skills. Architecture & Flow * [@Application & 4 lifecycle hooks](https://github.com/agbossjs/agboss/issues/261 "@Application root, single DI container by inheritance, 4 lifecycle hooks, typed AppEnvironment.") * [Path Scope HDSL & .agboss/types.d.ts](https://github.com/agbossjs/agboss/issues/272 "Chain-per-line Path Scope HDSL: start().returns().toAgent(), .switch(caseOf()), .chooseOne(route()), and .agboss/types.d.ts.") * [Review loops & historyOf](https://github.com/agbossjs/agboss/issues/271 "Review loops with .returns(Dto, { about }), caseOf(Dto, { about }), historyOf(Dto, { limit }), and staleness checks.") * [Parallel waves & joins](https://github.com/agbossjs/agboss/issues/273 ".toParallelAgents() and .toParallelWorkflows() with SpawnPolicy, maxItems, maxConcurrency, and .join().") * [Quorums & @Arbiter](https://github.com/agbossjs/agboss/issues/279 ".joinQuorum() and .joinQuorumGroups() with deadline and @Arbiter chains (.acceptedBy(anyOf())).") * [Subworkflows & recursion](https://github.com/agbossjs/agboss/issues/263 "Child workflows via .toWorkflow(Child) and direct recursion via ThisWorkflow (maxDepth soft stop, maxConcurrentExecutions).") * [Transactional LIFO sagas (#339, #340)](https://github.com/agbossjs/agboss/issues/339 "Declarative compensateWith & committedBy on @Action, \[flow.uncompensated-action], and automatic LIFO saga unwinding on abort or quorum surplus.") Safety, FinOps & Scale * [@PiiPolicy & Code/Model Guardrails (#383)](https://github.com/agbossjs/agboss/issues/383 "@PiiPolicy classes, deterministic HMAC replaceWithToken(), and DecidedBy.Model / DecidedBy.Code @InputGuardrail & @OutputGuardrail.") * [@ToolInterceptor (#384)](https://github.com/agbossjs/agboss/issues/384 "Cascading @ToolInterceptor with BeforeToolCall (ToolCallDecision.replyToModel / block) and AfterToolCall.") * [@ModelProvider & custom transport (#385, #454)](https://github.com/agbossjs/agboss/issues/385 "@ModelProvider with OpenRouter pinning, custom ModelProviderHandler transport, RateLimitPolicy.of(), circuit breakers, and automatic catch-all yielding (#454).") * [@Budget FinOps caps (#261, #338)](https://github.com/agbossjs/agboss/issues/261 "@Budget components across Application, Tenant, and Execution scopes with UTC periods and immediate parallel branch abort.") * [Channels, @ReplyInterpreter & pauses (#267, #337)](https://github.com/agbossjs/agboss/issues/267 "@ChannelRole, @ReplyInterpreter, ChannelHandler.send(ask) & withdraw(ask), PendingPause records, and two-phase app.resume({ pauseId, reply }).") * [Multi-instance SQLite & leases (#265, #360)](https://github.com/agbossjs/agboss/issues/265 "10 repository contracts with Sqlite* WAL implementations, 30s leases, CAS, cross-instance app.cancel, and SIGTERM drain.") * [Distributed child work-stealing (#333)](https://github.com/agbossjs/agboss/issues/333 "ChildPlacement.AnyInstance, ChildExecutionQueueRepository, shared tree slots in RateLimitRepository, parent lease yielding, and per-child crash recovery.") * [@HealthCheck & K8s probes (#300, #332)](https://github.com/agbossjs/agboss/issues/300 "@HealthCheck components, checkLiveness(), checkReadiness(), and maxConcurrentSteps saturation probes.") Test, Eval & AI DX * [10 unit runners & trajectory matchers (#395)](https://github.com/agbossjs/agboss/issues/395 "testWith, 10 unit runners (including testCompaction & testToolInterceptor), tool trajectory matchers, and \[vcr.stale-cassette].") * [@EvalSuite, pass^k & eval ui (#396, #397)](https://github.com/agbossjs/agboss/issues/396 "@EvalSuite, pass@k & pass^k, --baseline regression gates, Cohen's kappa calibration, --chaos, --metamorphic, generate eval-case --from-execution, and agboss eval ui.") * [Built-in AI coding skills (13 modules) & ESLint (#305, #388, #424)](https://github.com/agbossjs/agboss/issues/305 "Committed AGENTS.md + CLAUDE.md, 13 reference modules (including LangGraph migration), agboss/eslint flat config, and offline auto-sync.") * [Crystallization join keys & describe –json (#268, #395)](https://github.com/agbossjs/agboss/issues/268 "agboss describe --json with configHash, promptHash, and schemaHash join keys on every model component and execution trace.") * [7 verified examples & flow-patterns (#419, #420)](https://github.com/agbossjs/agboss/issues/420 "Full $0 examples/flow-patterns harness and 7 verified standalone apps in agbossjs/agboss-examples (including path-scope-proof).") * [A/B profile comparisons (#125, #396)](https://github.com/agbossjs/agboss/issues/125 "Bidirectional A/B YAML profile comparisons with \[judge.same-model-family] verification.") * [Langfuse tracing](https://github.com/agbossjs/agboss/issues/59 "Self-hosted Langfuse tracing with scripts/langfuse.sh.") Connect, Mesh & Context * [MCP client, server & dedupe (#164, #398)](https://github.com/agbossjs/agboss/issues/164 "@McpServer, @McpTool, @McpExpose, and idempotent _meta.callId deduplication.") * [Tools from OpenAPI](https://github.com/agbossjs/agboss/issues/163 "agboss generate openapi creates typed DTOs, tools, and services.") * [Agent Mesh, @A2AExpose & check –federation (#387, #398, #457)](https://github.com/agbossjs/agboss/issues/398 "Multi-tenant a2aHttpListener(app, { expose, callerResolver }), @A2AExpose, typed @A2AAgent, wireCode (#457), signed MeshEnvelope (agboss-mesh), and agboss check --federation.") * [AGBoss Mesh Hub (agboss hub) (#401)](https://github.com/agbossjs/agboss/issues/401 "ExecutionHistoryRepository, agboss records export, and agboss hub show --html / agboss hub ui (Global Mesh Graph, Cross-Service Trees, Crystallization Radar, Blast Radius).") * [@Rag & Custom @Compaction (#382)](https://github.com/agbossjs/agboss/issues/382 "@Rag contract, SqliteFtsConnector (BM25), and @Compaction (SummaryQueue, SingleSummary, Custom CompactionHandler, None).") 2. Now ### External Cluster Storage, Worker Pool Affinity & Cloud IAM Adapters Dedicated storage packages for PostgreSQL/Redis/DynamoDB, tag-based worker pools, native cloud model transports, hybrid vector RAG, and autoscaling telemetry. * **Tag-Based Worker Pool Affinity & Weighted Fair-Share Scheduling**Extend `ChildPlacement.AnyInstance` and `ChildExecutionQueueRepository` with capability tags (`gpu-worker`, `browser-sandbox`, regional compliance zones) and weighted tenant fair-share queue scheduling across specialized Kubernetes node pools. * **Relational & In-Memory Cluster Stores (`@agboss/postgres`, `@agboss/redis`)**Official PostgreSQL (Aurora, Cloud SQL, Neon, Supabase) implementations using `FOR UPDATE SKIP LOCKED` and atomic CAS upserts, plus Redis / Valkey (`@agboss/redis`) sub-millisecond Lua (`EVALSHA`) scripts for cluster-wide `RateLimitRepository` sliding windows, `ExecutionLeaseRepository`, and `MeshRequestRepository` deduplication. * **Serverless, Geo-Distributed & Cold-Archive Stores (`@agboss/dynamodb`, `@agboss/firestore`, `@agboss/cockroachdb`, `@agboss/s3-archive`)**Zero-ops conditional-write adapters for AWS Lambda/Fargate and Cloud Run, multi-region active-active state with tenant data-residency pinning, and automatic S3 / GCS / R2 WORM cold-tiering for completed `ExecutionRecord` trajectories and checkpoints. * **Cloud IAM Model Transports & 50% Batch Offload (`@agboss/bedrock`, `@agboss/vertex`, `@agboss/azure-openai`)**Keyless AWS SigV4, GCP Workload Identity, and Azure Managed Identity `@ModelProvider` packages with cross-region failover and automatic provider Batch API routing (`-50%` token cost) for background workflows and nightly `agboss eval` suites. * **Hybrid Vector `@Rag` & KEDA Autoscaling (`@agboss/pgvector`, `@agboss/qdrant`, `@agboss/opensearch`, `@agboss/metrics`)**Dense + BM25 Reciprocal Rank Fusion (RRF) connectors with strict `ExecutionContext.tenant` isolation, plus Prometheus / OpenTelemetry queue-depth exporters and KEDA `ScaledObject` manifests for `0 → N` pod autoscaling. 3. Built independently · funding takes it further Everything above is built in the open. What comes next scales adoption and enterprise operations — microVM code sandboxes, time-travel replay, model distillation, event-bus and Physical AI bridges, KMS cryptographic erasure, managed Mesh Hub deployments, and dedicated SLA support. 4. Next ### Sandboxes, Time-Travel Replay, Event Ingress & Physical AI Bridges Ephemeral microVM tool isolation, mid-graph checkpoint forking, automated SFT/DPO distillation, broker backpressure, and robotics/IoT protocols. * **Isolated Code & Browser Sandboxes (`@agboss/sandbox` — Firecracker, Docker, E2B, Playwright)**Run untrusted LLM-generated code, shell commands, and Computer-Use browser actions inside ephemeral microVMs with strict network egress allowlists and automatic filesystem snapshot rollback on saga `compensateWith`. * **Time-Travel Debugger & Mid-Graph Forking (`agboss replay –from-step `)**Fork any historical production `ExecutionRecord` from an arbitrary node in the graph — replaying upstream steps at `$0` from the checkpoint while testing a new prompt, model, or tool fix on the downstream branches. * **Crystallization Distillation & Fine-Tuning Exporter (`agboss distill`)**One-command export of verified `JudgeVerdict.passed()` trajectories and human-labeled `eval ui` runs (with `@PiiPolicy` tokens masked) into SFT / DPO JSONL datasets to distill expensive frontier `@Agent` and `@Router` nodes into compact in-house models or `DecidedBy.Code` handlers. * **Event-bus & streaming ingress (`@agboss/kafka`, `@agboss/nats`, `@agboss/rabbitmq`, `@agboss/sqs`, `@agboss/pubsub`)**Consumer/producer adapters wired directly to `maxConcurrentSteps` backpressure (pausing partitions when pod slots saturate), `MeshEnvelope` (`agboss-mesh`) header propagation, and idempotent deduplication. * **Turnkey Human-in-the-Loop `@Channel` packages (`@agboss/slack`, `@agboss/teams`, `@agboss/telegram`, `@agboss/github`, `@agboss/linear`, `@agboss/pagerduty`)**Signed webhook verification, interactive approval modals for two-phase `app.resume({ pauseId, reply })`, and automatic message withdrawal (`ChannelHandler.withdraw`) when a parallel quorum branch becomes `BranchVerdict.Surplus`. * **Envelope PII Encryption & Cryptographic Erasure (`@agboss/kms`)**AWS KMS, GCP KMS, Azure KeyVault, and HashiCorp Vault providers encrypting `PiiTokensRepository` with per-tenant DEKs — enabling instant GDPR Right-to-be-Forgotten compliance by rotating or destroying a tenant key without rewriting historical traces. * **Physical AI, Robotics & Industrial IoT Bridges (`@agboss/ros2`, `@agboss/mqtt`, `@agboss/opcua`)**Low-latency telemetry ingestion and hardware safety-interlock bridges connecting System-2 HDSL mission planners to physical robot fleets, drones, and warehouse automation. * **Federation contract registry & CI gates**Automated snapshot publishing and pull-request blast-radius checks for `agboss check –federation` across multi-repo organizations. 5. Then ### AGBoss Mesh Hub (Enterprise Control Plane) & SLA Support Fleet-wide control plane and paid guarantees for engineering organizations — while the core framework remains 100% [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) open source. * **Global Agent Mesh Graph**Interactive map of all `@Application` services across the company (`describe –json` + `MeshEnvelope`), with live RPS, latency, `pass^k`, and USD spend per node. * **Cross-Service Execution Trees**Unified execution tree across `rootExecutionId` (`Service A → A2A → Service B → MCP → Service C`) with human pauses, retries, and LIFO saga rollbacks. * **Blast Radius & Federation + Progressive Crystallization Radar**Preview who breaks across the federation when a DTO or prompt changes, plus automatic heatmaps of `DecidedBy.Model` / low-`pass^k` steps with one-click PR generation for `DecidedBy.Code` or `@ToolInterceptor`. * **Dedicated SLA, LTS & Day-0 security patches**Extended maintenance windows, backported fixes, signed advisories, and compliance audit packs. ## Want something on this list? Open an issue or upvote an existing one on GitHub. [Open issues →](https://github.com/agbossjs/agboss/issues) # Why AGBoss? > Why Senior, Staff, and Principal engineers use AGBoss and HarnessDSL to enforce 7 compile-time distributed-systems invariants on AI coding agents. Modern engineering teams already know two truths: complex problems require networks of specialized agents, and engineers no longer write orchestration boilerplate by hand — **AI coding agents (Codex, Cursor, Windsurf, Copilot, Gemini CLI, Claude Code)** write the code under an engineer’s architectural direction. **AGBoss** (*Agent Graph Builder Open Source System*) is the TypeScript framework, static graph compiler (`agboss check`), and production runtime on LangGraph (`npm i @agboss/core`), and **HarnessDSL (HDSL)** is the declarative graph grammar and verification specification built into AGBoss — giving coding agents hard distributed-systems guardrails so graphs with hundreds of nodes stay deterministic, cache-invariant, and refactorable. [Explore the 7 Architectural Principles →](/concepts/architecture/)[Try the quickstart](/getting-started/quick-start/) 1 ### Computational Role Segregation & Canonical Topology In raw graph SDKs, coding agents create “god nodes” and format the same topology ten different ways. AGBoss segregates `@Agent`, `@Router`, `@Arbiter`, and `@Action`, and enforces a **1-to-1 isomorphism** between graph topology and code layout (`in-degree = 1` inline, `in-degree ≥ 2` top-level entry, 1 chained call per line). 2 ### Path-Scoped SSA Dataflow & Provenance Shared mutable state bags (`Annotation.Root`) collapse at 50+ nodes when coding agents hallucinate nullable fields or consume stale loop state. `agboss check` runs static Path-Scope dominance analysis and emits `.agboss/types.d.ts` with narrowed `Produced` inputs, rejecting missing (`[input.missing-on-path]`) or stale (`[input.outdated-on-path]`) values before runtime. 3 ### Prompt Prefix Cache Invariance & Boundary PII Interpolating dynamic inputs into system prompts or masking PII with random UUIDs destroys LLM KV-cache hit rates (up to 10x higher token cost). AGBoss enforces `[prompt.input-in-instructions]` at compile time and uses deterministic HMAC pseudonymization (`PiiAction.replaceWithToken()`) so identical values produce identical tokens across turns while restoring real values inside `@Tool` calls. 4 ### Transactional LIFO Sagas & Idempotent Execution When a parallel branch fails, a quorum cancels slow siblings as `Surplus`, or a budget trips, external side effects cannot be left dangling. Side-effecting `@Action` nodes declare `compensateWith` and `committedBy` (verified by `[flow.uncompensated-action]`) and receive a deterministic `step.idempotencyKey` for safe retry and LIFO rollback. 5 ### Zero-Leakage DI & 4-Stage Infrastructure Scaling Every component is wired inside a single `@Application` root using class-inheritance DI (`override(Base, Fake)`) and deterministic lifecycle hooks (`OnInit → OnStart → OnStop → OnDestroy`). Scale from zero-setup `InMemory*` stores in tests to `Sqlite*` WAL single-node durability and multi-pod 30s lease handoff without changing a single line of business logic. 6 ### Progressive Crystallization & Universal Coding Skills Start with flexible `DecidedBy.Model` routers and multi-tool agents, then join `agboss describe –json` (`configHash`, `promptHash`, `schemaHash`) with production traces to crystallize settled paths into zero-cost `DecidedBy.Code` routers and `@Action` nodes — guided by `AGENTS.md` and 13 offline-synced skill modules. ## Deep-dive into the architecture [7 Invariants](/concepts/architecture/) ### [Distributed-Systems Architecture](/concepts/architecture/) [How AGBoss turns raw LLM orchestration into a compiler-verified distributed system across all 7 architectural invariants.](/concepts/architecture/) [Read the Architecture Guide →](/concepts/architecture/)[AGENTS.md · Skills](/concepts/ai-coding-skills/) ### [AI-Assisted Engineering & Skills](/concepts/ai-coding-skills/) [How AGENTS.md and the 13 offline-synced skill reference modules keep Codex, Cursor, Windsurf, Copilot, and Claude Code 100% aligned with your compiler.](/concepts/ai-coding-skills/) [Read AI Coding Skills →](/concepts/ai-coding-skills/)[describe –json](/concepts/progressive-crystallization/) ### [Progressive Crystallization](/concepts/progressive-crystallization/) [Join content-addressed configHash, promptHash, and schemaHash with traces to evolve LLM steps into deterministic TypeScript code.](/concepts/progressive-crystallization/) [Read Progressive Crystallization →](/concepts/progressive-crystallization/)[.agboss/types.d.ts](/concepts/path-scope/) ### [Path Scope & Dataflow](/concepts/path-scope/) [Static Single Assignment (SSA) dominance, .returns(Dto, { about }) provenance, historyOf() windows, and narrowed Produced\ inputs.](/concepts/path-scope/) [Read Path Scope & Dataflow →](/concepts/path-scope/)[01..07 Proofs](/examples/job-scout/) ### [7 Verified Examples](/examples/job-scout/) [Run `01-hello-agboss` through `07-langgraph-migration` in `agbossjs/agboss-examples` offline for $0 to see every compile-time and runtime invariant in action.](/examples/job-scout/) [Explore the 7 Verified Examples →](/examples/job-scout/) ## Ready to build with compile-time guarantees? Scaffold a new project with built-in AI coding skills and Path Scope verification in under a minute. [Try the quickstart →](/getting-started/quick-start/)