cloudflare / cloudflare/agents
Think: `TurnResult.message` is the session leaf, not the message this turn produced
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
## Summary
`TurnResult.message` is documented as part of the wait-mode result, but `_enrichTurnResult` fills it from `session.getLatestLeaf()` after the turn queue has released. That is the newest message in the conversation, not the assistant message this turn produced.
Two consequences:
1. A queued follow-up can already have appended its user message, so a completed wait turn resolves with `message: undefined` (see #2243).
2. After `submitMessages` + `inspectSubmission`, there is no message at all: `ThinkSubmissionInspection` has `submissionId`, `status`, `error`, timestamps — no `message`. A workflow that admitted the turn by submission id cannot tell which assistant message that turn wrote.
## Environment
- `@cloudflare/think@0.17.0`
- `runTurn({ mode: "wait" })` and `submitMessages` / `inspectSubmission`
## What the types allow
```ts
type TurnResult = SaveMessagesResult & {
message?: SessionMessage;
continuation: boolean;
};
type ThinkSubmissionInspection = {
submissionId: string;
idempotencyKey?: string;
requestId?: string;
status: ThinkSubmissionStatus;
error?: string;
metadata?: Record;
createdAt: number;
startedAt?: number;
completedAt?: number;
};
```
## Reproduction (wait)
Two `runTurn({ mode: "wait" })` calls on the same DO, overlapping:
```ts
await Promise.all([
stub.runTurn({ mode: "wait", input: "q1" }),
stub.runTurn({ mode: "wait", input: "q2" }),
]);
```
The first result can be `{ status: "completed", message: undefined }` because the leaf is already the second turn's user message. #2243 has the `_enrichTurnResult` / `TurnQueue` trace.
## Reproduction (submit)
```ts
const { submissionId } = await agent.submitMessages([userMessage], {
idempotencyKey: "import:instance:capture:homes",
});
const inspection = await agent.inspectSubmission(submissionId);
// inspection.status === "completed"
// inspection.message is not a field
```
The caller has to walk `getMessages()` and guess which assistant row belongs to this submission.
## Workaround
Record the user message id at admit time and take the first assistant after it in `getMessages()`. That is stable for both wait and submit. For wait, do not trust `result.message`.
## Suggested fix
Attach the assistant message produced by *this* turn to the result, captured while the queue slot is still held (or looked up by `requestId`). For submissions, put that message (or its id) on `ThinkSubmissionInspection` once `status === "completed"`.
Contributor guide
Research direction
Start with _enrichTurnResult and the TurnQueue trace referenced in #2243, then compare runTurn({ mode: "wait" }) with submitMessages and inspectSubmission. Reproduce overlapping turns and inspect the result and submission fields. Done means each completed turn identifies the assistant message it produced, including after submission inspection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend-api-design, distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100