cloudflare / cloudflare/agents

No per-turn correlator exposed to beforeToolCall / onChatResponse — cross-hook turn identity cannot be threaded safely

Open
#2,127 0 comments 0 reactions 0 assignees View on GitHub
think
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.