///|
/// Lux IR 流式事件 — 块生命周期模型(v1)
///
/// 依据:docs/lux-ir-design.md §7
/// canonical 选 Anthropic 块生命周期(四家里语义最完备),补齐 Discard/Meta/Annotations
///|
/// LucentErrorKind — 错误类型化
pub(all) enum LucentErrorKind {
RateLimit
InvalidRequest
Authentication
ServerError
ContentFilter
Native(String)
} derive(Eq, Debug)
///|
/// LucentError — 错误结构化
pub struct LucentError {
kind : LucentErrorKind
message : String
provider_code : String?
} derive(Eq, Debug)
///|
/// LucentConversationMeta — 会话元信息(ConversationStart 携带)
pub struct LucentConversationMeta {
id : String?
model : String?
usage_input : Int?
} derive(Eq, Debug)
///|
/// LucentBlockType — 块类型(类型化,替代字符串)
pub(all) enum LucentBlockType {
Text
ToolCall(String, String) // id, name(块开始时若已知)
Thinking
Refusal
Image
Audio
Video
Native(String)
} derive(Eq, Debug)
///|
/// LucentBlockDelta — 块增量(类型化)
pub(all) enum LucentBlockDelta {
TextDelta(String)
ThinkingDelta(String)
SignatureDelta(String) // Anthropic signature_delta / Responses signature
ToolArgumentsDelta(String) // JSON 参数片段
RefusalDelta(String)
NativeDelta(String, Json) // 厂商私有增量
} derive(Eq, Debug)
///|
/// LucentStreamEvent — 流事件(canonical:块生命周期)
pub(all) enum LucentStreamEvent {
/// 会话开始(Anthropic message_start / Responses response.created)
ConversationStart(LucentConversationMeta)
/// 新会话项开始(Responses output_item.added)
ItemStart(Int, LucentConversationItem) // item_index, item skeleton
/// 内容块开始(Anthropic content_block_start)
BlockStart(Int, LucentBlockType) // block_index, 类型化块类型
/// 内容块增量(Anthropic content_block_delta / OpenAI delta.content / Gemini parts[].text)
BlockDelta(Int, LucentBlockDelta) // block_index, 类型化增量
/// 内容块结束(Anthropic content_block_stop)
BlockEnd(Int) // block_index
/// 会话项结束(Responses output_item.done)
ItemEnd(Int) // item_index
/// 块丢弃(Gemini 全量帧覆盖前一帧时用,其他厂商不触发)
BlockDiscard(Int) // block_index
/// 标注事件(OpenAI Responses annotations.delta / 各家 URL/file 引用)
Annotations(Int, Array[LucentAnnotation]) // block_index, annotations
/// 结束原因
Finish(LucentFinishReason)
/// Token 用量
Usage(LucentUsage)
/// 错误
Error(LucentError)
/// 流结束
Done
} derive(Eq, Debug)
///|
/// 构造器与辅助
///|
pub fn LucentErrorKind::from_string(s : String) -> LucentErrorKind {
match s {
"rate_limit" | "rate_limit_exceeded" | "429" => RateLimit
"invalid_request" | "invalid_request_error" | "400" => InvalidRequest
"authentication" | "auth_error" | "401" | "403" => Authentication
"server_error" | "internal_server_error" | "500" | "503" => ServerError
"content_filter" | "content_policy" => ContentFilter
other => Native(other)
}
}
///|
pub fn LucentError::new(
kind : LucentErrorKind,
message : String,
provider_code : String?,
) -> LucentError {
{ kind, message, provider_code }
}
///|
pub fn LucentError::simple(message : String) -> LucentError {
{ kind: Native(""), message, provider_code: None }
}
///|
pub fn LucentConversationMeta::new(
id : String?,
model : String?,
usage_input : Int?,
) -> LucentConversationMeta {
{ id, model, usage_input }
}
///|
pub fn LucentBlockType::text() -> LucentBlockType {
Text
}
///|
pub fn LucentBlockType::tool_call(
id : String,
name : String,
) -> LucentBlockType {
ToolCall(id, name)
}
///|
pub fn LucentBlockType::thinking() -> LucentBlockType {
Thinking
}
///|
pub fn LucentBlockType::refusal() -> LucentBlockType {
Refusal
}
///|
pub fn LucentBlockType::native(tag : String) -> LucentBlockType {
Native(tag)
}
///|
pub fn LucentBlockDelta::text_delta(s : String) -> LucentBlockDelta {
TextDelta(s)
}
///|
pub fn LucentBlockDelta::thinking_delta(s : String) -> LucentBlockDelta {
ThinkingDelta(s)
}
///|
pub fn LucentBlockDelta::signature_delta(s : String) -> LucentBlockDelta {
SignatureDelta(s)
}
///|
pub fn LucentBlockDelta::tool_arguments_delta(s : String) -> LucentBlockDelta {
ToolArgumentsDelta(s)
}
///|
pub fn LucentBlockDelta::refusal_delta(s : String) -> LucentBlockDelta {
RefusalDelta(s)
}
///|
pub fn LucentBlockDelta::native_delta(
tag : String,
raw : Json,
) -> LucentBlockDelta {
NativeDelta(tag, raw)
}
///|
/// 流事件累加器 — 将 LucentStreamEvent 流累加为 LucentResponse
///
/// 用于:
/// - 宿主语言选择「流式接收但累加成完整响应」的简化场景
/// - 跨协议一致性测试的断言依据
///
/// 累加规则:
/// - 按 item_index / block_index 跟踪活跃块
/// - Text 块累加 Text 内容
/// - ToolCall 块累加 ToolArguments 片段,结束时用 BlockStart 携带的 id/name 构造 ToolUse
/// - Thinking 块累加 Thinking + Signature 增量
/// - Refusal 块累加 Refusal 增量
/// - Gemini 风格的 BlockDiscard 会清空该块的已累加内容(仅保留 block 类型信息以备后续 BlockStart)
/// - ConversationStart 提取 id/model 用于响应
/// - Usage/Finish/Attachments 最后写入响应
///|
/// AccumulatedResponse — 累加器产物:response + 未进入 response 的辅助信息
pub struct AccumulatedResponse {
response : LucentResponse
annotations : Array[LucentAnnotation]
native_events : Array[LucentStreamEvent]
agent_actions : Array[LucentAgentAction]
diagnostics : Array[ConversionDiagnostic]
} derive(Eq, Debug)
///|
/// 完整累加:把流事件合成为 LucentResponse,同时保留 annotations/native_events/agent_actions
/// 及对应诊断。原 lucent_stream_events_to_response 是其薄包装(取 .response)。
pub fn lucent_stream_events_to_accumulated(
events : Array[LucentStreamEvent],
fallback_id : String,
fallback_model : String,
) -> AccumulatedResponse {
// 会话元信息(ConversationStart 携带,缺则用 fallback)
let mut resp_id = fallback_id
let mut resp_model = fallback_model
let mut usage : LucentUsage? = None
let mut finish : LucentFinishReason = Stop
let mut had_error = false
// 累加内容:单一 assistant 消息(流式语义:一个响应对应一个 assistant 回复)
let content : Array[LucentContent] = []
// 消息级 reasoning(vLLM/DeepSeek/Fireworks 流式累加,非流式一致)
let mut reasoning : LucentThinking? = None
// 块状态跟踪:block_index -> (类型, 已累加的文本)
// 用线性数组替代 Map(MoonBit 风格),下标即 block_index
let block_types : Array[LucentBlockType] = []
let block_texts : Array[StringBuilder] = []
let block_thinking : Array[StringBuilder] = []
let block_signatures : Array[StringBuilder] = []
let block_tool_ids : Array[String] = []
let block_tool_names : Array[String] = []
// 辅助信息侧信道(不进入 response 主体)
let annotations : Array[LucentAnnotation] = []
let native_events : Array[LucentStreamEvent] = []
let agent_actions : Array[LucentAgentAction] = []
let diagnostics : Array[ConversionDiagnostic] = []
///| 块辅助:确保 block_index 在跟踪数组中占位
fn ensure_block(idx : Int) -> Unit {
while block_types.length() <= idx {
block_types.push(Text) // 占位,真实类型由 BlockStart 覆盖
block_texts.push(StringBuilder())
block_thinking.push(StringBuilder())
block_signatures.push(StringBuilder())
block_tool_ids.push("")
block_tool_names.push("")
}
}
///| 块辅助:把一段 thinking 累加进消息级 reasoning(文本拼接,签名取最后,redacted 取 OR)
fn merge_reasoning(th : LucentThinking) -> Unit {
let new_text = match reasoning {
Some(acc) => acc.text + th.text
None => th.text
}
let new_sig = match th.signature {
Some(s) => Some(s)
None =>
match reasoning {
Some(acc) => acc.signature
None => None
}
}
let new_redacted = match reasoning {
Some(acc) => acc.redacted || th.redacted
None => th.redacted
}
reasoning = Some(LucentThinking::new(new_text, new_sig, new_redacted, None))
}
///| 块辅助:按类型把已累加内容刷入 content 数组
fn flush_block(idx : Int) -> Unit {
ensure_block(idx)
match block_types[idx] {
Text => {
let s = block_texts[idx].to_string()
if s != "" {
content.push(LucentContent::text(s))
block_texts[idx].reset()
}
}
ToolCall(_id, _name) => {
let id = block_tool_ids[idx]
let name = block_tool_names[idx]
let args = block_texts[idx].to_string()
content.push(LucentContent::tool_use(id, name, args, None))
block_texts[idx].reset()
}
Thinking => {
// 优先取独立 thinking 缓冲;空则回退 block_texts(兼容旧事件流)
let th_text = if !block_thinking[idx].is_empty() {
block_thinking[idx].to_string()
} else {
block_texts[idx].to_string()
}
let th = LucentThinking::new(
th_text,
match block_signatures[idx].to_string() {
"" => None
s => Some(s)
},
false,
None,
)
merge_reasoning(th)
block_texts[idx].reset()
block_thinking[idx].reset()
block_signatures[idx].reset()
}
Refusal => {
let s = block_texts[idx].to_string()
if s != "" {
content.push(Refusal(s))
block_texts[idx].reset()
}
}
Image | Audio | Video | Native(_) => () // 流中不累加媒体块
}
}
///| 块辅助:丢弃该块已累加内容(Gemini 全量帧覆盖语义),保留类型信息
fn discard_block(idx : Int) -> Unit {
ensure_block(idx)
block_texts[idx].reset()
block_thinking[idx].reset()
block_signatures[idx].reset()
block_tool_ids[idx] = ""
block_tool_names[idx] = ""
}
let mut i = 0
while i < events.length() {
match events[i] {
ConversationStart(meta) => {
match meta.id {
Some(id) => resp_id = id
None => ()
}
match meta.model {
Some(m) => resp_model = m
None => ()
}
}
ItemStart(_, item) =>
// Responses 风格:独立 Item(非 Message 内的块)
// 对于 ToolCall/ToolResult/Reasoning/AgentAction 直接 flush 到 content
match item {
Message(m) =>
// 流中单 assistant 消息模型:把 Message 的内容并入 content
for c in m.content {
content.push(c)
}
ToolCall(tu) => content.push(ToolUse(tu))
ToolResult(tr) => content.push(ToolResult(tr))
Reasoning(th) => merge_reasoning(th)
AgentAction(aa) => {
// v1 不展开到 response,保留到侧信道 + 诊断
agent_actions.push(aa)
diagnostics.push(
ConversionDiagnostic::new(
"item.agent_action",
Unsupported,
Some("agent action not expanded in v1 response"),
),
)
}
}
BlockStart(idx, bt) => {
ensure_block(idx)
block_types[idx] = bt
block_texts[idx].reset()
block_thinking[idx].reset()
block_signatures[idx].reset()
match bt {
ToolCall(id, name) => {
block_tool_ids[idx] = id
block_tool_names[idx] = name
}
_ => ()
}
}
BlockDelta(idx, delta) => {
ensure_block(idx)
match delta {
TextDelta(s) => block_texts[idx].write_string(s)
ThinkingDelta(s) => {
// 思考增量独立累加,不污染文本缓冲
block_thinking[idx].write_string(s)
// 块类型非 Thinking 时(如未显式 BlockStart(Thinking))→ Degraded 诊断
match block_types[idx] {
Thinking => ()
_ =>
diagnostics.push(
ConversionDiagnostic::new(
"delta.thinking",
Degraded,
Some("thinking delta on non-thinking block"),
),
)
}
}
SignatureDelta(s) => block_signatures[idx].write_string(s)
ToolArgumentsDelta(s) => block_texts[idx].write_string(s)
RefusalDelta(s) => {
// Refusal 块类型若未显式 BlockStart,按 Refusal 处理
match block_types[idx] {
Text => block_types[idx] = Refusal
_ => ()
}
block_texts[idx].write_string(s)
}
NativeDelta(vendor, raw) => {
// 厂商私有增量:保留到侧信道 + 诊断
native_events.push(events[i])
diagnostics.push(
ConversionDiagnostic::new(
"delta.native",
Unsupported,
Some(
"native delta (" +
vendor +
") not accumulated: " +
raw.stringify(),
),
),
)
}
}
}
BlockEnd(idx) => flush_block(idx)
ItemEnd(_) => ()
BlockDiscard(idx) => discard_block(idx)
Annotations(_, anns) => {
// 标注事件:保留到侧信道 + 诊断
for a in anns {
annotations.push(a)
}
diagnostics.push(
ConversionDiagnostic::new(
"item.annotations",
Unsupported,
Some("annotations not merged into v1 response"),
),
)
}
Finish(fr) => finish = fr
Usage(u) => usage = Some(u)
Error(_) => {
had_error = true
finish = Error
}
Done => ()
}
i = i + 1
}
// 流结束时未闭合的块:按块索引顺序 flush
let mut j = 0
while j < block_types.length() {
// 仅 flush 有内容的块(避免空 Text 块污染)
if !block_texts[j].is_empty() ||
!block_signatures[j].is_empty() ||
!block_thinking[j].is_empty() {
flush_block(j)
}
j = j + 1
}
// 构造 assistant 消息:若内容为空且发生错误,保留空消息;否则用累加内容
let final_content : Array[LucentContent] = if had_error &&
content.length() == 0 {
[LucentContent::text("")]
} else {
content
}
let message : LucentMessage = {
role: Assistant,
content: final_content,
phase: None,
reasoning,
}
let choice = LucentChoice::new(0, message, finish, None)
{
response: LucentResponse::new(
resp_id,
resp_model,
None,
[choice],
usage,
None,
),
annotations,
native_events,
agent_actions,
diagnostics,
}
}
///|
/// 薄包装:取累加结果的 response(既有调用方兼容)
pub fn lucent_stream_events_to_response(
events : Array[LucentStreamEvent],
fallback_id : String,
fallback_model : String,
) -> LucentResponse {
lucent_stream_events_to_accumulated(events, fallback_id, fallback_model).response
}
///|
/// 便利:从 LucentStreamEvent 流中提取所有 Annotations 事件(宿主语言收集标注用)
pub fn lucent_collect_annotations(
events : Array[LucentStreamEvent],
) -> Array[LucentAnnotation] {
let result : Array[LucentAnnotation] = []
let mut i = 0
while i < events.length() {
match events[i] {
Annotations(_, anns) =>
for a in anns {
result.push(a)
}
_ => ()
}
i = i + 1
}
result
}