LLM Agent framework with hexagonal (ports-and-adapters) architecture. Defines 9 traits + Agent loop. Depends on moonbitlang/async.
Dependencies
API docs: https://mooncakes.io/docs/colmugx/posoco · 中文 README: README-zh.mbt.mdWARNING: Posoco is experimental (0.x). The public API is subject to change.
moon add colmugx/posoco@0.9.0| Port | Purpose |
|---|---|
| ModelPort | Model chat + context compaction (chat, compact, provider_config) |
| ToolProvider | Tool discovery and execution |
| SessionStore | Load / save conversation session |
| Observer | Read-only turn-event observation |
| Hook | Pipeline interception with default methods: rewrite messages or abort before model (before_model), approve / defer / reject before tool (before_tool), read-only after each effect (on_post_event) |
| MemoryPort | Long-term memory storage and retrieval |
| Lifecycle | Async resource cleanup on shutdown |
| CommandPort | User-side slash-command enumeration and dispatch |
| UiPort | Structured UI intents + interaction requests (Status / Notice / Widget / Input / Confirm / Select) |
| SystemPromptContributor | Declare system-prompt sections (assembled and injected before the model call) |
| Extension | Self-report protocol: extension_id + manifest declaring which ports an extension contributes |
| ProviderConfig | Model-side provider config (companion type, not a runtime port) |
// 1. Implement the port trait(s) your extension contributes.
// (ToolProvider shown here; the body is omitted for brevity.)
///|
pub impl @posoco.Extension for ReadTools with fn extension_id(_self) -> String {
"posoco_ext_read"
}
///|
/// 2. Declare which ports ReadTools contributes. The same `self` goes under
/// every slot whose trait ReadTools implements; the rest stay empty.
pub impl @posoco.Extension for ReadTools with fn manifest(self) -> @posoco.ExtensionManifest {
{
id: "posoco_ext_read",
models: [],
tools: [self],
sessions: [],
observers: [],
hooks: [],
memory: [],
lifecycle: [],
commands: [],
ui: [],
prompt_contributors: [],
}
}
///|
/// 3. Expose Extension methods for dot-syntax callers and so `&ReadTools`
/// coerces to `&@posoco.Extension` inside `Array[&@posoco.Extension]`.
pub ReadTools with @posoco.Extension::{extension_id, manifest}
///|
/// 4. Optional factory for hosts that construct from defaults.
pub fn read_extension() -> ReadTools {
ReadTools::ReadTools()
}let agent = @posoco.Agent(
exts=[
model_ext, // an extension contributing ModelPort
read_ext, // the ReadTools extension built above, contributing ToolProvider
],
config={
max_tool_rounds: Some(10),
temperature: None,
max_output_tokens: None,
model_context_window: None,
},
)
let input : @posoco.Message = @posoco.UserMessage(content=[
@posoco.Content::Text("hello"),
])
let result = agent.run_turn(input, "session_1")| # | Chapter | Status |
|---|---|---|
| 01 | Agent Loop principles: how to write your own | ✅ |
| 02 | Your first agent with Posoco (10 lines) | ✅ |
moon check --target native --output-json
moon test --target native --output-json
moon fmt
moon infopub struct Agent {
// private fields
}fn Agent::Agent(exts~ : Array[&Extension], config~ : AgentConfig, ui_projection? : Bool) -> Agent raise CompositionErrorasync fn Agent::run_turn(self : Agent, input : Message, session_id : String) -> TurnResult raise AgentErrorfn Agent::with_runtime(exts~ : Array[&Extension], config~ : AgentConfig, runtime~ : &Runtime, ui_projection? : Bool, catalog_source? : &CatalogSource) -> Agent raise CompositionErrorpub(all) struct AgentConfig {
max_tool_rounds : Int?
temperature : Double?
max_output_tokens : Int?
model_context_window : Int?
}impl ToolProvider for CompositeToolProviderasync fn execute(_self : CompositeToolProvider, name : String, _call : ToolCall) -> ToolOutcome raise RuntimeErrorimpl UiPort for CompositeUiPortasync fn CompositeUiPort::request(self : CompositeUiPort, req : UiRequest) -> UiResponse raise UiErrorimpl Extension for ManifestOnlypub(all) struct MemoryRetrievalHook {
memories : Array[&MemoryPort]
top_k : Int
on_failure : (String) -> Unit
}impl Hook for MemoryRetrievalHookfn MemoryRetrievalHook::MemoryRetrievalHook(memories~ : Array[&MemoryPort], top_k? : Int, on_failure? : (String) -> Unit) -> MemoryRetrievalHookpub(all) struct NoopCommandPort {
}impl CommandPort for NoopCommandPortasync fn invoke(_self : NoopCommandPort, id : String, _args : Json) -> CommandOutcome raise CommandErrorpub(all) struct NoopUiPort {
}impl UiPort for NoopUiPortpub(all) enum RecordedHookOutcome2 {
PassedThrough
AbortedWith(String)
DeferredWith(String)
}pub(all) struct RecordingHook {
records : Array[HookRecord]
abort_on : Map[String, String]
defer_on : Map[String, String]
}impl Hook for RecordingHookimpl Observer for RecordingObserverimpl SessionStore for RecordingSessionStoreasync fn save(self : RecordingSessionStore, id : String, session : Session) -> Unit raise SessionErrorfn 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) -> ToolOutcome raise RuntimeErrorfn RecordingToolProvider::RecordingToolProvider(tool_defs : Array[ToolDef], outcomes : Map[String, ScriptedToolOutcome]) -> RecordingToolProviderasync fn RecordingToolProvider::execute_direct(self : RecordingToolProvider, name : String, call : ToolCall) -> ToolOutcome raise RuntimeErrorpub(all) struct RecordingUiPort {
rendered : Array[UiRender]
scripted_responses : Array[UiResponse]
unsupported_mode : Bool
descriptor : UiDescriptor
}impl UiPort for RecordingUiPortasync fn RecordingUiPort::request(self : RecordingUiPort, _req : UiRequest) -> UiResponse raise UiErrorpub struct ScopeRecordingModel {
scripted : ScriptedModel
compact_result : CompactResult?
chat_scopes : Array[InvocationScope]
compact_scopes : Array[InvocationScope]
}impl ModelPort for ScopeRecordingModelasync fn chat(self : ScopeRecordingModel, scope : InvocationScope, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions, stream : StreamMode) -> ModelCallResult raise ModelErrorasync fn compact(self : ScopeRecordingModel, scope : InvocationScope, _messages : Array[Message], _options : ChatOptions, _trigger : CompactTrigger) -> CompactResult raise ModelErrorfn ScopeRecordingModel::ScopeRecordingModel(steps : Array[ScriptedModelStep], compact_result? : CompactResult) -> ScopeRecordingModelpub(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, _scope : InvocationScope, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions, stream : StreamMode) -> ModelCallResult raise ModelErrorasync fn compact(_self : ScriptedModel, _scope : InvocationScope, _messages : Array[Message], _options : ChatOptions, _trigger : CompactTrigger) -> CompactResult raise ModelErrorasync fn ScriptedModel::chat_direct(self : ScriptedModel, messages : Array[Message], tools : Array[ToolDef], options : ChatOptions) -> ModelCallResult raise ModelErrorpub(all) enum ScriptedModelStep {
Respond(ModelCallResult)
Stream(chunks~ : Array[StreamChunk], response~ : ModelCallResult)
Fail(ModelError)
}pub(all) enum SessionOp {
OpLoad(id~ : String, result~ : Result[Session, SessionError])
OpSave(id~ : String, metadata~ : Map[String, Json], result~ : Result[Unit, SessionError])
}impl Hook for SystemPromptHookfn SystemPromptHook::SystemPromptHook(base_prompt~ : String, contributors~ : Array[SystemPromptSection]) -> SystemPromptHookfn SystemPromptSection::SystemPromptSection(id~ : String, contributor~ : &SystemPromptContributor) -> SystemPromptSectionpub(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) -> ToolOutcome]
}impl ToolProvider for ToolRegistryasync fn execute(self : ToolRegistry, name : String, call : ToolCall) -> ToolOutcome raise RuntimeErrorfn ToolRegistry::register(self : ToolRegistry, tool : ToolDef, executor : (ToolCall) -> ToolOutcome) -> Unitfn ToolRegistry::register_strict(self : ToolRegistry, tool : ToolDef, executor : (ToolCall) -> ToolOutcome) -> Unit raise RuntimeErrorimpl Hook for UiRenderHookfn tk_ext(id~ : String, model? : &ModelPort?, tools? : Array[&ToolProvider], sessions? : Array[&SessionStore], observers? : Array[&Observer], hooks? : Array[&Hook], memory? : Array[&MemoryPort], lifecycle? : Array[&Lifecycle], commands? : Array[&CommandPort], ui? : Array[&UiPort], prompt_contributors? : Array[&SystemPromptContributor]) -> ManifestOnlylet model = ScriptedModel(..)
let tools = RecordingToolProvider(..)
let store = RecordingSessionStore()
let observer = RecordingObserver()
let agent = Agent(
exts=[
tk_ext("model", model=Some(model)),
tk_ext("tools", tools=[tools]),
tk_ext("io", sessions=[store], observers=[observer]),
],
config=tk_config(),
)LLM Agent framework with hexagonal (ports-and-adapters) architecture. Defines 9 traits + Agent loop. Depends on moonbitlang/async.
Dependencies