moonstream

    Incremental, chunk-invariant parsing of streaming JSON tool-call arguments.

    llm
    json
    streaming
    parser
    incremental
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    19 hours ago
    Downloads
    4

    #README 示例(参与编译与测试)

    本文件与 README.md 的示例保持同步,存在的唯一目的是让 README 中的代码真正被 moon check / moon test 覆盖——README.md 本身是普通 Markdown,其中的代码块不会被工具链编译。

    #快速开始

    ///|
    test {
    let parser = Parser::new()
    // 逐块喂入已经提取出来的参数片段字节
    let first = parser.feed(b"{\"city\":\"上")
    inspect(first.length(), content="3")
    let second = parser.feed(b"海\",\"count\":12}")
    inspect(second.length(), content="5")
    // 只有正常结束才可能得到可交付的文档
    let (_, result) = parser.finish()
    match result {
    FinishResult::Completed(doc) =>
    inspect(doc.to_string(), content="{\"city\":\"上海\",\"count\":12}")
    _ => fail("应当完成")
    }
    }

    #资源限制

    ///|
    test {
    let limits = Limits::new(max_depth=3)
    let parser = Parser::new(limits~)
    let failed = try {
    let _ = parser.feed(b"[[[[1]]]]")
    false
    } catch {
    ParseError::LimitExceeded(..) => true
    _ => false
    } noraise {
    _ => false
    }
    inspect(failed, content="true")
    }

    #事件路径

    ///|
    test {
    let path = Path::root().child_key("items").child_index(0).child_key("name")
    inspect(path.to_string(), content="$.items[0].name")
    }

    #部分树预览(M2)

    ///|
    test {
    let parser = Parser::new()
    let preview = @preview.Preview::new()
    for event in parser.feed(b"{\"city\":\"上") {
    preview.apply(event)
    }
    // 字符串正在增长,不是最终值
    inspect(preview.to_preview_string(), content="{\"city\":\"上…,…}")
    inspect(
    preview.lookup(Path::root().child_key("city"))
    is @preview.NodeState::IncompleteString(..),
    content="true",
    )
    }

    #多调用路由(M2)

    ///|
    test {
    let session = @session.Session::new()
    let first = @session.CallKey::new(response="resp-1", choice=0, index=0)
    let second = @session.CallKey::new(response="resp-1", choice=0, index=1)
    session.open(first, id=Some("call_a"), name=Some("create_ticket"))
    session.open(second, id=Some("call_b"), name=Some("send_mail"))
    // 片段交错到达
    let _ = session.feed(first, b"{\"title\":\"登")
    let _ = session.feed(second, b"{\"to\":\"a@b.c\"}")
    let _ = session.feed(first, b"录失败\"}")
    let (_, first_result) = session.finish(first)
    let (_, second_result) = session.finish(second)
    inspect(
    match first_result {
    FinishResult::Completed(doc) => doc.to_string()
    _ => "?"
    },
    content="{\"title\":\"登录失败\"}",
    )
    inspect(
    match second_result {
    FinishResult::Completed(doc) => doc.to_string()
    _ => "?"
    },
    content="{\"to\":\"a@b.c\"}",
    )
    }

    ParseError

    pub(all) suberror ParseError {
    InvalidSyntax(pos~ : Int, expected~ : String, found~ : String)
    InvalidUtf8(pos~ : Int, kind~ : Utf8ErrorKind)
    InvalidEscape(pos~ : Int, detail~ : String)
    InvalidNumber(pos~ : Int, lexeme~ : String)
    InvalidUnicodeEscape(pos~ : Int, detail~ : String)
    DuplicateKey(pos~ : Int, key~ : String, path~ : Path)
    TrailingContent(pos~ : Int)
    LimitExceeded(kind~ : LimitKind, pos~ : Int, allowed~ : Int, actual~ : Int)
    AlreadyFinished(op~ : String)
    InvalidLimits(detail~ : String)
    } derive(Eq, ToJson,
    Debug
    )

    解析器可以抛出的错误。

    任何一次抛出都使解析器进入终止状态:不会“尽力恢复”后继续产出事件。

    CompletedDocument

    pub struct CompletedDocument {
    text : String
    byte_length : Int
    } derive(Eq, ToJson,
    Debug
    )

    已经过完整合法性检查的 JSON 文档。

    本类型没有公开构造器,构造权属于解析器。它只代表“该文档在 MoonStream 的严格语法策略下完整且合法”,代表模型意图正确、符合业务 Schema、 获得执行授权,或可以安全地产生副作用。

    CompletedDocument::byte_length

    fn CompletedDocument::byte_length(self : CompletedDocument) -> Int

    文档的原始字节数。

    这是解析器实际消费的字节数,对非 ASCII 文档与 to_string().length() (UTF-16 码元数)不同。

    CompletedDocument::to_json

    转换为 Json

    这是显式操作:Json 的数值语义会做双精度转换,需要无损数字时请使用 事件流里的 NumberLiteral。转换失败时抛出 JSON 解析错误。

    CompletedDocument::to_string

    fn CompletedDocument::to_string(self : CompletedDocument) -> String

    原始完整文本。

    EndReason

    pub(all) enum EndReason {
    EndOfStream
    Cancelled
    Truncated
    } derive(Eq, ToJson,
    Debug
    )

    finish 的结束原因。

    只有 EndOfStream 可能产生 CompletedDocument:取消或截断都不代表 模型已经说完,即使此刻文本看起来恰好完整。

    Event

    pub(all) enum Event {
    ObjectBegin(path~ : Path, span~ : Span)
    ObjectEnd(path~ : Path, span~ : Span)
    ArrayBegin(path~ : Path, span~ : Span)
    ArrayEnd(path~ : Path, span~ : Span)
    KeyComplete(path~ : Path, name~ : String, span~ : Span)
    StringDelta(path~ : Path, text~ : String, span~ : Span)
    ValueComplete(path~ : Path, value~ : LeafValue, span~ : Span)
    } derive(Eq, ToJson,
    Debug
    )

    解析器在输入增量上产生的结构事件。

    事件是增量观察,不是最终结果:ValueComplete 只表示该叶值已被语法边界确认, 整个文档是否合法仍由 Parser::finish 决定。

    Event::path

    fn Event::path(self : Event) -> Path

    事件所属路径。

    Event::span

    fn Event::span(self : Event) -> Span

    事件对应的字节区间。

    FinishResult

    pub(all) enum FinishResult {
    Completed(CompletedDocument)
    Incomplete(pos~ : Int, expected~ : String)
    Aborted(reason~ : EndReason)
    } derive(Eq, ToJson,
    Debug
    )

    finish 的结果。

    LeafValue

    pub(all) enum LeafValue {
    Null
    Bool(Bool)
    Number(NumberLiteral)
    String(String)
    } derive(Eq, ToJson,
    Debug
    )

    已完成叶值的取值。

    LimitKind

    pub(all) enum LimitKind {
    Depth
    TokenBytes
    Nodes
    TotalBytes
    } derive(Eq, ToJson,
    Debug
    )

    资源限制的类型。

    Limits

    pub(all) struct Limits {
    max_depth : Int
    max_token_bytes : Int
    max_nodes : Int
    max_total_bytes : Int
    } derive(Eq, ToJson,
    Debug
    )

    解析器的资源限制。

    四个上限都必须为正数;Parser::new 会拒绝非法组合。 限制只约束解析器自身的开销,不承诺能抵御任意恶意输入。

    Limits::new

    fn Limits::new(max_depth? : Int, max_token_bytes? : Int, max_nodes? : Int, max_total_bytes? : Int) -> Limits raise ParseError

    自定义限制。任一参数非正数即抛出 InvalidLimits

    Limits::strict

    fn Limits::strict() -> Limits

    默认的严格限制:深度 128、单 token 1 MiB、节点数 1 Mi、总输入 64 MiB。

    NumberLiteral

    pub(all) struct NumberLiteral {
    lexeme : String
    } derive(Eq, ToJson,
    Debug
    )

    数字的原始词法形式。

    保留 1e400123456789012345678901234567890 这类输入的原样文本, 避免在解析阶段就把大整数或高精度小数静默舍入为 Double

    NumberLiteral::lexeme

    fn NumberLiteral::lexeme(self : NumberLiteral) -> String

    原始文本。

    NumberLiteral::new

    fn NumberLiteral::new(lexeme : String) -> NumberLiteral

    NumberLiteral::to_double

    fn NumberLiteral::to_double(self : NumberLiteral) -> Double?

    按 IEEE 754 双精度解析。这是显式的有损转换

    • 上溢(如 1e309)返回 None
    • 下溢(如 1e-400)按 IEEE 754 归零,返回 Some(0.0)
    • 词法不是数字时返回 None

    NumberLiteral::new 不校验词法,因此传入 inf / nan 这类文本时结果由底层 解析器决定;解析器自己产出的字面量总是合法 JSON 数字。需要无损语义时请使用 lexeme

    Parser

    pub struct Parser {
    inner : ParserState
    }

    增量 JSON 解析器。

    它消费已经提取出来的参数片段字节,不负责连接模型或解析 SSE。 同一个解析器实例只处理一个 JSON 文档。

    内部状态收在一个私有类型的字段里:.mbti 只显示这一个不透明字段, 改动它的内部字段不会改变公开接口(改类型名会)。

    Parser::feed

    fn Parser::feed(self : Parser, bytes : Bytes) -> Array[Event] raise ParseError

    消费一段输入,返回本次产生的结构事件。

    空块不改变语义。任何错误都会使解析器进入终止状态:之后再次 feed finish 抛出 AlreadyFinished

    Parser::finish

    fn Parser::finish(self : Parser, reason? : EndReason) -> (Array[Event], FinishResult) raise ParseError

    结束输入并判断文档是否完整合法。

    返回 (收尾事件, 结果):一个恰好以数字结尾的文档,它的 ValueComplete 只有在调用 finish 时才能被确认,所以收尾事件必须一并交付。

    只有 EndOfStream 可能返回 Completed;取消或截断返回 Aborted

    test {
    let parser = Parser::new()
    let _ = parser.feed(b"[1")
    let (events, result) = parser.finish()
    // 末尾数字只有在 finish 时才能被确认,收尾事件一并交付。
    inspect(events.length(), content="1")
    inspect(result is FinishResult::Incomplete(..), content="true")
    }

    Parser::is_finished

    fn Parser::is_finished(self : Parser) -> Bool

    是否已经 finish(无论结果是完成、未完成还是取消)。

    解析出错后也会返回 true:错误使解析器进入终止状态。

    Parser::limits

    fn Parser::limits(self : Parser) -> Limits

    当前生效的资源限制。

    Parser::new

    fn Parser::new(limits? : Limits) -> Parser raise ParseError

    创建解析器。限制不合法时抛出 InvalidLimits

    test {
    let parser = Parser::new()
    let events = parser.feed(b"{\"city\":")
    inspect(events.length(), content="2")
    }

    Parser::offset

    fn Parser::offset(self : Parser) -> Int

    已消费的绝对字节数。

    ParserState

    type ParserState

    解析器的全部可变状态。私有实现细节,不是可用 API。

    Path

    pub(all) struct Path {
    segments : Array[PathSegment]
    } derive(Eq, ToJson,
    Debug
    )

    文档内某个值的位置。根路径为空。

    路径只描述结构位置,不携带任何“该值已经合法”的承诺。

    Path::child_index

    fn Path::child_index(self : Path, index : Int) -> Path

    追加一个数组下标片段。

    Path::child_key

    fn Path::child_key(self : Path, key : String) -> Path

    追加一个对象键片段。

    Path::is_root

    fn Path::is_root(self : Path) -> Bool

    是否为空路径(文档根)。

    Path::length

    fn Path::length(self : Path) -> Int

    路径深度。

    Path::root

    fn Path::root() -> Path

    根路径(空路径)。

    Path::to_string

    fn Path::to_string(self : Path) -> String

    渲染为 $$.city$.items[0].name 形式,便于日志与测试断言。

    PathSegment

    pub(all) enum PathSegment {
    Key(String)
    Index(Int)
    } derive(Eq, ToJson,
    Debug
    )

    JSON 文档内的路径片段。

    Span

    pub(all) struct Span {
    start : Int
    end : Int
    } derive(Eq, ToJson,
    Debug
    )

    输入中的绝对字节区间,start 含、end 不含。

    偏移以“本次解析会话收到的全部字节”为基准,与 chunk 切分方式无关。

    Span::length

    fn Span::length(self : Span) -> Int

    区间长度(字节数)。

    Span::new

    fn Span::new(start : Int, end : Int) -> Span

    Utf8ErrorKind

    pub(all) enum Utf8ErrorKind {
    InvalidLeadByte
    InvalidContinuation
    OverlongEncoding
    SurrogateCodePoint
    OutOfRange
    } derive(Eq, ToJson,
    Debug
    )

    UTF-8 解码失败的类别。