v2.7 is mostly additive: respond() / respondStream() gain message and allowedFlows parameters and return richer observability (endedFlows, metadata.tokensUsed), providers accept a pre-configured SDK client, and restoreSession joins the public barrel. Four behavior changes are worth calling out even though none changes a type signature: the default history bound is now active, step finalize runs before persistence, crashed tools report failure to the model instead of throwing, and session load failures propagate.
If your integration passes explicit histories and catches errors broadly, the upgrade is usually zero-code. The sections below cover each case.
Both respond() and respondStream() accept two new optional fields on the shared params object.
message — pass the user's turn instead of appending it to the history yourself. The engine appends it to what the model sees this turn and records it on the returned session's history, then appends the assistant reply on top. Callers who hold sessions no longer maintain history arrays by hand.
allowedFlows — restricts this turn's routing candidates to the given flow ids/titles. Directive targets (goTo etc.) still resolve against the full registry. Use for entry-pin funnels instead of cloning or filtering agents.
Before, you re-derived exits from executedSteps plus session-cursor inspection; endedFlows removes the guesswork. metadata.tokensUsed is present only when the provider reports usage, and covers only the turn's primary generation call (routing and extraction sub-calls are not included). On streaming turns, provider usage rides on chunk metadata; stream chunks do not carry endedFlows.
3. Default history bound is active (400)
A hard bound now applies to session.history even when you configure no compaction: entries beyond maxHistoryMessages (default 400) are trimmed at end-of-turn finalize — and on interim auto-saves — always keeping whole assistant/tool pairs together. Each trim logs a warning recommending compaction for summarization.
typescript
const agent = createAgent({
// …
maxHistoryMessages: 1000, // raise the bound…
// …or configure compaction for summarization instead of truncation:
// compaction: { maxTokens: 8000 },
});
Set maxHistoryMessages: 0 to disable bounding entirely (previous unbounded behavior). Long-running chat() / stream() sessions can no longer grow until the provider context limit bricks them.
4. Step finalize now runs BEFORE persistence
The step finalize hook previously ran after the auto-save, so state writes it made could miss the persisted row when the conversation ended on that turn. The order inside end-of-turn finalization is now:
History bound + deterministic compaction
finalize hook — its state writes land on this turn's session
Auto-save to persistence
Live-session sync
No code change is needed — but hooks that "didn't stick" across restarts now do, and a control-flow directive returned by finalize queues for the start of the next turn as before.
5. Crashed tools soft-fail to the model
A tool handler that throws used to surface as an error escaping respond(). It is now reported to the model as a failed tool result — a role: "tool" message shaped {"success":false,"error":"…"} — so the model can react to the failed call instead of the turn dying (or worse, a fabricated success confirming an action that never happened). Unknown tool names behave the same way.
typescript
// The turn survives; the model sees the failure and can apologize/retry.
handler: () => {
throw new Error("upstream API is down");
},
If you relied on catching handler crashes out of respond(), catch them inside the handler instead and return { success: false, error } — same signal to the model, explicit in your code.
6. Tool directives work end-to-end
Directives emitted by tools now reliably reach the engine (previously they could be collected and dropped):
ctx.dispatch(directive) mid-handler and returning { directive } are equivalent.
State fields (dataUpdate, contextUpdate) apply immediately.
A reply directive short-circuits the remaining tool loop — its verbatim text becomes the final message with no follow-up LLM call.
Control-flow fields queue on session.pendingDirective and steer the next turn (same deferred semantics as agent.dispatch()).
Two related changes to how turn failures reach your catch:
ProviderError propagates bare out of respond() — instanceof ProviderError works directly; no unwrapping through ResponseGenerationError.details.originalError. Same for SessionConflictError.
ResponseGenerationError is exported and exposes the original error on the native .cause (as well as details.originalError). All other turn failures still wrap into it.
typescript
// ─── v2.6: unwrap through details ───
catch (err) {
const original = (err as { details?: { originalError?: unknown } }).details?.originalError;
if (original instanceof ProviderError) { /* … */ }
}
// ─── v2.7: instanceof survives ───
import { ProviderError, ResponseGenerationError } from "@falai/agent";
catch (err) {
if (err instanceof ProviderError) {
// err.code, err.provider, err.cause — branch directly
}
if (err instanceof ResponseGenerationError) {
// err.cause is the original error
}
}
On streaming turns, errors arrive wrapped as ResponseGenerationError on the final chunk's error field (with the original on .cause) rather than thrown.
8. Step hooks: shorthand and hooks.* both run
Declaring a top-level prepare/finalize alongside hooks.prepare/hooks.finalize used to silently pick one. Now both run — the shorthand first, then the hook — with their directive returns merged via Algorithm 4. One new validation: combining a tool-form handler (a tool id or Tool object) with a function hooks.* entry throws FlowConfigurationError at construction, since a single position cannot compose a tool reference with a function.
typescript
{
id: "enrich",
prepare: (context, data) => ({ dataUpdate: { tier: lookupTier(context, data?.email) } }),
hooks: {
// ALSO runs — after the shorthand above, results merged
prepare: ({ data }) => {
if (data.tier === "blocked") return { halt: true, reply: "Account on hold." };
},
},
}
9. Provider client injection and retry classification
AnthropicProvider and GeminiProvider accept a client option — a pre-configured SDK client that overrides the internally-constructed one. Intended for tests injecting scripted transports; production callers should keep passing apiKey.
Retry semantics are now uniform and documented: deterministic failures (auth 401/403, invalid request 400/404/422, caller aborts) fail fast without burning the retry budget; retriable failures are rate limits, overloads, timeouts, and network faults. retryConfig.timeout also bounds time-to-first-token on streaming calls, so a stream that opens and then stalls is treated as failed and retried before the first delta is committed.
10. restoreSession exported
restoreSession<TData>(state) joins the public barrel as the canonical inverse of createPersistedState(session) — restore a persisted blob verbatim (collected data and completed-flow history survive the round trip). Prefer it over the ambiguous createSession(state) overload in custom persistence code.
Not breaking in the type sense, but observable at runtime:
SessionManager load failures propagate. A failed sessionRepository.findById (transient DB error) used to be swallowed and fall through to creating a blank session — which then saved over the existing row, erasing the conversation. The error now throws out of getOrCreate() / the first turn. A missing row still creates a new session as before.
MemoryAdapter state writes now match the SQL adapters.updateStatus / updateCollectedData / updateFlowStep bump version + updatedAt exactly like SQLite/PostgreSQL (previously Memory mutated in place without either), and incrementMessageCount refreshes timestamps without moving version. If you asserted on MemoryAdapter versions in your own tests, they may shift by design; all adapters are pinned to one contract in tests/adapter-contract.test.ts.
agent.dispatch persists when an adapter is configured. Previously the queued directive was memory-only until the next turn's auto-save — lost if the process ended first (webhook/cron callers). With an adapter + autoSave (the defaults) dispatch now saves immediately through the per-session save queue, so the directive survives process boundaries, and a stale session copy throws SessionConflictError instead of silently clobbering another writer. No adapter, or autoSave: false: unchanged memory-only behavior.
Failed-turn rollback is unchanged, but combined with §7 more failures now arrive as typed instances you can branch on rather than wrapped messages to string-match.
Verification
After migrating, run the type checker — it will flag any code that depended on ResponseGenerationError being name-matched only, or on provider options that have since widened:
bash
bun run typecheck
To confirm the behavior changes land as described, the relevant suites are:
bash
bun test tests/consumer-fit-api.test.ts # message/allowedFlows, endedFlows, bare error propagation
bun test tests/tool-loop-correctness.test.ts # soft-failing tools
bun test tests/directive-wiring.test.ts # tool directives + finalize-before-persist ordering
Cross-References
Agent reference — the page that replaced the createAgent reference