feat(agent-loop): surface max-tokens as a distinct turn-end reason

Add a `max-tokens` variant to `TurnEndReasonMap` and carry the model
finish reason up from `runStep` to `runTurn`, applying the rule "any
max-tokens step in the turn surfaces as max-tokens" (disposed/aborted/
error still take precedence). This lets consumers distinguish a clean
stop from a truncated one — the contract RFC 010's ACP bridge maps to
the `max_tokens` stop reason.

Also add an AGENTS.md rule: write an ADR when (and only when) a PR makes
a durable, contested, surprising decision.
This commit is contained in:
Tianyi Cui
2026-06-15 23:53:47 +08:00
parent 11e166db71
commit add59a3336
8 changed files with 156 additions and 5 deletions

View File

@@ -12,6 +12,21 @@ export function textResponse(text: string): StreamChunk[] {
]
}
/**
* Like {@link textResponse} but the stream ends with a `max-tokens` finish —
* the model was cut off at the output-token ceiling (DeepSeek's `length`).
* Used to exercise the turn-end `max-tokens` surfacing rule.
*/
export function maxTokensResponse(text: string): StreamChunk[] {
return [
{ type: 'block-start', index: 0, blockType: 'text' },
...Array.from(text, (char): StreamChunk => ({ type: 'text-delta', index: 0, text: char })),
{ type: 'block-end', index: 0, block: { type: 'text', text } },
{ type: 'usage', usage: { inputTokens: 10, outputTokens: text.length } },
{ type: 'finish', reason: { kind: 'max-tokens' } },
]
}
export function toolCallResponse(rawCallId: string, name: string, args: object, text?: string): StreamChunk[] {
const callId = CallId(rawCallId)
const argumentsJson = JSON.stringify(args)