Skip to content

feat: client-provided tools over AG-UI (Model A over a retained record) - #880

Merged
blove merged 36 commits into
mainfrom
blove/client-provided-tools-impl
Sep 30, 2026
Merged

blove merged 36 commits into
mainfrom
blove/client-provided-tools-impl

Conversation

@blove

@blove blove commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Closes #743. Implements the design in #744 (docs/superpowers/specs/2026-09-18-client-provided-tools-design.md) with your §12 decisions. Implementation plan: docs/superpowers/plans/2026-09-30-client-provided-tools.md.

What it does

A route named in server.agui.clientTools now runs the tools an AG-UI client sends, instead of just tolerating them.

  • What the model sees: each client tool as client_<name>.
  • What the client sees: tool-call frames under its own tool name, then an ordinary RUN_FINISHED. It runs the tool and sends the result as a role: "tool" message on its next run. This is the flow existing CopilotKit and AG-UI clients already use.
  • Server side: the call parks with interrupt(), and the server keeps a durable record of it. It is never shown to the client and carries no credential.
  • Answering: a result reaches the model only if it answers a call on the record that is on this thread, was issued by this route, and is still outstanding and unexpired. The server resumes that park internally.
  • What the client can't do: the client's message array is never trusted beyond that one string. An already-answered result is treated as resent history.
Package Adds
@b4run/sdk ClientToolCallStore, ClientToolRecorder, createMemoryClientToolCallStore
@b4run/sqlite-storage / @b4run/postgres-storage durable stores. The node default is SQLite at .b4/client-tool-calls.sqlite.
@b4run/core the client_<name> stub. It checks the clientTool permission once before the park, records the call, then parks. replayOnly rebuilds the stub for a parked call.
@b4run/permissions clientTool is an exact-match key and never inherits tool:<name>
@b4run/langchain a tool's run receives the provider's tool-call id
@b4run/cli definition bounds, AG-UI wiring, abandonment, hiding parks from clients, Agent Protocol guards, b4 check reserves client_*

Configuration: server.agui.clientToolTtlMs (default 10 min) and server.agui.clientToolStore.

Where the implementation departs from the spec

The spec's §3 table has been updated to match.

  1. A second result for an answered call is not a 409. AG-UI clients resend their whole history, so a result for an already-answered call is history and is ignored. Replay is still impossible: each park is resumed exactly once.
  2. Abandonment closes the call, then runs the new message. When the client never answers, abandoning a call writes an error ToolMessage into the checkpoint (updateState as the tools node). Results from sibling tools that finished are kept. The new user message then runs as an ordinary turn. A plain resume would have discarded that message.
  3. Results are tied to the route on the record. The issuing route is stored on the record when the call is issued (routeId), not read from parked_route metadata. That metadata is written after RUN_FINISHED, which raced with fast clients and went stale.

Security review

