cloudflare / cloudflare/agents
No per-turn correlator exposed to beforeToolCall / onChatResponse — cross-hook turn identity cannot be threaded safely
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
**What we're building.** A durable, exactly-once "hard stop this turn" mechanism on top of `Think` (agents 0.20.1 / think 0.15.1): a stop request must bind to the *specific* turn the user saw, survive retries/eviction, and never affect a later turn.
**The gap.** A subclass has no way to correlate SDK-invoked hooks with the turn they belong to:
- `TurnContext` (backing `beforeTurn`) carries `system/messages/tools/model/continuation/body` — no turn/request id, no `abortSignal`.
- `ToolCallContext` (backing `beforeToolCall`) documents exactly `stepNumber/messages/abortSignal` — no correlator.
- `onChatResponse` is likewise a plain override invoked by the SDK with no per-turn handle.
- `AsyncLocalStorage` propagation was tested empirically and does not bridge these hooks (the SDK re-enters via its own snapshots).
- Interactive ws-chat turns never create a `cf_think_submissions` row, so the submissions table cannot serve as identity for the primary chat path either (only `submitMessages` queue turns get rows).
**Consequence.** Any state a subclass keeps between `beforeTurn` and later hooks must live in shared mutable instance fields, which a subsequent turn's admission can overwrite — making "stop exactly this turn" impossible to guarantee for overlapping/rerun shapes. We currently work around it with closure-captured identity in `stopWhen`, self-minted ids at admission, and a one-shot token around submission execution, but `beforeToolCall`/`onChatResponse` remain structurally uncorrelatable.
**Ask.** Expose a stable per-turn correlator (the internal `requestId` would do) on `TurnContext`, `ToolCallContext`, and the `onChatResponse` payload — or accept a callback-config shape where `beforeTurn` can return per-turn closures for these hooks.
**Related observation (possibly a separate issue):** `runTurn({mode:"submit"})` drains with `options.body ?? this._lastBody`, so a queue turn silently inherits the previous interactive turn's custom body fields — surprising when body carries per-turn client correlation data.
Contributor guide
Research direction
Start by tracing the TurnContext and ToolCallContext hook contracts, the onChatResponse payload, and the existing stopWhen and runTurn({mode:"submit"}) paths described in the issue. Compare exposing a stable per-turn correlator with returning per-turn hook closures; done means overlapping and retried turns can be identified safely across all three hooks without relying on shared mutable state.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100