LLM Agent framework with hexagonal (ports-and-adapters) architecture. Defines 9 traits + Agent loop. Depends on moonbitlang/async.
| Port | 职责 |
|---|---|
| ModelPort | 普通或流式模型调用 |
| ToolProvider | 工具发现与执行 |
| SessionStore | session 加载与保存 |
| Observer | 只读 turn event 观察 |
| Compressor | 每轮模型调用前返回压缩指令 |
| PipelineHook | 修改、延迟或中止 pipeline stage |
| MemoryPort | 长期记忆存储与检索 |
| Lifecycle | async 资源清理 |
| CommandPort | 用户侧命令枚举与调用 |
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")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 等rtk moon check --target native --output-json
rtk moon test --target native --output-json
rtk moon fmt
rtk moon infopub(open) trait CommandPort {
fn commands(Self) -> Array[CommandDef]
async fn invoke(Self, id : String, args : Json) -> CommandOutcome raise CommandError
}pub(open) trait Compressor {
fn compress(Self, messages : Array[Message], ctx : CompressContext) -> CompressAction
}pub(open) trait Lifecycle {
async fn on_shutdown(Self) -> Unit
}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
}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 = _
}pub(open) trait SessionStore {
fn load(Self, id : String) -> Result[Session, SessionError]
fn save(Self, id : String, session : Session) -> Result[Unit, SessionError]
}pub(open) trait ToolProvider {
fn list_tools(Self) -> Array[ToolDef]
async fn execute(Self, name : String, call : ToolCall) -> ToolResult raise RuntimeError
}pub(all) suberror AgentError {
Model(String)
Session(SessionError)
Runtime(RuntimeError)
ToolLoopExceeded
HookAborted(String)
} derive(Debug)impl Show for AgentErrorpub(all) suberror CommandError {
NotFound(String)
InvalidArgs(reason~ : String)
ExecutionFailed(String)
} derive(Debug)impl Show for CommandErrorimpl Show for CompositionErrorpub(all) suberror ModelError {
RequestBuild(String)
Transport(String)
ResponseParse(String)
} derive(Debug)impl Show for ModelErrorpub(all) suberror RuntimeError {
UnknownTool(String)
InvocationFailed(String)
Transport(String)
} derive(Debug)impl Show for RuntimeErrorpub(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
}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 CompositionErrorasync fn Agent::run_turn(self : Agent, input : Message, session_id : String) -> TurnResult raise AgentErrorpub(all) struct AgentConfig {
max_tool_rounds : Int
temperature : Double?
max_output_tokens : Int?
tool_choice : ToolChoice?
reasoning_effort : ReasoningEffort?
model_context_window : Int?
}pub(all) struct ChatOptions {
temperature : Double?
max_output_tokens : Int?
tool_choice : ToolChoice?
reasoning_effort : ReasoningEffort?
} derive(Eq, Debug)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)impl ToolProvider for CompositeToolProviderasync fn execute(_self : CompositeToolProvider, name : String, _call : ToolCall) -> ToolResult raise RuntimeErrorpub(all) struct NoopCommandPort {
}impl CommandPort for NoopCommandPortasync fn invoke(_self : NoopCommandPort, id : String, _args : Json) -> CommandOutcome raise CommandErrorpub(all) struct NoopCompressor {
}impl Compressor for NoopCompressorfn compress(_self : NoopCompressor, _messages : Array[Message], _ctx : CompressContext) -> CompressActionpub(all) struct NoopHook {
}pub(all) struct RecordingHook {
records : Array[HookRecord]
abort_on : Map[String, String]
defer_on : Map[String, String]
}impl PipelineHook for RecordingHookimpl Observer for RecordingObserverimpl SessionStore for RecordingSessionStorefn RecordingSessionStore::fail_load(self : RecordingSessionStore, id : String, err : SessionError) -> Unitfn RecordingSessionStore::save_snapshots(self : RecordingSessionStore) -> Array[(String, Map[String, Json])]fn RecordingSessionStore::seed(self : RecordingSessionStore, id : String, session : Session) -> Unitpub(all) struct RecordingToolProvider {
tool_defs : Array[ToolDef]
outcomes : Map[String, ScriptedToolOutcome]
list_calls : Int
exec_records : Array[ToolExecRecord]
ops : Array[ToolProviderOp]
}impl ToolProvider for RecordingToolProviderasync fn execute(self : RecordingToolProvider, name : String, call : ToolCall) -> ToolResult raise RuntimeErrorasync fn RecordingToolProvider::execute_direct(self : RecordingToolProvider, name : String, call : ToolCall) -> ToolResult raise RuntimeErrorfn RecordingToolProvider::new(tool_defs : Array[ToolDef], outcomes : Map[String, ScriptedToolOutcome]) -> RecordingToolProviderpub(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]
}impl ModelPort for ScriptedModelasync fn chat(self : ScriptedModel, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions) -> ModelResponse raise ModelErrorasync fn chat_streaming(self : ScriptedModel, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions, on_chunk : (StreamChunk) -> Unit) -> ModelResponse raise ModelErrorasync fn ScriptedModel::chat_direct(self : ScriptedModel, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions) -> ModelResponse raise ModelErrorasync fn ScriptedModel::chat_streaming_direct(self : ScriptedModel, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions, on_chunk : (StreamChunk) -> Unit) -> ModelResponse raise ModelErrorpub(all) enum ScriptedModelStep {
Respond(ModelResponse)
Stream(chunks~ : Array[StreamChunk], response~ : ModelResponse)
Fail(ModelError)
}pub(all) enum SessionOp {
OpLoad(id~ : String, result~ : Result[Session, SessionError])
OpSave(id~ : String, metadata~ : Map[String, Json], result~ : Result[Unit, SessionError])
}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)pub(all) struct StreamAccumulator {
text : String
reasoning : String
tool_calls : Array[ToolCallBuilder]
finish_reason : FinishReason
usage : Usage?
} derive(Debug)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)pub(all) struct ToolExecRecord {
requested_name : String
call_id : String
tool_name : String
arguments : Json
outcome : ScriptedToolOutcome
}pub(all) struct ToolRegistry {
tools : Map[String, ToolDef]
executors : Map[String, (ToolCall) -> ToolResult]
}impl ToolProvider for ToolRegistryasync fn execute(self : ToolRegistry, name : String, call : ToolCall) -> ToolResult raise RuntimeErrorfn ToolRegistry::register(self : ToolRegistry, tool : ToolDef, executor : (ToolCall) -> ToolResult) -> Unittype ToolRoutingpub(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)pub(all) struct TurnResult {
message : Message
tool_results : Array[ToolResult]
final_session_id : String
} derive(Eq, Debug)Install
Download zipLLM Agent framework with hexagonal (ports-and-adapters) architecture. Defines 9 traits + Agent loop. Depends on moonbitlang/async.