blob: 9c29e8a47e5ea38ecb473abb36f2b1eb4afe947d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* PR-UI-C0 review fixup (@kenji msg 7885a347) — the Anthropic
* extended-thinking stream the renderer accumulates from
* `ThinkingDeltaEvent` / `ThinkingCompleteEvent`.
*
* The original C0 implementation appended `event.text` directly
* into the live-turn projection and rendered with
* `<pre>{thinkingText}</pre>` — no Markdown, no redaction, no size
* cap. Two trust-boundary failures: (1) model thinking output can
* echo prompts / env / tool stderr / pasted credentials, so the
* raw text must NOT enter React state without secondary
* `redactSecrets`; (2) extended thinking can stream tens or
* hundreds of KB, and `<pre>` `max-height: 320px` only bounds
* VISUAL height, not the DOM text length / React state / DevTools
* snapshot.
*
* The pipeline that answers both lives in `stream-delta`, shared with
* `assistant-stream`; everything below is thinking's own caps, markers, and
* recovery direction.
*
* The renderer stores both the accumulated text AND a per-session
* monotonic `truncated` flag so the UI can show a "已截断" pill
* in the `ReasoningPanel` header.
*/
import type { UiLocale } from '@maka/core/ui-locale';
import { getSharedUiCopy } from './shared-ui-copy.js';
import {
applyStreamComplete,
applyStreamDelta,
type ApplyStreamOptions,
type ApplyStreamResult,
} from './stream-delta.js';
/**
* Default caps. Tuned to:
* - 4 KB per single delta: matches A3 tool-output's per-chunk
* cap and the runtime's `TOOL_OUTPUT_DELTA_MAX_CHARS`.
* - 32 KB total per session: thinking can run longer than tool
* stream (multiple paragraphs of reasoning before the answer),
* so 2× A3's per-tool cap. Above this we tail-keep so the
* "most recent" reasoning is what the user sees scrolling.
*/
export const THINKING_MAX_DELTA_CHARS = 4 * 1024;
export const THINKING_MAX_TOTAL_CHARS = 32 * 1024;
export interface ApplyThinkingOptions extends ApplyStreamOptions {
/** Resolved UI locale for user-visible truncation markers. */
locale: UiLocale;
}
export type ApplyThinkingResult = ApplyStreamResult;
/** Apply a single `thinking_delta` to the prior accumulated text. */
export function applyThinkingDelta(
prev: string,
rawDelta: string,
options: ApplyThinkingOptions,
): ApplyThinkingResult {
const copy = getSharedUiCopy(options.locale).stream;
return applyStreamDelta(prev, rawDelta, {
maxDeltaChars: options.maxDeltaChars ?? THINKING_MAX_DELTA_CHARS,
maxTotalChars: options.maxTotalChars ?? THINKING_MAX_TOTAL_CHARS,
recovery: 'tail',
chunkMarker: copy.thinkingChunkTruncated,
totalMarker: copy.thinkingHeadTruncated,
...(options.redactionState === undefined
? {}
: { redactionState: options.redactionState }),
});
}
/**
* Apply a `thinking_complete` final payload. The provider's
* `ThinkingCompleteEvent.text` is the FULL final thinking text
* (not an incremental delta), so we replace rather than append.
*/
export function applyThinkingComplete(
rawText: string,
options: ApplyThinkingOptions,
): ApplyThinkingResult {
return applyStreamComplete(rawText, {
maxTotalChars: options.maxTotalChars ?? THINKING_MAX_TOTAL_CHARS,
recovery: 'tail',
totalMarker: getSharedUiCopy(options.locale).stream.thinkingHeadTruncated,
});
}