///|
/// 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)
///|
/// explicit: Eq::equal kept as a regular method (implicit promotion is deprecated, E0079)
pub extend LucentErrorKind with Eq::{equal}
///|
#deprecated("Use `!=` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentErrorKind with Eq::{not_equal}
///|
#deprecated("Use `Debug::to_repr` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentErrorKind with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// LucentError — 错误结构化
pub struct LucentError {
kind : LucentErrorKind
message : String
provider_code : String?
} derive(Eq, Debug)
///|
/// explicit: Eq::equal kept as a regular method (implicit promotion is deprecated, E0079)
pub extend LucentError with Eq::{equal}
///|
#deprecated("Use `!=` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentError with Eq::{not_equal}
///|
#deprecated("Use `Debug::to_repr` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentError with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// LucentConversationMeta — 会话元信息(ConversationStart 携带)
///
/// 通用字段(所有/多家 provider 共有):
/// - `id`:会话/响应 ID
/// - `model`:模型名
/// - `usage_input`:输入 token 数
/// - `created_at`:创建时间戳(OpenAI 系的 created / created_at)
///
/// 非通用字段通过 `extra` 透传(如 OpenAI Responses 的 instructions、status 等)。
pub struct LucentConversationMeta {
id : String?
model : String?
usage_input : Int?
created_at : Int?
extra : Json?
} derive(Eq, Debug)
///|
/// explicit: Eq::equal kept as a regular method (implicit promotion is deprecated, E0079)
pub extend LucentConversationMeta with Eq::{equal}
///|
#deprecated("Use `!=` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentConversationMeta with Eq::{not_equal}
///|
#deprecated("Use `Debug::to_repr` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentConversationMeta with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// LucentBlockType — 块类型(类型化,替代字符串)
pub(all) enum LucentBlockType {
Text
ToolCall(String, String) // id, name(块开始时若已知)
Thinking
Refusal
Image
Audio
Video
Native(String)
} derive(Eq, Debug)
///|
/// explicit: Eq::equal kept as a regular method (implicit promotion is deprecated, E0079)
pub extend LucentBlockType with Eq::{equal}
///|
#deprecated("Use `!=` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentBlockType with Eq::{not_equal}
///|
#deprecated("Use `Debug::to_repr` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentBlockType with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// 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)
///|
/// explicit: Eq::equal kept as a regular method (implicit promotion is deprecated, E0079)
pub extend LucentBlockDelta with Eq::{equal}
///|
#deprecated("Use `!=` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentBlockDelta with Eq::{not_equal}
///|
#deprecated("Use `Debug::to_repr` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentBlockDelta with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// 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)
///|
/// explicit: Eq::equal kept as a regular method (implicit promotion is deprecated, E0079)
pub extend LucentStreamEvent with Eq::{equal}
///|
#deprecated("Use `!=` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentStreamEvent with Eq::{not_equal}
///|
#deprecated("Use `Debug::to_repr` instead", skip_current_package=true)
#doc(hidden)
pub extend LucentStreamEvent with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// 构造器与辅助
///|
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, created_at: None, extra: None, }
}
///|
/// 创建完整的 LucentConversationMeta(含 created_at 和 extra)
pub fn LucentConversationMeta::new_full(
id : String?,
model : String?,
usage_input : Int?,
created_at : Int?,
extra : Json?,
) -> LucentConversationMeta {
{ id, model, usage_input, created_at, extra, }
}
///|
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)
///|
/// explicit: Eq::equal kept as a regular method (implicit promotion is deprecated, E0079)
pub extend AccumulatedResponse with Eq::{equal}
///|
#deprecated("Use `!=` instead", skip_current_package=true)
#doc(hidden)
pub extend AccumulatedResponse with Eq::{not_equal}
///|
#deprecated("Use `Debug::to_repr` instead", skip_current_package=true)
#doc(hidden)
pub extend AccumulatedResponse with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// BlockAccumulator — 块状态跟踪器
///
/// 管理流式解码中的块生命周期:按 block_index 跟踪类型、文本、
/// thinking、签名、工具调用信息。提供 ensure/flush/discard 操作。
pub(all) struct BlockAccumulator {
block_types : Array[LucentBlockType]
block_texts : Array[StringBuilder]
block_thinking : Array[StringBuilder]
block_signatures : Array[StringBuilder]
block_tool_ids : Array[String]
block_tool_names : Array[String]
}
///|
/// 创建空的 BlockAccumulator
pub fn BlockAccumulator::new() -> BlockAccumulator {
{
block_types: [],
block_texts: [],
block_thinking: [],
block_signatures: [],
block_tool_ids: [],
block_tool_names: [],
}
}
///|
/// 确保 block_index 在跟踪数组中占位
pub fn BlockAccumulator::ensure_block(
self : BlockAccumulator,
idx : Int,
) -> Unit {
while self.block_types.length() <= idx {
self.block_types.push(Text)
self.block_texts.push(StringBuilder())
self.block_thinking.push(StringBuilder())
self.block_signatures.push(StringBuilder())
self.block_tool_ids.push("")
self.block_tool_names.push("")
}
}
///|
/// 按类型把已累加内容刷入 content 数组
pub fn BlockAccumulator::flush_block(
self : BlockAccumulator,
idx : Int,
content : Array[LucentContent],
reasoning : LucentThinking?,
) -> LucentThinking? {
self.ensure_block(idx)
match self.block_types[idx] {
Text => {
let s = self.block_texts[idx].to_string()
if s != "" {
content.push(LucentContent::text(s))
self.block_texts[idx].reset()
}
reasoning
}
ToolCall(_id, _name) => {
let id = self.block_tool_ids[idx]
let name = self.block_tool_names[idx]
let args = self.block_texts[idx].to_string()
content.push(LucentContent::tool_use(id, name, args, None))
self.block_texts[idx].reset()
reasoning
}
Thinking => {
let th_text = if !self.block_thinking[idx].is_empty() {
self.block_thinking[idx].to_string()
} else {
self.block_texts[idx].to_string()
}
let th = LucentThinking::new(
th_text,
match self.block_signatures[idx].to_string() {
"" => None
s => Some(s)
},
false,
None,
)
let new_reasoning = merge_reasoning(reasoning, th)
self.block_texts[idx].reset()
self.block_thinking[idx].reset()
self.block_signatures[idx].reset()
new_reasoning
}
Refusal => {
let s = self.block_texts[idx].to_string()
if s != "" {
content.push(Refusal(s))
self.block_texts[idx].reset()
}
reasoning
}
Image | Audio | Video | Native(_) => reasoning
}
}
///|
/// 丢弃该块已累加内容(Gemini 全量帧覆盖语义),保留类型信息
pub fn BlockAccumulator::discard_block(
self : BlockAccumulator,
idx : Int,
) -> Unit {
self.ensure_block(idx)
self.block_texts[idx].reset()
self.block_thinking[idx].reset()
self.block_signatures[idx].reset()
self.block_tool_ids[idx] = ""
self.block_tool_names[idx] = ""
}
///|
/// 流结束时 flush 所有未闭合的块
pub fn BlockAccumulator::flush_remaining(
self : BlockAccumulator,
content : Array[LucentContent],
reasoning : LucentThinking?,
) -> LucentThinking? {
let mut r = reasoning
let mut j = 0
while j < self.block_types.length() {
if !self.block_texts[j].is_empty() ||
!self.block_signatures[j].is_empty() ||
!self.block_thinking[j].is_empty() {
r = self.flush_block(j, content, r)
}
j = j + 1
}
r
}
///|
/// 把一段 thinking 累加进消息级 reasoning(文本拼接,签名取最后,redacted 取 OR)
fn merge_reasoning(
acc : LucentThinking?,
th : LucentThinking,
) -> LucentThinking? {
let new_text = match acc {
Some(a) => a.text + th.text
None => th.text
}
let new_sig = match th.signature {
Some(s) => Some(s)
None =>
match acc {
Some(a) => a.signature
None => None
}
}
let new_redacted = match acc {
Some(a) => a.redacted || th.redacted
None => th.redacted
}
Some(LucentThinking::new(new_text, new_sig, new_redacted, None))
}
///|
/// 处理 BlockDelta 事件
fn handle_block_delta(
acc : BlockAccumulator,
idx : Int,
delta : LucentBlockDelta,
events : Array[LucentStreamEvent],
event_idx : Int,
diagnostics : Array[ConversionDiagnostic],
native_events : Array[LucentStreamEvent],
) -> Unit {
acc.ensure_block(idx)
match delta {
TextDelta(s) => acc.block_texts[idx].write_string(s)
ThinkingDelta(s) => {
acc.block_thinking[idx].write_string(s)
match acc.block_types[idx] {
Thinking => ()
_ =>
diagnostics.push(
ConversionDiagnostic::new(
"delta.thinking",
Degraded,
Some("thinking delta on non-thinking block"),
),
)
}
}
SignatureDelta(s) => acc.block_signatures[idx].write_string(s)
ToolArgumentsDelta(s) => acc.block_texts[idx].write_string(s)
RefusalDelta(s) => {
match acc.block_types[idx] {
Text => acc.block_types[idx] = Refusal
_ => ()
}
acc.block_texts[idx].write_string(s)
}
NativeDelta(vendor, raw) => {
native_events.push(events[event_idx])
diagnostics.push(
ConversionDiagnostic::new(
"delta.native",
Unsupported,
Some(
"native delta (" + vendor + ") not accumulated: " + raw.stringify(),
),
),
)
}
}
}
///|
/// 完整累加:把流事件合成为 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 {
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
let content : Array[LucentContent] = []
let mut reasoning : LucentThinking? = None
let acc = BlockAccumulator::new()
let annotations : Array[LucentAnnotation] = []
let native_events : Array[LucentStreamEvent] = []
let agent_actions : Array[LucentAgentAction] = []
let diagnostics : Array[ConversionDiagnostic] = []
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) =>
match item {
Message(m) =>
for c in m.content {
content.push(c)
}
ToolCall(tu) => content.push(ToolUse(tu))
ToolResult(tr) => content.push(ToolResult(tr))
Reasoning(th) => reasoning = merge_reasoning(reasoning, th)
AgentAction(aa) => {
agent_actions.push(aa)
diagnostics.push(
ConversionDiagnostic::new(
"item.agent_action",
Unsupported,
Some("agent action not expanded in v1 response"),
),
)
}
}
BlockStart(idx, bt) => {
acc.ensure_block(idx)
acc.block_types[idx] = bt
acc.block_texts[idx].reset()
acc.block_thinking[idx].reset()
acc.block_signatures[idx].reset()
match bt {
ToolCall(id, name) => {
acc.block_tool_ids[idx] = id
acc.block_tool_names[idx] = name
}
_ => ()
}
}
BlockDelta(idx, delta) =>
handle_block_delta(
acc, idx, delta, events, i, diagnostics, native_events,
)
BlockEnd(idx) => reasoning = acc.flush_block(idx, content, reasoning)
ItemEnd(_) => ()
BlockDiscard(idx) => acc.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
}
reasoning = acc.flush_remaining(content, reasoning)
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
}