Changelog - agbossjs/agboss GitHub Wiki
Breaking changes and notable additions, newest first. The API reports
(packages/agboss/api/*.api.md) hold the exact public surface of each entry.
-
Added β
onFencedEffect(OnFencedEffect,FencedEffectEvent,FencedEffectEventOutcome:Ran,Replayed,Joined,Refused) (Observability): every call ofFencedEffectRepository.saveResultIfAbsentfrom a step, never its result. -
Added β
FencedEffectRecord, the lease log'sFencedEffectrow (Several instances): with the takeover and the hand-over, a refused zombie, one that never wrote and a replay are told apart fromapp.leaseEventsOfalone. -
Moved β
FencedEffectAsyncWorkErrorto the SQLite store's module; still exported from the root entry.
-
Added β
onStepHandedOver(OnStepHandedOver,StepHandedOverEvent,HandOverReason,HandOverSite) (Observability): a step stopped by a lost lease, a fenced write or the app's stop is told once per error, at the innermost site β a step, a tool call, a tool interceptor, an undo, a branch, a child. -
Added β
StepHandedOverRecord, the lease log'sHandedOverrow (Several instances), written before the moving call rejects;ExecutionLeaseEventRecordis nowLeaseTakeoverRecord | StepHandedOverRecordandcheckpointIdis the takeover's own field. -
Added β
ExecutionHistoryRecord.leaseEvents: a kept execution carries its tree's rows of the lease log (Execution history), and so doesagboss records export.
-
Added β
ExecutionLease.takenOverFrom(PreviousLeaseHolder:holderInstanceId,leaseEpoch):acquirenames the holder whose lease it took over after it expired β never after a release. Optional: an externalExecutionLeaseRepositorystill compiles; the publishedexecutionLeaseContracthas two more cases, so a store that runs it reports it. -
Added β
onExecutionLeaseTakeover(OnExecutionLeaseTakeover,ExecutionLeaseTakeoverEvent,LeaseTakeoverReason) (Observability). -
Added β the lease log (Several instances): the
infrastructure contract
ExecutionLeaseEventRepository(ExecutionLeaseEventRecord=LeaseTakeoverRecord,ExecutionLeaseEventKind), its in-memory default,SqliteExecutionLeaseEventRepository(tableexecution_lease_events), the contract suiteexecutionLeaseEventContract,app.leaseEventsOf({ tenantId, rootExecutionId })(LeaseEventsQuery) and@Application({ leaseEventRetention })(defaulthours(24), removed by the recovery sweep). Readiness pings it.
-
Added β
FencedEffectRepository<TTransaction>(Several instances, ADR-0026):saveResultIfAbsent(fencedEffectKeyOf(executionContext, name), work)makes a protected write once per step β the lease, the scope's mark and the key checked in the write's transaction;FencedEffectStatus.Ran/Replayed/Joined; a zombie getsExecutionFencedError. The built-inInMemoryFencedEffectRepositoryandSqliteFencedEffectRepository(withSqliteExecutionLeaseRepository; its work synchronous,FencedEffectAsyncWorkErrorotherwise). -
Added β
@Application({ fencedEffects: { maxResultSize, retention } })(64 KB, 24 h;[application.fenced-effects-invalid]),FencedEffectSettings,FencedEffectResultTooLargeError,[application.fenced-effect-lease-store]andFencedEffectStoreMismatchError; the recovery sweep removes the rows past the retention. -
Added β
fencedEffectContractin@agboss/core/testing/contracts; unit runners hold their lease in the store's lease store. -
Changed β flow-patterns' credit desk charges through
SqliteFencedEffectRepository:PaymentServiceandPaymentKeyReusedErrorare gone,payment_chargeshas no idempotency key. -
agboss describelistsFencedEffectRepositoryamong the infrastructure contracts.
-
Fixed β
workflowOf(W)on the package installed from npm (Testing): the application next toWwas imported by Node, outside vitest β no decorators, or a second module graph. Added β@agboss/core/testing/vitest:agbossTest(test?), thetestoptions withserver.deps.inlinegaining/@agboss\/core/andsetupFilesgaining@agboss/core/testing/setup(merged, never replaced).agboss createwrites it. -
Added β
TestSetupErrorfromworkflowOf(W)without the preset:the application of <workflow> was loaded outside vitest's module graph; use agbossTest() from @agboss/core/testing/vitest in vitest.config.ts, the original error as itscause. -
Added β
npm run check:packedand CI'spacked-projectjob: the packed packages in a project of their own, the positive run and the negative one;RELEASE_CHECKLIST.mdruns it too. -
Migration β in an existing
vitest.config.ts, wrap thetestoptions:import { defineConfig } from "vitest/config"; import { agbossTest } from "@agboss/core/testing/vitest"; export default defineConfig({ test: agbossTest({ include: ["tests/**/*.test.ts"] }) });
The
setupFiles: ["@agboss/core/testing/setup"]entry may go; kept, it is not added twice.
-
Removed β
examples/job-scout(Example, ADR-0025): it is08-job-scoutof agbossjs/agboss-examples, on the published@agboss/core, its history kept.examples/flow-patternsis the one in-repo example: the root'snpm run studioandnpm run check:modelsrun on it (its newcheck:modelsscript), and every core and CLI test that used job-scout uses it. -
Added β a
check:docsrule: a link toexamples/job-scoutoutside the ADRs and the Changelog fails the check, naming the doc, the line and where the folder went. -
Added β
RELEASE_CHECKLIST.mdβ Examples against the release candidate: every example of agbossjs/agboss-examples runs locally against the packed candidate with real models before a release; their CI stays offline.
-
Changed β the shipped skills (Components,
Naming): every snippet of
SKILL.mdand references 01β13 is one class per file, named by itsfile=, at the pathagboss generatewrites βsrc/[<domain>/]<kind>/β¦,workflows/<w>/with the workflow's starts and finishes,agents/<n>/(and every prompt-bearing kind) with its<n>.prompt.md, DTOs indto/, the application andenvironments/at thesrc/root,tests/mirroringsrc/.SKILL.mdallows one domain level (src/billing/agents/refund/refund.agent.ts); the "condensed" references are gone;@ToolInterceptorlives intool-interceptors/<n>.tool-interceptor.ts. -
Changed β
examples/flow-patterns(Example):dtos/βdto/, each workflow in<domain>/workflows/<w>/with its starts and finishes, each agent, router, judge, arbiter, compaction and guardrail in its own folder; its tests mirrorsrc/(Testing), the cassette intests/desk/workflows/desk/__snapshots__/. Component names, versions, cassettes, snapshots and eval baselines are unchanged. -
Added β the layout check (
packages/agboss/tests/docs/skills-layout.test.ts): one class per block, a class block withoutfile=, a workflow file outsideworkflows/<w>/, a flat prompt-bearing component, a*.prompt.mdnot beside its component, a domain with a loose file, a test outside the mirror β each named by file and line, over the skills, the example's real files and whatagboss generatewrites.
-
Added β
@agboss/core/testing/contracts(Several instances, Testing, ADR-0024): the core's store contract suites, one function per contract βexecutionLeaseContract,executionRecordContract,executionTreeContract,childExecutionQueueContract,pausedExecutionContract,executionHistoryContract,costLedgerContract,rateLimitContract,meshRequestContract,checkpointSaverContractβ over aContractTarget(name,instance(seam),reset?,close?;StoreSeam: the suite's clock,instanceId,application). The in-memory and SQLite stores run the same suites;@agboss/core/testingdoes not re-export them.
-
Added β
executionContext.fence(Several instances, ADR-0023): the lease the step runs under βscopeKey,tenantId,leaseExecutionId,token(itsleaseEpoch),confirmedUntil,assertHeld(),confirmNow(),fenced(observedToken);ExecutionFenceandExecutionFencedError(execution.lease-lost.fenced,extends ExecutionLeaseLostError) in the root entry. -
Changed β a fenced error is a hand-over everywhere: found in a cause chain or an
AggregateError, before the signal's reason β no.onError, no undo, noonToolEnd, nothing to the model, no failed record; the moving call rejects with it. A tool's or an interceptor's lost lease ends the agent's loop instead of reaching the model as a tool error. -
Fixed β the remote-children wait yields the root's lease only while no other step of the tree
runs (a sibling
ParentInstancebranch used to run with the lease released, #333 PR 4). -
Added β testing: the
leasesfixture oftestWith(stall/resume,TestLeases) and thefenceoption of every unit runner andtestExecutionContext(TestFenceOptions,clock).
-
Added β pauses and
app.resumeinChildPlacement.AnyInstancechildren (Workflow as a step, Several instances): when a remote child rests at a pause, its worker completes the queue row withExecutionRecordStatus.Paused(ChildExecutionEnd.statusnow includesExecutionRecordStatus.Paused) and the parent's node interrupts with aremote: truewaiting marker so the pause appears inresult.pendingwith itschildExecutionIdanditemIndexand recordsPauseAnswerRecordinresult.children[i].pausesandresult.record.pauses. Onapp.resumefrom any instance, the parent's node forwards the child's resume map through the queue so any instance continues only that child on<parentThreadId>/child/<id>. -
Removed β
GraphRuleErrorCodevaluegraph.remote-child-can-pause: remote children may now use tools that pause. -
Changed β yielding the root lease and per-child crash recovery (#333 S7, S9): while a parent
waits on remote children inside its step, it yields its root lease in
ExecutionLeaseRepositoryand re-acquires it before continuing. Each worker holds a 30 s lease inExecutionLeaseRepositoryunder(tenantId, childExecutionId); on a worker crash or a graceful stop pastshutdownTimeout(S7), the queue row staysRunning, and once the child's lease is free a live worker reclaims only that child and continues it from its last checkpoint on<parentThreadId>/child/<id>without re-running finished siblings.
-
Added β shared tree slots in
RateLimitRepository(FinOps, Several instances): when a tree can launch a child withChildPlacement.AnyInstance, itsmaxConcurrentExecutionscap (and any smaller subtree cap) is held as leases inRateLimitRepository(tree:<tenantId>:<rootExecutionId>and:subtree:<executionId>, renewed while held, polled on theClock), so the cap holds across instances and a crashed instance's slot expires by itself. Local-only trees stay in-process. -
Changed β immediate ledger recording for remote children (Workflow as a step):
a worker running an
AnyInstancechild writes each superstep's usage to the root's budget counters inCostLedgerRepositoryas it streams, and the parent marks those recordsisRecordedElsewherewhen folding in the child's state so nothing is counted twice. Depth (maxDepth) and the root's budgets are checked on the worker.
-
Example β
SameOrderMemoryStrategyremoved (Example): the helpline's agent no longer setsmemoryStrategy, which is v1 memory. The example shows memory the v2 way: the desk'sCompactionStrategy.Customcompaction andhistoryOfin the review loop.@Agent({ memoryStrategy })stays in the framework (Memory management).
-
Docs β several instances on SQLite (Several instances):
testWith(App, { instances: 2 })shares the in-memory defaults, but listed SQLite stores onsqlitePath: ":memory:"are one database per instance. A multi-instance test names a file. No code change.
-
Breaking β
A2ACallerRejectedError.codeis the class's code (A2A): an instance'scodeisa2a.caller-rejected, like everyAGBossError. The code the server sent (a2a.caller-unauthenticated,a2a.caller-forbidden) moves to the new fieldwireCode. Migration: readerror.wireCodewhere you readerror.codefor the server's code, and in tests writetoFailWith(A2ACallerRejectedError, { httpStatus: 401, wireCode: "a2a.caller-unauthenticated" }).
-
Added β
ChildPlacement(Workflow as a step):{ placement: ChildPlacement.AnyInstance }on.toWorkflow(W, β¦),.toParallelWorkflows(W, { β¦, placement })androute(W, β¦)lets any instance of the application run the child; defaultChildPlacement.ParentInstance(as before). The parent's node waits for it inside its step (#333 S6) βFailFast,app.cancel, a lost lease and budgets as for a local child; its failure isChildExecutionFailedErrorwith the child's error chain asRemoteChildFailures. New rule codes[flow.conflicting-placement]and, until #333 PR 4,[graph.remote-child-can-pause]. -
Added β infrastructure
ChildExecutionQueueRepository(in memory;SqliteChildExecutionQueueRepository, tablechild_execution_queue) andExecutionRecordStatus.Queued(Several instances): queued children claimed atomically by instances with a free step slot (#332); the assembly warning[application.in-memory-child-queue].
-
Changed β
OpenRouterModelProviderandDecisionsModelProviderare catch-alls (Model providers): each serves its models except those another listed provider claims byserves, so a project's provider for its own prefix (a transport onharness/β¦) is listed next to the built-ins with no narrowedOpenRouterProvidersubclass. Before, that combination failed with[model.ambiguous-provider]; two of the project's own providers claiming one model still do. A subclass with its own@ModelProviderdoes not yield. The key check at start is unchanged. flow-patterns dropsFlowPatternsOpenRouterModelProvider.
-
Added β declared edges and the blast radius (Mesh Hub):
agboss hub show/hub uidraw every contract a snapshot declares as an edge consumer β producer (dashed when no record shows a call, red when broken), list the problemsagboss check --federationfinds between the newest snapshots β the samecheckFederationβ with their blast radius, and who calls an application whose latest eval run lost pass^k.--jsonaddsedges.declaredandfederation;hub uiadds/api/blast-radius.
-
Added β
agboss hub ui <pathβ¦> [--port N](Mesh Hub): the Hub on 127.0.0.1 behind a printed token, onagboss eval ui's own server (one local server, its token,Hostand CSP rules), read-only: the page,/recordsβ every export's executions searched, a page at a time β and/api/mesh,/api/applications/<name>,/api/trees/<rootExecutionId>,/api/records,/api/radar.
-
Added β
ExecutionStepRecord.decision(Observability): what each visit decided βroute:<node>,launch:<node>,β¦,variant:<class>oranswer:<hash>; none when it gave nothing. Additive. -
Added β the Crystallization Radar on
agboss hub show(Mesh Hub): model steps ranked by spend Γ stability (the share of one decision for oneinputHash); a candidate at β₯ 50 calls, stability β₯ 0.95 and no failing eval of its workflow.--json:radar.
-
Changed (internal) β ADR-0022, reversing #263
S24: a child execution runs as a root graph on its own checkpoint thread
(
<parentThreadId>/child/<childExecutionId>), awaited in its parent's step; its launching task keeps a marker of it; a child resting at a pause makes its parent's node wait with a marker the drain follows (Workflow as a step). No public change: paths, records, events, budgets, pauses and undo behave as before. A child that started before this version is continued the old way (one version).
-
examples/flow-patterns' desk keeps its executions (executionRecordRetention) and itsnpm run checkrunsnpm run records:check: one offline run,agboss records export, andagboss generate eval-case --from-execution --dry-run. Example.
-
Changed β the run artifact is
schemaVersion4: each run result keeps itstenantId(the tenant its turns ran for); versions 1 to 3 are still read (Eval suites). -
Fixed β Save as golden case reads the run's own execution: on the run's thread, of the run's
tenantId; an execution id kept for several tenants no longer answers with a CLI hint (--tenant) β for a run from before version 4 it is409, worded for the page: run the suite again (Eval suites).
-
Added β Cross-Service Execution Trees on
agboss hub show's page (Mesh Hub): each tree that crosses applications, stitched from their exports by each receiver'sorigin(the calling execution and its call'sidempotencyKey, never the tenant); a receiver that exported nothing is a stub;--jsonlists the trees.
-
Added β
agboss hub show <pathβ¦> [--html <file>](Mesh Hub): many applications'describe --jsonsnapshots,agboss.eval-runartifacts andagboss.execution-recordexports read from files and folders, each by its marker, into one mesh β every application, the calls between them seen in the records (remote/origin, each counted once by its step'sidempotencyKey), each one's health from its records and eval runs β and one self-contained page in the eval report's shell. No project, nothing runs, no network.hub.input-schema,hub.duplicate-application,hub.input-not-found,hub.report-too-large.
-
Added β Save as golden case on a run of
agboss eval uiwith an execution (Eval suites): its kept execution drafted asagboss generate eval-case --from-executiondrafts it and written into the suite's file β atomically, only while the file's ETag is the page's (else 409), never over a case of the same name (409); withoutexecutionRecordRetentionthe page says why.agboss eval uitakes--applicationand--env.
-
examples/flow-patternsaddssrc/intake/(joinQuorumGroups, tested withTestConcurrency.inOrderandfailWith(ModelFailure.RateLimited)),historyOfin the review loop, a pause resumed on a second instance (instances: 2), and in the helpline a stubbed@McpServer/@McpTool(mcpOf) and a custom memory strategy (@agboss/core/memory). Example.
-
examples/flow-patternsreplays the desk from a cassette recorded through its transport model provider with no key (#439), and itsnpm run checkaddsagboss check --cassettes. Example.
-
Added β
agboss generate eval-case <name> --from-execution <executionId> [--suite <name>] [--tenant <id>](Eval suites): reads one execution from the application's history (Execution history) and adds a case to its suite β its input as the turn, its contexts, the thread's earlier finished turns ashistory, the calls it ran astools.required, masked as kept, its classes imported; nothing runs.records.not-found,records.ambiguous-execution,records.not-kept,[eval.case-start-mismatch].
-
Fixed β a VCR test records through a transport
@ModelProvider(Testing):RECORDandRECORD_STALEstopped withits chat models are not loaded (chatExtensionsFor); the recording now builds the providers' gateway ascreateAppdoes β the chat extensions loaded, the transport created by the test app's container on its first call, with its lifecycle hooks. A replay still creates no provider.
Unreleased β execution history: ExecutionHistoryRepository, executionRecordRetention, agboss records export (#401 PR 1)
-
Added β
@Application({ executionRecordRetention })(Execution history): unset β nothing is kept (as before); set, every root execution'sExecutionHistoryRecordβ itsExecutionRecordwithexecutionId,rootExecutionId,threadId,tenantId,application,workflow,status(ExecutionHistoryStatus),startedAt/endedAt,start+input,finish+output,contexts,errorβ masked like its traces, saved when its tree rests or ends and removed by the recovery sweep once it ended longer ago. On the built-in in-memory store it warns[application.in-memory-execution-history]. -
Added β infrastructure
ExecutionHistoryRepository(in memory,SqliteExecutionHistoryRepository), additive:ExecutionRecordRepositoryis unchanged; readiness pings it. -
Added β
agboss records export (--tenant <id> | --all-tenants) --out <file.jsonl> [--since] [--until]: JSON Lines, one{ "kind": "agboss.execution-record", "schema": 1, β¦ }per line, read from the stores without running anything;records.tenant-required,records.not-kept.
-
examples/flow-patternsaddsdesk-eval-suitewith a code eval judge, run offline on a newofflineenvironment bynpm run eval:checkβeval runagainst a checked-in baseline with--fail-on-regression,eval show --html,eval calibrateon checked-in labels β and runs its tests withisolate: false(62 s β 24 s). The desk's offlineagboss runnow takes--env offline. Example.
-
examples/flow-patternsserves the desk over A2A fromsrc/desk.application.ts(@A2AExpose, a caller resolver) to a newsrc/helpline/consumer (@A2AAgent); itsnpm run checkaddsagboss check --federationon both applications andagboss describe --json, and its tests thea2aServerfixture. Example.
-
Breaking β
allow_fallbacksisallowFallbacks(Model providers): the TypeScript API of OpenRouter's routing is camelCase everywhere; only the request body stays snake_case. Migration: inrequestFields: { provider: { β¦ } }renameallow_fallbackstoallowFallbacks(a compile error until you do). -
Added β
quantizations: Quantization[](Quantization.Bf16,Fp8, β¦) besideonly,ignore,order,sortandallowFallbacksinOpenRouterRouting. -
Added β
UsageRecord.servedBy: the host that served each call, from OpenRouter's answer (plain or streamed). -
Added β
agboss check --modelschecks the pins (CLI): a pinned host or quantization no endpoint of a served model has is[model.no-matching-endpoint].
Unreleased β flow-patterns as the zero-cost harness: the operational contracts and every unit runner (#419 PR 2)
-
examples/flow-patterns/src/desk/adds a code@JudgeontoolCalls, a@Ragknowledge base, aPiiPolicyHandlerdetector, an observer, a@HealthCheckandmaxConcurrentSteps; the tests now run all ten unit runners andreplyWithInvalidAnswerβAgentAnswerInvalidError. Example.
-
examples/flow-patternsgainssrc/desk/β a custom compaction, input and output code guardrails, a tool interceptor (allow/replyToModel/block,afterToolCall) β on a transport@ModelProviderthat answers by the request, an offlinesrc/desk.application.ts, its ownOpenRouterProvidernarrowed tomoonshotai/β¦, and its owneslint.config.jswith@agboss/core/eslint; tests for the trajectory matchers, invalid arguments and the unit runnerstestToolInterceptor,testCompaction,testGuardrailandtestTool. Example.
-
references/13-migrating-from-langgraph.mdβ a skill reference for coding agents that move a LangGraph graph to AGBoss: the read / write matrix (one state β narrow DTOs per producer, routing flags β output variants or routes, reducers β joins, counters βmaxVisits), a one-to-one table fromStateGraph,addConditionalEdges,Command,Send,interrupt, subgraphs and checkpointers to AGBoss primitives, a compiled "before β after" example and the move totestWith. Routed fromSKILL.mdand theAGENTS.mdthatagboss createwrites; the project skills are nowSKILL.mdand 13 references (skills: β¦ (14 files)). CLI.
-
Added β
agboss eval run --metamorphic(Eval suites): each case'svariants(its last turn reworded) run too, each on a thread of its own, and the checkmetamorphicβ after the other checks, under the cascade β passes when every variant calls the same set of tool classes as the case's last turn and meets itstools.requiredagain. The artifact's optionalmetamorphic: true(read byevalRunReportOf); a case'scaseHashcovers its variants only with the flag.
Unreleased β agboss check --federation: declared contracts compared across applications (#398 PR 4)
-
agboss check --federation <snapshotβ¦>β the application's consumed and exported contracts against its neighbours'agboss describe --jsonfiles (or folders of them), no network:[federation.missing-target],[federation.incompatible-schema](a structural comparison of the JSON Schemas, field by field) with each problem's blast radius,[federation.snapshot-schema],[federation.duplicate-application],[federation.version-not-bumped](a warning;--strictmakes it an error). A2A Β§ 8. -
describe --json: eachapplication.contracts.importsMCP entry getsusedBytoo (the tools of the application's agents that reach its server). -
check --json: a problem may carryblastRadius({ application, workflow, tool }[]).
Unreleased β declared contracts: @A2AExpose, a typed @A2AAgent, the contract price, the envelope's audience, describe --json contracts (#398 PR 3)
-
Breaking β
@A2AAgent({ name, url, target, input, output, price?, deps })and a typedexecute(input)(A2A):target: { application, start },inputandoutputare required ([a2a.target-required]); the classextends A2AClient<In, Out>andexecute(input, call?)replacesexecute(input, OutputDto, call?)β the input is validated before it is sent, the output when it comes. Names:-a2a-agenton aβ¦A2AAgentclass ([name.kind-suffix]). Migration: move the output DTO from eachexecutecall into the decorator withtargetandinput, rename the classβ¦A2AAgent; a client built by hand passescontract: { target, input, output }(A2AContract) β without oneexecutethrows[a2a.no-contract]. -
Breaking β
a2aHttpListener(app, { expose, callerResolver, meshVerifier?, base? }):expose(an@A2AExposelisted in@Application({ exposes }), else[application.expose-unlisted]) replacesstart;POST <base>/executetakesstart?(400 a2a.unknown-startfor a start it does not export, or none with several), and the call chain is checked once the body is read. The same fortestWith'sa2aServer({ expose, callerResolver }). Migration:@A2AExpose({ name: "<app>-a2a-expose", version, exports: [Start] }), list it inexposes, pass it asexpose. -
Added β
@A2AExpose({ name, version, exports })(mirrors@McpExpose) and@Application({ exposes })(@A2AExpose/@McpExpose):[application.expose-unknown-start],[application.not-an-expose],[application.duplicate-expose],[name.kind-suffix](-a2a-expose,β¦A2AExpose). -
Added β the contract price: the calling tool's spend records the larger of
priceand a trusted peer's verified report (the price alone without one),a2a:<application>. -
Added β the envelope's
audience:target.application(MCP: the new@McpServer({ target: { application, expose } })); a receiver of another name refuses it before its resolver β401 mesh.audience-mismatch/ an MCP error result; the client's error message starts with the code. -
Added β
agboss describe --jsonapplication.contracts:exports(@A2AExposestarts with their input and finishes' outputs,@McpExposetools),imports(@A2AAgents withtarget,usedBy,price;@McpServertools withtargetornull), every DTO with itsschemaHash, andschemaswith their JSON Schema.
-
Added β
agboss eval run --chaos <rate> --chaos-seed <n> [--chaos-failures <list>](Eval suites): 429s, timeouts and truncated answers injected into the run's model calls at the provider's HTTP client and a transport'scomplete(), under the retry policy, the rate limit and the breaker, on the real budgets; seeded, the rate and the seed together (usage.missing-option/usage.option-value). -
Added β in
@agboss/core/eval:ChaosFailure { RateLimited, Timeout, TruncatedResponse }andEvalRunChaos; the artifact's optionalchaos(read byevalRunReportOf). The injection is an option ofeval run's internal build path only, never ofcreateApp.
-
Added β labels in
agboss eval ui(Eval suites): a form per judge verdict on each run's page β passed / failed, a 1β4 score, failure tags, a note β saved toeval/labels/<suite>.jsonlin #396 PR 4's format (the same run, judge and labeler replaced, the file written atomically), and the run's calibration (calibrateJudges) on every load. New options--labels-dir,--taxonomy,--labeler; the failure taxonomyeval/failure-taxonomy.yaml; new codeeval.taxonomy-invalid.
-
Added β
agboss eval ui [--dir <folder>] [--port N](Eval suites): the run artifacts of a folder (default.agboss/eval) served read-only β the runs newest first with each case's attempt dots, each run's report page β on 127.0.0.1 only, behind a token it prints, theHostchecked;eval.no-runsfor a missing folder. -
Changed β the run artifact is
schemaVersion3:profile,agbossVersion,models(EvalModelUse) andpiiPolicies, shown in the report's header; versions 1 and 2 are still read (β¦ this agboss reads 1 to 3).EvalRunOptions.profilenames the profile for the artifact.
-
Changed β
agboss compare/compareProfilesjudge each pair in both orders (Profiles and comparisons): two sure verdicts for one side are a win or a loss, anything else a tie; a verdict for the same position both times is counted inPairwiseResult.positionFlips(new, required) and the table'sflips. The A/B order is no longer randomised (therandomargument of the internalpairwiseis gone); each task costs two pairwise calls. -
Breaking β
compareProfiles(Workflow, options)βcompareProfiles(Application, { start, β¦ })(Testing suites and comparisons): the workflow a start of the application opens, assembled ascreateAppassembles it. Migration: pass the application andstart: <its start>. -
Added β the warning
[judge.same-model-family](Judges): a model judge of the judged agent's vendor (the slug's prefix), or a suite's judge of the vendor of an agent of its start's workflow; an error underagboss check --strict. -
Fixed β the eval judge's prompts say "answer" (
agboss eval score,compare), not "replyWith".
-
Added β trusted MCP calls are deduplicated (MCP):
a
tools/calltrusted by the session'speerwith a_meta.callIdruns once per(tenant, peer, callId)inMeshRequestRepositoryβ a repeat gets the kept result, one in flight an error result with_meta["agboss.dev/error"]mesh.request-in-progress. -
Fixed β
originon every leg: a trusted request's execution events carry itsoriginon a resume and a recovery too (read from the checkpointed contexts), not only on its first leg.
Unreleased β the run's timeline: traces in the artifact, the baseline diff, where attempts part ways (#397 PR 2)
-
Changed β the run artifact is
schemaVersion2 (Eval suites): each run keeps its last turn'strace(EvalExecutionTraceβ steps withinputHash, model calls, tool calls, guardrail decisions, judge verdicts; handled errors, pauses, undo actions, children, the output β masked like the execution record) and each case itsexpectation(EvalCaseExpectation). The one reader (evalRunReportOf) reads versions 1 and 2; version 3 or any other iseval.run-invalid(β¦ this agboss reads 1 to 2). New public types in@agboss/core/eval:EvalExecutionTrace,EvalChildTrace,EvalStepTrace,EvalModelCallTrace,EvalToolCallTrace,EvalGuardrailDecisionTrace,EvalJudgeVerdictTrace,EvalHandledErrorTrace,EvalPauseTrace,EvalCompensationTrace,EvalTraceError,EvalCaseExpectation,EvalExpectedToolCallTrace,EvalRunSchemaVersion. -
Added β the HTML report shows each run's timeline, each case's expectation, the comparison
with a baseline (a run made with
--baseline), and for an unstable case the node where its attempts part ways (attemptDivergenceOf,AttemptDivergenceReason); one value is cut at 2,000 characters on the page. A page above 16 MB leaves out the timelines of cases with pass^k, then all, beforeeval.report-too-large(now only for a page too large without any timeline).
Unreleased β mesh trust: signed envelopes, origin, the caller's budget and deadline, run reports, dedupe, /cancel (#398 PR 2)
-
Added β
@Application({ mesh: { signingKey?, peers?, idempotencyRetention? } })(A2A):MeshOptions/MeshPeerβ the field of the application's Ed25519 private key (aSecret, PKCS #8 PEM) and each peer's public key field (text, SPKI PEM). Envelopes and run reports are signed (JWSEdDSA) withissuedAt,expiresIn,bodyHash, and the caller'sbudget/deadlineclaims;origin.step.tool. New rules[mesh.signing-key-missing],[mesh.unknown-peer-key],[mesh.duplicate-peer]; at start[mesh.invalid-key].maxMeshHopsis unchanged (flat). -
Added β trusted peers:
A2ACaller.peer?(A2APeer) β the peer a resolver authenticated; a request is trusted when its envelope carries that peer's valid signature. Before the resolver the listener refuses a bad signature or an expired envelope (401 mesh.signature-invalid/mesh.envelope-expired), after it a body that is not the signed one (401 mesh.body-mismatch).a2aHttpListener(app, { meshVerifier })β aMeshVerifier(verifyKeyOf(issuer)) for peers' keys. MCP:McpSessionOptions.peer. -
Added β what a trusted request gets: the caller's remaining budget as the execution budget
caller-budget(the receiver only lowers its own), its deadline, and its origin βexecutionContext.origin,ExecutionRecord.origin,ExecutionEvent.origin(ExecutionOrigin). -
Added β
app.execute(β¦, { deadline })andDeadlineExceededError(limit.deadline:deadline,deadlineAt; aLimitExceededError): checked before every step and model call. -
Added β run reports: a trusted answer's
report(A2A) /_meta["agboss.dev/report"](MCP), a signedMeshRunReport(MeshSpendReport); the caller verifies it and records the spend (a2a:<issuer>/mcp:<issuer>) andToolCallRecord.remote(ToolCallRemote). TheA2AExecutionResponsegainsexecutionIdandspend. -
Added β
MeshRequestRepository(infrastructure, in memory;SqliteMeshRequestRepository, tablemesh_requests;MeshRequestKey,MeshRequestRecord): trustedPOST /executerequests deduplicated by(tenant, peer, idempotencyKey)β a repeat gets the kept answer, one in flight409 mesh.request-in-progress; removed afteridempotencyRetention(default 30 min) by the recovery sweep; pinged by readiness. -
Added β
POST <base>/cancel { threadId }(A2ARoute.Cancel) andA2AClient.cancel(threadId). -
Changed β an invalid streaming body:
400JSON, as for a non-streaming one (it was an SSE failure event).
-
Added β
agboss eval calibrate --report <run.json> [--labels <labels.jsonl>](Eval suites): a saved run's judges against people's labels (eval/labels/<suite>.jsonlby default, oneEvalLabelper line:runId,executionId,judge,labeler,passed,score?,failureTags,note?) β pairwise Cohen's ΞΊ (the judge with each labeler, the labelers with each other), a confusion matrix per labeler, the disagreements, and the rubric by the judge'smetricskeys; runs nothing. -
Added β public types in
@agboss/core/eval:EvalLabel,EvalCalibration,EvalJudgeCalibration,EvalRaterAgreement,EvalConfusionMatrix,EvalDisagreement,EvalMetricGroup,EvalMetricValue,EvalScoreCounts. -
Added β codes
eval.labels-not-found,eval.labels-invalid(exit 2).
-
Added β
--baseline <run.json> [--fail-on-regression](Eval suites): each case against the same case of a run artifact (a run ofmain) βCaseComparison { Regressed, Improved, Unchanged, FlakyOnBaseline, New, Removed, Changed }; a changedcaseHashis not compared, a case that flapped on the baseline is quarantined. The table and the counts, the artifact'sbaseline(EvalRunComparison). A baseline that is no run artifact of the suite iseval.run-invalid(no file:eval.run-not-found), exit 2 β read by the same one reader aseval show(#397 S4). -
Added β exit 5 (
quality):--fail-on-regressionfails witheval.regressedwhen a case stable on the baseline regressed (CLI). -
Added β the artifact's types are public in
@agboss/core/eval:EvalRunReport,EvalCaseReport,EvalRunResult,EvalCheckResult,EvalRunError,EvalRunSummary,EvalRunComponent,EvalRunSpend,EVAL_RUN_KIND,EVAL_RUN_SCHEMA_VERSION,CaseComparison,EvalRunComparison,EvalCaseComparison,EvalCasePasses(no longer in@agboss/core/internal). -
Added β
agboss eval run --profile <name>: the suite on a profile of its workflow. -
Changed β a case's history is
ExecutionRecordStatus.Written(new member): turns of their thread, read by its memory as before, but never scored byagboss eval scorenor saved byagboss golden add(before, they were recorded asanswered).
-
Added β
modelComponentsinagboss describe --json(CLI): model judges (schemaHashof theirreply), guardrails, arbiters, compactions that write summaries, model reply interpreters and model PII detection, each aComponentRecordwith itspromptHash/schemaHash;ComponentKindgainsCompaction,ReplyInterpreterandPiiPolicy. -
Changed β stale cassettes cover them (Testing): a recording of
any of them whose hashes changed is
StaleCassetteError/[vcr.stale-cassette], re-recorded byAGBOSS_VCR_MODE=RECORD_STALE npm test.
-
Added β
agboss eval run β¦ --html <file>(Eval suites): the run as one self-contained HTML page β header, summary (pass@k, pass^k, cases, attempts, spend), unstable cases, every case with an attempt dot per run (passed, failed, error, limit) and each run's checks β no external request (aContent-Security-Policyof its own style and script), light and dark, phone width, keyboard; bounded at 16 MB (eval.report-too-large). -
Added β
agboss eval show <run.json> [--html <file>]: a saved run artifact read back β its table, and the same page β without running anything;eval.run-not-found,eval.run-invalidfor a file that is not an artifact this version reads.
-
Breaking β
agboss evalis made of subcommands (CLI): today's scoring of recorded runs isagboss eval score(the same options); the bareagboss evalprints its subcommands and exits 2 (usage.missing-argument). Migration: addscoreafterevalβagboss eval score --start β¦(job-scout'snpm run evaldoes it). -
Added β
agboss eval run [--suite <name>] [--runs <k>] [--report <run.json>] [--env <name>](Eval suites): the application built ascreateAppbuilds it, an@EvalSuiteassembled on it, each case run k times β its history written into its thread, its turns run with the case's tenant and execution values, the trajectory check overresult.toolCalls, judges in the frame of the case's last execution, the cascade β pass@k / pass^k per case,unstablefor pass@k without pass^k; the run artifact (kind: "agboss.eval-run",schemaVersion: 1) with--report, the envelope'sresultwith--json. A run ending at a finish its judge does not judge fails that check. New codeeval.unknown-suite. -
Added β
agboss checkchecks eval suites (Loop 0): everyeval-suites/*.eval-suite.tsof the application assembled, its problems on its file,eval-suitesin the checks. -
Fixed β the generated types' context blocks are sorted: two applications that declare their
own contexts no longer make
.agboss/types.d.tsdepend on the order of the workflows.
-
Added β
testCompaction(Compaction, options?)(Testing): the tenth unit runner β aCompactionRunnerthat compacts oneCompactionInput(previousSummaries,turnsToCompact) and gives aCompactionResult(summaries,requests:CompactionRequest { instructions, input }per model call) forSummaryQueueandSingleSummaryon a scripted model, and forCompactionStrategy.Custom(#382) on its own container. It checks the request and the plumbing; survival of facts is an eval case.
-
Added β the mesh envelope (A2A,
MCP): every
A2AClientcall made inside an execution sendsagboss-mesh(MeshEnvelope:schema,issuer,originβMeshOrigin,MeshOriginStepβ,callChainofMeshHop/MeshEntry; an unsecured JWS, ids only) and a W3Ctraceparent; an@McpServercall sends_meta.callId(the step's idempotency key β before, the MCP client sent no_metaat all),_meta["agboss.dev/mesh"]and_meta.traceparent. Outside an execution, nothing. -
Added β
A2ACallerRequest.envelope?: the envelope as the listener read it, a claim (not verified: never the tenant, a budget or a deadline); a malformed one is ignored. -
Added β loops and hops refused before they run: a request whose chain already lists the
receiver's
(application, start)β409/ an MCP error resultlimit.mesh-loop; one whose chain already lists@Application({ maxMeshHops })applications (new, default 8,[limits.max-mesh-hops-invalid]) βlimit.hops. New errorsMeshLoopDetectedError(entry,chain) andHopsLimitExceededError(entry,chain,maxMeshHops,hops), bothLimitExceededErrors (MeshLimitPlacefor their constructors); thrown by the client, and a tool that meets one fails its step with it β never an error result for the model. -
Added β
A2AExecutionResponse.code/.details: a failed or limited run's error code (everyAGBossError), and for the two mesh limits their facts, so the caller fails the same way.
-
Added β cassette version 2 and
StaleCassetteError(Testing): every recorded call keeps its component and that component'spromptHash/schemaHash; a replay of an agent or model router that changed since isStaleCassetteError(vcr.stale-cassette,component,changes: CassetteChange[]). Version 1 cassettes keep replaying, without stale detection, with a hint. -
Added β
VCRMode.RECORD_STALEandAGBOSS_VCR_MODE: re-records only the stale components' calls;AGBOSS_VCR_MODE=<member>overrides every test's own mode (an unknown value fails the test naming the modes). -
Added β
agboss check --cassettes(CLI): the same verdict without a test.
-
Added β
callToolWithInvalidArguments(Tool, args),callToolWithTruncatedArguments(Tool, partialArgs),replyWithInvalidAnswer(Dto, fields)(Testing): a validation retry is tested without a cast β an invalid or a cut tool call (recordedToolCallStatus.InvalidArguments, counted instep.validationAttempts, the tool never runs) and an answer that does not fit its class (the third fails the step withAgentAnswerInvalidError). ReplacereplyWith(Dto, { β¦ } as never)withreplyWithInvalidAnswer(Dto, { β¦ }).
-
Breaking β
mockLlm(X).toHaveCalledTools([β¦])istoHaveScriptedToolCalls([β¦])(Testing): the script matcher (the model's calls, exactly, in order) is renamed β replace the name in a test;toHaveCalledToolsnow asserts a run result. -
Added β
expect(result).toHaveCalledTools([A, B])(Testing): each tool called at least once by the root execution's agents, in any order, whatever its status;.not.toHaveCalledTools([β¦])β none of them was. -
Added β
expect(result).toHaveToolCallMatching(Tool, partialOrCheck): some call of the tool has an input matching a deep partial or a check (ToolCallExpectation<TInput>, typed by the tool's input);.notβ none does.ToolCallMatchers/toolCallMatchersin@agboss/core/testing.
-
Changed β a failed visit keeps its records (Observability):
the tool calls, judge verdicts and guardrail decisions of an agent whose step fails
(
QualityGateError,ToolBlockedError,GuardrailBlockedError, a limit, any error) are inresult.toolCalls,record.toolCallsand itsrecord.stepsentry when a side path takes the failure, in a failed branch's too, and in theChildExecutionRecordof a child the failure ends. -
Added β
ChildExecutionRecord.steps: each child's ownExecutionStepRecords. -
Added β
ToolCallStatus.UnknownTool: a call to a tool the agent does not have is recorded.
-
Changed β transport providers' lifecycle (Model providers):
created on its first call, a transport's
onInitβonStartrun, awaited, before that call'scomplete()β once under concurrent first calls; a failing hook fails that call with the start's error ([di.hook-failed] X.onStart failed: β¦) and the next call tries again;app.close()runsonStop/onDestroyfor the transports that were created, none for the others. Before, a transport created on a call got noonInit/onStart.
-
Breaking β
a2aHttpListener(app, { start, callerResolver, base? })(A2A): built from the application (A2AApp:createApp's ortestWith'sapp), not from an adapter; the positional forma2aHttpListener(adapter, Start, base)is removed.callerResolveris required β there is no default tenant: a single-tenant host returnsA2ACallerResolution.resolved({}). Migration:a2aHttpListener(new A2AAdapter(app), Start, "/a2a")βa2aHttpListener(app, { start: Start, callerResolver: MyCallerResolver, base: "/a2a" }), withMyCallerResolveran@Injectablelisted inproviders. -
Breaking β
threadβthreadIdon the wire (the request body, the response) and in the client:A2AExecutionRequest.threadId,A2AExecutionResponse.threadId,A2AClientCall.threadId,A2ARequestSchema/A2AResponseSchema(one field name per meaning:app.execute'sthreadId). -
Breaking β
IA2AAdapter.cancel(threadId, caller)(A2ACallOptions): cancels the caller's tenant's thread only;cancel(thread)is removed.A2AAdaptertakes anA2AApp. -
Added β the caller resolver:
A2ACallerResolver<typeof Start>(resolveCaller({ route, http }),A2ACallerRequest,A2ARoute), answered byA2ACallerResolution.resolved(caller)/.unauthenticated({ reason, challenge? })/.forbidden({ reason })(branded,A2ACallerResolutionKind,ResolvedA2ACallerResolution,RejectedA2ACallerResolution,A2ACallerUnauthenticatedOptions,A2ACallerForbiddenOptions);A2ACaller<typeof Start>is typed by the start's application. The resolver is an@Injectablein@Application({ providers })or a@Workflow({ providers }), taken from the container β not listed β[a2a.caller-resolver-not-listed]when the listener is built. Resolved before the body is read or a header written:401(+WWW-Authenticate),403,500(a2a.caller-unauthenticated,a2a.caller-forbidden,a2a.caller-resolution-failed, body{ code, error }); a refusal runs nothing β no observer event, no record. -
Added β
A2ACallerRejectedError(a2a.caller-rejected;httpStatus, the wirecode):A2AClient.sendon a401/403carrying ana2a.caller-*code. -
Added β
testWith'sa2aServer({ start, callerResolver, base? })β{ url }(A2AServerFixture,A2ATestServer): the listener on127.0.0.1:0, the loopback open for the test, closed at its end;TestApp.resolve(Class)once the app is built (test.app-not-readybefore). -
Changed β
@Application({ providers })documents that ordinary@Injectableservices may be listed there too (they always could be: one instance for every workflow).
-
Added β
result.toolCalls,result.record.toolCalls,ChildExecutionRecord.toolCalls(Observability): oneToolCallRecordper tool call, in call order βstep(the tool as itscomponent, withpromptHash/schemaHash; the agent's visit),input,output,status: ToolCallStatus(Succeeded,Failed,InvalidArguments,Declined,TimedOut; a tool interceptor'sRepliedandBlocked, #384),error?,isResultTruncated,startedAt,endedAt. The result's are real values, the record's masked.toolCallsOf(Tool, toolCalls)βTypedToolCall(one tool's calls, typed once narrowed bystatus:SucceededToolCall,FailedToolCall,UncheckedToolCall). -
Added β
result.record.steps: oneExecutionStepRecordper visit βstep,inputHash,judgeVerdicts(JudgeVerdictRecord),guardrailDecisions(GuardrailDecisionRecord),spend,startedAt,endedAt,error?. -
Added β
JudgeInput.toolCalls(Judges): the visit's calls, every attempt's, real values;JudgeInput<T>is nowJudgedReply<T> & { toolCalls }.testJudgetakestoolCallsoptionally ([]). A hand-madeJudgeInput(callingjudge()directly) needstoolCalls. -
Fixed β a truncated tool call is no silent empty reply: a call whose arguments are no JSON
(the provider's
invalid_tool_calls) is anInvalidArgumentscall the model reads about, counted invalidationAttemptsand inmaxToolCalls; a truncated answer is an invalid answer (no more "answer with a tool, not with text"). - Changed β the agent loop's prompt texts gained the invalid-arguments message, so every agent's prompt version changes once.
-
Added β transport providers (Model providers):
a
@ModelProviderclass thatimplements ModelProviderHandlerwithcomplete(request)β nobaseUrl/apiKey, its services throughdeps, created by the application's container on its first call (one instance,TenantContextread in the calling execution). New public names in@agboss/core/models:ModelProviderRequest,ModelProviderResponse,ModelProviderMessage,ModelProviderMessageRole,ModelProviderToolDefinition,ModelProviderToolCall,ModelProviderUsage,ModelProviderFailureError(model.provider-failure,ModelProviderFailureOptions:retryAfter?,cause?),FinishReason;complete?onModelProviderHandler;depson@ModelProvider; the rule[model-provider.both-modes]. Every policy of the gateway runs around it;agboss describeprintstransport: X.complete(); part ofconfigHash(only where there is one). -
Changed β
ModelProviderHandler.capabilitiesis optional (required of an HTTP provider by the decorator's overload; a transport without it is not checked against its models). -
Changed β
ModelProviderOptionsis the union of the HTTP options and the transport options (baseUrl/baseUrlField/apiKeyvsdeps);ModelProviderTypetakes constructor arguments. Code that spreads one provider's options into another's decorator narrows them first. - Changed β a breaker's fallback is at the chat model: while the primary's breaker is open the call goes to the fallback provider's own chat model (its own wire form, not the primary's request re-addressed), so HTTP and transport providers fall back to each other. Accounting is unchanged β the fallback's model, its prices.
-
Added β
@ToolInterceptor(Tools): a class that implementsBeforeToolCall(beforeToolCall(call: ToolCallContext)βToolCallDecision.allow()/.replyToModel({ message })/.block({ reason })) and / orAfterToolCall(afterToolCall(call: ToolCallResultContext)β the result the model reads), listed intoolInterceptorsof@Applicationβ@Workflowβ@Agentβ@Tool/@McpTool(outer first), removed withexcludeToolInterceptorson an agent or a tool.replyToModelanswers the model without calling the tool;blockfails the step with the newToolBlockedError(step.tool-blocked, aStepFailedError); a throwing interceptor fails it withAgentFailedError(its error thecause).beforeToolCallreads the arguments PII-revealed; the result afterafterToolCallis validated, cut atmaxResultSize, then masked. Rule codes[tool-interceptor.not-an-interceptor],[tool-interceptor.exclude-not-inherited],[tool-interceptor.missing-handler];agboss describeprintstool interceptors,describe --jsoneach agent's chains,configHashcovers them (a workflow without one keeps its hash);testToolInterceptorin@agboss/core/testing. Replaces the tool hooks removed in #120 S123. -
Changed (internal) β the framework's own per-call tool context is
ToolInvocationContextnow;ToolCallContextis the interceptor's (root entry).
-
Added β the entry
@agboss/core/eval(Eval suites):@EvalSuite({ name, description, start, checks, cascade, cases })ineval-suites/<name>.eval-suite.ts(-eval-suite), the case buildersevalCase,turn,pastTurn,toolCall,toolInvariant, the checkscheckTrajectory()andjudgedBy(Judge), the enumsEvalCascade { StopAtFirstFailure, RunAll }andCheckStatus { Passed, Failed, Skipped }; typesEvalSuiteOptions,EvalSuiteMeta,EvalCase,EvalCaseOf,EvalTurn,EvalPastTurn,EvalValueOf,EvalCheck,TrajectoryEvalCheck,JudgeEvalCheck,EvalToolExpectations,ExpectedToolCall,ToolArgumentsOf,ToolInvariant. Declarations only β nothing runs at import; the runner comes with PR 2. Assembly rules[eval.unknown-start],[eval.duplicate-case],[eval.judge-wrong-reply],[name.kind-suffix]; case values βEvalCaseInvalidError(eval.case-invalid, root entry, withEvalCaseIssue). -
Added β code judges (Judges):
@Judge({ decidedBy: DecidedBy.Code })decides byjudge(input)alone β no model, no cost β in an agent's quality gate and in an eval suite;modeldoes not compile on it,[judge.model-on-code-judge]for amodelor an injectedJudgeModel;testJudgeruns it without amodelscript.decidedByis optional (DecidedBy.Modelby default); new typesModelJudgeMeta,CodeJudgeMeta. -
Changed β
JudgeMetaandJudgeOptionsare unions discriminated bydecidedBy(JudgeOptionsis now a type alias, no longer an interface): code that readsmeta.modelnarrows first.
-
Fixed β
agboss/no-component-statemissed components (Components):this.x = β¦outside the constructor is now reported for@InputGuardrail/@OutputGuardrail,@Budget,@Arbiter,@ReplyInterpreter,@Compactionand@ModelProvidertoo; the staleGuardrailname is gone. The decorators come from one list of the framework (src/components/component-decorators.ts), and a test fails when a class decorator a public entry exports is neither listed nor named as not a component β a new decorator cannot be forgotten. A project whose components kept state there now gets lint errors for it.
-
Added β lint in a new project (CLI):
agboss createwriteseslint.config.jsand thelintscript (eslint ., also innpm run check) with its dev dependencies βtypescript-eslint(strictTypeChecked),agboss/no-component-stateandagboss/no-direct-timeonsrc/outside the tests,@stylistic/newline-per-chained-callon every*.workflow.ts; the project passes it as written. -
Added β the entry
@agboss/core/eslint(#388 S2):agbossPlugin(agboss/no-component-state,agboss/no-direct-time) andFLOW_CHAIN_RULEfor a project's ESLint config; the repository's own config uses it too (lint runs on the build).@typescript-eslint/utilsis a new optional peer ofagboss, needed only by this entry. -
Changed β
agboss/no-direct-time's message no longer says "framework code":`Date.now` reads the time directly: inject the application's Clock β¦.
-
Breaking β ten
RuleCodemembers are removed:graph.two-next-steps,graph.router-not-last-in-chain,graph.cycle-without-router,graph.duplicate-node,router.self-without-agent-before,router.empty-route-text,router.unbounded-cycle(the transition DSL before #272) andbatch.invalid-batch-size,batch.invalid-concurrency-limit,batch.strategy-not-decorated(batchParallel, removed). No rule reported them; a project that names one (aswitchoverRuleCode, a test expecting it) no longer compiles β the codes that replaced them are[graph.duplicate-node-name],[router.no-prompt]and[graph.cycle-without-max-visits]. The skill's error catalog (reference 11) drops its "reserved" entries; a test checks everyRuleCodemember is reported by some rule.
-
Added β
CompactionStrategy.Custom(Memory management): a@Compactionclass thatimplements CompactionHandlerβcompact({ previousSummaries, turnsToCompact })(CompactionInput, each turn aCompactionTurn:startName,input,finishName,output) returns the summaries readers see from now on;depsinjected by the container, run in the frame of the execution's start. Counted, run lazily, PII-masked and stored like the built-in strategies;[compaction.missing-handler];agboss describeprintsstrategy: custom (X.compact()), the class is part ofconfigHash. -
Changed β
CompactionError.summaryMaxTokensisSummaryMaxTokens | undefined:undefinedfor a custom compaction.
-
Added β guardrails decided by code (Guards):
@InputGuardrail/@OutputGuardrail({ decidedBy: DecidedBy.Code })β the class'scheck(text)(implements GuardrailHandler) returnsGuardrailVerdict.passed()/.blocked()on the masked text; no model call, cost 0; the same refusal (text orrefusal()),GuardrailBlockedError(probability1,threshold1), levels,excludeGuardrails, events andtestGuardrail; a throwingcheckfails closed (GuardFailedError, its error ascause);[guardrail.missing-handler];agboss describeprintsdecidedBy: code.decidedByis optional,DecidedBy.Modelby default: every existing guardrail compiles and hashes as before. New public typesCodeGuardrailTextOptions,CodeGuardrailComputedOptions;GuardFailedErrortakes an optionalcause.
-
Added β the rest of the coding-agent skill in
packages/agboss/skills/agboss/references/: 02 (every flow form, with the rule codes each can hit, compensation on side paths and the automatic undo), 06 (budgets, model providers, retry, rate limits, tenant-fair queues, circuit breakers with a fallback), 08 (results, records, observers, the log, Langfuse), 09 (stores, several instances, health probes, the graceful stop, Kubernetes, deploys by config hash), 11 (every CLI command, its exit codes and JSON envelope, Studio as it is, and the full catalog β everyRuleCodeand the application warnings with a before β after fix;check:docsnow checks the catalog is complete) and 12 (four multi-file recipes that compile and whose tests pass); compensation added to 03 (compensateWith,committedBy,compensationTimeout, the hooks) and 10 (toHaveCompensated,result.compensations).
-
Added β durable automatic undo
(Several instances): the reserved tree keys
@agboss/unwinding({ reason, cause }),@agboss/unwinding:<childExecutionId>and@agboss/compensated/<compensationKey>; an undo interrupted by a stop, a crash or a lost lease is finished by the instance that takes the tree over β recovery before its compatibility check, a resume at its lease take β the undos marked done skipped, then the tree ends failed with the recorded reason, without a pass (compensation.recoveredin the log); a child re-entered mid-undo finishes its own before any step. -
Changed β the root's record is written after its undo
(Rollback):
runningwhile the undo runs, then failed with the final error βCompensationFailedErrorwhen an undo failed (before, the record kept the original error). -
Changed β
app.cancelrejects withCompensationFailedErrorwhen it ran the undo itself (a paused tree, or a paused tree of another instance's request) and an undo failed β the others ran, the tree is cancelled (Cancellation); before, it returned{ cancelled: true }. Breaking for a caller that expected the cancel to resolve.
-
Added β
skills/agboss/in theagbosspackage (SKILL.mdand 12 references; 02, 06, 08, 09, 11 and 12 stubs until #305 PR 3) β CLI β Project skills, ADR-0021. -
Added β
agboss createwritesAGENTS.md, a one-lineCLAUDE.mdand.claude/skills/agboss/(with.agboss-skills.json), and.claude/skills/agboss/in the generated.gitignore. -
Added β
agboss checkandagboss generatesync the skills folder with the installedagboss(skills: β¦ installed | updated | repaired), warnings[skills.local-edit],[skills.unmanaged-file],[skills.sync-failed],[skills.no-framework]β never a changed exit code;--jsonhasresult.skills;"agboss": { "skills": false }turns it off. -
Added β
check:docschecks the skills: their snippets compile, their version header, codes and matrix coverage (Home).
-
Added β the content of the coding-agent skill shipped in
packages/agboss/skills/agboss/:SKILL.md(the 10 invariants, thenpx agboss generate β check β describe/npm testloop, the gate's rules, the routing table) and references 01 (composition and DTOs), 03 (components, DI, lifecycle), 04 (MCP and A2A), 05 (channels and pauses), 07 (guardrails and PII), 10 (testing, VCR, evals) β every ts block compiled bycheck:docs, every matrix name checked against the API reports (tests/skills/coverage.ts). CLAUDE.md and QUALITY.md sayreasoning, notthinking.
-
Added β the undo of an execution that ends without a finish
(Rollback): an uncaught failure or limit
(
CompensationReason.ExecutionFailed), a cancel or a launch'sFailFastabort (ExecutionCancelled) undoes the execution's open undoable actions, newest first, before it ends β a child in its parent's step before its error is raised (Workflow as a step), a root by its lease's holder before its keys are removed andonExecutionErroris observed, after awaiting the tree's running step bodies up tocompensationTimeout(one that returns later is undone at once, in process). -
Changed β
app.cancelof a paused tree takes its lease, withdraws its pauses, undoes, then returns{ cancelled: true }(Cancellation); a cancel request (#360) found at lease take undoes first, then ends the tree as before. -
Changed β
CompensationFailedErrorpasses a child's boundary as it is, never wrapped inChildExecutionFailedError: no.onError/.onJoinErrorof the parent takes it, and the parent undoes its own open actions. -
Added β
ExecutionErrorEvent.compensations(Observability): the undo actions that ran for the failed execution. Breaking for a project that buildsExecutionErrorEventobjects itself (a test double of an observer): the field is required. - Changed β the undo's service spend is written to the ledger at once when no state will hold it: a failed join that fails the execution, an execution ending without a finish (FinOps).
- README: "declared action compensation (
compensateWith/committedBy)" is back in the feature line.
-
Fixed β
agboss checkprintedokwhen type generation found no workflow (CLI): an application module outsidesrc/that does not export its workflows wrote no.agboss/types.d.ts. Now it is[types.no-flow](a CLI problem, not aRuleCode), naming the application file and the fix βexport { MyWorkflow }from it, or keep it insrc/beside its*.workflow.tsfiles β exit 3, in the printed problems and in--json. How workflows are found is unchanged. -
Fixed β a DTO name clash inside one workflow names both files:
[dto.duplicate-name]said "(unknown file)"; the diagnostic of type generation, which names the files, is kept instead.
-
Fixed β
agboss checkprintedokwithout writing.agboss/types.d.ts(Workflow, CLI): two DTO classes of one name in two workflows (seen by no single workflow's rules) failed type generation silently. Now[dto.duplicate-name]names both files, in the printed problems and in--json, exit 3; any error that kept the file from being written is reported, and a failed write is[types.not-written](a CLI problem, not aRuleCode).createApp/testWithare unchanged: they check DTO names per workflow.
-
Changed β
app.close()/ SIGTERM interrupts the running executions a grace before the end of the shutdown timeout βmin(seconds(5), 20 %)of it (seconds(25)β 5 s,seconds(10)β 2 s) β and spends the grace on the step bodies they left running, then releases the leases and runsonDestroy: oneclose()now waits for those bodies, and the process always ends withinshutdownTimeout(App β app.close(), Several instances). Several calls: the stop ends at the earliest end any call set (before: the latest);close({ timeout: 0 })stops waiting for the bodies at once. The grace is not a setting; no public API change.
-
Added β
[action.committer-without-compensator](Rollback), an assembly error: an@ActionwithcommittedBybut nocompensateWithβ the option has no effect, since nothing undoes the action. The message names the action and its committer: addcompensateWith, or removecommittedBy.RuleCodegains the code.
-
Added β compensation records (Rollback):
every
@Action({ compensateWith })that succeeds opens a record in the execution's state β committed by itscommittedByaction or a finish (a child's by its own finish), closed by its undo on a side path (CompensationReason.SidePath).ExecutionOutput.compensations,ExecutionRecord.compensations:CompensationRecord { compensated, compensator, reason, status, error?, startedAt, endedAt };CompensationReason,CompensationStatus. -
Added β a failed join undoes its finished branches (Rollback,
Parallel): before
.onJoinErroror the failure, newest first, after awaiting the launch's running branches β each undo in its own frame (key<step key>:compensation,StepRecord.compensates, a clean-up signal), no slot, no budget check (CompensationReason.JoinFailed). An undo that fails β the others still run, thenCompensationFailedError(compensation.failed,failures,cause), taken by no side path. -
Added β
@Application({ compensationTimeout })(App), defaultseconds(15). -
Added β observer hooks
onCompensationStart/onCompensationEnd(Observability); matchertoHaveCompensated(Action, { by, reason?, status? })(Testing);agboss describemarks a launched undoable action(on a failed join: automatic). - The undo of an execution that ends without a finish (a cancel, an uncaught failure or limit, an aborted child) came with #340 PR 3 (above).
-
Fixed β
app.close()/ SIGTERM no longer runsonDestroyor releases a tree's lease while a step body its interrupted pass left running still runs (App β app.close(), Several instances): LangGraph rejects the pass before a body that ignores its signal returns, so a recovering instance could rerun the step with the same idempotency key at the same time. The instance now keeps the lease until the tree's bodies returned and startsonDestroyonce every body did, at the latest at the end of the shutdown timeout; a body still running then is logged asapp.step-still-running(LogLevel.Warn). The interrupted call still rejects withApplicationStoppingError, once its lease is released. No public API change.
-
Added β
@Action({ compensateWith, committedBy })(Rollback, ADR-0020, replacing ADR-0019's "no automatic compensation"): an action names the@Actionthat undoes it and, optionally, the one after which nothing undoes it. Checked at assembly:[action.compensator-not-an-action],[action.committer-not-an-action],[action.self-compensation],[action.compensator-compensates],[action.invalid-compensator-input]; an.onError/.onJoinErrorside path after an open undoable action reaches a finish only through its undo, newest first ([flow.uncompensated-action],[flow.compensation-out-of-order]); withcommittedByevery path runs the committer or the undo ([flow.uncommitted-action]); a committer no path reaches is a warning ([flow.committer-unreachable]). -
Added β
agboss describeprints each undoable action (compensateWith β¦ Β· committedBy β¦),--jsonits node'saction(CLI). -
Added β
examples/flow-patterns/src/hiring/: the send-offer workflow, its eighth workflow. - Nothing runs automatically yet: the undo where no path can say it (a cancel, an uncaught failure, a failed join's finished branches) is #340.
-
Fixed β
app.cancel(threadId)on any instance cancels the thread's execution running on another (Several instances β Cancelling from any instance): before, it returned{ cancelled: false }unless it reached the instance moving the tree. The cancel leaves a request under the reserved tree key@agboss/cancel-request; the holder stops withExecutionCancelledErrorwithin one lease renewal (10 s, or its next checkpoint write), and a request made while the holder is down ends the execution at recovery β on an instance of any config version β instead of continuing it. No public API change. -
Changed β tree keys starting with
@agboss/are reserved for the framework. - A cancel from another instance before the execution's
runningrecord exists (its budgets, its thread's compaction) returns{ cancelled: false }.
-
Added β
@Application({ maxConcurrentSteps })(Several instances β An instance's cap on steps): how many agent, router and action steps (launch branches too) one application instance runs at once, over every tree and tenant β a whole number from 1 ([limits.max-concurrent-steps-invalid]); unset β no cap (no change). A step without a free slot waits in the instance's queue in the tree queue's order (closing a branch first, then the deepest, aged every 10 s on theClock); a step in a provider's rate-limit queue keeps its slot; waiters atapp.close()are interrupted at the shutdown timeout and recovered elsewhere. -
Added β
HealthComponentKind.StepSlots(Health checks): the readiness componentstep-slots(only with a cap) βDegradedwhile every slot is busy,reason"32 of 32 step slots busy, 7 steps waiting"; liveness unchanged. - Changed β the recovery sweep of a capped instance starts at most its free step slots' worth of recoveries per look (none when full) and goes on where a cut-short look stopped.
-
agboss describeprintsmaxConcurrentSteps;describe --jsonhasapplication.maxConcurrentSteps(nullwhen unset).
-
Added β
maxResultSize(Tools β Result size): what a model reads of one tool result at most, in bytes of UTF-8 β on@Tool/@McpTooland indefaults.toolsof@Application/@Workflow(the most specific wins), default 128 KB. A longer rendered result (an error's too, a knowledge base's search tool too) is cut before PII masking, at the last whitespace within the final 256 bytes, and ends with\n[result truncated: N bytes, the first M kept]; a context-mode knowledge block is bounded by the agent's value.[tools.max-result-size-invalid]at assembly. -
Added β
kilobytes()andBytesin@agboss/core/units(1 KB = 1,024 bytes). -
Breaking β
ToolEndEvent.isResultTruncated: booleanis required: an object built as aToolEndEvent(an observer test) sets it. -
Changed β every workflow's
configHashonce: the truncation marker is a framework text of the agent loop; a paused execution resumed on the new version isIncompatibleResumeErrorunlessonIncompatibleResumeaccepts it. -
Added β
agboss describeprints each tool'smaxResultSizeand where it came from;describe --jsonhasschemas.tools[name].maxResultSizeand each agent'smaxResultSize.
Unreleased β a decided quorum withdraws its waiting branches' pauses; channels may withdraw (#337)
-
Added β
withdraw(withdrawal)on channels (Pauses β Taking a question back): the pair ofsendβ a no-op onChannelOf(a role's implementation writesoverride), optional onChannelHandler;ChannelWithdrawal<TRole>(root entry) is{ pauseId, value, resolution: PauseCancellation }. Called once when a pause the channel sent closes without an answer βWithdrawnorCancelledβ in the pause's frame, under its tool'stimeout; never for a pause decided at once or never sent. A throw or a timeout is the warningchannel.withdraw-failed. -
Fixed β a decided quorum withdraws its waiting branches' pauses: an execution whose
.joinQuorum(β¦)/.joinQuorumGroups(β¦)was decided while a branch waited on a channel rested paused (the join never ran); the pending pauses are now withdrawn (PauseResolution.Withdrawn) and the join runs in the same call. A deadline passing while the tree rests is acted on by its next pass (Parallel). -
Fixed β
FailFastwithdraws a paused sibling child's pause under.toParallelWorkflows(β¦)(and inside a child's own launch): the execution failed to fail, resting paused. A pause record hasbranchKeys(the launch branches it waits under;SqlitePausedExecutionRepositorykeeps them in its JSON column, a record stored before reads with its own step's key). -
Changed β
mockOf(Implementation)also replaces what a contract that resolves to that class gets (a channel role, anyextends): before, only a dependency on the class itself got the mock. -
PauseResolution.Withdrawnreads "aFailFastsibling failed, or its launch's quorum was decided".
-
Fixed β a root's
configHashcovers its child workflows (Terns and evaluation β What the config hash covers): the snapshot addschildrenβ every child reachable from the flow, recursively, each once by name,ThisWorkflowexcluded. A run paused or running inside a child that a deploy changed is nowIncompatibleResumeError/recovery.incompatible, decided by the root'sonIncompatibleResume, instead of continuing silently on the changed child (Several instances β Recovery). -
Breaking β hashes of workflows with children change once: every workflow that launches a
child gets a new
configHash(records, events,describe --json; on durable records the first start after the upgrade warns once that its version changed without a bump); runs of such a workflow paused or running before the upgrade are incompatible with it unless itsonIncompatibleResumesays"resume". Workflows without children keep their hash. -
Breaking β
IncompatibleResumeErroris anAGBossErrorwithstatic code = "resume.incompatible"(the same string; its instancecodeis typedstring), so.onError(AGBossError, β¦)anderror instanceof AGBossErrorinclude it.
-
Added β
agboss describe --jsonis each workflow's declarative snapshot (CLI β The declarative snapshot): next to what the text prints, every entry ofworkflows[]hasconfigHash(the full hash;configstays its first 8 characters),starts,finishes,nodes(kind, hashes,input/outputby DTO hash, an agent's model, provider, reasoning, limits, memory, tools, knowledge bases, the guardrails and PII policies in effect, judges and instructions; a router'sdecidedBy, model and routes withspawn/failure),edges, theonErrortable,launches(quorum, arbiters with their hashes) andschemasβ every DTO by class and every tool by name with itsschemaHashand JSON Schema. -
Breaking β
ComponentRecordis{ name, kind, promptHash, schemaHash }(Observability β Join keys):kindis the newComponentKindenum (root entry);promptHashhashes what the component's model is told,schemaHashwhat it reads and writes (each DTO by its JSON Schema), eachundefinedwhen it has none β the valuesdescribe --jsongives. It is every record that names a component:event.step.component,executionContext.step.component,ExecutionOutput.finish,ExecutionEndEvent.finish, child records'start/finish,handledErrors'at/by,StepFailedError.component,VisitsLimitExceededError.component(its constructor takes the record) andBudgetExceededError.budget(kind: ComponentKind.Budget;BudgetBreach.budgetnames it bynameonly). A test comparing one withtoEqual({ name })compares withtoMatchObject. -
Breaking β
WorkflowRecordhasconfigHash: the workflow's fingerprint, the one join key β in every execution event,executionContext.workflow, child records and limit errors; there is nographHash. -
Breaking β
StepRecordhastoolLoopIterationandvalidationAttempts: an agent's model call in the visit (from 1, during the call and its move's tool calls;undefinedelsewhere) and its answers and tool arguments that failed validation so far (0outside an agent's loop). The idempotency key's format is unchanged (#335). A pause keeps both and its agent's record:record.pauses[].stepnames the iteration that asked (a pause stored before reads as an agent's record without hashes, iteration unknown). -
Not built β a branch's
{ verdict, status }onStepRecord(S93): its status and, under a quorum, its verdict are known only after the step ends; they stay inresult.launchesandonQuorumDecided, joined bynodeId,copyNumberanditemIndex(open question on #268).
-
Fixed β one value, one token across instances (Several instances β PII
tokens): without
@Application({ pseudonymKey })the tokens ofsensitivefields were made with a key of each instance, so after a pause resumed, a failover or a recovery on another instance one value got a second token in the same conversation. Now each root execution makes a key of its own at its start, keeps it in its checkpoint next topiiTokensand hands it to its child workflows; every instance that continues it gives one value the same token. WithpseudonymKey(or undertestWith, its fixed test key) nothing changes and no key is checkpointed; a checkpoint from before keeps the instance's key. No API change. -
Changed β
[application.pii-instance-key]'s text:no @Application({ pseudonymKey }): sensitive fields make PII tokens with a key of each execution β one value gets another token in each execution (in logs and traces: on each instance), so tokens compare across executions only with pseudonymKey(Environments β The PII key).
Unreleased β generators and naming: full names, tool names, timeouts with units, Executionβ¦ (#268, PR 4)
-
Breaking β
agboss generatetakes full names (CLI): the positional name ends with its kind (billing-agent,escalation-router,refund-tool,policies-rag,valid-score-arbiter,onboarding-workflow),--workflowis a workflow's full name (desk-workflow, found in the application ofpackage.jsonor--application, its children included) β no more--workflow src/x/x.workflow.tsβ and--agentan agent's (billing-agent). Nothing is completed: a short name is exit 2 withDid you mean "billing-agent"?, an unknown workflow or agent too, and nothing is written.generatetakes--application <path>. -
Breaking β
agboss generate workflow <name>-workflowwrites the workflow beside the application file (src/onboarding/, with its test) and lists it in@Application({ workflows })β it no longer makes an application of its own; its scripts arechat:<name>/run:<name>by its start's full name. A name the application already has is a conflict (exit 4). -
Breaking β
agboss create <project>writes the application atsrc/<project>.application.ts(its path inpackage.json) with, beside it,environments/,config/budget.config.ts,model-providers/,guardrails/prompt-injection.guardrail.ts(now the application's,@Application({ guardrails, defaults: { guardrail } })) andservices/api.service.ts; the first workflow<project>-workflowinsrc/<project>/.--agents/--toolsand the questionnaire take full names ("triage-agent:Sorts requests","answerer-agent:search-orders-tool"); the default agent is<project>-assistant-agent, the knowledge base<project>-notes-rag. The scripts run a start by its full name ("chat": "agboss chat --start <project>-text-workflow-start"). -
Changed β generated code: tools
name: "<name>-tool", knowledge basesname: "<name>-rag"(class<Name>Rag, the index in the project's git-ignored.agboss/rag/, not the home folder), MCP tools<server>-<tool>-toolwith the server injected as<server>Server, MCP servers kebab-case, the OpenAPI client<Name>ApiServiceinjected as<name>ApiService, OpenAPI tools<operation>-tool; nomodels.ts. -
Breaking β tool names (Tools, Naming): a
@Tool/@McpToolname is kebab-case ending with-tool([name.kind-suffix]at assembly, with the name to use); every name a model sees fits[a-zA-Z0-9_-], at most 64 characters ([tool.name-too-long], new[tool.name-invalid]). A knowledge base's tool issearch-<rag name>-tool(wassearch_<name>). Every tool of job-scout, flow-patterns and the docs is renamed (order_statusβorder-status-tool,greenhouse_jobsβgreenhouse-jobs-tool, β¦); job-scout's knowledge base iscompany-notes-rag. -
Breaking β timeouts have units:
@Tool({ timeoutMs: 5000 })/@McpTool({ timeoutMs })βtimeout: seconds(5)(aMillisecondsvalue; default 30 s); a tool's timeout error readstimed out after 5 s. Internal:DEFAULT_TOOL_TIMEOUT,JEV_TIMEOUT. -
Breaking β
Executionβ¦, notrunβ¦(Naming):ExecutionOutput.runIdβexecutionId;IncompatibleResume/IncompatibleResumeError{ thread, runId }β{ threadId, executionId };MemoryTurn.runIdβexecutionId;RunIdβExecutionId,PausedRunβPausedExecution(PausedExecutionRepository),runAgentβexecuteAgent,RunDepsβAgentExecutionDeps,RunOptionsβAgentExecutionOptions,runVersionsβexecutionVersions,RunStreamEventβExecutionStreamEvent,RunTracingβExecutionTracing. -
New β the Naming page: kind suffixes, tool names,
Workflowβ¦vsExecutionβ¦, units, full names in the CLI.
-
Fixed β parallel branches overspending a
@Budget(FinOps β At run time): every call of a tree is now counted the moment it is made β a live tally of the pass on this instance; the ledger is still written once per superstep and nothing is counted twice β so a branch of.toParallelAgents,.toParallelActions,.toParallelWorkflowsor a quorum sees its siblings' spend while they run. The call that exhausts an execution, tenant or application budget aborts the in-flight model and tool calls of the other branches throughexecutionContext.signal; they fail with thatBudgetExceededError(their spend so far kept), and the launch fails with it into.onJoinError(BudgetExceededError, toAction(β¦)β¦ | toFinish(β¦)), else the execution fails. Before, every branch checked only its own view (the spend at the launch plus the siblings that had ended), so N branches could each spend the whole remainder. The promise is now "a budget is exceeded by at most the calls in flight when it was exhausted"; an aborted call's partial cost is unknown and not recorded. A call that exhausts a budget also runs none of the tools its move asks for (an answer it gave is kept). Other instances still see the spend through the ledger only. No public API change.
-
Changed β contracts are function-typed properties, checked on the decorator line
(Components β Component rules):
RagConnector,PiiPolicyHandler,PiiRefusal,GuardrailRefusal,BudgetHandler,HealthCheckHandler,ModelProviderHandler,ChannelHandler,ReplyInterpreterHandler,ActionHandler,RouterHandler,FinishHandlerdeclarereadonly method: (β¦) => β¦instead of a method signature (ToolHandlerandJudgeHandleralready did), so a method that takes a narrower parameter than the contract no longer compiles β on@Rag,@PiiPolicy(a detector'sfindMatches, a declarative policy'srefusal(kind)) and@ModelProviderwhether or not the classimplementsit. Classes keep writing ordinary methods; nothing to migrate unless one relied on a narrower parameter. -
Changed β prompts are normalised at load (Components β File conventions):
no byte order mark,
\nline breaks, no blank lines at the edges, Unicode NFC; inside kept as written;promptUrlsparts joined with one blank line. Covers guardrails, compactions, reply interpreters, an arbiter's prompt and criteria and profile overrides. A prompt file's trailing newline is no longer sent. -
Changed β the config hash covers what an element's model reads outside its decorator
(Terns and evaluation): per agent and
model router the PII policies in effect,
uncompactedTurnsand the compaction, an agent's judges; and the framework's own texts. Every workflow'sconfigHashchanges once with this release, so a pause taken before it resumes only whereonIncompatibleResumeaccepts it. -
New β
agboss describeshows a computed guardrail refusal asClassName.refusal()(tested; printed since #120 PR 2).
-
Fixed β a lease store down past the lease (Several instances β The execution lease):
the holder trusts its lease until the
Clockread before its last confirmed renewal plus 30 seconds; a store error before then lets the checkpoint write go on, at that moment the execution's signal aborts withExecutionLeaseLostErrorβ the agent's tool loop starts no further model or tool call, no.onError, the record staysrunningand another instance recovers the tree. Before, the background renewal retried forever and the holder kept calling tools after its lease had expired in the store. The error's message names the case (expired without takeover); no new field. -
Fixed β a provider repeating a tool-call id within one move
(Tools β Agents with tools): each repeated id gets its position (
${id}:${index}), so two calls no longer share a result slot and an idempotency key. The key format is unchanged; ids the provider gives once are kept as they are.
-
New β
agboss describedescribes the application (CLI): first its tree β the listed workflows with their starts and the child workflows they launch (by which step, one or per item withmaxItems/maxConcurrency;ThisWorkflowmarked withmaxDepth) β, each contract's implementation and why, the model providers and why, each channel (a role's question β answer, who answers, its reply interpreter, its implementation and why), the budgets by scope and each workflow's execution budget, the observers, the warnings and the environment once; then each workflow, now with its PII policies and their refusals (refusal: "β¦"orCardPiiPolicy.refusal()) andmaxDepthamong its limits, and each agent's compaction with where it came from (compaction conversation-compaction (job-scout-workflow):(own), the workflow or the application).describe --json'sapplicationcarries the same as data (tree,implementations,modelProviders,channels,budgets,executionBudgets, β¦); a workflow's data no longer repeats the application, and gainspiiPolicies. -
New β the check report (CLI):
agboss checkprints, per start, the worst case of one call β workflow executions, at the same time, agent and router steps of the whole tree, nesting depth β each with how it is made up, from the decorators only;--json:result.report.describeprints the same report under each workflow's limits (it was one line). -
Changed β
agboss check --env <name>lists the variables asprod environment needs 3 process variables:with alignedNAME value | secret[, optional | , has a default]lines β the field name is no longer printed, never a value (Environments). -
Breaking β
describeWorkflowis removed from the public API (it was a root export): describing is the CLI's β useagboss describe [--json]. It stays internal (the CLI and the framework's tests); the examples' tests that printed it now rely on the CLI's job-scout and flow-patterns describe tests. The root import loads nodescribe/module any more. -
Internal (
@agboss/core/internal):describeApplication(assembled, { source, profile, environment }),describedApplicationOf(assembled)(takes the assembled application;executionBudgetsper workflow,modelProviders),describedTreeOf,describedChannelsOf,describedPiiPoliciesOf,worstCaseReportLines/worstCaseRowsOf(WorstCaseMeasure),worstCaseLimitsOf; aWorstCasecarries itsworkflow,isStepsCapped,treeStepsandchildren(each edge into a child:count,atOnce,maxItems,maxConcurrency,isRecursive, the child's worst case). -
job-scout: a
prodenvironment (agboss check --env prod).
-
New β the instance's probes (Health checks):
app.checkLiveness()(no I/O: the application βUnhealthy,stopping, fromapp.close()on β and the recovery loop βUnhealthywhen its last sweep ended more than three sweep intervals ago) andapp.checkReadiness()(liveness's components, every store'sping(), every model provider's circuit breaker β open βDegraded, never a model call β and the project's checks), each βHealthReport { status, components }withHealthStatusandHealthComponentKind. Checks run at once, each under its timeout on theClock; one that throws or times out is that component'sUnhealthywith itsreasonβ neither call rejects.testWith'sapphas both, also during and afterclose(). No HTTP server: the project's exposes them. -
New β
@HealthCheck({ name, timeout, deps? })+implements HealthCheckHandler(check(): Promise<HealthCheckResult>, built only withHealthCheckResult.healthy()/.degraded({ reason })/.unhealthy({ reason })), listed in@Application({ healthChecks }); rules[health.not-a-health-check],[health.duplicate-name],[name.kind-suffix](-health-check). -
Breaking β every #265 store contract has
abstract ping(): Promise<void>(CostLedgerRepository,ExecutionRecordRepository,PausedExecutionRepository,ExecutionLeaseRepository,ExecutionTreeRepository,RateLimitRepository): the in-memory defaults resolve, the SQLite ones runSELECT 1. Migrating: a project's own implementation addsping()β one fast round trip that rejects when the store is unreachable (Several instances β Your own implementation). A subclass of a framework implementation inherits it.
-
New β
IdGenerator(root export, App β Infrastructure): an infrastructure contract like theClockβnewThreadId(),newExecutionId(); every thread id the framework mints and every execution id (roots, children, recursion levels) comes from it. The built-inRandomIdGenerator(root export) keeps the formats (random UUIDs); a class in@Application({ providers })thatextends IdGeneratorreplaces it. ApauseIdstays a random 128-bit token outside it. -
New β
SequentialIdGenerator(@agboss/core/testing):testWithruns on it unless the application lists its own generator βthread-1, β¦ andexecution-1, β¦ per test (Testing β Ids). -
Breaking β the records repositories'
deps:SqliteExecutionRecordRepositorytakes[SqliteConnection, Clock, ApplicationContext, IdGenerator](was[SqliteConnection, Clock, NEW_THREAD_ID], an internal token), the built-in in-memory one[Clock, IdGenerator];NEW_THREAD_IDis removed. A project now subclassesSqliteExecutionRecordRepositorythrough public tokens only (Several instances β Extending a SQLite implementation). -
Breaking β
createApp/createAppDepsoptions: the undocumentednewThreadIdandnewRunIdoptions are removed β list anIdGeneratorinstead. -
Changed β execution ids under
testWith:execution-1,execution-2, β¦ (wererun-1, β¦).
-
New β
agboss dev [--strict] [--env <name>](CLI): watch mode over the application'ssrc/β every change runsagboss checkin a short-lived child process (debounced, 200 ms; never two at once), so.agboss/types.d.tsis regenerated and the assembly problems are printed as they appear. -
New β
agboss studio [--port N] [--env <name>]: every workflow of the application (the children too) in LangGraph Studio; the CLI writes.agboss/studio.tsand.agboss/langgraph.json(git-ignored, rewritten on every run) and startslanggraphjs devon them. A child is a subgraph inside its parent,.toParallelWorkflows(β¦)included; an assembly problem or a missing variable stops it before the server starts (exit 3). -
Breaking β
studioGraphOfis removed fromagboss(and from the deprecated@agboss/core/core), and the examples'src/studio.tsand the rootlanggraph.jsonare gone. Migrating: delete the project'sstudio.tsandlanggraph.json, runagboss studio(job-scout:npm run studio). The graphs come fromstudioOf(Application, { env })in@agboss/core/internal, the CLI's. -
Changed β
@langchain/langgraph-cliis a dependency ofagboss-cli(it was the monorepo root's dev dependency), soagboss studioworks in any project.
-
New β
examples/flow-patterns/src/approvals/(Example):offsite-booking-workflowβ two buyers in one.toParallelAgents(β¦)launch, eachplace_purchasecall waiting for the thread's owner through thePurchaseApprovalChannelrole, answered in the terminal (TerminalPurchaseApprovalChannel); scripted tests: both pauses pending, both answers taken, a second answerNotPausedError, each question asked once. -
Changed β flow-patterns runs with a real model: its
devenvironment readsOPENROUTER_API_KEY(openRouterApiKey, aSecret) and, optionally,OPENROUTER_BASE_URL(openRouterBaseUrl) for the built-in providers;testis unchanged. Every store of its application is SQLite now (records, pauses, checkpoints, leases, tree keys, besides the ledger).npm run checkalso runsagboss check --models --env test(no key);npm run answeranswers waiting pauses from another process. ItsREADME.mdsays how to run the manual checks #263 M1, #264 M1 and #267 M1.
-
New β
@Compaction({ name, description, prompt?, promptUrls?, model, strategy, turnsBetweenCompactions, summaryQueueLength?, summaryMaxTokens })(Memory Management):compactions/*.compaction.ts, a-compactionname;CompactionStrategy.SummaryQueue(a queue of self-contained summaries, the latestsummaryQueueLengthkept),SingleSummary(one summary, rewritten),None(no model, no prompt, no parameters β the window only). A strategy that writes summaries without amodel, orNonewith one, does not compile. Root exports:Compaction,CompactionStrategy,TurnLimit,CompactionError, the option types,UncompactedTurns,SummaryMaxTokens,TokenLimit. -
New β
compactionanduncompactedTurns: one setting each on@Application,@Workflow,@Agentand@Routerβ the lowest level that sets one wins (AC4);uncompactedTurnsdefaults to 5,TurnLimit.ALLis every turn; on a code router neither compiles. A compaction summarises everything older than the smallestuncompactedTurnsamong the agents and routers that use it; a reader sees its window, every turn its compaction has not summarised yet, and only the summaries its turns do not cover. -
New β
summaryMaxTokensis the summarising call'smax_tokens(TokenLimit.MODEL_MAXallowed, thinking off): cut at it β one "shorter" retry, thenCompactionError(codecompaction;compaction,summaryMaxTokens; a failed model call is one too, as itscause) β never a cut summary. -
Changed β compaction runs lazily, before a new execution's first read of its thread (S132),
never after a turn: a finished turn's answer is always kept, a thread that ends pays for none, and
a
CompactionErrorfails the new execution before any of its work (no model call, no step, no turn kept). Its spend is the new execution's;result.compactedsays what ran before it. -
Breaking β
TokenLimit.MODEL_MAXreplaces theMODEL_MAXconstant (S132), an enum next toTurnLimit.ALL(both from the root). Migrating:maxTokens: MODEL_MAXβmaxTokens: TokenLimit.MODEL_MAX,import { MODEL_MAX }βimport { TokenLimit }; the value is still"max". - New β compaction behind the PII boundary (S84 i): its model reads the turns through the application's and the workflow's PII policies; the summary is revealed before it is stored, and each reader's own policies mask it at its boundary.
-
New β assembly rules
[compaction.not-a-compaction],[compaction.invalid-setting],[compaction.decision-model],[compaction.uncompacted-turns-invalid]; compaction models are checked againstservesascompactions.<name>. -
Breaking β removed:
defaults.history(limit,summaries),@Agent({ historyLimit, historySummaries }),@Workflow({ compaction: { every, keep, model }, compactionPrompt }), themockCompactionfixture and the typesHistoryDefaults,CompactionOptions(the old one),ModelOptions. Migrating:defaults: { history: { limit: 8 } }βuncompactedTurns: 8on the same decorator (outsidedefaults);historyLimit: 3βuncompactedTurns: 3;compaction: { every: 5, keep: 10, model: { model } }β a@Compactionclass withstrategy: CompactionStrategy.SummaryQueue, turnsBetweenCompactions: 5, summaryQueueLength: 10, model, summaryMaxTokensnamed bycompaction: ConversationCompaction;mockCompaction()βmockLlm(ConversationCompaction). A profile'scompaction:iscompactions: { <name>: β¦ },defaults.history.limitisdefaults.uncompactedTurns. -
Breaking β summaries are kept per compaction:
ExecutionRecordRepository.uncovered,latestSummariesandsummaryCounttake the compaction's name after(tenantId, threadId), aSummaryhascompaction(SQLite migration 13; a summary written before it belongs to no compaction and is not shown again).result.compactedis a list ({ compaction, fromTurn, toTurn, summaries, summaryQueueLength }, one per compaction that ran); spend iscompaction:<name>(categorycompaction). -
Changed β a compaction's model failure used to be swallowed (the next turn retried); it is now
a
CompactionErrorlike a summary that does not fit (#77 AC5 replaced, ADR-0007 amended).
-
Breaking β the CLI works on one application, set once (CLI):
"agboss": { "application": "src/job-scout.application.ts" }in the project'spackage.json(relative to its folder), or--application <path>, which wins; neither β exit 3 saying where to set it.--workflow <path>is gone fromchat,run,describe,check,eval,replay,golden,compareandrag(an unknown option, exit 2); the generators keep theirs until PR 4. Migrating: add theagbossfield topackage.jsonand drop--workflow β¦from the scripts ("chat": "agboss chat");agboss createwrites both. -
New β
--start <full name>onchat,run,eval,replay,golden add,compare: the workflow is chosen by its start (--start screening-workflow-start); optional with one start in the application; with several and none given, exit 2 listing each start's name, description and workflow; an unknown name, exit 2 withDid you mean "β¦"?and the list β the lookup is exact, nothing runs under a guessed name. -
Breaking β
agboss run --inputis JSON:--input '<json>'/--input-file <file>give the start's input, checked against itsInputOf(one DTO, alistOf(β¦), a map of fields) before the app starts β every problem at once by field (company.name: β¦; request.criteria: β¦), exit 2; the task's words are the text of a start takingWorkflowStartTextDto(--input <text>was that before).agboss chatneeds a start takingWorkflowStartTextDto. -
Breaking β
agboss rag index [--rag <name>](wasrag:index --kb): every knowledge base of the application, each once;--ragtakes the full name, an unknown one is exit 2 with a hint. -
Changed β
describeandcheckcover every workflow of the application:describe --jsonis{ application: { name, version }, workflows: [ β¦ ] };checkassembles the application ascreateAppdoes (its rules across workflows too) and its problems name the application's path (ok: src/job-scout.application.ts β β¦);result.applicationreplacesresult.workflow. -
Internal (
@agboss/core/internal):loadApplicationClass(path)loads the one exported@Applicationof a module (ApplicationLoadError, CLI codeapplication.load) β the--workflowbridgesenvironmentOwnerOfFile,textStartOfWorkflowandworkflowsOfApplicationare gone; newstartsOfApplication(App)(ApplicationStart: name, description, workflow, class, input β from the decorators),startInputIssuesOf(Start, payload),applicationOf,environmentOwnerOf;applicationWarningsOftakes the assembled application.
-
Breaking β
JudgeInput<TReply>carriesvariant(S125, Judges β The variant of aoneOfreply): the class the answer validated as, from the answer tool the agent used β never guessed from the answer's shape, never compared by name. For aoneOf(A, B)judgeJudgeInput<A | B>is a discriminated union ({ variant: typeof A; reply: A; task; attempt } | { variant: typeof B; β¦ }), narrowed byisVariant(input, A); a single DTO,listOf(XDto)orPlainTextDtojudge hasvariantas that one class. Migrating: a judge that told variants apart by their fields ("salary" in reply) usesisVariant(input, OfferDto). -
Breaking β a
listOf(oneOf(A, B))judge readsreply: readonly VariantOf<A | B>[]: each item{ variant, value }, as the answer's items are kept, narrowed byisVariant(item, A); it has novariantof its own. Migrating:implements JudgeHandler<readonly (A | B)[]>becomesJudgeHandler<readonly VariantOf<A | B>[]>(the old form does not compile), each item read asitem.value. -
New β
VariantOf<T>(@agboss/core/dto, the root),JudgedValueOf<TShape>(what a judge of areplyshape reads) andJudgeInputOf<TJudge>;isVariant(x, XDto)also narrows anything that carriesvariant(a judge's input, aVariantOfitem). -
Breaking β
testJudge(J, β¦)({ variant, reply, task, attempt })takesvarianttoo, typed the same way: avariantwith another class's value does not compile.
-
New β a decision model may read an approval's reply (S124 a, Pauses):
@ReplyInterpreter({ decidedBy: DecidedBy.Model, model: "typesafe/jev-1.13" })on an approval role (approves) makes oneDecision.noul("approved")about the question and the reply β approved when P(yes) > 0.5, the answer{ approved }, never areasonoroverrideArguments; counted underreply-interpreter:<name>, a model use of purpose decision (decisions-model-provider). A question role on a decision model stays[reply-interpreter.decision-model], its message saying why (the answer DTO needs more than a yes / no). Tests:mockLlm(X).thenReturn(decideWith({ approved: 0.9 })). -
Breaking β a code interpreter returns its outcome (S124 b):
ReplyInterpreterHandler<Self>.interpretreturnsReplyInterpretation.understood(decision)orReplyInterpretation.notUnderstood({ reason })(sync or a promise;ReplyInterpretationKind,UnderstoodReply,NotUnderstoodReply); a plain object does not compile.notUnderstoodβUninterpretableReplyErrorwith thatreason, the pause pending; athrowis a crash only (unreadable too, its error thecause). Migrating: wrap each returned decision inReplyInterpretation.understood(β¦), and replace athrowthat declines withreturn ReplyInterpretation.notUnderstood({ reason }); a test'smockOf(I).interpret.mockResolvedValue(decision)becomesmockReturnValue(ReplyInterpretation.understood(decision)). -
New β
UninterpretableReplyError.reason(every unreadable reply: the interpreter's reason, or its error's message); its constructor takes{ reason, cause? }(UninterpretableReplyErrorOptions). -
New β
ChildExecutionRecord.pauses(S124 c, Pauses β Who answered): each child's record inresult.childrenandresult.record.childrenlists the resolved pauses its own steps asked; the root'srecord.pauseskeeps the whole tree's.PauseAnswerRecord.executionIdnames the execution a pause waited in (stored with each answer: apause_answersrow written before this change has none and fails to parse). The state's records are the internalEndedChildRecord(exported as a type; it isChildExecutionRecordwithoutpauses). -
Rule β new runtime subsystems load lazily (S124 d, #120 S123 c,
QUALITY.mdβ Design): the decision-model and code reading are behind the lazily loaded reply reader; the root import stays under its 1,400-module budget (1,397).
-
New β
@InputGuardrail/@OutputGuardrail({ name, description, prompt | promptUrls, model?, threshold, refusal?, deps? })(Guards β Guardrails): a yes / no question on a decision model (Decision.noul) or a chat model (the answer toolanswer-guardrail);modelelsedefaults.guardrail.model(never the router's,[guardrail.no-model]). An input guardrail reads what an agent's or a model router's model reads β its input message, masked by that element's PII policies (S84 i) β before the call; an output guardrail reads an agent's answer. At P(yes) β₯thresholdthe execution fails withGuardrailBlockedError(guardrail.blocked;refusal,key: guardrails.<name>.threshold,probability,threshold); a model that does not answer fails closed (GuardFailedError,step.guard). -
New β the refusal: a text in the decorator, or
refusal(): string | Promise<string>of a class thatimplements GuardrailRefusal(its services and the execution throughdeps); neither does not compile (the decorator's overloads); at assembly[guardrail.missing-refusal],[guardrail.refusal-twice]. -
New β levels:
guardrailson@Application,@Workflow,@Agent,@Routeradd up,excludeGuardrailson an agent or a router removes an inherited one; a model router takes the input guardrails, a code router none. New rules:[guardrail.not-a-guardrail],[guardrail.exclude-not-inherited],[guardrail.output-on-router],[guardrail.no-model-call],[guardrail.no-prompt],[guardrail.threshold-invalid]; names end with-guardrail. -
New β
[application.pii-instance-key](S94, Environments β The PII key): no@Application({ pseudonymKey })and no policy that needs it β sensitive fields and keyless policies use a key of the instance; the start goes on with this warning inapp.warningsand the start log (an error underagboss check --strict, silent in tests). -
Breaking β
@Workflow({ guards })is removed with the framework's guard texts (prompt_injection,pii),GuardOptions,GuardsOptions. Migrating: one@InputGuardrail/@OutputGuardrailclass per guard (guardrails/*.guardrail.ts), listed inguardrails. The guard that ran on the execution's input now runs on what each model reads (the router's first); the one on the output on each agent's answer. -
Breaking β the
@Guardrailhook classes are removed (beforeToolCall,afterToolCall,onChannelDecision,beforeAgentAnswer,GuardrailContext,overrideGuardrails,disableGuardrails,guardrailson@Tool); checks of tool calls come back as tool-point judges (#123).testGuardrail(G, options)(text, { element })now checks a text and gives{ isBlocked, probability, refusal? }(GuardrailCheck);GuardrailDecision/GuardrailHookResultare removed. -
Changed β observer events:
GuardrailStartEvent.inputis{ side: GuardrailSide, element, text }(the masked text),GuardrailEndEvent.updateisGuardrailOutcome { isBlocked, probability }. Spend:guardrail:<name>(categoryguards; wasrouter:guard:<name>); tests script it withmockLlm(G).thenReturn(decideWith({ blocks: 0.9 }))β an unscripted guardrail passes. A VCR cassette records it asdecide guardrail:<name>. -
agboss describeprints each guardrail (side, threshold, model and where it came from, refusal β the text orClass.refusal()β, the elements it guards);agboss createwritesguardrails/prompt-injection.guardrail.ts.
-
Free-text replies (Pauses β free text):
app.resume({ pauseId, reply }, options)βResumeRequestis nowDecisionResumeRequest | ReplyResumeRequest, exactly one ofdecision/ a non-emptyreply(elseDtoValidationError). The role'sreplyInterpreterβ a@ReplyInterpreterclass (-reply-interpreter):DecidedBy.Model(a chat model, one answer tool, staticprompt/promptUrls) orDecidedBy.Code(implements ReplyInterpreterHandler<Self>,interpret({ ask, reply }),deps, created by the container) β reads the reply with the pause's question; its answer is validated like a decision. A model interpreter's call resolves its provider byserves, is checked against the tree's budgets first, counted in their ledger and in the execution'sspend(once) and traced underreply-interpreter:<name>. Either kind reads in the pause's execution frame (itsExecutionContext: tenant, owner, the waiting step); a model one through the application's and the workflow's PII policies, its answer revealed before validation (an unknown token is a decline). -
UninterpretableReplyError(pause.uninterpretable-reply): a reply nobody can read β the interpreter declined, or its answer is invalid (the reason ascause); the pause staysPending.ReplyInterpreterMissingError(pause.no-reply-interpreter): areplyfor a role without an interpreter, the pause untouched. - Assembly:
[reply-interpreter.not-an-interpreter],[reply-interpreter.decision-model],[reply-interpreter.no-interpret-method],[name.kind-suffix]for-reply-interpreter; model interpreters are model uses ([model.unserved],agboss check --models:replyInterpreters.<name>). - Tests:
mockLlm(Interpreter).when({ reply })scripts a model interpreter (Testing β Reply interpreters);mockOf(Interpreter)a code one. -
Who answered each pause (Pauses β who answered, S107):
ExecutionRecord.pauses: PauseAnswerRecord[]β every resolved pause of the tree in the order asked (pauseIdβ none for aPauseAnswerer.Channelpause β,step,role,answeredBy,responder,resolution,askedAt,resolvedAt,decisionPII-masked, none when withdrawn or cancelled);enum PauseResolution { Decided, Replied, Withdrawn, Cancelled }. Written when a pause is resolved into the execution records β the permanent history, untouched by the retention: new abstract methods onExecutionRecordRepository(a project's own implementation adds them)savePauseAnswer(tenantId, rootExecutionId, answer)andfindPauseAnswersByTenantIdAndRootExecutionId(tenantId, rootExecutionId); the built-in and SQLite ones keep them in apause_answerstable (migration 12 of the record store). -
Retention:
@Application({ pauseRecordRetention }), defaulthours(24)β resolved records (Taken,Cancelled) are removed by the recovery sweep throughPausedExecutionRepository.deleteResolvedBefore(cutoff); within the window a late answer isNotPausedError, after itUnknownPauseError;Pendingis never removed. -
Breaking β
PausedExecutionRepository(a project's own implementation changes):PauseRecord.resolution(the decision and who answered) is nowanswer: PauseAnswer(decision,answerer,usage?) β the interfacePauseResolutionbecamePauseAnswer, andPauseResolutionis the enum above; new record fieldscopyNumber,askedAt,resolution,resolvedAt.resolveByTenantIdAndPauseId(tenantId, pauseId, answer, at),cancelByTenantIdAndPauseId(tenantId, pauseId, resolution: PauseCancellation, at); newdecideByTenantIdAndPauseId(a channel'sChannelOutcome.decided(β¦)closes its record asTaken/Decidedinstead of deleting it) anddeleteResolvedBefore(cutoff);deleteByTenantIdAndPauseIdis removed.SqlitePausedExecutionRepositorymigrates a PR 2 file.
-
Breaking β
@Judge({ name, model, reply, deps })+JudgeHandler<TReply>(Judges and Quality Gates): a judge declares what it judges βreplyis any shape of an agent'soutputβ a DTO class,oneOf(A, B)(typedA | B),listOf(XDto)(readonly XDto[],listOf(oneOf(β¦))the items' values) orPlainTextDtofor a text agent β andjudge(input: JudgeInput<TReply>)gets the agent's validated answer object ({ text }for a text agent), never a serialized string.JudgeHandlerandJudgeInputtake the reply's type. -
Breaking β
JudgeVerdict.feedbackisreason, and verdicts are built only withJudgeVerdict.passed({ metrics? })/JudgeVerdict.failed({ reason, metrics? })β a plain{ passed, feedback }object no longer compiles. Thereasongoes back to the agent for its retry.QualityGateError.feedbackisrejections: [{ judge, reason }](JudgeFeedbackβJudgeRejection). -
New rule
[judge.wrong-reply]: an agent whoseoutputdoes not fit a listed judge'sreplyfails assembly (alsoagboss check) β a text agent with a DTO judge, a DTO agent with another class, aoneOfvariant the judge does not cover, a list agent with a judge of one value or the other way round. -
testJudge(Judge, β¦)({ reply, task, attempt })is typed by the judge:replyis a plain object of its DTO; another shape does not compile. - Migrating: add
replyto every@Judge, writeimplements JudgeHandler<XDto>andJudgeInput<XDto>, readreply.textin a text agent's judge, and returnJudgeVerdict.passed()/JudgeVerdict.failed({ reason }).
-
ThisWorkflowis typed per workflow (Workflow as a step β Recursion):agboss checkwrites aThisWorkflowOutputsentry per recursive workflow (input,output,join), keyed by itsname;[types.this-workflow-outputs]is removed β recursive workflows with different finishes live in one project. No project code changes. -
The
.returns(β¦)after aThisWorkflowlaunch (.toWorkflow,route,workflow,toWorkflowas a side path, the join of.toParallelWorkflows) is checked by@Workflow({ name, flow }): a wrong class fails onflow, no longer at the.returns(β¦). -
Builder types (API reports): every path head (
SourceHead,RouteHead,CaseHead,SideHead,GroupHead) takes aclaimstype parameter;.switch,.chooseOne,.onError,.onJoinErrorand.joinQuorumGroupsare generic over their sub-paths;WorkflowOptions<TName, TFlow>andWorkflow(β¦)are generic; newThisWorkflowOutputs,ThisWorkflowCheck,ThisWorkflowStepClaim,ThisWorkflowJoinClaim,ClaimsOf,WithClaims,ChildLaunched. The generatedProducerOutputs,InputsandJoinOutputsno longer hold athis-workflowentry.
-
@Workflow({ maxConcurrentExecutions })works (FinOps β Concurrency of a tree): the root's value (default 10) caps how many executions of its tree work at once; a step without a slot waits in the tree's queue β no error. A slot is held only while an agent, a router or an action runs (an execution's parallel branches share one); at a join, while its child runs or paused an execution holds none. -
The queue's order: a parent closing its branch, then the deepest;
seconds(10)of waiting counts as one level deeper (theClock). A child's own smaller cap limits its subtree. -
A quorum's
deadlineexcludes the time its branches wait in the tree's queue; aQuorumNotReachedErrorpast it names the effective deadline (the deadline passed (2026-01-05T09:01:10.000Z: the ticket's deadline plus 40 s in the tree's queue)). -
New rules:
[limits.max-concurrent-executions-invalid](not a whole number from 1, an error),[limits.child-concurrency-above-parent](a child's cap above its parent's, a warning). -
agboss describeprintsmaxConcurrentExecutions N (set | default)for a workflow that launches children;describe --jsonlimits carrymaxConcurrentExecutionsandisMaxConcurrentExecutionsSet.
-
Parallel branches in tests (Testing β Parallel branches):
testWith(App, { concurrency })βTestConcurrency.Sequential, the new default: the branches of a launch run one at a time, in listed / item order;TestConcurrency.Realruns them as production does (withinmaxConcurrency);TestConcurrency.inOrder([step(A), step(B, partial)])makes them complete in that order through completion gates (step(Node, partial?)takes the matcher ofwhen(β¦)). A step waiting at its gate is cancelled at once by aFailFast, a decided quorum orapp.cancel; when no branch can move and the next step is not waiting, the execution fails at once withtest.unreachable-step; a listed step never reached fails the test at its end. Breaking for tests: a test that relied on branches running at once (a sliding window, a surplus answer, an agent waiting for its siblings) setsconcurrency: TestConcurrency.Real. -
Unit runners (Testing β Unit runners):
testTool,testAction,testRouter,testArbiter,testRag,testJudge,testGuardrailandtestPiiβ one component on its own container (overrides: [override(Class, Replacement)],environment), every call in its step's frame (executionContext.signalwith atimeouton the runner'sVirtualClock,run.clock;SpendRecorderand its model calls intospend), its model by script (model: decideWith(β¦),replyWith(β¦),routeTo(β¦)); a call gives{ output, spend }and rejects as the component threw. The calls run for the tenant and execution values given β{ tenantContext, tenant, executionContext, execution }, astestExecutionContexttakes them, validated the same way.testPii([Policies])checks the policies' own rules first (-pii-policy) and givesmask(text)/reveal(text). Theagboss create/agboss generate tool/agboss generate openapitool tests usetestTool. -
testWith(App, { instances: 2 })is the documented way to test across instances: one store set, one clock, a container each β a lease, a pause resumed on the other instance, a rate-limit slot (Several instances). -
testWith(App, { overrides: [override(Class, Replacement)] })(#267 S104): a class replaced by a class in the application's container, over any listed one β a channel role answered byoverride(RefundApprovalRole, AutoApproveChannel)(the replacement'ssendmust fit the role).AutoApproveChannel/AutoRejectChannel'ssendnow givesDecidedOutcome<AutoDecision>(approved,reason), so they fit any role that approves a tool. -
GuardrailContextis exported fromagboss(the contexttestGuardrail's hooks take). - New page The test split: unit tests with scripts β evaluation and golden sets β
make smoke; no test calls a real model.
-
ThisWorkflow(Workflow as a step β Recursion): a workflow launches itself β.toWorkflow(ThisWorkflow),route(ThisWorkflow),.toParallelWorkflows(ThisWorkflow, β¦); the nodethis-workflow, run from the registry (Studio:recursive: maxDepth N). -
@Workflow({ maxDepth })required on a recursive workflow ([graph.recursion-without-depth]); the root is depth 1,ChildExecutionRecord.depthand each child's trace metadata carry it. -
The soft stop on the last level: an agent's recursive variants leave its schema, a model
router's recursive routes are hidden, a code router returning one is
RouterDecisionError; every such branch keeps an exit ([graph.recursion-without-exit]). -
DepthLimitExceededError(limit.depth;workflow,maxDepth,depth): a launch by code pastmaxDepthdoes not start; the level above gets it as thecauseofChildExecutionFailedError. -
[graph.mutual-recursion]: mutual recursion, a flow naming its own class, a class undefined while the flow is built.[types.this-workflow-outputs]: recursive workflows of one project with different finish classes (removed in PR 3). -
agboss describe: therecursive:line, per-item child results counted (this-workflow Γ0β5); the worst case counts the levels down tomaxDepth.
-
Children run (Workflow as a step):
flow.child-not-runnableis removed. A child is a node of its parent whose subgraph is the child's compiled graph (Studio draws it inside), on the parent's checkpoint thread under its task'scheckpoint_ns; its ownexecutionId, kept in its state across a pause, a restart and a crash (no secondonExecutionStart); its own path, steps andmaxSteps; no thread history; the parent's thread,ownerId, tenant and execution fields inherited; the root's budgets spent (a child workflow's own execution budget applies only as a root). A child need not be listed in@Application({ workflows }): collected transitively, it goes through every application rule a listed one does (names, models, DI and infrastructure, channel roles), budget coverage aside. - PII tokens (#120): a child starts with its parent's tokens and gives its own back β with its value, or with its error (into the parent's state, never on the error) to the side path that takes it, a per-item child's too.
-
ChildExecutionFailedError(step.child, aStepFailedError:cause,childExecutionId,itemIndex,workflow): an uncaught error of the child, its ownmaxSteps/maxVisitsincluded; budgets and cancellation pass through unwrapped (S25).PaidStepErrorstays the base ofStepFailedError(step.failed, #273 S71). -
toWorkflow(W)side-path starter (S85):.onError(E, toWorkflow(W)β¦), also in.onJoinError. -
Records:
result.children/result.record.children(ChildExecutionRecord,ChildExecutionStatus),result.handledErrors(HandledErrorRecord); observer events carryparentExecutionId/rootExecutionId;executionContext.parent,root,depth,itemIndex,executionOf(W)read the tree;ExecutionTreeRepositorykeys are the root's. -
Pauses inside a child (#267): the root's β in
result.pendingwithchildExecutionIdand, per item,itemIndex; several children may wait at once; resumed bypauseIdlike any other. -
Testing:
toHaveChildExecutions(W, [...]),toHaveHandledError(E, { at, by }). -
The worst case of a start counts its child executions (
maxItemsΓ the child's, at mostmaxConcurrencyat once) and their depth. -
Example:
examples/flow-patterns/src/vendor/β a child workflow per vendor.
-
Several pauses at once (Pauses): a paused branch of a parallel launch does not stop
its siblings; the execution rests when nothing else can move. Breaking:
ExecutionOutput.pauseβpending: readonly PendingPause[](pauseId?,answeredBy,agent,channel,itemIndex,childExecutionId,askβ theToolCallApprovalAskDto), empty unless paused;ExecutionPauseEvent.pauseβpending, fired only when the execution rests (again after a partial resume). -
Breaking β resume by pause id:
app.resume(threadId, decision, { tenantId, ownerId })βapp.resume({ pauseId, decision }, { tenantId, ownerId | answeredBy })(ResumeRequest,ResumeOptions = ThreadOwnerResumeOptions | ChannelResumeOptions). The pause is found by(tenantId, pauseId)anywhere in the tree; unknown or another tenant's βUnknownPauseError(pause.unknown); with others left the execution pauses again with the rest. APauseAnswerer.ThreadOwnerpause lists its id and takesownerId; aPauseAnswerer.Channelpause takes a non-emptyansweredBy(recorded on the pause and inonExecutionResume, new fieldansweredBy) and its id is only in the channel'sask.pauseId; the other credential βPauseAnswererError(pause.wrong-answerer). A concrete@Channelnamed as is answers as the thread's owner. The CLI, the MCPresume_workflowtool ({ pauseId, decision }) and the A2A adapter (resume({ pauseId, decision }),POST /resume { pauseId, decision }, the response'spauseIds) follow. -
Breaking β
ToolCallApprovalDecisionDtois{ approved, reason?, overrideArguments? }:byis gone (who answered comes from the resume),feedbackβreason;overrideArgumentsonly withapproved: trueand a valid input of the tool β onresume(the pause keeps waiting) and onChannelOutcome.decided(β¦)(the step fails). -
Breaking for your own
PausedExecutionRepository(#267 S35):PausedRunkeeps the run and its versions only; each pause is aPauseRecordof its own, keyed by its step's idempotency key βfindOrCreateByStepKey,findByTenantIdAndPauseId,findAllByTenantIdAndRootExecutionId,markSentByTenantIdAndPauseId,resolveByTenantIdAndPauseId(atomicPending β Resolved),cancelByTenantIdAndPauseId,markTakenByTenantIdAndPauseId,deleteByTenantIdAndPauseId;resolveByTenantIdAndThreadIdis removed.PauseStatusgainsTakenandCancelled.SqlitePausedExecutionRepositoryadds apausestable and drops the oldstatus/decisioncolumns ofpaused_executions(a pause written before cannot be answered by id). -
Concurrent answers (Q58): one
Command({ resume })per pass maps every answered pause; the holder drains decisions recorded meanwhile; a double click βNotPausedError; a reply that lands whilesendis returning is taken at once.FailFastwithdraws paused siblings' pauses (Cancelled; a late answer βNotPausedError); so doesapp.canceland the end of a tree. The join after a withdrawal gets the branch's own error object (.onJoinError(E, β¦)by class and cause chain); a channel'ssendthat throws fails the agent's step (AgentFailedError, the channel's error ascause). - Spend while several loops are paused: the execution's spend and the ledger hold every paused loop's spend, summed, each once; a resume records nothing twice.
-
resumeAgent(paused, resume, deps)(internal wiring) takes the tree's answers (TreeResume) instead of one decision.
-
when(β¦)onmockLlm(X)(Testing): turns bound to the node's input values,PartialValues<InputOf<X>>β values only,undefinedfor an absent field, lists by length and position,variant(XDto, { β¦ })for aoneOfelement (varianttakes a partial value there). A call no script matches fails naming its input (test.no-matching-script). -
Scripts are expectations: a
thenReturnturn no call took, awhen(β¦)nothing matched or a scripted tool failure never caused fails a test whose body passed (test.unused-script;thenReturnAlwaysnever does). Breaking for tests that scripted turns their run never reaches: drop them, or usethenReturnAlways. -
Typed scripts:
replyWith(XDto, fields)must be a class of the agent's output androuteTo(...targets)followRoute<R>β one target after.chooseOne(β¦), theOnDemandtargets (or none, with anAlwaysroute) before.toParallelAgents(β¦), whichrouteTo(β¦)now scripts too. The generatedRouterAnswershas an entry for every router, model routers included. -
waitFor(duration): the next turn answers only after that long on the test's clock. -
A caller's own thread id (App):
app.execute(Start, input, { threadId })with an id the tenant does not have yet creates that thread β keyed bytenantId+threadId(the same id in another tenant is another thread), bound toownerId, created only once the budgets pass. Before, it wasUnknownThreadError; that stays forcancelof an unknown thread. A bad id (empty, over 200 characters, a control character) is the newInvalidThreadIdError(run.invalid-thread-id). Breaking for your ownExecutionRecordRepository:createThread({ workflow, tenantId, ownerId?, threadId? }: NewThread)takes one object and returns the thread's id; every read by thread names both values βlastTerns(tenantId, threadId, limit),uncovered(tenantId, threadId),before(tenantId, threadId, ternId, limit),turnNumber(tenantId, threadId, ternId),latestSummaries(tenantId, threadId, limit),summaryCount(tenantId, threadId); aSummaryand aMemoryTurncarrytenantIdnext tothreadId. The SQLite thread store's migration 11 keys threads by(tenant_id, id)(owners, Terns and summaries by(tenant_id, thread_id)); existing threads aredefault's and keep their ids. -
mockAction(A)(the action still runs) andmockTool(T).thenFail(ToolFailure.Timeout); the call matcherstoHaveBeenCalledWith,toHaveBeenCalledTimes,toHaveReturnedonmockLlm(X)/mockAction(A)(Vitest's own on anything else) andmockLlm(X).inputs. -
Breaking β
toFailWith(ErrorClass, { β¦fields, cause })replacestoFailWith({ code, node })(ExpectedFailureremoved):toFailWith({ code: "step.action" })βtoFailWith(ActionFailedError),{ node: MainRouter }βtoFailWith(RouterFailedError, { nodeId: nameOf(MainRouter) }), a budget by its name βtoFailWith(BudgetExceededError, { budget: { name: "tenant-daily-budget" } }). The rootErrorClasstakes the error's type (ErrorClass<TError>).
-
env.optional("X"),env.integer.optional("X"),env.flag.optional("X")β a variable that may be unset: typedT | undefined, unset βundefined(not a missing variable), a set value still checked;agboss check --envmarks it(optional)(Environments). New public typeEnvironmentValueReader<T>;EnvironmentVariableReaderextends it. -
The base-URL field repeats no default:
openRouterBaseUrl: env.optional("OPENROUTER_BASE_URL")β unset β the decorator'sbaseUrl(Model providers); job-scout andagboss createdeclare it this way.
-
@PiiPolicyis rebuilt (Guards β PII policies): declarative{ name, description, rules: [pii(PiiKind | PiiGroup, PiiAction.β¦)], detectWithModel?, pseudonymKey?: secret("β¦") }, or a project's own detector{ name, action, deps }+implements PiiPolicyHandler(findMatches(text)βPiiMatch { start, end }). Breaking: thePiiPolicyinterface (mask/maskJson) is removed; names end with-pii-policy. -
Actions
PiiAction.replaceWithKind(),replacePart({ keep⦠} | { pattern }),replaceWithToken()(HMAC-SHA256),blockWithRefusal({ refusal } | ())+PiiRefusal;PiiRefusedError(pii.refused),PiiDetectionFailedError(pii.detection-failed). -
Levels:
@Application({ piiPolicies })(new) β@Workflowβ@Agent/@Router; the lists add up,excludePiiPoliciesremoves. Breaking:overridePiiPolicies/disablePiiPoliciesare removed, and so are the PII policies of@Tool(a tool sees real values; its agent's policies close its result). Assembly rules[pii.conflicting-rules],[pii.exclude-not-inherited],[pii.missing-refusal],[pii.unused-refusal],[pii.kind-needs-model],[pii.not-a-policy]. -
The model boundary masks everything a model reads and reveals its moves before validation;
the token map is the execution state's
piiTokenschannel (S38, replaces #121's store). -
Observability: events, the log, Langfuse spans and
result.recordare masked by the application's and the workflow's lists (Observability); breaking:onPiiPolicyStart/onPiiPolicyEnd,OnPiiPolicyStart/OnPiiPolicyEnd,PiiPolicyStartEvent/PiiPolicyEndEventandPiiPolicyPayloadare removed β an observer that implements one of the hooks fails assembly with[observer.unknown-hook]; delete the method (there is nothing to migrate to: masking happens at the model boundary, not in a hook). -
Breaking β
sensitive: true | { kind }is replaced bysensitive: PiiAction.β¦(masking actions) on DTO and context fields;WorkflowStartTextDto.authorisreplaceWithToken(); a channel shows such a field through its action instead of***. -
@Application({ pseudonymKey: "piiPseudonymKey" })names aSecretfield of the environment (Environments): the HMAC key ofsensitivefields, project detectors and every policy without its own key (now optional) β one token per value on every instance. A policy's own key wins; a policy that needs the application's when none is set fails the start; a field the contract lacks is[environment.missing-field].
-
Channels are found through the tools that name them (Tools β Approval channels):
breaking β
@Workflow({ channelClasses })is removed; one channel instance per application. -
@ChannelRoleβ a project's abstract class (extends ChannelOf<Self>) withapproves: XDtoorask/answer, a requiredansweredBy: PauseAnswerer.ThreadOwner | PauseAnswerer.Channeland an optionalreplyInterpreter; its one implementation (@Channel,extendsthe role) is listed in@Application({ providers }). New assembly codes:[channel.wrong-tool],[channel.shared-interpreter],[channel.role-form],[di.missing-implementation],[di.ambiguous-implementation]; a@Channelin a workflow'sprovidersis[di.infrastructure-in-workflow]. -
Breaking β the channel contract: one method,
send(ask: ChannelAsk<Role>): Promise<ChannelOutcome<Role>>(ask = { pauseId, value }, typed by the generatedChannelRoles), returningChannelOutcome.awaitReply()(pause) orChannelOutcome.decided(decision)(no pause).requestApproval,ChannelRequestandChannelDecisionare removed. -
Breaking β
InboundChannelAdapterandSemanticInboundChannelAdapterare removed (and@Channel({ inboundAdapter })):app.resume(threadId, decision)takes theToolCallApprovalDecisionDtoitself until free-text replies come with #267 PR 3. -
AutoApproveChannel/AutoRejectChannelnow decide at once (decided) β no pause.
-
rateLimitPolicyon@ModelProvider(Model providers):RateLimitPolicy.of({ maxConcurrentCalls?, perMinute?: { calls?, tokens? }, balancing? })β one limit for the whole application, every execution, tenant and instance, its state inRateLimitRepository. A call without room waits (its signal ends the wait βapp.cancelwithExecutionCancelledError; the attempt'stimeoutstarts with the room); waiting tenants share free slots byQueueBalancingStrategy(TenantRoundRobindefault,TenantLeastInFlight,TenantWeightedbytenant.weight,Fifo); a 429 pauses the provider's whole queue forRetry-After(else the retry delay) on every instance, then halves the instance's concurrency, back by one a minute (AIMD). No policy β no limit. -
retryPolicyis the only backoff; a wait between attempts holds no slot. -
RateLimitRepositorygets windows and pauses:addToWindow,clearWindow,pauseUntil,pausedUntilβ abstract, so breaking for your own implementation (in memory andSqliteRateLimitRepositoryimplement them; a SQLite file gets two new tables on open) (Several instances). -
Breaking β the circuit breaker's state is in
RateLimitRepository(an open breaker holds on every instance) and on the application'sClock:CircuitBreakertakes(provider, policy, { rateLimits, clock })and its methods are async;CircuitBreakersand the process-wide breakers are removed. -
Breaking β provider keys are
Secrets of the environment:apiKeynames the field (apiKey: "openRouterApiKey"), set withopenRouterApiKey: secret("OPENROUTER_API_KEY"); a base URL may come from a text field too (baseUrlField: "openRouterBaseUrl",env.optional("OPENROUTER_BASE_URL")since S90).EnvironmentVariable,MissingEnvironmentVariableError,SettingValue,OPENROUTER_API_KEYandOPENROUTER_BASE_URL_SETTINGare removed βenv()/secret()are the one way to read a process variable (Environments). -
agboss checkwithout--modelsneeds no secret values (it reads the contract's shape);--modelsconnects and fails on a missing secret, naming its variable. -
Breaking β provider names end with
-model-provider([name.kind-suffix]): the built-ins areopenrouter-model-provideranddecisions-model-provider(indescribe, the startup log and model problems).
-
Quorums run (Parallel β At run time): decided in the launch's
superstep as answers arrive β each branch's arbiter chain in its slot and frame, the answer
recorded at once (
BranchVerdict.CountedwithacceptedSequence,RejectedwithrejectedBy,Surplus), the rest cancelled once the quorum is fixed; the join decides from the records (counted answers in counting order, the winning group's own path).flow.quorum-not-runnableis removed. -
New errors:
QuorumNotReachedError(step.quorum) andArbiterError(step.arbiter), bothStepFailedErrors, taken by.onJoinError(E, β¦);QuorumShortfall. -
Deadline on the application's
Clock:deadlineAtat dispatch, kept in the launch's ticket; a pause counts. After a resume or a crash the quorum is rebuilt from the finished branches' records; an expired deadline cancels the rest without running it. -
Under a quorum a branch's own step failure is
Skipped(its place stays empty) whatever the route's failure policy; anArbiterErroror a limit fails the launch. -
Model arbiters: one decision per answer under
arbiter:<name>(answer-arbiteron a chat model), budgets checked before each call; their prompts and criteria in the prompt version, their model a model use (arbiters.<name>). Code arbiters are created by the container at start. -
Shown:
LaunchRecord.isQuorumReached,LaunchRecord.group(QuorumGroupRecord),BranchRecord.verdict/acceptedSequence/rejectedBy; trace entries; the hookonQuorumDecided(OnQuorumDecided,QuorumDecidedEventβ a node event,stepthe launching step). -
Testing:
mockLlm(ModelArbiter).thenReturn(decideWith({ counts: 0.9 })). -
CLI:
agboss generate arbiter <name> --workflow β¦ [--agent β¦]β a code arbiter with one@OnAnswermethod per agent of a launch.
-
Breaking: observers are registered only in
@Application({ observers })(Observability);@Workflow({ observers })is removed (does not compile). One manager per application instance, for every execution of every workflow. -
Breaking: hooks are named after executions β
onExecutionStart,onExecutionEnd,onExecutionPause,onExecutionResume,onExecutionError,onExecutionRejectedreplaceonWorkflowStart/End/Pause/Resume,onErrorandonRunRejected(interfacesOnExecutionβ¦; eventsExecutionStartEvent,ExecutionEndEventβfinish,spend, once per execution at a finish βExecutionPauseEvent,ExecutionResumeEvent,ExecutionErrorEvent,ExecutionRejectedEvent). -
Breaking: one event object per hook, no
AppState(removed): every execution event extendsExecutionEvent(executionId,parentExecutionId,rootExecutionId,threadId,tenantId,workflow); node events extendNodeEventand addstep(StepRecord).ValuesOutdatedEventandLaunchItemsTruncatedEventgaintenantId. -
Breaking (#269):
ExecutionOperation.Execute | Resume | CancelinExecutionRejectedEvent.operationreplaces the strings; the event carriestenantId. -
Breaking: the untyped
executionContext/metadataexecute options are removed, withChannelRequest.metadata/.executionContext,GuardrailContext.metadata/.executionContext(no longer generic) andRunContext.metadata: components read the typed contexts through an injectedExecutionContext;McpService.connect(transport, session?)/handleSse(res, endpoint, session?)takeMcpSessionOptions(tenant,execution,ownerId);A2ACallOptionsissignal,ownerId,tenant,execution. -
New: the
Loggercontract (infrastructure;ConsoleLoggerby default,LogLevel,LogEntry,formatLogEntry) replacesprocess.emitWarning(execution.recovered,recovery.incompatible,recovery.failed,app.close-failed,observer.failed); in tests an unlisted logger is silent.ExecutionObserverManager's default failure listener no longer warns. -
Breaking: renames to executions β
WorkflowObserverβExecutionObserver,ObserverManagerβExecutionObserverManager,WorkflowCancelledErrorβExecutionCancelledError(codeworkflow.cancelledβexecution.cancelled);agboss migrate importsrewrites the imports. -
New:
nameOf(C)β thenameof any AGBoss-decorated class (its literal type where the generated types brand it), for filtering serialized events and records. -
New: the start log β what started and the worst case of one call of each start; no
maxSpend()at start, nothing per tenant;testWithlogs none of it. -
New: assembly warnings
[application.in-memory-cost-ledger],[application.in-memory-pauses]β inapp.warningsand the start log, errors underagboss check --strict;agboss checkruns a newapplicationcheck. The CLI'srun/chatno longer printapp.warningsthemselves (the start log has them). -
New in
agboss describe: the application β each contract's implementation and why, budgets by scope with their periods, the workflow's execution budget, observers, warnings β and alimitssection (maxStepsset or default,maxVisits, worst cases);--jsonaddsapplicationand a richerlimits. - flow-patterns lists
SqliteCostLedgerRepository(environmentsqlitePath) so its strict check passes.
-
New: quorums (Parallel): after
.toParallelAgents(β¦),.joinQuorum({ min, max, deadline })(options always an object;maxdefaults tomin) and.joinQuorumGroups(group(A, B).priority(n).returns(β¦).toβ¦(β¦), β¦, { deadline })β every group continues its own path, nothing follows the groups but.onJoinError(β¦). New builder names:group,anyOf; typesQuorums,QuorumJoined,GroupsJoined,GroupStart,GroupValues,GroupPath,GroupHead,AgentsLaunched,QuorumOptions,QuorumGroupsOptions,AnyOfArbiters,ArbiterChoice; ASTQuorumKind,CountQuorum,GroupsQuorum,Quorum,QuorumGroup,AcceptedBy,ArbiterClass(LaunchStep.acceptedBy,JoinStep.quorum). -
New:
@Arbiterand@OnAnswer(Agent), attached with.acceptedBy(X)(a chain is an AND,anyOf(X, Y)the first that accepts, S57): by model (criteriaper agent) or by code (one method per agent class, typed by the decorator). New@agboss/core/units:hours(). -
What
.returns(β¦)may promise: after.joinQuorum({ min })withminbelow the agents launched a single answer isoptional(XDto)(compile error and[flow.quorum-return-not-optional]otherwise); a repeated class or a sharedoutputislistOf(XDto)in every form; a group's agents are required in its.returns(β¦)(JoinOutputs["<launch> group(<agents>)"]). -
New rules:
[flow.quorum-impossible],[flow.quorum-return-not-optional],[arbiter.not-an-arbiter],[arbiter.unhandled-agent],[arbiter.unknown-agent],[arbiter.duplicate-agent],[arbiter.answer-method-on-model],[arbiter.method-name];[input.missing-on-path]names the agent a quorum may leave out and the fix. -
agboss describeprints quorums and counts a quorum list's producers (critic-agent Γ1β2). - A workflow with a quorum did not assemble until #279 PR 2 (
flow.quorum-not-runnable, removed there). -
@QuorumRouter,QuorumStrategy,QuorumManagerand.joinQuorum(n)stay removed (AC11).
-
Budgets are checked (FinOps): every execution's budgets β its workflow's
own execution budget (else the default), every tenant budget, every application budget β have
their
maxSpend()read once, at its start, in itsExecutionContext, and kept in its state for the whole execution (a resume and a recovery count against the same amounts). Before the first step an exhausted one rejects the execution (BudgetExceededError,onRunRejected) leaving no trace β no thread (a new thread is now created after the check), no record, no call; every model call's cost is then counted in all of them, and before every step and model call the first exhausted stops the execution. Nothing is reserved (#202's holds are gone): a small overrun on a race, or by one call, is accepted. -
New assembly rules:
[budget.execution-budget-required],[budget.duplicate-execution-budget],[budget.duplicate-default-execution-budget],[budget.tenant-budget-required],[budget.application-budget-required]β every application lists an execution budget for every workflow (own or the default), a tenant and an application budget. -
Breaking β
settings().limitsis removed:WorkflowSettings.builder().limits(β¦),WorkflowLimits,PerRunLimits,PerDayLimits,PerRunLimitRequiredError([limits.per-run-required]),WorkflowSettingsError,LimitKey,SpendAccountandRunOptions.accountare gone; money is@Budgets only.settings()keepsonIncompatibleResume. -
Breaking β the limit errors (FinOps):
StepsLimitExceededError(limit.steps:workflow,maxSteps,steps;support-workflow took 41 steps; its maxSteps is 40) formaxSteps;BudgetExceededErrornames its budget βbudget({ name }),scope,key(the application's name, thetenantId, the execution's id),maxSpend,spent, a sentence (tenant acme spent $40.05 today; tenant-daily-budget allows $40) β instead of the keyslimits.perRun.cost/limits.perDay.cost.toFailWith({ code })matches a budget by its name. -
Breaking β
CostLedgerRepositorycounts per budget, scope key and period:findSpentByCounter(counter),saveAll(counters, records),findMaxSpendByAccount(account),saveMaxSpend(account, maxSpend)(new typesBudgetAccount,CostCounter; werefindSpentToday(key)/saveAll(key, records)). The ledgers read no time any more β the runtime names each counter's period from the application'sClockβ soInMemoryCostLedgerRepository()andSqliteCostLedgerRepository(connection)take no clock.SqliteCostLedgerRepositorydrops the old per-workflow day table of a file written before (#265 S69 e). -
maxStepsand launches (#273 S68 f): the default (agents + routers) Γ 3 must hold the worst case of a per-item launch (maxItemsruns) β else[limits.max-steps-too-low]; set@Workflow({ maxSteps })..onError(StepsLimitExceededError, β¦)(or a parent class) counts as a way out for the steps rules. -
Eval / replay / compare run within the application's budgets; the judge's calls count in the
application budgets, held to their last known
maxSpend()(the eval account<workflow>:evalis gone). -
agboss describe:limits steps 2β2 (maxSteps 12)(no money there; budgets by scope come with PR 6).
-
Clockis public (rootagboss, an infrastructure contract):now()andsleep(delay, signal?). Every time the framework reads and every wait it makes goes through the application's one clock β the ledger's day, records, leases and their renewals, the recovery sweep, the shutdown timeout, a tool'stimeoutMs, a model call's timeout, retry backoff andRetry-Afterpauses. The system's clock unless@Application({ providers })lists a class thatextends Clock(created first; it may not depend on the contexts); in a workflow'sprovidersit is[di.infrastructure-in-workflow]. A component that needs the time injects it (deps: [Clock]). Testing β The clock. -
VirtualClockis public (@agboss/core/testing): theclockfixture andapp.clockare one per test, the application's clock in every test (also when it lists its own);new VirtualClock(start?)in unit tests. Breaking: theTestClocktype is gone βVirtualClockin its place (agboss migrate importsrewrites the import); itsnowis a method, soclock.now()reads as before butclock.nowis no longer a function to pass around. -
Lint
agboss/no-direct-timefailsDate.now(),new Date()/Date()without arguments,performance.now(),setTimeout,setInterval,AbortSignal.timeoutand thenode:timersmodules in the framework's sources (SystemClockexcepted). -
SqliteConnectionis a value export (#265 S70): the DI token a subclass of a SQLite implementation names. Breaking for a project that constructs one of them itself: the constructors take(connection, clock: Clock, applicationContext: ApplicationContext)(the leases and rate-limit slots),(connection, clock)(the paused executions; the ledger takes(connection)since #261 PR 5) β Several instances β Extending a SQLite implementation. -
Breaking: the
clockoption ofcreateApp/createAppDeps(AppOptions,AppDepsOptions) is removed β one way to set the clock: list aClocksubclass in@Application({ providers }); tests get theVirtualClockfromtestWith.
-
New: a workflow is a step (Workflow as a step):
.toWorkflow(W),route(W),workflow(W)(the own path of a child reached from two places) and.toParallelWorkflows(W, { maxConcurrency, maxItems })(one child per item,maxItemsrequired, joined like any launch;WorkflowLaunchOptions). Checked and typed byagboss check; a workflow with a child did not assemble until #263 PR 2 (flow.child-not-runnable, removed there). -
The start is chosen per edge, by class: one start β it; per item β the start taking the item;
else the start taking the chain's current value, of several the one whose other required fields
are on the path β
[graph.no-matching-start]/[graph.ambiguous-start]otherwise, also compile errors through the child'sInputsentry.agboss describeprints each edge's start (children;--json:children). -
The child's result is its finishes' output classes (
ProducerOutputs):.returns(XDto), or.returns(oneOf(β¦)).switch(β¦)with one case per class; after a per-item launch the join giveslistOf(oneOf(β¦))(JoinOutputskeytoParallelWorkflows(<child>) per item). The child's scope starts with its start input only. -
New rules:
[graph.child-without-failure-finish](a warning, an error under--strict) and[graph.limit-handler-uses-model]β breaking for flows that catch a limit: an.onError/.onJoinErrorside path that can catch aLimitExceededError(its class, a subclass or a parent such asAGBossError/Error) now fails assembly when it reaches an agent, a router or a child. - A workflow class mentioned as another kind (
toAgent(SomeWorkflow)) is[graph.wrong-kind], no longer[graph.not-a-node]. -
New:
agboss check --strictβ the analyser's warnings ([flow.path-too-deep],[graph.child-without-failure-finish]) are errors (exit 3); flow-patterns'npm run checkuses it.
-
The context levels (Context and Authorization):
ApplicationContext(name,version,instanceId,startedAt,environmentName,environment,hostβHostContext) βTenantContext(tenantId,weight, #266 S60) βExecutionContext(new:tenant), each its type and its DI token. New public names:ApplicationContext,HostContext,TenantContext,ContextValues<T>,TenantValues,TenantContextClass,ExecutionContextClass,CallOptions,ResumeOptions,ExecutionOptionsOf,ExecutionOptionsArgs;@agboss/core/testing:TestContextOptions,TestApplicationOptions. -
The project's subclasses:
@Application({ tenantContext, executionContext }); values per execution,app.execute(Start, input, { tenant, execution }), typed per start by the generated types (theWorkflowStartsentry names the start'stenant/executionclasses, and a declared execution subclass'stenantis typed as the tenant subclass), validated before the first step (dto.invalid,onRunRejected), frozen, kept in the checkpoint. New rule codes:[context.prompt-unused],[context.not-declared]. -
Breaking β
ENVis removed: read the environment asapplicationContext.environment(deps: [ApplicationContext]);[di.missing-environment]is gone (an application without a contract has an empty environment). -
Breaking β the options are named like the context fields:
app.execute(β¦, { threadId, ownerId })(wasthread,owner),app.resume(threadId, decision, { tenantId, ownerId }),app.cancel(threadId, { tenantId, ownerId });ExecutionOutput.threadId(wasthread) and the newExecutionOutput.tenantId. -
Breaking β a thread belongs to its tenant: its key is
tenantId+threadId;app.cancelof a thread the tenant does not have throwsUnknownThreadError(it answered{ cancelled: false }). The storage contracts take the tenant:ExecutionRecordRepository.createThread(workflow, tenantId, owner?),hasThread(workflow, tenantId, threadId),threadOwner(workflow, tenantId, threadId); every record carriestenantId(Tern store migration 10);PausedExecutionRepository.findByTenantIdAndThreadId,resolveByTenantIdAndThreadId,deleteByTenantIdAndThreadId(wereβ¦ByThreadId), keyed by the paused run'stenantId. Leases and tree keys use the execution's tenant (was always"default");holderInstanceIdisApplicationContext.instanceId. -
SQLite (#265 S69 e):
paused_executionsis keyed by(tenant_id, thread_id)and thecheckpoints/checkpoint_writesrows carrytenant_id(the run'sconfigurable.tenant_id); a file written before is migrated on first use, its rows in the tenant"default".SqliteConnectionreadssqlitePathfromapplicationContext.environment. -
Traces carry the tenant: a run's LangGraph
metadata(tenantId,executionId,rootExecutionId,threadId,workflowName,workflowVersion) andtags(tenant:<id>), the Langfuse trace's tagtenant:<id>and metadatatenantId;TraceContext.tenantId. -
testExecutionContext({ tenantContext, tenant, executionContext, execution, application }).
-
Model providers are the application's (Model providers):
@ModelProviderclasses are listed only in@Application({ providers })β none listed β the built-inOpenRouterModelProvider+DecisionsModelProvider; one listed β the list is complete, the built-ins are not added next to it. A@ModelProviderin a workflow'sprovidersβ[di.infrastructure-in-workflow]. -
Breaking β no provider setting on a workflow:
WorkflowSettings.builder().modelProviders(β¦),.defaultModelProvider(β¦)andWorkflowSettings.modelsare removed; move the list to the application'sproviders.AssembledWorkflow.modelsβmodelProviders(the application's list). -
Every model resolves to exactly one provider by
serves, at assembly β agents, model routers, the default router, guards, compaction, judges: new rule codes[model.unserved]and[model.ambiguous-provider](all at once, before any model call). No default breaks a tie: one model through two providers is two model strings. -
Breaking β
ModelProblemCodemodel.no-providerβmodel.unserved;ModelProviderDirectory.of(classes)takes no default provider (byName,defaultProvidergone). -
agboss describeprintsmodel: β¦ β <provider>for each model (--json:models);agboss check --modelschecks every model of every workflow of the application.
-
Launches run (Workflow β Parallel launches at run time):
each branch is one LangGraph
Sendof one superstep,maxConcurrencya sliding window (a paused branch frees its slot), the join a node of its own reading only its launch's outcome records (launch id${executionId}:${nodeId}:${visitNumber}); the join's lists keep listed / item order.flow.launch-not-runnableis gone. -
New:
StepFailedError(step.failed, #273 S22; lineage e.g.[step.agent, step.failed, step]) β a step's own failure, naming it:component,nodeId(critic-agent#2for a copy),visitNumber,itemIndex;StepCoordinates. Breaking (class hierarchy) βAgentFailedError,GuardFailedError,RouterFailedError(withRouterDecisionError) andActionFailedErrorextend it instead ofPaidStepErrordirectly (stillPaidStepErrors);AgentAnswerInvalidError's andActionFailedError'scomponent/nodeIdcome from it, and every step error's recorddetailsnow carriesvisitNumber(anditemIndexin a per-item run). -
FailFastandSkip(S21): a failed branch aborts its siblings (BranchStatus.Cancelled, their spend counted) and the join takes the most specific.onJoinError(E, β¦)by class and cause chain, else the execution fails with the branch's error;FailurePolicy.Skipdrops only aStepFailedErrorβ a limit in aSkipbranch fails the launch;app.cancelduring a launch cancels the whole execution (WorkflowCancelledError), never routed by the join. -
One visit per launch: every copy and item of a launch is the same visit of its node (the
launch's
visitNumber, told apart bycopyNumber/itemIndex);maxVisitscounts launches. -
Router launches (S23, #280 S47 f): a model router decides its
OnDemandroutes in one call β aDecision.noulper route on a decision model, one answer toolanswer-launchwith a boolean per route on a chat model; all no without anAlwaysroute βRouterDecisionError. A code router'sroute()list is launched beside itsAlwaystargets. Tests script both withdecideWith({ "<target>": P(yes) })(Testing β Parallel launches). -
New:
result.launches(ExecutionOutput.launches: LaunchRecord[]βlaunch,items,taken,branches: BranchRecord[]),BranchStatus, and the hookonLaunchItemsTruncated(OnLaunchItemsTruncated,LaunchItemsTruncatedEvent) when a per-item launch dropped items pastmaxItems(Observability β Parallel launches); a trace entry per ended branch (critic-agent#2,company-check-agent[3]), "launch items truncated" and "launch decision". Agent and action events name a copy by its copy id;toFollowPathtakes the class for a copy. A list a join gathered from several agents names each element's producer (ScopeValueRecord.elementProducers). -
Example:
examples/flow-patterns/src/review/β a section reviewer per section, a mapping action, then a style critic beside two fact checker copies.
-
Every storage contract has a ready SQLite implementation, enabled only by listing it in
@Application({ providers }):SqliteExecutionRecordRepository,SqliteCostLedgerRepository,SqlitePausedExecutionRepository,SqliteCheckpointSaver(our ownBaseCheckpointSaveronnode:sqlite),SqliteExecutionLeaseRepository,SqliteExecutionTreeRepository,SqliteRateLimitRepository(Several instances β SQLite). One connection per application instance on the environment'ssqlitePathβ required only when one is listed ([environment.missing-field]) β WAL,busy_timeout, atomic operations inBEGIN IMMEDIATE, time from the framework's clock. -
Breaking for your own
ExecutionRecordRepository: a new abstract methodcompleteIfStatus(ternId, expected, outcome): Promise<boolean>β a compare-and-set status write. A resume marks its paused recordrunningwith it, so a write that lands after the holder finished the tree no longer leaves a finished recordrunning(#265 S66 h). -
Cross-process persistence is back for
--thread,agboss eval/replay/goldenwhen the application lists the SQLite implementations (#261 S49 b): a workflow run on its own (createAppDeps) now uses the infrastructure of the one application that lists it, not always the in-memory defaults. job-scout lists all seven (dev:.agboss/job-scout.sqlite,AGBOSS_SQLITE_PATH; tests::memory:).
-
Launches (Workflow β Parallel launches):
.toParallelAgents(A, B, B, { maxConcurrency })and.toParallelActions(β¦), a per-item launch (one target right after.returns(listOf(XDto)),{ maxItems }required), the mandatory.join(),.onJoinError(ErrorClass, side path)and.join().returns(β¦)β checked by the compiler against the new generated tableJoinOutputs(JoinOutputOf<Key>) and by the analyser. New public types:LaunchOptions,Launched,Joined,JoinErrorHandling,Launches,RouterLaunches,ValuesSource/StepValues/JoinValues, the AST'sLaunchStep,JoinStep,JoinErrorHandler,LaunchTargetKind;StepKind.Launch,StepKind.Join. -
Breaking (types only) β
Returning<TSource, β¦>takes a values source (StepValues<X>,JoinValues<Key>) instead of a component instance;AfterValueandRouterStephave a new optional type parameter. -
New rule codes:
[launch.join-required],[launch.max-items-required],[launch.max-items-without-list],[launch.item-not-read],[launch.branch-continued],[launch.branch-outdates-sibling](Path scope β Parallel branches, #271 AC9),[router.spawn-required],[router.prompt-on-always];[router.no-prompt]for anOnDemandroute without prompt;[router.all-routes-always]now applies to router launches. -
A model router states
spawnon every route before a launch (#273 S50, Routing patterns β 6); a code router'sRouterAnswerOf<Self>is a list when a launch follows it. -
agboss describeprints launches and joins; the steps checks count the copies that always run; the worst case of one call of each start is computed with the analysis. -
Not run yet: a workflow with a launch passed
agboss check, butcreateAppstopped withflow.launch-not-runnableuntil #273 PR 2 (above).
-
Breaking β environments are typed object literals: the contract is a plain class with declared
fields only (
environments/app.environment.ts), declared once in@Application({ environment: AppEnvironment }); each environment isexport const environment: AppEnvironment = { β¦ }inenvironments/<name>.environment.ts.defineEnvironment,fromEnv,FromEnv,FromEnvOptions,EnvironmentValues,EnvironmentDefinitionand thedeclare module "@agboss/core" { interface Environment β¦ }contract are removed;agboss checkwritesinterface Environment extends AppEnvironment {}into.agboss/types.d.ts, soENVis typed by the contract (Environments). -
New:
env("X"),env.integer("X"),env.flag("X")(each with{ default }),secret("X")andSecret(prints as***everywhere,.reveal()gives the value; a literal in aSecretfield does not compile);EnvironmentErrorCode(environment.not-found,environment.missing-variables,environment.invalid-value,environment.no-contract). A missing variable fails the start asDATABASE_URL is not set (AppEnvironment.databaseUrl, prod);agboss check --env <name>lists the variables it needs, marked value / secret. -
Breaking β tests run on
test:testWith(App)loadstest.environment.tsand fails without it (no fallback todev);testWith(App, { environment })andcreateApp(App, { environment })are removed β name an environment withenv. Breaking β@Application({ environment })is required: an application with no settings declaresenvironment: EmptyEnvironment(new, a contract without fields), which needs no environment file (its environments are{}). -
New assembly rules:
[environment.contract-value](a value in the contract),[environment.missing-field](a listed framework implementation's field missing from the contract),[guardrail.no-model]. -
The
defaultscascade:@Application({ defaults })β@Workflow({ defaults })β the component's own field, merged field by field, the most specific level wins (Configuration). Breaking: guards takedefaults.guardrail.model(new), neverdefaults.router.modelβ a guard with no model anywhere fails with[guardrail.no-model].[router.no-model]now readsβ¦ uses DecidedBy.Model but has no model β set model or defaults.router.model.agboss describeshows each router's model and each agent's temperature / maxTokens / history with where it came from ((own),(<application or workflow>),(built-in)). -
loadWorkflow,studioGraphOf,compareProfilesandworkflowOffrom@agboss/core/testingassemble a workflow in the application that lists it (its*.application.tsnext to it or above): its providers anddefaults.
-
Recovery (Several instances β Recovery): an execution's record
is
ExecutionRecordStatus.Runningwhile its tree moves (new public enum, root entry:Running,Answered,Guarded,Failed,Paused); every instance looks forrunningroots at its start and every 30 s, takes the lease of one whose holder died or stopped and continues it from its last checkpoint (a resolved pause first) β only the interrupted step runs again. A root of another config is left to a compatible instance and logged once. -
Breaking β
ExecutionRecordRepository: a new abstractfindByStatus(status, limit, after?)(a page of the records with that status, oldest first). A record is now written when the run starts (running) and completed when it ends, instead of appended at the end. -
Breaking β
WorkflowResumeEventhas areason: ResumeReason(new public enum:Answeredβapp.resume;Recoveredβ recovery,decisiononly when it took one). -
The graceful stop:
app.close(options?)stops the sweep, runsonStop, waits for the running executions up to the shutdown timeout β@Application({ shutdownTimeout }),app.close({ timeout })(CloseOptions), defaultseconds(25)β then interrupts them: the call rejects withApplicationStoppingError, noonError, the lease released at once, the record leftrunningfor recovery (App β app.close()). -
SIGTERM / SIGINT:
createAppcallsapp.close()on either (a second one interrupts at once); the listeners are removed whenclose()finishes, and the signal is raised again when nothing else listens.testWithnever listens.
Unreleased β the paths DSL revised: one value per .returns, { about } in the flow, historyOf (#299)
-
Breaking β
.returns(value, options?)names one value (#272 S54): a DTO class,optional(β¦),listOf(β¦)oroneOf(β¦). Several values (a start's map of fields) are a chain,.returns(CompanyDto).returns(optional(SearchRequestDto)), in any order;.returns(A, B)does not compile (untyped code:FlowSyntaxError).ReturnsStepholds onevalue. -
Breaking β a
oneOfoutput is written before its switch:.returns(oneOf(A, B)).switch(β¦); a.switchwithout it, or with anotheroneOf, is[flow.returns-mismatch]. -
Breaking β
about(TargetDto, shape)is removed (withAboutWrapper,DeclaredOutput): components declare a plainoutput; the flow says what a value is about β.returns(CritiqueDto, { about: PlanDto }),{ about: [PlanDto, BudgetEstimateDto] }, and for aoneOfon the case,caseOf(ChangesRequestedDto, { about: OfferDraftDto })(never on.returns(oneOf(β¦))). A value'saboutslots are kept onScopeValueRecord.about.{ about }on a start's.returns(β¦)does not compile ([io.about-on-start]at assembly).[io.ambiguous-cycle]hints name the.returns/caseOfto write it on. -
Breaking β no string tags (#272 S56):
{ as: "before" }, the tag slots, their labels (DraftDto[before]#1) and[flow.unused-tag]are removed. NewhistoryOf(Dto, { limit })ininput(root entry and@agboss/core/dto;HistoryOf<T>,HistoryWrapper,HistoryOptions): the field is{ current, previous }β the current value and the earlier versions oldest first, at mostlimitβ typedHistoryOf<Produced<Dto, P>>inInputOf, shown in the input message (### draft, earlier versions (oldest first)) and bydescribe. It needs a currentDtoon every path ([input.missing-on-path]); the replaced versions are never[input.outdated-on-path]; alimitthat is not a positive integer β[input.history-limit]. -
[io.ambiguous-about]now: a producer whose values are{ about: T }readingTin two or more fields (withhistoryOf(T)the answers are about its current value);[flow.ambiguous-field]: two fields of one node that read one slot. -
Breaking β
listOf(oneOf(A, B))is one slot (#272 S53): the filteredlist:ADto/list:BDtoslots are gone; alistOf(ADto)field after such a step is[input.missing-on-path]. - Path scope, Workflow, Components, Standard DTOs.
-
New infrastructure contracts (root entry), each with a built-in in-memory default, listed only
in
@Application({ providers })and found by inheritance:ExecutionLeaseRepository(acquire(tenantId, rootExecutionId, lease),renew,release;ExecutionLease { tenantId, rootExecutionId, holderInstanceId, leaseEpoch, expiresAt }),ExecutionTreeRepository(saveIfAbsent(key, value)/findById(key)inside an execution; an implementation providessaveIfAbsentInTree,findByIdInTree,deleteAllInTree) andRateLimitRepository(acquire(key, maxSlots, lease),renew,release;RateLimitLease). There is noExecutionSlotRepository(Several instances). -
The execution lease (Q58):
app.executeandapp.resumemove a tree only while holding its lease (seconds(30), renewed every 10 s and before every checkpoint write, released when the pass ends). An instance that lost it stops, and its call rejects withExecutionLeaseLostError(execution.lease-lost) β no.onError, noonError, no failed record: the new holder continues. -
Breaking β
PausedExecutionRepository: a paused run has astatus(PauseStatus.Pending/Resolved) and, once resolved, itsdecision; a new abstractresolveByThreadId(threadId, decision)turns a pending pause to resolved atomically. A resume records its decision first (a second resume of the same pause βNotPausedError), then waits for the tree's lease; the holder takes decisions resolved meanwhile and the waiting call returns the tree's current output. -
Tree keys of
ExecutionTreeRepositoryare removed when the tree finishes, fails or is cancelled; a pause keeps them. -
Testing:
testWith(App, { instances: 2 })with the fixturesappsandclock;clock.advancetakesseconds(β¦)too and returns a promise βawaitit (the framework's waits sleep on the test clock) (Testing).
-
Breaking β
@Applicationis the one entry:@Application({ name, version, workflows, providers, budgets })(nameends with-application, the class withApplication), andcreateApp(App, { env })/testWith(App)take it.createApp(Workflow)andtestWith(Workflow)are removed β they do not compile (the generated.agboss/types.d.tsbrands every workflow) and fail at run time naming the fix.app.execute(Start, β¦)runs the workflow of that start (any workflow of the application);resume/cancelfind the workflow of the thread;app.name/app.versionare the application's. Environments are found next to the application file (App, Testing). -
New assembly rules (
GraphRuleError, all at once):[application.no-workflows],[application.not-a-workflow],[application.duplicate-workflow],[application.ambiguous-start](one start in two workflows),[graph.duplicate-node-name]across workflows,[budget.not-a-budget],[di.infrastructure-in-workflow],[name.kind-suffix]on the application and on budgets. -
Infrastructure:
CostLedgerRepository,ExecutionRecordRepository,PausedExecutionRepository(new, root entry) and LangGraph'sBaseCheckpointSaverβ listed only in@Application({ providers }), found by inheritance, each with a built-in in-memory default. Breaking:createApp({ stores, pausedRuns }),AppStores,PausedRunRepositoryandcreateSqlitePausedRunRepositoryare removed, and nothing is written to the home folder any more: execution records (Terns) and the spend ledger were SQLite / JSONL files under~/.langgraph-agents/(TERN_DB,SPEND_LEDGER_DIR) and are now in memory β gone on restart, soagboss eval/replayonly see records of their own process until the SQLite implementations of #265 are listed (App β Infrastructure). -
Budgets, declarative:
@Budget({ name, scope, period?, workflow?, deps? })on a class thatimplements BudgetHandler(maxSpend(): Usd | Promise<Usd>),BudgetScope.Application | Tenant | Execution,BudgetPeriod.Day | Week | Month, all listed in@Application({ budgets })(config/budget.config.ts). They are components (injectable,mockOf-able); the checks at run time come with #261 PR 5 β until thensettings().limitscaps the money (FinOps). -
Tests: every framework fixture and test runs through an
@Application;app.ready()builds the test's application for a test that executes nothing; each test runs on the infrastructure the application lists (else the in-memory defaults), new per test. -
CLI (until #268):
agboss run/chat --workflow <file>run the application that lists the workflow β exported by its module, or by a*.application.tsin its folder or a folder above it β at that workflow's text start.agboss create/generate workflowwrite the workflow's*.application.tsandconfig/budget.config.ts.
-
Breaking β no scopes: one container per application instance, one instance per class (a class
listed by two workflows is one instance). The
scopeoption of every DI decorator, theScopetype, the run-scoped container and[di.scope-mismatch]/[di.run-scope-outside-run]are gone (against #184). Components keep no state: read the execution fromExecutionContext, keep data that outlive a step in the graph state, caches in your own service over external storage (Components β Components keep no state). -
Breaking β lint:
agboss/no-component-statereplacesagboss/no-run-state-in-singletonβthis.x = β¦outside the constructor of any component is an error. -
Lifecycle hooks:
OnInit(new in the root entry),OnStart,OnStop,OnDestroyβonInit(dependencies first) βonStartat start,app.close()βonStopβ the running executions awaited βonDestroy(dependants first). A failingonInit/onStartstops the start ([di.hook-failed] <Component>.<hook> failed: β¦); failing stop hooks are collected into oneAggregateError. Afterapp.close()execute/resumereject with the newApplicationStoppingError(app.stopping, anonRunRejected);cancelstill works. Breaking:AfterAssembleis removed,OnDestroynow runs once at shutdown, not per run (App β Lifecycle hooks). -
Implementation by inheritance: a dependency on a contract gets the one listed class that extends
it; two β
[di.ambiguous-implementation]. One token with different values β[di.conflicting-values];provide(Class, β¦)β[di.instance-provider](nonew: the container creates every instance) (Components). -
createAppDeps(β¦).close(executionsDone?)takes what to wait for betweenonStopandonDestroy.
-
DecidedBy.Code: a router decided by its class βimplements RouterHandler<Self>,route(input: InputOf<Self>)returns one of its targets (Route<Self>, generated; a target outsideroutesdoes not compile);depsin the constructor like an action, the execution throughdeps: [ExecutionContext]. No model call, cost 0, the same flow as a model router (Routers β Routers decided by code, Routing patterns). -
@Routeroptions are a union bydecidedBy(ModelRouterOptions/CodeRouterOptions):model,prompt,promptUrls,promptVariablesand routeprompts only on a model router;depsonly on a code router. -
spawn/failureon routes (SpawnPolicy.Always | OnDemand,FailurePolicy.FailFast | Skip), both kinds β the options of a parallel launch (#273). DefaultsAlways+FailFast; a code router's routeOnDemand. With.chooseOne(β¦)an explicit one β[router.policy-with-choose-one]andRoute<R>is every target; before a launchRoute<R>leavesAlwaystargets out and every routeAlwaysβ[router.all-routes-always]. The generatedRouterAnswerstable types whatroute()returns. -
Breaking β router errors (S19):
RouterFailedError(step.router, the original ascause: a model call orroute()failed) andRouterDecisionError(step.router.invalid-decision, now aRouterFailedError: an answer outside the routes, a list after.chooseOne(β¦), an unreadable answer β never retried). Both carrycomponent,nodeId,visitNumber. The instance codesrouter.failed/router.unknown-route, the typeRouterFailureCodeand the fieldrouterare gone (routerβnodeId); an unreadable model answer is nowRouterDecisionError, not a failure..onError(RouterFailedError, β¦)takes both;.onError(DomainError, β¦)the error inside. -
agboss describelists code routers ascode;mockLlm(CodeRouter)throws (a code router calls no model βmockOf()its services).
-
Style: a flow chain has one call per line β
start(A)/.returns(B)/.toFinish(C), androute(X)/.toFinish(F)inside achooseOne(β¦)(Workflows β The grammar). In the repo,*.workflow.tsmodules and the flow fixtures are left out of Prettier (which joins short chains), formatted bymake fmtand checked by ESLint's@stylistic/newline-per-chained-call; the docs' and Wiki's ts snippets that build paths are checked bynpm run check:docs(doc:line: β¦ (@stylistic/newline-per-chained-call)).agboss create/agboss generatewrite workflow modules one call per line.
-
scopeholds current values only: a write moves the older value of its slot and everything built from it tooutdatedValues, each withoutdated: { byValueId, reason }βbyValueIdthe root trigger,reasonanOutdatedReason(Replaced,AboutReplaced,BuiltFromReplaced) (Path scope β At run time).OutdatedValueRecord.outdatedgainsreason. -
onValuesOutdated(OnValuesOutdated,ValuesOutdatedEvent): one event per cascade, never empty, no DTO values; withExecutionEvent's fields (executionId,parentExecutionId,rootExecutionId,threadId,workflowas aWorkflowRecord) andstep(aStepRecord) (Observability); one "values outdated" trace entry per cascade. -
Records:
WorkflowRecord,StepRecord,ScopeValueReference,OutdatedValueReference,OutdatedMark;WorkflowDescriptor,StepDescriptorandComponentDescriptorextend them. -
agboss describeprints eachabout(β¦)output (CLI).
-
ExecutionContext: the one context of an execution, injected through the constructor βdeps: [ExecutionContext], a parameter namedexecutionContextβ into tools, actions, judges and services (Context and Authorization). Read-only data:executionId,threadId,ownerId,signal,workflow,step(idempotencyKey=${executionId}:${nodeId}:${visitNumber}[:${itemIndex}][:${callId}],callId,visitNumber, β¦) and the tree (parent,root,depth,executionOf(W)). A read outside an execution or after its step ended throwsExecutionContextUnavailableError(context.unavailable). -
No
ctxparameter:ToolHandler.run(input),@McpToolalike;ToolContextandRunContextare gone (ctx.run.runIdβexecutionContext.executionId,ctx.run.ownerβ.ownerId,ctx.run.signal/ctx.signalβ.signal,ctx.callIdβ.step.callId, side effects key on.step.idempotencyKey).ctx.run.metadataandctx.run.inputhave no replacement in the context: domain data come through a node'sinput.ctx.pause(question)is removed until #267 (questions as part of the graph; S44) β approval of a tool call through@Tool({ channel })keeps working.ctx.reportCost(usd)βdeps: [SpendRecorder],spendRecorder.record({ service, cost })(S43), counted in the money limits; a tool'sROUTER_FACTORYdecisions are accounted as its spend by the framework. -
A tool call's signal (S45): in a tool call
executionContext.signalaborts on the execution's cancel and at the tool's owntimeoutMs. -
Judges:
judge(input: JudgeInput)(reply,task,attempt) instead ofjudge(reply, ctx: JudgeContext); the judge's model throughdeps: [JudgeModel](judgeModel.invoke/judgeModel.decide). -
Testing:
testExecutionContext({ β¦ })replacestestRunContext();toolOf(tool).invoke(input)takes no context;app.tool(T).invoke(input)runs the tool in an execution of its own. - An exported tool called over MCP runs in an execution of its own; the client's
_meta.callIdis itsstep.callId.
-
Outdating (#271, Path scope): a field reads only current values β a new value outdates everything built from the older one, and assembly proves no node reads an outdated value (
[input.outdated-on-path]). In a cycle of classes built from each other the component that answers says what about:output: about(DraftDto, oneOf(ApprovalDto, DraftChangesDto))([io.ambiguous-cycle],[io.ambiguous-about]). A review loop written withoutaboutno longer assembles; the hint names the output to wrap. -
The flow is a list of paths (Workflow, ADR-0019):
start(S).returns(Dto).toRouter(R).chooseOne(route(A)β¦),.toAgent/.toAction/.toFinish,.switch(caseOf(β¦)),.onError(E, <side path>); own pathsagent(X)β¦/router(R)β¦/action(A)β¦for shared nodes. Removed:from(β¦).next(β¦),from(R).routes(),chain,node,catchError/compensateWith,nextParallel/join,batchParallel,joinQuorum,@QuorumRouter,parallel(β¦)/optional(β¦)route targets,Self, nested@Workflownodes, sagas andcompensate, theagboss/concurrencyentry. Parallel launches return in #273, child workflows in #263, code routers in #280. -
Generated types:
agboss checkwrites.agboss/types.d.ts(git-ignored) β what types the paths,InputOf<X>/OutputOf<X>andapp.execute(Start, input). A generated project runs it on install (npm run types);declare readonly inputon a start is no longer needed. -
Typed data on paths (Path scope):
@Agent/@Action/@Router/@WorkflowFinishtakeinputfields andoutputshapes; wrapperslistOf,oneOf,optional,variant,isVariant(@agboss/core/dto). Agents read an input message after static instructions and answer a structuredoutputthrough answer tools;{{item}}and{{input.β¦}}in instructions are gone ([prompt.input-in-instructions]). -
Names: every component
nameends with its kind (main-router,scout-agent,chat-workflow-start,charge-action,job-scout-workflow) and is unique in the application; DTO classes on paths end withDtoβ the standard DTOs are renamed (WorkflowStartTextβWorkflowStartTextDto,MoneyβMoneyDto, β¦, Standard DTOs). LangGraph node ids are the names (no<kind>.prefix). -
@WorkflowActionβ@Action,execute(input)with its fields from the path's scope (ActionHandler<Self>).ctx.run,ctx.idempotencyKeyandctx.executionContextare gone from actions β since #280 the execution comes throughdeps: [ExecutionContext](above). -
Limits:
@Workflow({ maxSteps })replaceslimits.perRun.steps;maxVisitson routers and agents (VisitsLimitExceededError, catchable with.onError); money stays insettings(). -
Results:
ExecutionStatus.Finished | Paused;result.finishis{ name };{ record: true }adds the execution record. The thread's history keeps finished executions asstart <name>: <input>/finish <name>: <output>. -
Enums instead of strings (#269):
Reasoning.β¦instead ofthinkingstrings on components,RagMode.Tool | Context,ModelKind,ProviderSort,ImageDetail,ExecutionStatus. -
defaults.routeris{ model }(nokind); a@RoutertakesdecidedBy: DecidedBy.Modeland a requiredinput. -
job-scout: one path per turn, names with their kinds, profiles and golden sets under
job-scout-workflow/(Example).