Repository navigation
feat(langchain): run agent routes on createAgent; returnDirect ends only on success - #820
Merged
Merged
Conversation
…nly on success Agent routes now materialize with LangChain's `createAgent` instead of LangGraph's deprecated `createReactAgent`. Both prebuilt agents route a `returnDirect` tool's result to the end of the graph by tool name, so a failed call (the tool threw, or the model's arguments failed its schema) ended the run and the model never got to retry (upstream: langchain-ai/langgraphjs#2878, langchain-ai/langchainjs#11711). `createAgent`'s middleware lets B4 route these itself: the converter's flag is cleared on the LangChain tool, and a `beforeModel` hook jumps to the end only when this turn's tool results include a successful result from a `returnDirect` tool. What `createReactAgent` did through options moves into B4 middleware: - prompt fragments re-render from live route state in `wrapModelCall`; - summarization splits into a `beforeModel` hook (running summary plus the condensed view, written every turn) and `wrapModelCall`, which sends that view; - `wrapToolCall` returns tool errors to the model as `status: "error"` results in LangGraph's exact `Error: … Please fix your mistakes.` form (without it `createAgent` treats an escaping tool error as fatal, and its own error messages carry no status); - route state is a `StateSchema` of `ReducedValue`s (`createAgent` does not take an `Annotation.Root`). The model/tool middleware has no graph node and the loop-entry middleware exists only for routes that summarize or have a `returnDirect` tool, so ordinary routes keep the same supersteps per loop against `recursionLimit`. The system prompt is handed over as a `SystemMessage` so the provider payload stays a plain string rather than a content block. Found along the way: - LangGraph persists checkpoints asynchronously, and the checkpoint that owns a parked interrupt can be put after a failed run's stream rejects. `/runs/wait`'s failure arm read the checkpoint at rejection and could miss the park; it passed on `createReactAgent` by microseconds. `trackCheckpointWrites` wraps the graph's checkpointer and the adapter drains in-flight writes before a failure propagates. - `createAgent` imports `initChatModel`, whose `import(config.package)` the Vercel function validator rejects. The Vercel esbuild plugin rewrites that one expression into a clear failure and fails the build if it changes. - The lockfile keeps zod 4.4.3 and `@b4run/cli` declares `langchain` and `zod` dev dependencies, so one `@langchain/langgraph` copy serves the CLI and `@b4run/langchain` (two copies broke `instanceof Command` on resume). `@b4run/langchain` now depends on `langchain` ^1.5.12, and its `@langchain/core` peer floor rises to ^1.2.12. Docs: `returnDirect` ends the run only on success, so a validating tool can use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
blove
had a problem deploying
to
vercel-preview
September 23, 2026 19:41 — with
GitHub Actions
Failure
…e fixture's LangChain pins A cancelled turn's settle already waits for the abandoned route to unwind, and the drain there waited on checkpoint writes chained behind the still-running tool, holding the turn open until the tool finished (the "parked /runs/wait turn is cancelled mid-flight" gate test failed 10/15 locally). Drain only on a genuine failure. The native Vercel fixture pinned @langchain/core 1.2.5, below the ^1.2.12 langchain requires, so its remote `b4 build` failed to import. Pin core 1.2.12, langgraph 1.4.17 and langgraph-checkpoint 1.1.5, the versions the workspace uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
blove
temporarily deployed
to
vercel-preview
September 23, 2026 20:14 — with
GitHub Actions
Inactive
Merged
2 tasks done
blove
added a commit
that referenced
this pull request
Sep 23, 2026
…ove the dead main job (#823) The seo-lastmod workflow was meant to regenerate the manifest on main so PRs never touch it, but it has never pushed a commit: main's branch protection requires the validate check, so every push was rejected (24 failures; the 11 successes had nothing to push). PRs kept regenerating the manifest, and because dates came straight from git history, a squash merge made main's manifest look stale in any full-history checkout and the next regenerating PR rewrote unrelated routes' dates. A route whose content digest matches its recorded entry now keeps that entry exactly; git is read only for routes that changed or are new, and removed routes drop out. So a squash merge cannot move a date, PRs only rewrite the entries for pages they changed, and seo:lastmod:check passes on main with full history. The workflow is deleted along with its audited fixture entries, and AGENTS.md says plainly that content PRs regenerate the manifest after committing their content. The regenerated manifest updates /docs/cli and /docs/tools, which #820 edited without regenerating. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Merged
6 tasks done
blove
added a commit
to liveloveapp/hashbrown
that referenced
this pull request
Sep 25, 2026
…nder (#585) ## Summary Bumps the invoicing server's seven `@b4run/*` pins from 0.11.0 to 0.12.0 and has the `render` tool adopt `returnDirect`. #575 moved to 0.11.0 but deliberately left `render` without `returnDirect`: in 0.11.0 the flag ended the run on *any* result from the tool, including an `invalid_ui` rejection, so the model never got the retry the tool description promises. B4 0.12.0 (cacheplane/b4run#820) moves agent routes to LangChain's `createAgent` and routes `returnDirect` itself: **a successful call ends the run, a failed call goes back to the model**. That is exactly `render`'s contract, so: - `src/app/assistant/tools/render.ts` exports `returnDirect = true`. A valid render ends the run without the closing model turn (~2 to 4 s, per the 0.11.0 measurement); a rejected render still goes back to the model, which fixes the named component and renders again. - `evals/harness.spec.ts`: the scripted-answer test no longer scripts a closing reply and expects an empty final message (a leftover model turn would now fail the replay for lack of a fixture). The retry-contract test from #575 stays, now asserting the fixed render ends the run the same way. The route middleware's `after` hook already returned an empty final message for validated runs, so the browser, which renders from the `render` call's own arguments, sees no difference beyond the run finishing sooner. Lockfile: the stale `@b4run/*` entries were removed before `npm install` (the ERESOLVE trap from the 0.10.0 bump). One copy each of `@langchain/core` 1.2.12 (satisfies 0.12.0's new `^1.2.12` peer floor) and `@langchain/langgraph` 1.4.17 is installed; `langchain` 1.5.12 arrives as a new transitive dependency of `@b4run/langchain`. Also updates `examples/invoicing/README.md`: the 0.12.0 pin, how a successful render now ends the run, and the eval section's note on `run.finalMessage` and pre-change fixtures. Review feedback: `returnDirect` is documented with a TSDoc block (AGENTS.md). ## Test plan - [x] `tsc --noEmit -p examples/invoicing/server/tsconfig.json` - [x] `vitest run --config examples/invoicing/server/vitest.config.mts`: 26 files, 223 tests - [x] `eslint examples/invoicing/server`: 0 errors - [x] `nx run invoicing-server:eval` (replay, no network): 9 cases, mean 1.00, gate passed. Fixtures recorded before this change still hold the model's closing call; replay leaves it unused. - [x] Live run against `gpt-5-mini` in the React client ("Which customers have invoices more than 90 days overdue, and how much do they owe?"): `ledgerSummary`, three `aging` calls, then `render` streamed as 1,007 `TOOL_CALL_ARGS` deltas; `RUN_FINISHED` (success) arrived in the same millisecond as `render`'s `TOOL_CALL_RESULT`, with no closing model turn and no text chunks. The panel shows the prose, both over-90 invoice tables and both aging summaries, and no "could not finish" alert. - [x] `b4 typegen`: `render`'s tool description still comes from the default export's doc block (the new `returnDirect` TSDoc does not leak into it). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
blove
added a commit
that referenced
this pull request
Sep 28, 2026
The endpoint test seeds grant rows against a stub graph and a canned pending write, and the core park-site test drives a bare compiled graph, so nothing showed that the minter the adapter injects into config.configurable still reaches the permission gate when the tool runs inside the agent loop, which #820 rebuilt on langchain createAgent. A real agent() route now parks an approve-gated tool through aimock, the grant is disclosed on pending_interrupts, one resume runs the tool once, a replay is refused with stale_interrupt without running it again, and a resume without the grant is refused with grant_required. Both fail when the adapter stops injecting the minter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
blove
added a commit
that referenced
this pull request
Sep 28, 2026
* feat(approvals): single-use grants bound to the parked tool call B4.run parked a tool call for human approval and resumed it when an answer arrived, but said nothing about how many times. The parked interrupt was addressed by `interruptId` and `resumeKey`, and neither is a credential: `interruptId` is a millisecond timestamp plus ~31 bits of `Math.random`, disclosed in the persisted envelope, and `resumeKey` is LangGraph's deterministic XXH3 of the checkpoint namespace. `ThreadAccessPolicy` gates who may touch a thread, but disclosure control is not consumption control — inside a session that legitimately held the thread, nothing stopped an approval being replayed, or a stale approval from an earlier proposal being applied to the current one. The only defence was emergent from LangGraph advancing the checkpoint; nothing enforced or tested it. A grant is 32 CSPRNG bytes minted at the park site in `@b4run/core`, stored only as a SHA-256 hash in a B4-owned `interrupt_grants` table added by an additive versioned migration under the existing `runMigrations` advisory lock. It rides out on the channels that already carry the prompt and comes back as an opaque `grant` on the resume entry. Consumption is a conditional `UPDATE ... WHERE consumed_at IS NULL AND voided_at IS NULL`, so the row count decides the winner: atomic, durable across a restart, correct across replicas. A replay is refused rather than re-executed. The minter is injected through LangGraph's `config.configurable` — the same channel this repo already uses for live per-call identity — so nothing in core's call graph grows a storage handle. That injection is optional by construction, and the absence fails closed: under `approvals.grants: "required"`, a park with no minter aborts the turn instead of parking a prompt nobody can answer safely. The mode deliberately does NOT travel the same way; a mode that went missing alongside the minter could never detect the minter's absence. Off by default. Refs #736, #738. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(cli): approval grants through a real agent route The endpoint test seeds grant rows against a stub graph and a canned pending write, and the core park-site test drives a bare compiled graph, so nothing showed that the minter the adapter injects into config.configurable still reaches the permission gate when the tool runs inside the agent loop, which #820 rebuilt on langchain createAgent. A real agent() route now parks an approve-gated tool through aimock, the grant is disclosed on pending_interrupts, one resume runs the tool once, a replay is refused with stale_interrupt without running it again, and a resume without the grant is refused with grant_required. Both fail when the adapter stops injecting the minter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(web): regenerate SEO lastmod for the approval-grants docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(changeset): approval grants take patch bumps on the 0.x fixed group Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 28, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Agent routes now run on LangChain's
createAgentinstead of LangGraph's deprecatedcreateReactAgent, and areturnDirecttool ends the run only when it succeeds. A failed call (the tool threw, or the model's arguments failed its schema) goes back to the model so it can correct the call and retry; the first success ends the run. This unblocks validating tools such as hashbrown's invoicingrendertool (liveloveapp/hashbrown#575), which could not adoptreturnDirectbecause aninvalid_uirejection ended the run.Both prebuilt agents route a
returnDirectresult to the end by tool name alone (upstream fixes proposed in langchain-ai/langgraphjs#2879 and langchain-ai/langchainjs#11712).createAgent's middleware lets B4 route it itself, so this does not wait on upstream. It reverses the caveat #811 documented.How
What
createReactAgentdid through options moves into B4 middleware (packages/langchain/src/agent-middleware.ts):createReactAgent)createAgent)returnDirectflag on the LangChain tool, routed by namebeforeModel(canJumpTo: ["end"]) jumps to end on a successfulreturnDirectresult (endsOnReturnDirect)ToolNodeturns tool errors intostatus: "error"resultswrapToolCallproduces the identical message; without itcreateAgenttreats an escaping tool error as fatal and sets no statuspromptre-renders fragments from live statewrapModelCallsets the system message from live statepreModelHooksummarization →llmInputMessagesbeforeModelwrites the running summary and condensed view (every turn, so no stale view);wrapModelCallsends itstateSchemaasAnnotation.RootStateSchemaofReducedValuesTwo middlewares, split by what they may write: a
beforeModelnode writes back every field its middleware declares, which would re-apply anappendreducer, so the node-free model/tool middleware reads route fields and the loop-entry middleware declares only replace-semantics fields. The loop-entry middleware exists only for routes that summarize or have areturnDirecttool, so ordinary routes keep the same supersteps per loop againstrecursionLimit. The system prompt is passed as aSystemMessageso the provider payload stays a plain string (a stringsystemPromptbecomes a content block increateAgent).Found along the way
/runs/wait's failure arm read the checkpoint at rejection and missed the park (pending-interrupts-endpoint"keeps gating when a parked /runs/wait turn then fails"); onmainit passes by ~0.2 ms. NewtrackCheckpointWriteswraps the graph's checkpointer and the adapter drains in-flight writes before a failure propagates, which makes the gate's "the route is done writing" assumption true on Postgres too.createAgentimportsinitChatModel, whoseimport(config.package)the self-contained-function validator rejects. The Vercel esbuild plugin rewrites that one expression into a clear failure (B4 always passes a model instance) and fails the build if a futurelangchainchanges it.@langchain/langgraphcopy. Azod ^4.4.3dependency first resolved to 4.6.5 and split langgraph into two peer-variant copies, so the CLI'sCommandfailedinstanceofand resumes stopped resuming. The lockfile keeps zod 4.4.3;@b4run/clideclareslangchain/zoddev deps so resolution matches.Dependencies
@b4run/langchaingainslangchain^1.5.12 andzod^4.4.3; its@langchain/corepeer floor rises from ^1.1.47 to ^1.2.12 (whatlangchainrequires). Patch changeset for@b4run/langchain,@b4run/core,@b4run/cli.Test plan
return-direct-graph.test.tsthrough the real materialized graph: success ends after one model call; a throwing tool and schema-invalid args each go back to the model and the retry ends the run; withoutreturnDirectthe model closes. Mutation-checked in both directions (old name-only routing fails the error cases; removingendsOnReturnDirectfails the success case).agent-middleware-graph.test.ts: fragments render from live state; anappendfield is applied once with the loop-entry node present; summarization sends the condensed view and keeps the running summary.checkpoint-writes.test.ts: drains in-flight and just-issued writes.pnpm build,lint,typecheck,check-docs,check:build-cache,check:release-inventory,test:release-integrity,pack:checkpnpm test: 7601 passed; the only 2 failures are Docker round-trips invercel-target.test.ts("streams Agent Protocol requests …"), which time out identically onmainin this environmentvalidate, harness lanes,vercel-native,copilotkit-examples-e2e🤖 Generated with Claude Code