Sign in

    posoco

    LLM Agent framework with hexagonal (ports-and-adapters) architecture. Defines 9 traits + Agent loop. Depends on moonbitlang/async.

    llm
    agent
    framework
    ports-and-adapters
    ai-runtime
    Download zip
    Author
    Version
    0.4.3
    License
    Apache-2.0
    Last updated
    3 months ago
    Downloads
    3K

    #colmugx/posoco

    Posoco 是一个使用 ports-and-adapters 架构的 MoonBit LLM Agent 框架。核心依赖 moonbitlang/async,由 9 个开放 trait、共享领域类型和一个 Agent::run_turn 编排循环组成;模型、工具、session、observer 等运行时能力由调用方注入。

    #当前公开结构

    Port职责
    ModelPort普通或流式模型调用
    ToolProvider工具发现与执行
    SessionStoresession 加载与保存
    Observer只读 turn event 观察
    Compressor每轮模型调用前返回压缩指令
    PipelineHook修改、延迟或中止 pipeline stage
    MemoryPort长期记忆存储与检索
    Lifecycleasync 资源清理
    CommandPort用户侧命令枚举与调用

    Agent 持有 &Trait 引用,没有泛型参数。公开签名的生成事实源是 src/pkg.generated.mbti,源文件布局与开发规则见 AGENTS.md。

    #最小组合

    下面的构造方式由 src/quickstart_fixture_test.mbt 作为黑盒测试编译验证:

    let agent = @posoco.Agent::new(
    model,
    tools=[],
    hooks=[],
    memory=[],
    compressors=[@posoco.NoopCompressor::{ }],
    observers=[],
    sessions=[store],
    lifecycle=None,
    config={
    max_tool_rounds: 10,
    temperature: None,
    max_output_tokens: None,
    tool_choice: None,
    reasoning_effort: None,
    model_context_window: None,
    },
    )
    let result = agent.run_turn(input, "session_1")

    完整可运行的 port 实现和验证命令见 docs/01-quickstart.md。外部 workspace 的本地 core 解析仍由 M0-T02 门禁跟踪;该问题完成前,文档不会把可能退回 registry 旧版本的 配置写成已验证安装方式。

    #关键运行语义

    • Agent::new 在两个 provider 声明同名工具时立即 raise CompositionError::ToolCollision;不存在 last-wins 路由或事后查询 warning。
    • ToolCallResult 的错误状态只从对应 ToolResult.is_error 派生。
    • provider raise 的 RuntimeError 会转换成同一个 error ToolResult,作为工具级 非致命 outcome 返回给模型;模型、session、hook 中止和工具循环超限会终止 turn。
    • 加载出的未知 session metadata 会原样保留;NewThread 的 parent_session_id lineage 由框架拥有并保存。
    • 已发布 TurnStarted 的 turn 恰好发布一个 terminal event:成功为 TurnCompleted,失败为 TurnFailed。After/Error hook 的 secondary failure 通过 Custom(source="posoco.core", label="secondary_failure", ...) 可观察, 且不覆盖 primary outcome。
    • malformed 或不完整的 streamed tool call 会 raise ModelError::ResponseParse,不会退回空参数继续执行。
    • Agent::shutdown 不吞 lifecycle failure;清理失败直接向调用方暴露。

    #分层

    Layer 3 — Agent / composition Agent::new + Agent::run_turn Layer 2 — Runtime adapters 调用方提供 ModelPort、ToolProvider、SessionStore 等具体实现 Layer 1 — Effect interfaces and shared types 9 个 pub(open) trait + Message / ToolCall / Session / TurnEvent 等

    压缩器在每一轮模型调用前执行,而不是每个 turn 只执行一次。Observer 返回 Unit、不控制流程;PipelineHook 返回 typed Result,可以改变控制流。

    #验证

    主门禁是 native target,所有 check/test 命令都输出 JSON:

    rtk moon check --target native --output-json rtk moon test --target native --output-json rtk moon fmt rtk moon info

    测试数量、其他 target 的实际能力和 extension 分类见 docs/TESTING.md 与 docs/support-matrix.md。不要把 default/wasm 只执行的 同步测试描述成完整 async 测试集。

    #扩展与设计文档

    #License

    Apache-2.0

    CommandPort

    pub(open) trait CommandPort {
    fn commands(Self) -> Array[CommandDef]
    async fn invoke(Self, id : String, args : Json) -> CommandOutcome raise CommandError
    }

    User-initiated, enumerable, side-effecting commands. Distinct from ToolProvider (LLM-initiated, results re-enter session). Both panel TUIs and slash command lines consume this without knowing command semantics. pub(open) so any ext can declare commands.

    Compressor

    pub(open) trait Compressor {
    fn compress(Self, messages : Array[Message], ctx : CompressContext) -> CompressAction
    }

    Compressor: pure function that decides how to shrink the message context. Returns instructions (None/Replace/NewThread) — never saves sessions or emits events directly; the Agent and Observer handle side effects.

    Lifecycle

    pub(open) trait Lifecycle {
    async fn on_shutdown(Self) -> Unit
    }

    Lifecycle: resource cleanup hook. Called once on agent shutdown to release connections, flush buffers, etc. async to allow IO during cleanup.

    MemoryPort

    pub(open) trait MemoryPort {
    fn store(Self, entry : MemoryEntry) -> String raise MemoryError
    fn search(Self, query : MemoryQuery) -> Array[MemoryEntry] raise MemoryError
    fn delete(Self, id : String) -> Unit raise MemoryError
    }

    MemoryPort: durable cross-session memory store/search/delete. Distinct from SessionStore (conversation history) — memory holds durable decisions/knowledge (e.g. brain.md integration).

    ModelPort

    pub(open) trait ModelPort {
    async fn chat(Self, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions) -> ModelResponse raise ModelError
    async fn chat_streaming(Self, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions, on_chunk : (StreamChunk) -> Unit) -> ModelResponse raise ModelError = _
    }

    ModelPort: the LLM calling interface. Every ext-deepseek / ext-openai / ext-anthropic implements this. chat is the primary call; chat_streaming has a default that ignores on_chunk and falls back to chat().

    Observer

    pub(open) trait Observer {
    fn on_event(Self, event : TurnEvent) -> Unit
    }

    Observer: read-only event listener. Never blocks or mutates — use PipelineHook for interception/abortion. Receives TurnEvent for every significant turn lifecycle event.

    PipelineHook

    pub(open) trait PipelineHook {
    async fn on_stage(Self, stage : Stage) -> Result[Stage, HookError]
    }

    Pipeline hook: observe or mutate stages before/after each phase. Before* stages may rewrite or abort; After*/*Error stages run under finally semantics (always invoked) in reverse hook order.

    SessionStore

    pub(open) trait SessionStore {
    fn load(Self, id : String) -> Result[Session, SessionError]
    fn save(Self, id : String, session : Session) -> Result[Unit, SessionError]
    }

    SessionStore: persist and load conversation sessions by id.

    ToolProvider

    pub(open) trait ToolProvider {
    fn list_tools(Self) -> Array[ToolDef]
    async fn execute(Self, name : String, call : ToolCall) -> ToolResult raise RuntimeError
    }

    Unified tool interface: declare tools via list_tools(), execute via execute().

    AgentError

    pub(all) suberror AgentError {
    Model(String)
    Session(SessionError)
    Runtime(RuntimeError)
    ToolLoopExceeded
    HookAborted(String)
    } derive(
    Debug
    )

    impl Show for AgentError

    CommandError

    pub(all) suberror CommandError {
    NotFound(String)
    InvalidArgs(reason~ : String)
    ExecutionFailed(String)
    } derive(
    Debug
    )

    Errors raised by CommandPort.invoke: unknown command id, invalid args, or execution failure.

    CompositionError

    pub(all) suberror CompositionError {
    ToolCollision(String, String)
    EmptyPort(String)
    } derive(
    Debug
    )

    Composition errors raised during Agent::new (tool collisions, empty required ports).

    MemoryError

    pub(all) suberror MemoryError {
    Store(String)
    Search(String)
    Delete(String)
    } derive(
    Debug
    )

    impl Show for MemoryError

    ModelError

    pub(all) suberror ModelError {
    RequestBuild(String)
    Transport(String)
    ResponseParse(String)
    } derive(
    Debug
    )

    impl Show for ModelError

    RuntimeError

    pub(all) suberror RuntimeError {
    UnknownTool(String)
    InvocationFailed(String)
    Transport(String)
    } derive(
    Debug
    )

    SessionError

    pub(all) suberror SessionError {
    Load(String)
    Save(String)
    } derive(
    Debug
    )

    Agent

    pub(all) struct Agent {
    model_port : &ModelPort
    tool_routing : ToolRouting
    pipeline_hook : &PipelineHook?
    compressors : Array[&Compressor]
    observers : Array[&Observer]
    memories : Array[&MemoryPort]
    sessions : Array[&SessionStore]
    lifecycle : &Lifecycle?
    config : AgentConfig
    }

    Agent — array-based composition: tools routed by name, hooks chained, observers fan-out, compressors first-wins, sessions write-all/read-first.

    Agent::new

    fn Agent::new(model_port : &ModelPort, tools~ : Array[&ToolProvider], hooks~ : Array[&PipelineHook], memory~ : Array[&MemoryPort], compressors~ : Array[&Compressor], observers~ : Array[&Observer], sessions~ : Array[&SessionStore], lifecycle~ : &Lifecycle?, config~ : AgentConfig) -> Agent raise CompositionError

    Construct an Agent with array-based composition.

    • tools: routed by tool name; a name collision RAISES CompositionError::ToolCollision (fail-fast, not last-wins)
    • hooks: chained in order (first runs first)
    • memory: fan-out store/search
    • compressors: first non-None wins
    • observers: all receive every event
    • sessions: write-all, read-first

    Agent::run_turn

    async fn Agent::run_turn(self : Agent, input : Message, session_id : String) -> TurnResult raise AgentError

    Run one full agent turn. Tool calls execute in parallel via @async.all.

    Agent::shutdown

    async fn Agent::shutdown(self : Agent) -> Unit

    AgentConfig

    pub(all) struct AgentConfig {
    max_tool_rounds : Int
    temperature : Double?
    max_output_tokens : Int?
    tool_choice : ToolChoice?
    reasoning_effort : ReasoningEffort?
    model_context_window : Int?
    }

    AgentConfig::to_chat_options

    fn AgentConfig::to_chat_options(self : AgentConfig) -> ChatOptions

    ChatOptions

    pub(all) struct ChatOptions {
    temperature : Double?
    max_output_tokens : Int?
    tool_choice : ToolChoice?
    reasoning_effort : ReasoningEffort?
    } derive(Eq,
    Debug
    )

    CommandDef

    pub(all) struct CommandDef {
    id : String
    label : String
    description : String
    category : String
    ctype : CommandType
    params : Array[CommandParam]
    aliases : Array[String]
    shortcut : String?
    icon : String?
    visible : Bool
    metadata : Json?
    } derive(Eq,
    Debug
    )

    Command declaration. params is the source of truth for argument rendering (panel form) and parsing (slash). Redundant fields (aliases/shortcut/icon/ visible/metadata) are deliberate "escape hatches" for ext authors to use without changing the trait — meta does not define their semantics.

    CommandOutcome

    pub(all) enum CommandOutcome {
    Success(feedback~ : String, structured~ : Json?, ui_hint~ : UiHint?)
    Failure(reason~ : String)
    NeedsInput(prompt~ : String)
    } derive(Eq,
    Debug
    )

    Outcome of invoking a command. feedback is for slash (print directly); structured + ui_hint are for panel (deep render + UI action). NeedsInput supports multi-step commands.

    CommandParam

    pub(all) struct CommandParam {
    name : String
    label : String
    description : String
    ptype : ParamType
    required : Bool
    default : Json?
    choices : Array[String]?
    positional : Bool
    } derive(Eq,
    Debug
    )

    A single command parameter. Explicit and human-facing, in contrast to ToolDef.input_schema (which is JSON Schema for LLM consumption).

    CommandType

    pub(all) enum CommandType {
    Action
    Toggle
    Select
    Input
    MultiStep
    } derive(Eq,
    Debug
    )

    Semantic command type. Determines how a panel renders the control (button / toggle / dropdown / input / multi-step form) and how a slash parser expects arguments.

    CompositeToolProvider

    pub(all) struct CompositeToolProvider {
    providers : Array[&ToolProvider]
    }

    Internal: merges multiple providers' tool lists. Agent routes via ToolRouting instead.

    CompressAction

    pub(all) enum CompressAction {
    None
    Replace(Array[Message])
    NewThread(messages~ : Array[Message], new_session_id~ : String, messages_before~ : Int, messages_after~ : Int)
    } derive(Eq,
    Debug
    )

    Action returned by a Compressor. None = no compression; Replace = substitute the message list; NewThread = start a fresh session with a summary carried over.

    CompressContext

    pub(all) struct CompressContext {
    session_id : String
    model_context_window : Int?
    } derive(Eq,
    Debug
    )

    Context passed to Compressor::compress.

    Content

    pub(all) enum Content {
    Text(String)
    Image(String, String)
    } derive(Eq,
    Debug
    )

    FinishReason

    pub(all) enum FinishReason {
    Stop
    Length
    ToolCalls
    Other(String)
    } derive(Eq,
    Debug
    )

    HookChain

    pub(all) struct HookChain {
    hooks : Array[&PipelineHook]
    }

    Internal: chains multiple hooks in sequence.

    HookError

    pub(all) enum HookError {
    Aborted(String)
    Deferred(reason~ : String)
    } derive(Eq,
    Debug
    )

    impl Show for HookError

    HookRecord

    pub(all) struct HookRecord {
    stage_in : Stage
    outcome : RecordedHookOutcome
    }

    A recorded hook invocation: the stage received and its exact decision.

    MemoryEntry

    pub(all) struct MemoryEntry {
    id : String
    content : String
    metadata : Map[String, Json]
    score : Double?
    } derive(Eq,
    Debug
    )

    MemoryQuery

    pub(all) struct MemoryQuery {
    query : String
    top_k : Int
    threshold : Double?
    filter : Map[String, Json]
    } derive(Eq,
    Debug
    )

    Message

    pub(all) struct Message {
    role : Role
    content : Array[Content]
    tool_calls : Array[ToolCall]
    tool_call_id : String?
    name : String?
    reasoning_content : String?
    } derive(Eq,
    Debug
    )

    A conversation message. reasoning_content carries provider reasoning (e.g. DeepSeek deepseek-reasoner思维链, OpenAI o-series reasoning content). Providers that don't produce reasoning leave it None. Wire encoders decide whether to resend it in subsequent turns (DeepSeek: must resend for turns with tool_calls; optional otherwise).

    ModelResponse

    pub(all) struct ModelResponse {
    message : Message
    tool_calls : Array[ToolCall]
    finish_reason : FinishReason
    usage : Usage?
    reasoning_content : String?
    } derive(Eq,
    Debug
    )

    NoopCommandPort

    pub(all) struct NoopCommandPort {
    }

    No-op CommandPort: declares no commands, invoke always raises NotFound. Useful as a default or for testing.

    NoopCompressor

    pub(all) struct NoopCompressor {
    }

    NoopHook

    pub(all) struct NoopHook {
    }

    NoopLifecycle

    pub(all) struct NoopLifecycle {
    }

    NoopMemoryPort

    pub(all) struct NoopMemoryPort {
    }

    ParamType

    pub(all) enum ParamType {
    Str
    Int
    Bool
    Strs
    } derive(Eq,
    Debug
    )

    Value type of a command parameter. Human-friendly (unlike JSON Schema which targets LLMs). Panel/slash consumers render and parse directly from this.

    ReasoningEffort

    pub(all) enum ReasoningEffort {
    Off
    Minimal
    Low
    Medium
    High
    Maximum
    } derive(Eq,
    Debug
    )

    Reasoning effort requested of the model. Provider-agnostic: each ext maps these to its own effort tiers (e.g. DeepSeek collapses to on/off via model choice; OpenAI/Anthropic have explicit tiers). Maximum means the provider's absolute top tier (Claude ultra, DeepSeek Max, OpenAI high).

    RecordedHookOutcome

    pub(all) enum RecordedHookOutcome {
    Returned(Stage)
    AbortedWith(String)
    DeferredWith(String)
    }

    The exact decision made by a RecordingHook invocation.

    RecordingHook

    pub(all) struct RecordingHook {
    records : Array[HookRecord]
    abort_on : Map[String, String]
    defer_on : Map[String, String]
    }

    PipelineHook fake with configurable per-stage behavior. By default it is a pass-through (returns the stage unchanged). Records every call.

    RecordingHook::abort_when

    fn RecordingHook::abort_when(self : RecordingHook, trigger : String, msg : String) -> Unit

    Configure the hook to abort (HookError::Aborted) when it sees a stage whose debug string contains trigger.

    RecordingHook::defer_when

    fn RecordingHook::defer_when(self : RecordingHook, trigger : String, reason : String) -> Unit

    Configure the hook to defer when it sees a stage whose label contains trigger.

    RecordingHook::new

    RecordingHook::records

    RecordingObserver

    pub(all) struct RecordingObserver {
    events : Array[TurnEvent]
    }

    Observer fake that records every TurnEvent in arrival order. Unlike a counter, it keeps the full sequence for trace assertions.

    RecordingObserver::events

    Snapshot of all recorded events in order.

    RecordingObserver::new

    RecordingSessionStore

    pub(all) struct RecordingSessionStore {
    store : Map[String, Session]
    load_failures : Map[String, SessionError]
    save_failures : Array[String]
    ops : Array[SessionOp]
    }

    SessionStore fake that keeps an in-memory map and records every load/save with the metadata snapshot at save time. Load/save failures are configurable per id.

    RecordingSessionStore::fail_load

    fn RecordingSessionStore::fail_load(self : RecordingSessionStore, id : String, err : SessionError) -> Unit

    Configure load to fail for a given id.

    RecordingSessionStore::fail_save

    fn RecordingSessionStore::fail_save(self : RecordingSessionStore, id : String) -> Unit

    Configure save to fail for a given id (all subsequent saves to that id).

    RecordingSessionStore::new

    RecordingSessionStore::ops

    Recorded operations in order.

    RecordingSessionStore::save_snapshots

    fn RecordingSessionStore::save_snapshots(self : RecordingSessionStore) -> Array[(String, Map[String, Json])]

    All save operations, in order, as (id, metadata_snapshot) pairs.

    RecordingSessionStore::seed

    fn RecordingSessionStore::seed(self : RecordingSessionStore, id : String, session : Session) -> Unit

    Seed a session id with an existing session (including metadata).

    RecordingToolProvider

    pub(all) struct RecordingToolProvider {
    tool_defs : Array[ToolDef]
    outcomes : Map[String, ScriptedToolOutcome]
    list_calls : Int
    exec_records : Array[ToolExecRecord]
    ops : Array[ToolProviderOp]
    }

    ToolProvider fake that declares a fixed tool list and routes execute() by tool name through a configurable outcome map. Unknown tools raise RuntimeError::UnknownTool. Every execute is recorded with its call id.

    RecordingToolProvider::exec_records

    Snapshot of recorded executions (call id, tool name, outcome).

    RecordingToolProvider::execute_direct

    async fn RecordingToolProvider::execute_direct(self : RecordingToolProvider, name : String, call : ToolCall) -> ToolResult raise RuntimeError

    Direct (non-trait) execute entry point for testkit self-tests.

    RecordingToolProvider::list_call_count

    fn RecordingToolProvider::list_call_count(self : RecordingToolProvider) -> Int

    Number of times list_tools was called.

    RecordingToolProvider::new

    fn RecordingToolProvider::new(tool_defs : Array[ToolDef], outcomes : Map[String, ScriptedToolOutcome]) -> RecordingToolProvider

    RecordingToolProvider::ops

    Snapshot of list/execute operations in their exact observed order.

    Role

    pub(all) enum Role {
    System
    User
    Assistant
    Tool
    } derive(Eq,
    Debug
    )

    ScriptedModel

    pub(all) struct ScriptedModel {
    steps : Array[ScriptedModelStep]
    index : Int
    calls : Int
    received_messages : Array[Array[Message]]
    received_tools : Array[Array[ToolDef]]
    received_options : Array[ChatOptions]
    received_chunks : Array[StreamChunk]
    }

    ModelPort fake that plays a fixed script. Each chat call consumes one step in order. When the script is exhausted the next call fails with ModelError::Transport("scripted_model_exhausted at call <N>") — it never silently repeats the last response.

    ScriptedModel::call_count

    fn ScriptedModel::call_count(self : ScriptedModel) -> Int

    Number of chat/chat_streaming calls observed so far.

    ScriptedModel::chat_direct

    async fn ScriptedModel::chat_direct(self : ScriptedModel, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions) -> ModelResponse raise ModelError

    Direct (non-trait) chat entry point for testkit self-tests. Trait methods on concrete types must be dispatched through a &ModelPort reference; this wrapper lets tests call chat without an Agent.

    ScriptedModel::chat_streaming_direct

    async fn ScriptedModel::chat_streaming_direct(self : ScriptedModel, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions, on_chunk : (StreamChunk) -> Unit) -> ModelResponse raise ModelError

    Direct streaming entry point for testkit self-tests.

    ScriptedModel::new

    ScriptedModel::options_received

    fn ScriptedModel::options_received(self : ScriptedModel) -> Array[ChatOptions]

    Snapshot of all chat options received, in call order.

    ScriptedModelStep

    pub(all) enum ScriptedModelStep {
    Respond(ModelResponse)
    Stream(chunks~ : Array[StreamChunk], response~ : ModelResponse)
    Fail(ModelError)
    }

    A single scripted model interaction: a response, an exact stream transcript, or a typed error. Stream never synthesizes chunks from the final response.

    ScriptedToolOutcome

    pub(all) enum ScriptedToolOutcome {
    OutcomeOk(ToolResult)
    OutcomeErr(RuntimeError)
    }

    Configurable outcome for a tool call: success result, or raised runtime error. Constructors are prefixed with Outcome to avoid ambiguity with Result::Ok/Result::Err at unqualified call sites.

    Session

    pub(all) struct Session {
    messages : Array[Message]
    metadata : Map[String, Json]
    } derive(Eq,
    Debug
    )

    SessionOp

    pub(all) enum SessionOp {
    OpLoad(id~ : String, result~ : Result[Session, SessionError])
    OpSave(id~ : String, metadata~ : Map[String, Json], result~ : Result[Unit, SessionError])
    }

    A recorded session operation. Constructors are prefixed with Op to avoid ambiguity with SessionError::Load/SessionError::Save.

    Stage

    pub(all) enum Stage {
    BeforeTurn
    BeforeModel(Array[Message])
    AfterModel(ModelResponse)
    ModelError(String)
    BeforeTool(ToolCall)
    AfterTool(ToolCall, ToolResult)
    ToolError(ToolCall, RuntimeError)
    AfterTurn(TurnResult)
    TurnError(AgentError)
    } derive(
    Debug
    )

    Pipeline hook stages. The Before* variants run hooks in forward order (short-circuit on error); the After*/*Error variants run in reverse order (onion semantics) without short-circuiting (finally semantics).

    StreamAccumulator

    pub(all) struct StreamAccumulator {
    text : String
    reasoning : String
    tool_calls : Array[ToolCallBuilder]
    finish_reason : FinishReason
    usage : Usage?
    } derive(
    Debug
    )

    Accumulates StreamChunk events into a ModelResponse.

    StreamAccumulator::new

    StreamAccumulator::push

    fn StreamAccumulator::push(self : StreamAccumulator, chunk : StreamChunk) -> Unit

    StreamAccumulator::to_response

    Assemble accumulated stream chunks into a ModelResponse.

    T07 error-transparency: malformed tool-call argument JSON raises ModelError::ResponseParse instead of silently becoming {}. This prevents a corrupted stream from masquerading as a valid empty-args call.

    StreamChunk

    pub(all) enum StreamChunk {
    TextDelta(token~ : String)
    ReasoningDelta(token~ : String)
    ToolCallDelta(index~ : Int, id~ : String?, name~ : String?, arguments_delta~ : String?)
    Usage(Usage)
    Finish(reason~ : FinishReason)
    } derive(Eq,
    Debug
    )

    ToolCall

    pub(all) struct ToolCall {
    id : String
    name : String
    arguments : Json
    } derive(Eq,
    Debug
    )

    ToolCallBuilder

    pub(all) struct ToolCallBuilder {
    id : String
    name : String
    arguments_json : String
    } derive(
    Debug
    )

    ToolCallBuilder::new

    ToolChoice

    pub(all) enum ToolChoice {
    Auto
    NoTool
    Required
    NamedTool(String)
    } derive(Eq,
    Debug
    )

    ToolDef

    pub(all) struct ToolDef {
    name : String
    description : String
    input_schema : Json
    metadata : Json?
    source : String?
    } derive(Eq,
    Debug
    )

    ToolExecRecord

    pub(all) struct ToolExecRecord {
    requested_name : String
    call_id : String
    tool_name : String
    arguments : Json
    outcome : ScriptedToolOutcome
    }

    A recorded tool execution: call id, tool name, and outcome category.

    ToolProviderOp

    pub(all) enum ToolProviderOp {
    ListTools
    ExecuteTool(requested_name~ : String, call~ : ToolCall)
    }

    Unified provider operation trace, preserving list/execute interleaving.

    ToolRegistry

    pub(all) struct ToolRegistry {
    tools : Map[String, ToolDef]
    executors : Map[String, (ToolCall) -> ToolResult]
    }

    Dynamic runtime tool registration. Implements ToolProvider.

    ToolRegistry::new

    ToolRegistry::register

    fn ToolRegistry::register(self : ToolRegistry, tool : ToolDef, executor : (ToolCall) -> ToolResult) -> Unit

    ToolRegistry::unregister

    fn ToolRegistry::unregister(self : ToolRegistry, name : String) -> Unit

    ToolResult

    pub(all) struct ToolResult {
    content : String
    structured : Json?
    is_error : Bool
    } derive(Eq,
    Debug
    )

    ToolRouting

    type ToolRouting

    Internal routing table: tool name → owning provider. Built during Agent::new.

    TurnEvent

    pub(all) enum TurnEvent {
    TurnStarted
    ToolCallPending(ToolCall)
    ToolCallResult(call~ : ToolCall, result~ : ToolResult, is_error~ : Bool)
    ModelResponseReceived(message~ : Message, usage~ : Usage?, reasoning_content~ : String?)
    SessionRedirect(from~ : String, to~ : String, messages_before~ : Int, messages_after~ : Int)
    TurnCompleted
    TurnFailed(String)
    ToolCallDeferred(call~ : ToolCall, reason~ : String)
    StreamChunkReceived(chunk~ : StreamChunk)
    Custom(source~ : String, label~ : String, data~ : Json)
    } derive(Eq,
    Debug
    )

    impl Show for TurnEvent

    TurnResult

    pub(all) struct TurnResult {
    message : Message
    tool_results : Array[ToolResult]
    final_session_id : String
    } derive(Eq,
    Debug
    )

    UiHint

    pub(all) enum UiHint {
    RefreshModelList
    RefreshSettings
    ClosePanel
    Toast
    None
    } derive(Eq,
    Debug
    )

    Hint to a panel consumer about what UI action to take after invoke. Slash consumers ignore this. Fixed enum + None fallback; ext long-tail needs go through CommandDef.metadata.

    Usage

    pub(all) struct Usage {
    input_tokens : Int?
    output_tokens : Int?
    total_tokens : Int?
    } derive(Eq,
    Debug
    )

    assert_events_contain

    fn assert_events_contain(haystack : Array[TurnEvent], needle : Array[TurnEvent]) -> Unit

    Assert the recorded events contain a sub-sequence matching needle, in order. Reports the first needle event that could not be matched.

    assert_events_eq

    fn assert_events_eq(expected : Array[TurnEvent], actual : Array[TurnEvent]) -> Unit

    Assert two event traces are equal, reporting the first differing index, the expected and actual event, when they diverge.

    build_error_message

    fn build_error_message(call : ToolCall, error : RuntimeError) -> Message

    build_tool_result_message

    fn build_tool_result_message(call : ToolCall, result : ToolResult) -> Message

    event_trace_mismatch

    fn event_trace_mismatch(expected : Array[TurnEvent], actual : Array[TurnEvent]) -> String?

    Return the first structural trace mismatch. Kept pure so conformance tests can prove missing, duplicate, and out-of-order detection without catching an assertion panic.

    parse_slash_args

    fn parse_slash_args(text : String, params : Array[CommandParam]) -> Result[Json, String]

    Parse a slash argument string into a JSON object according to CommandParam definitions. Positional params fill in declaration order; non-positional params expect key=value form. Applies defaults for missing optional params. Returns Err with a reason string on type conversion failure or missing required params. Pure function (no IO).

    tk_config

    fn tk_config() -> AgentConfig

    Build a default AgentConfig for scripted tests.

    tk_error_result

    fn tk_error_result(content : String) -> ToolResult

    Build a business-error ToolResult (provider returns Ok but is_error=true).

    tk_ok_result

    fn tk_ok_result(content : String) -> ToolResult

    Build a successful ToolResult.

    tk_stop_response

    fn tk_stop_response(text : String) -> ModelResponse

    Build a minimal stop response (no tool calls) for scripted tests.

    tk_tool_call_response

    fn tk_tool_call_response(tool_name : String, call_id : String, args : Json) -> ModelResponse

    Build a response that requests a single tool call.

    tk_tool_def

    fn tk_tool_def(name : String, description : String) -> ToolDef

    Build a ToolDef with an empty object schema.

    tk_user_msg

    fn tk_user_msg(text : String) -> Message

    Build a user message.

    validate_args

    fn validate_args(def : CommandDef, args : Json) -> Result[Json, String]

    Validate a JSON object of args against a CommandDef's params. Checks required presence; optional params with no value and no default are omitted (not an error). Returns the (possibly defaulted) args on Ok. Pure function (no IO).