cloudflare / cloudflare/agents

Think: `TurnResult.message` is the session leaf, not the message this turn produced

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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.