Every task had a spec review and a code-quality review, and the security-critical ones also had a mutation check. Each of these guards fails at least one test when removed:

  • answering only outstanding, same-thread records, including interruptId mismatch, voided rows and garbage expiry
  • the route binding on the record
  • dropping client parks from the AG-UI stream and the live tap
  • the clientTool exact match, and deny winning over a tool: allow
  • bypassCache for per-run tools
  • deciding the permission once before the park, and validating the resume value
  • replay-only rebuilt stubs
  • closing before voiding
  • the claim/run-slot rechecks
  • the oversized-result screen, which stops a resent over-cap result from permanently blocking a thread
  • the Agent Protocol /runs/* and /resume refusals while a client call is parked

A final review of the whole branch found one cross-cutting gap, fixed here: an Agent Protocol run could silently discard a parked client call and leave the thread permanently broken.

Behaviour changes (in the changeset)

  • Reserved names: tools named client_* are reserved; a route defining one fails preparation, and b4 check reports it.
  • Body cap: AG-UI request bodies are limited to 8 MiB on every route.
  • Resuming: a trailing tool message counts as resuming for thread-access policies, and it takes the thread's resume claim.
  • Refusals while a client call is parked: /threads/:id/runs/stream, /runs/wait and /resume return 409 client_tool_pending.
  • Grants: AG-UI turns now void superseded approval grants at settle, which closes an existing gap.

Follow-ups (not in this PR)

  • Advertising the capability to clients (§9).
  • Keeping the record for all tool calls, not only client ones (§7).
  • Pruning client-tool records.
  • Splitting up the AG-UI handler.
  • Binding AG-UI approval resumes to the parked route. That is an existing gap, filed as a separate task.

Test plan

  • pnpm build, pnpm typecheck, pnpm lint, node scripts/check-docs.mjs, node scripts/check-changesets.mjs, seo:lastmod:check / :routes
  • Full @b4run/cli suite: 2524 passed, 4 skipped. The client-tool and resume suites pass, 185/185.
  • @b4run/core, @b4run/sdk, @b4run/sqlite-storage, @b4run/permissions, @b4run/langchain, apps/web
  • B4_TEST_PGSTORAGE=1 @b4run/postgres-storage: 141/141 on real Postgres, including concurrent single-winner answers and answer-vs-void races
  • CI, including copilotkit-examples-e2e and harness-verify

🤖 Generated with Claude Code

blove and others added 30 commits September 30, 2026 14:52
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…void

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…corded first

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed resume to the model

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rter honors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…prefix

prepareRouteExecution takes clientTools: on an agent() route each becomes a
client_<name> stub (filePath <client:name>), appended after tool scoping and
the approve/constrain wrapping, and the preparation bypasses the compiled-agent
cache, which is keyed without tools. Any other route kind refuses them. An
authored or capability tool named client_* fails route preparation.

streamResolvedRoute forwards clientTools to preparation and clientToolRecorder
to the adapter, which sets configurable[CLIENT_TOOL_RECORDER_KEY] after route
params so a URL param cannot shadow it. materializeResolvedRouteGraph takes
clientTools so a caller can rebuild the parked run's graph. RouteResumePayload
widens internally to carry a ClientToolResumeValue; the HTTP resume body is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent's tool name

A client tool call parks with interrupt() like a permission prompt, but the
client answers it with a role:"tool" message on its next run. So:

- normalizeB4Stream drops client-tool-call interrupt chunks (the turn still
  records as parked upstream in observeInterrupts) and ends with an ordinary
  RUN_FINISHED; it un-prefixes client_<n> to <n> on tool_call, tool_result
  and tool_call_args when <n> is one of this run's client tools (default:
  none, so behaviour is unchanged until the handler passes the set).
- GET /threads/:id/pending_interrupts and the attach state frame omit
  client parks.
- POST /threads/:id/resume and the AG-UI resume path exclude client parks
  before the exact-set match and the grant gate: naming one is stale (alone)
  or a set mismatch, and a permission park pending beside one is answerable
  on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed; keep parks off the live tap

- POST /threads/:id/resume answers 409 client_tool_pending whenever the
  pending set holds a client tool park, alone or beside a permission park.
  A partial resume of the permission parks alone is unsafe: the answered
  park's __interrupt__ write survives until the superstep completes (so it
  is re-demanded), and the client task re-runs on this path with no client
  stubs bound, so ToolNode resolves the park destructively.
- The AG-UI handler keeps its interim filter, now documented: mixed parks
  must be resolved in ONE resume map covering every pending park.
- tapLiveTurn publishes the client's view through the same helper as
  normalizeB4Stream: no client park frames, un-prefixed client tool names.
- withoutClientToolParks recognises a client park by type alone, and fails
  closed (malformed) on a client-typed envelope missing its ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l call; pin the record's defences

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
closeAbandonedClientToolCalls writes one ToolMessage per parked client
call through graph.updateState(..., "tools"). LangGraph folds the parked
superstep's successful pending writes in first and drops its interrupt
writes, so a sibling server tool's result survives, ahead of ours, and no
park remains. No resume key is needed, so an unanswerable park closes the
same way. A new user message then runs as an ordinary turn; with no new
message, CONTINUE_AFTER_CLOSE (an empty resume map) lets the model reply.

Refuses, before writing, a pending non-client park, a thread with no
client park, and any call set that does not name exactly the latest
assistant turn's unresolved client_ calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The close captures the checkpoint id with the parks and refuses to write
unless the state read sees the same one; after the write, the new
checkpoint's parent must be that id. A close_incomplete after the write
means the thread was mutated: callers must not retry or run the turn.
Each call's name must be client_<toolName> of the unresolved call it
closes. Error messages are fixed strings and never echo client ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rip)

POST /agui/:routeId now issues, matches and resumes client tool calls
(#743, Model A):

- server.agui.clientToolTtlMs (default 600000, positive integer <= 1 year,
  anything else fails the boot) and server.agui.clientToolStore; node
  fallback resolveClientToolCallStore opens .b4/client-tool-calls.sqlite
  only when some route opts in. Boot-resolved into a ClientToolRuntime.
- The AG-UI body is read through readBoundedText (8 MiB, 413).
- Opted-in tools are validated (422 B4_E5401) and preflighted with
  checkRouteClientToolsSupport (422 client_tools_not_supported); no store
  -> 503 client_tool_store_unavailable, for new tools and for pending parks.
- A trailing tool message on an opted-in route counts as resuming; resumes
  and any turn on a thread with a client park pending take the resume claim
  before the snapshot the decision is made on.
- resolveClientToolTurn over the full snapshot: none -> normal turn with the
  per-request recorder and client tool names wired; partial -> RUN_STARTED +
  RUN_FINISHED without touching the graph; resume -> one Command covering the
  client results and any approval decisions; abandon -> INTERIM 409
  client_tool_pending (12b closes the calls).
- Results over 64 KiB for an outstanding park -> 413 before any answer.
- Parked stubs are rebuilt when a follow-up omits `tools`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…replay-only

Review fixes for the AG-UI client tool round trip (#743):

- A route that does not take client tools can no longer answer, resume or
  re-offer another route's client park: a pending client park there is
  409 client_tool_pending. A trailing role:"tool" message now counts as
  resuming on EVERY AG-UI route, so a thread-access step-up applies.
- Stubs rebuilt for parked calls are replay-only: createClientToolStub takes
  { replayOnly }, and a NEW call to such a stub returns
  CLIENT_TOOL_UNAVAILABLE_RESULT without gating, recording or parking.
- On an opted-in route, a trailing tool message that answers nothing pending
  is a RUN_STARTED/RUN_FINISHED no-op instead of re-running the last user
  turn, which also makes client retries idempotent.
- A request decided without the resume claim re-reads the snapshot under
  the run slot and refuses (409 client_tool_pending) if a client park
  appeared in between.
- resolveClientToolCallStore reopens an existing .b4/client-tool-calls.sqlite
  after the opt-in is removed, so parked threads are not bricked.
- Docs: thread-access `resuming` covers the trailing tool message; API
  table lists the two new core exports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urn decision

The post-begin recheck was skipped for any request holding the resume
claim. Since a trailing role:"tool" message now takes the claim on every
route, a claim-holding request could decide a new turn while another run was
still executing, and run past the client park that run left behind. The
recheck now follows the decision (no client park seen), not the claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run that does not answer a parked client tool call no longer gets the
interim 409 client_tool_pending. On an opted-in route, a new user message,
an expired call or an unanswerable park abandons the parked calls: under
the run slot, the calls are closed in the checkpoint on a graph built for
the route that parked them (answered calls keep their stored result, the
rest get ABANDONED_CLIENT_TOOL_RESULT), the abandoned records are voided
only after the close, and the turn runs — the new user message, or a
continuation so the model replies to the closed calls. A permission park
pending alongside is 409 resume_required. Close failures map to fixed
messages: 409 client_tool_close_failed, 500 client_tool_close_incomplete
(state may be mutated; the turn does not run), or a generic 500.

A route that is not opted in clears the dead end the same way on a new
user message, using only stored results (it never answers a tool
message); a trailing tool message, or a permission park alongside, stays
409 client_tool_pending.

Settling: the AG-UI handler now voids superseded approval grants as the
Agent Protocol handlers do, and a turn that settles without parking voids
the thread's outstanding client tool records (keeping any whose park is
still pending).

Recorder: has() is true only for a live record — outstanding, or answered
and resumed by this request — and record() refuses an id that already has
a record, so a provider-reused id can never park on an old answered or
voided record; the model sees a tool error instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the turn's instances

Review fixes for the AG-UI abandon (Task 12b):

- Only the route that PARKED a client call may answer or resume it. Before
  anything is stored, the opted-in branch reads the thread's parked_route;
  on any other route the request is treated like a non-opted one: tool
  messages are never answered, a trailing user message abandons (closing
  on the parked route's graph), anything else is 409 client_tool_pending.
- The close materializes the parked route with the same route-execution
  instances the turn uses (permissions and memory stores, route manifest,
  static modules, sandbox manager, thread id and store, run signal), so it
  no longer needs node fallbacks and matches the graph that parked.
  materializeResolvedRouteGraph accepts a threadId for this.
- The request's middleware context is reused for the close only when the
  parked route is this request's route; a missing or unresolvable
  parked_route is logged before the fail-closed 409.
- The settle-time client record void runs only once the route source has
  settled: inline for a drained or failed turn, after the source unwinds
  (and before the run slot is released) for an aborted one.
- Tests: cross-route answer refused and cross-route abandon, the close with
  per-route fallbacks armed to throw, close_incomplete and generic-500
  fault injection, and the keep-still-parked filter of the settle void.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`parked_route` is written only once a turn settles, after RUN_FINISHED has
reached the client, so it cannot decide who may answer a client tool call:
a fast same-route client was refused, a stale value let another route
answer, and a park landing on the abort path was never answerable.

ClientToolCallRecord gains `routeId`, the route key whose run issued the
call, written by the per-request recorder while the call is issued — before
the client can see it. The SDK memory store, the SQLite store and the
Postgres store carry it (`route_id` NOT NULL, no default; migration 1 is
edited in place because it has not shipped, and every INSERT names all
eleven columns).

The AG-UI handler answers or resumes client parks only when no parked
call's record names another route; otherwise the request is handled like
one on a non-opted route (tool messages ignored, a trailing user message
abandons, anything else is 409 client_tool_pending), decided before
anything is stored. The abandon's close materializes the route on the
parks' records (they must agree), falling back to `parked_route` only when
no parked call has a record at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t_ prefix

Route preparation already refuses a tool named client_*; b4 check now
reports an authored one statically, under B4_E1001, for every agent
route whether or not it declares a tool scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he changeset

- ag-ui: a Client-provided tools section (round trip, abandonment,
  limits, threat model, storage, error codes); the CopilotKit callout
  now says frontend tools 422 unless the route opts in, and that
  toolChoice is accepted but not interpreted
- configuration: server.agui clientTools/clientToolTtlMs/clientToolStore
- permissions: the reserved clientTool key (exact, allow by default,
  deny refuses, never inherits tool:<name>, no always); api/permissions
  behaviour contract permissions.clientTool.exact
- approval-grants: grants cover approval parks only
- cli: b4 check reports client_* tool names
- spec §3 failure table revised to what shipped
- changeset: patch for sdk, core, langchain, permissions, cli,
  sqlite-storage, postgres-storage

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
blove and others added 6 commits September 30, 2026 14:52
… answers

Every AG-UI client resends its history, so refusing any request that
still carried an over-cap result made the 413 permanent: every later
run on the thread, including a new user message after the TTL, was
refused. The 413 now applies only when the last message is a tool
message and an over-cap one answers an outstanding, unexpired parked
call. Otherwise the over-cap messages are dropped before the resolver
(which stores answers before deciding), so the call reads as unanswered
and is abandoned; the content never reaches the store or the model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- abandoned calls are written straight into the checkpoint (no tool-result
  frame, no offloading or stream transformers); expiry is checked on next
  use; a result stored before expiry is used, a later one discarded
- an approval and a client call parked together are answered in one run;
  any route can abandon; a non-issuing route over a pending approval gets
  client_tool_pending
- a trailing tool message that matches nothing is an empty run
- the 413 applies only when answering; otherwise the over-cap result is
  dropped
- the definitions budget is 32,768 characters, results 64 KiB of UTF-8
- copilotkitSuggest parks like any client tool on an opted-in route

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /threads/:id/runs/stream and /runs/wait now answer 409
client_tool_pending while a client tool call is parked, read under the
run slot after the thread-access gate. A new run's input made LangGraph
discard the park, leaving the model's tool call with no ToolMessage:
every later model call on the thread was rejected by the provider, and
no park remained for an AG-UI abandon to close. /resume shares the
refusal's fixed message, which now names answering or abandoning over
AG-UI.

Also reuse clientToolCallIds() for the resuming ids and the settle-time
void instead of inline copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, raw state

Also list the remaining behaviour changes in the changeset: AG-UI
turns void superseded grants at settle, a no-op trailing tool message,
the resume claim on a trailing tool message, and the Agent Protocol
run refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The run endpoints now read pending state for a parked client call, so
the fixture's read-once assertion ignores reads made while seeding the
thread's route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
b4-run Ready Ready Preview Sep 30, 2026 9:54pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@blove
blove merged commit 3b1be6e into main Sep 30, 2026
30 of 31 checks passed
@blove
blove deleted the blove/client-provided-tools-impl branch September 30, 2026 23:07
blove added a commit that referenced this pull request Oct 3, 2026
…#909)

* docs: design for generalizing the tool-call record to every tool call

Follow-up to #880 (§7, open question 7): server rows as identity only,
scope limited to runs with a resolved store, per-thread prune with a
configurable retention, and pendingToolCallIds derived from the record.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: state why the kind column carries a backfill-only default

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: implementation plan for generalizing the tool-call record

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: plan self-review fixes (column order, fixture route)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: prune runs under the run slot, after it is taken

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(sdk): tool-call record carries a kind and a settle; store gains settle and prune

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: plan — SQL stores share the SDK's canonical-before rule for prune

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(sqlite-storage): tool-call record migration 2 (kind, settled_at), settle and prune

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(postgres-storage): tool-call record migration 2 (kind, settled_at), settle and prune

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(core): client tool stubs carry a clientTool marker

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(langchain): record every server tool call through the run's recorder

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: spec — task calls are not recorded; a permission park does not settle

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(langchain): a permission park does not settle the server row

A gate that parks by throwing GraphInterrupt is issued but not settled;
the resumed re-execution issues again (no-op on the key) and settles when
the tool really returns or throws.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(ag-ui): pendingToolCallIds may be supplied asynchronously

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(ag-ui): a rejecting pendingToolCallIds read ends the run as RUN_ERROR

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(cli): server.agui.toolCallRetentionMs, and the store shape check requires settle and prune

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(cli): the turn resolver states the record invariant; partial carries no ids

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(cli): optional chain in answeredResult guard

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cli): the server-kind guard in the turn resolver is exercised on its own

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(cli): record every tool call on a run with a store, prune per thread, derive pendingToolCallIds from the record

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: spec — pending ids scoped to the request's parks; prune cutoff never shorter than the TTL

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(cli): prune never outruns the TTL; pendingToolCallIds scoped to this run's parks

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: the tool-call record covers every tool call; toolCallRetentionMs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* chore(web): regenerate SEO lastmod for the AG-UI and configuration docs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs: name the leftover-store case, refresh stale recorder comments, note rolling upgrades

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(web): regenerate SEO lastmod for the AG-UI docs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(web): regenerate SEO lastmod after merging main

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: spec — pruning reconciled with #898 at merge

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: server rows in the prune docs; sdk record row wording; sweep test comment

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(web): regenerate SEO lastmod for the cli, ag-ui and sdk api docs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(web): regenerate SEO lastmod after merging main

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

This branch was successfully deployed

1 active and 1 inactive deployments
Preview — 7a0327de Deployed Sep 30, 2026 by vercel[bot]
vercel-preview — 7a0327de Deployed Sep 30, 2026 by blove via vercel-native #2509
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement client-provided tools (RunAgentInput.tools), or stop accepting the field

1 participant