posoco

    LLM Agent framework with hexagonal (ports-and-adapters) architecture. Defines extension ports + Agent loop.

    llm
    agent
    framework
    ports-and-adapters
    ai-runtime
    Download zip
    Author
    Version
    0.23.3
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    4K

    #Posoco

    Posoco is a protocol-first LLM agent framework for MoonBit: the core owns the agent loop and its invariants, and every runtime capability — model, tool, session, observer, hook, UI, command, prompt, memory, lifecycle — is injected through open port traits. Products compose one Agent(exts~, config~); the core never dictates shape.

    WARNING: Posoco is experimental (0.x). The public API is subject to change.

    #Installation

    moon add colmugx/posoco@0.23.3

    #Ports

    Ports are the only seams between Posoco core and the capabilities an agent uses.

    PortPurpose
    ModelPortModel chat + context compaction (chat, compact, provider_config)
    DecisionPortProvider-neutral structured semantic judgement
    ToolProviderTool discovery and execution
    LogPortStructured application logging with caller-selected durability
    SessionPortLoad / save conversation session
    ObserverRead-only turn-event observation
    HookPipeline 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)
    MemoryPortLong-term memory storage and retrieval
    LifecycleAsync resource cleanup on shutdown
    CommandPortUser-side slash-command enumeration and dispatch
    UiPortStructured UI intents + interaction requests (Status / Notice / Widget / Input / Confirm / Select)
    SystemPromptContributorDeclare system-prompt sections (assembled and injected before the model call)
    ExtensionSelf-report protocol: extension_id + manifest declaring which ports an extension contributes
    ProviderConfigModel-side provider config (companion type, not a runtime port)

    #create an extension

    An extension is a struct that implements one or more port traits and @posoco.Extension. The same self reference goes in every manifest slot whose port the struct implements; the id string is used only for composition diagnostics (tool-collision messages, observer attribution), never for routing or persistence.

    The minimal shape is two methods plus one pub extend line:

    // 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; omitted slots default to [].
    pub impl @posoco.Extension for ReadTools with fn manifest(self) -> @posoco.ExtensionManifest {
    @posoco.ExtensionManifest::make(id="posoco_ext_read", tools=[self])
    }

    ///|
    /// 3. Expose Extension methods for dot-syntax callers and so `&ReadTools`
    /// coerces to `&@posoco.Extension` inside `Array[&@posoco.Extension]`.
    pub extend ReadTools with @posoco.Extension::{extension_id, manifest}

    ///|
    /// 4. Optional factory for hosts that construct from defaults.
    pub fn read_extension() -> ReadTools {
    ReadTools::ReadTools()
    }

    For a ModelPort extension, put self under models: [self] instead: make(id=..., models=[self]).

    #compose an agent

    Agent authors import extension packages and wire their instances into one Agent. exts is an array of self-reporting extensions — order-independent; Agent aggregates their manifests.

    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")

    AgentConfig has four fields: max_tool_rounds (Int? — None is unbounded and the recommended default; Some(n) allows n full tool rounds), temperature, max_output_tokens, model_context_window. run_turn(message, session_id) is async and raises AgentError. Composition is fail-fast and raises CompositionError:

    • MissingModel — no extension contributes ModelPort
    • MultipleModels — more than one extension contributes ModelPort directly (multi-model routing belongs inside a meta-extension such as posoco-ext-llm, not in the core)
    • ToolCollision — two extensions register the same tool name (no last-wins)
    • SessionPort is optional; without one the Agent runs ephemerally and skips durable session persistence

    A turn that has emitted TurnStarted emits exactly one terminal event: TurnCompleted on success, or TurnFailed on any primary failure.

    #Extension library

    colmugx/posoco-extension maintains a curated set of ready-to-use extensions, and other port implementations you can drop straight into Agent(exts~, config~).

    #Learn more — Posoco 101

    posoco-101/ is a chapter-by-chapter course that builds from "what is an agent loop" up to a mini coding agent. The outline and chapter status live in posoco-101/OUTLINE.md:

    #ChapterStatus
    01Agent Loop principles: how to write your own✅
    02Your first agent with Posoco (10 lines)✅

    #Development & validation

    moon check --output-json moon test --output-json moon fmt moon info

    #Why "Posoco"?

    "Posoco" comes from persocom — the humanoid computers in CLAMP's manga Chobits. Since Posoco is the agent runtime split out of Elyra's architectural philosophy, it takes the homophone Posoco.

    #License

    Apache-2.0.

    AgentError

    AutocompleteItem

    AutocompleteSource

    A source of autocomplete suggestions tied to a trigger character.

    CallId

    Stable identifier for a single tool call requested by the assistant. The reducer and scheduler only ever compare/store this string; it is opaque to the Kernel and is produced by the model adapter or by the host.

    Capability

    A composed capability an extension declares it consumes. Declaring is what populates the CompositionView delivered at Lifecycle::on_compose — the view gates on this declaration, so an undeclared capability reads as None. Extensible by adding variants; existing manifests are unaffected.

    ChatOptions

    Provider-agnostic chat options: only the two fields every provider accepts. Provider-specific tuning (tool_choice, reasoning effort, top_p, frequency_penalty, ...) belongs on the modelport's own config struct, not on this shared bag — that way adding a new provider never bloats the common type.

    CommandDef

    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.

    CommandError

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

    CommandOutcome

    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

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

    CommandPort

    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.

    CommandType

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

    CompactMode

    How posoco applies the result of ModelPort::compact.

    CompactResult

    Return value of ModelPort::compact.

    CompactTrigger

    What triggered a compact. Advisory — modelport may ignore.

    Completion

    A completed assistant step. The message field is the canonical assistant message that the reducer appends to the transcript; tool calls and reasoning live there (single source of truth, ADR §2.11). The usage field reports token consumption observed for this step, which the reducer may aggregate into the Run budget.

    Construction note: do NOT build this by hand in production code. The from_assistant_message constructor enforces the invariant that the message payload is consistent (assistant role, no ToolMessage shape, etc.).

    CompletionPayload

    Re-flattened assistant payload used inside Completion. We cannot reuse Message::AssistantMessage directly because that variant already carries finish_reason; the completion's message field is the transcript entry, while the usage lives at the completion level. This struct mirrors only the fields that travel together as the assistant's contribution to a single step.

    CompositeUiPort

    Fan-out UiPort: render delivers to every contributor; request tries each in order, first non-Unsupported wins.

    CompositionError

    Composition errors raised during Agent::new (manifest aggregation failures).

    Every variant that involves a specific extension carries manifest_id so callers can locate the offending extension without guessing. Multi-party collisions carry the list of involved manifest ids.

    CompositionView

    CompositionView: the curated read-only view of composed capabilities delivered to Lifecycle::on_compose after every composition gate has passed. This is the consumption direction of the manifest contract — an extension declares requires and the corresponding getter reads Some; undeclared capabilities read None.

    Curation discipline: the view only ever carries capabilities whose use cannot bypass governance. model calls still carry InvocationScope attribution; ui has no governance surface. The view NEVER exposes tool invocation (would bypass PipelineHook::before_tool), session stores (state ownership), or observer-event emission (core-owned). Fields are private with getters so new capabilities can be added without breaking extensions that destructure or construct views in tests.

    Content

    Canonical content block. Text is the common case; Image carries a stable media-type identifier plus opaque payload data (base64, URL, or an adapter-managed handle — the Kernel does not interpret it).

    ContextPressure

    Context-pressure advisory for a model-side call. Posoco resolves the effective window and threshold per turn (host config > provider report > core default) and tracks the latest verified occupancy; compact is the call that needs these numbers, so the pump attaches them to the InvocationScope instead of leaving modelports to re-estimate from characters. Read-only for ports; absent dimensions are genuinely unknown and must not be guessed.

    DecisionAnswer

    Provider-neutral typed answer. The question id is repeated so adapters can be validated without relying on response ordering.

    DecisionCallContext

    Per-call context for DecisionPort::evaluate. Build with DecisionCallContext::make; every field except purpose is optional.

    DecisionError

    Errors raised by DecisionPort::evaluate. Provider adapters normalize transport/protocol failures into this bounded provider-neutral surface.

    DecisionFailureMode

    Consumer-declared failure handling for a contextual call. Diagnostics and attribution only: the actual policy (which fallback, whether to close) is executed by the consuming extension, never by this port.

    DecisionNamedProbability

    One named probability in a choice distribution.

    DecisionOption

    One named alternative for a Choice question.

    DecisionPort

    A low-cost semantic judgement capability. The port does not decide final business policy: consumers map the returned probabilities into their own deterministic policy and authority flow.

    DecisionQuestion

    Typed question understood by a DecisionPort.

    DecisionRequest

    Structured state plus a non-empty set of independent semantic questions.

    DecisionResult

    Typed answers plus provider-reported model/usage metadata. model is descriptive observability only; consumers must not branch policy on a provider identity hidden behind this port.

    DecisionScope

    Explicit scope fact known at the call site (e.g. the session a background task belongs to when the caller actually knows it). Unknown scope is expressed as None by the caller; this port never infers scope from the active run or background-task ownership.

    DecisionUsage

    Optional token accounting supplied by decision providers.

    EventScope

    Attribution identity of one emitted TurnEvent / HookStage: which session and run the event belongs to. Projected from the committed event envelope at the Agent boundary — never reconstructed by scanning a transcript. Scope answers "which run does this belong to"; it does not carry effect identity (ToolCall.call_id already correlates tool events).

    ExecutionPolicy

    How a tool may be batched with other tools in the same model completion.

    • Sequential: occupies a single-element wave. Safe default; file/shell side effects never race.
    • Parallel: may share a wave with adjacent Parallel calls.
    • Exclusive: forms a barrier before and after; never shares a wave.

    Sequential is the safe default. Products opt into Parallel only for tools whose effects are independently safe, and use Exclusive at a visible side-effect boundary.

    Extension

    ExtensionManifest

    Self-reported port contributions. Build one with ExtensionManifest::make, which defaults every omitted field to an empty array; multi-trait extensions put self under each applicable field.

    requires is the consumption direction: the extension declares which composed capabilities it wants delivered at Lifecycle::on_compose. The declaration is load-bearing (the view gates on it) and auditable (composition can answer "which extensions declared model access").

    FinishReason

    Why the assistant finished a step. It is owned by the assistant message (per ADR §2.11: single source of truth).

    Length is preserved as a distinct variant because the reducer must NOT emit ExecuteTool effects after a length-truncated completion, even if the provider tried to stream partial tool calls.

    HookAbort

    Raised by PipelineHook::before_model to stop the run before the model is called.

    HookStage

    Describes what just completed, delivered after the effect is committed.

    InvocationResult

    Child invocation results do not conflate an interrupted scope with a completed business outcome. A propagated cancel is not proof of rollback.

    InvocationScope

    Invocation identity for a single model-side call (ModelPort::chat / ModelPort::compact, and their Runtime correspondences). Constructed by posoco at the effect-interpretation layer and passed down unchanged; ports and hosts MUST treat it as read-only.

    This is the canonical answer to "which session/run is this call serving": adapters that key behaviour on session identity (continuity, telemetry, cost attribution, per-session policy) read it from here instead of smuggling it through messages or config.

    Invariants:
    • session_id / run_id always identify the Run that issued the call, including compact (compact is host-driven but still scoped to a Run).
    • effect_id is Some iff the call executes a reducer-allocated CallModel effect; it is None for compact, which is not an effect. Hosts that propagate cancellation key in-flight model calls by this id, mirroring the tool side's EffectContext.effect_id.
    • pressure is Some only where posoco has a reading worth sharing (the compact path); ports treat it as advisory read-only input.

    Lifecycle

    Lifecycle: the extension-facing agent lifecycle, three phases.

    • on_compose: called once, synchronously, after every composition gate has passed (catalog atomic snapshot, model cardinality, collision checks) and before the Agent is ready. This is where the composed capabilities declared in the manifest's requires are delivered via CompositionView. Sync on purpose: ModelPort::chat and UiPort::request are async, so a sync composition point cannot use what it wires — the type system enforces "wire, don't act". Raise CompositionError::ExtensionComposeFailed to fail the composition loudly; no partial Agent is produced.
    • on_start: called once per Agent lifetime, inside the first run_turn (an async context), before the first TurnStarted is projected. This is the legitimate birth point for loading persisted state or spawning background loops — construction time has no async root. Not re-fired after Suspended resumes. Like other async slots the body may raise; the Agent wraps a raised error loudly and the first turn fails before any terminal event is emitted.
    • on_shutdown: resource cleanup hook, called once on agent shutdown in reverse registration order; cleanup failure propagates (loud).

    All methods have defaults: an extension overrides only the phases it cares about, and adding a phase is non-breaking (same pattern as PipelineHook).

    LogDurability

    Persistence acknowledgement requested by a log producer.

    • Buffered: the sink accepted the record; it may still live only in memory.
    • Written: the sink completed its normal write path.
    • Durable: the sink crossed its durability barrier before returning.

    LogError

    Errors raised by LogPort implementations. Logging failures stay explicit: each consumer decides whether a failed write is best-effort or fatal.

    LogLevel

    LogPort

    Structured logging port.

    Implementations must preserve call order for writes observed through the same LogPort instance and honour the requested durability before returning. flush commits any buffered records according to the sink's normal durable flush semantics. Implementations should validate records with LogRecord::validate before accepting them.

    LogRecord

    Provider-neutral structured log record.

    source, channel, and event are stable machine-readable identifiers. message is optional human-readable context. scope attributes records to a Posoco session/run/turn when one exists. data is producer-owned JSON; sinks must not inspect it to infer secrets or policy.

    MemoryError

    MemoryPort

    MemoryPort: session memory — the port a memory system plugs into (nowledge-mem today, OpenViking and any other scheme tomorrow). Four slots: inbound for session-opening recall, store/search/delete for the durable record. Core never labels or re-renders provider output — providers own their text format (community convention e.g. devkit envelopes); core owns only timing, placement, and stability.

    Message

    Canonical conversation message. The role determines which payload is legal, making invalid role/payload combinations unrepresentable.

    Invariants:
    • SystemMessage / UserMessage never carry tool calls.
    • AssistantMessage is the ONLY variant that carries tool calls, the ONLY variant that carries reasoning.
    • ToolMessage is the ONLY variant that carries a call_id correlation and a ToolOutcome. It never carries tool calls.

    ModelCallResult

    Return value of HostRuntime::call_model and ModelPort::chat. Bundles the model completion with the processed message tree.

    The pump ALWAYS replaces the transcript body with processed_messages, then appends completion as the new assistant entry.

    ModelError

    ModelFailure

    Categorises why a model step failed (ADR §2.4). The reducer maps this to a FailureReason::Model* variant. Each variant carries a safe bounded label, never a raw provider payload.

    ModelPort

    ModelPort: LLM calling interface. Decides HOW to compact; posoco decides WHEN. chat returns ModelCallResult with processed_messages.

    Both model-side entry points receive an InvocationScope identifying the session/run the call serves (chat additionally carries the reducer- allocated effect id). Adapters that key behaviour on session identity — continuity, telemetry, cost attribution, per-session policy — read it from scope; scope-agnostic adapters ignore it. Treat it as read-only.

    NoopLifecycle

    NoopUiPort

    Default UiPort for headless hosts. request raises Unsupported.

    NotExecutedReason

    Discriminator for a rejected model or child tool call. No provider started for this outcome (ADR §2.7).

    • UnknownTool: the requested tool name is not present in the catalog operation snapshot.
    • NotOffered: the name exists, but its captured exposure does not allow this entry point. No provider was called, including after hook rewrites.
    • OwnerNotAllowed: the caller manifest did not declare the target owner.
    • InvocationLimit: a child would exceed the shared call budget or depth.
    • PolicyConflict: a child needs stronger isolation than its parent's slot.
    • SchemaMismatch: the arguments parsed successfully but failed validation against the tool's input schema. The diagnostic carries a safe JSON path and type expectation; it never includes the raw argument payload.
    • RejectedByHook: a PipelineHook::before_tool returned Reject; the call was never dispatched and reason is the hook's steering text fed back to the model.
    • Revoked: the target is still present in the caller's captured catalog, but the contributing version that supplied it was hard-withdrawn. The provider was never entered, so this is neither an unknown tool nor a business failure.

    Malformed-argument cases never reach this variant — they are ModelParseFailure at the reducer boundary (ADR §2.4).

    Observer

    Read-only committed event projection. Run-bound events carry attribution; operation-level diagnostics may have no run scope. Use PipelineHook for interception, approval or asynchronous side effects.

    OwnerId

    Stable identifier for the provider that owns a tool. The Kernel uses this only for owner lookup in the runtime registry; it does not call into the owner through this type.

    ParamType

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

    PipelineHook

    Pipeline interception points covering nearly the whole agent lifecycle: turn begin/end, before/after the model, before/after tools. Registration is unified: an extension puts self (or any implementor) into the manifest's single hooks array and overrides the methods for the points it cares about.

    • on_turn_begin: async turn-start slot. Dispatched once per run_single_turn, after the TurnStarted observer projection and before the pump's first before_model. Symmetric with on_turn_end: pre-turn failures (agent-shutdown guard, lifecycle on_start raises) dispatch nothing — the turn never began. A raise is a secondary failure — reported to observers, never failing the turn — and later hooks still run; cancellation is not a defect.
    • before_model: invoked before the model is called. Can rewrite messages or raise HookAbort. Multiple hooks form a chained rewrite pipeline — each hook sees the previous hook's output.
    • before_tool: invoked before every tool execution. Every hook is evaluated; merge precedence on ToolDecision (consent beats Reject; Defer still aborts the run).
    • on_post_event: read-only, non-raising notification with the committing run's EventScope. Hooks run in registration order; interception belongs in before_model or before_tool.
    • on_turn_end: async turn-end slot, no payload. The terminal outcome already lives on the Observer channel — TurnCompleted / TurnFailed(String) — and an extension typically implements Observer and PipelineHook as two views over one shared struct, so a hook that needs the outcome reads its own observer-side state instead of a second payload type. Dispatched once per run_turn, after the terminal observer projection (TurnCompleted / TurnFailed) and before the call returns — on both the completed and the failed path. This is the slot for side effects that must finish by turn end, such as committing buffered writes to an external system. Implementations must carry their own timeout budget and may not block the turn's return indefinitely. Division of labor with Observer::on_event(TurnCompleted): the observer slot is sync, read-only and fires first; on_turn_end may await. A raise from on_turn_end is a secondary failure — reported to observers, never replacing the turn's primary outcome — and cancellation is not a defect.

    Async spread: the slots are async, so IO in an implementation body propagates through async. The pipeline dispatches hooks sequentially — each hook is awaited before the next runs — and never opens a thread or worker, so registration order is preserved at every point.

    ProviderConfig

    ProviderConfig: modelport's self-description of accepted config values. A String array (not typed enum) because each provider has its own vocabulary for e.g. reasoning_effort values.

    RateLimitInfo

    Structured quota/rate-limit verdict returned by a provider. Raised as ModelError::RateLimited instead of Transport so hosts can schedule recovery from typed data instead of parsing strings. Providers disagree on the carrier (HTTP 429, Kimi's 403, gateway JSON fields, free text), so the verdict deliberately carries no status code — only scheduling semantics.

    reset_at_ms is the Unix epoch in milliseconds at which the provider stated the quota window frees; None means no explicit reset time was stated, so waiting is not schedulable and the caller should treat the verdict as terminal for this turn. provider_code is the provider's own machine code for the verdict when available (for example z.ai "1308", OpenAI "usage_limit_reached"). message is a bounded excerpt of the provider message, safe to display.

    Reasoning

    Reasoning trace produced by the assistant (e.g. DeepSeek deepseek-reasoner chain-of-thought, OpenAI o-series reasoning content). The Kernel never interprets the content; it stores and replays it. None means the assistant produced no reasoning for this step.

    raw is a provider-defined replay payload: OpenAI Responses API reasoning items must be replayed verbatim when the host manually manages conversation state (per the official reasoning guide), so the adapter stores the raw items here. The kernel never interprets it; providers that replay plain content text (DeepSeek, Kimi, OpenAI-compatible) leave it None.

    RunId

    Stable identifier for a single Run. Supplied by the host in the Start input and carried through every state.

    RuntimeError

    Session

    A conversation session. messages is the linear transcript this thread owns; metadata is a free-form product-owned bag (used for lineage via parent_thread_id, product-specific flags, anything posoco does not interpret).

    messages carries canonical @kernel.Message values directly — the kernel ADT is the single protocol.

    SessionCheckpoint

    One append-safe session durability boundary. from_index is the number of messages the caller believes are already persisted. messages is the canonical tail to append. metadata=None means metadata is unchanged; Some(metadata) replaces the session metadata at the same checkpoint.

    Keeping message and metadata deltas in one operation prevents callers from committing transcript progress while silently dropping session state such as an automatic title, context state, or task bookkeeping.

    SessionError

    SessionId

    Stable identifier for a product session. The Kernel never opens or materializes session storage; it carries this identity through effects, journal entries, and committed event envelopes.

    SessionPort

    StreamAccumulator

    Accumulates StreamChunk events during a streaming chat call. Modelports that want a standard "double-write" implementation (one chunk to the Stream(cb) callback for telemetry, one chunk to the accumulator for the final completion) can use this helper. Modelports with more sophisticated needs (e.g. DeepSeek's dynamic tool-result removal during streaming) are free to ignore this and maintain their own state.

    to_completion() -> @kernel.Completion is the terminal read: chat returns a ModelCallResult whose completion field holds it.

    StreamChunk

    Canonical streaming chunk shape emitted by modelports through the Stream(cb) callback of ModelPort::chat. HostChunkCallback receives this type directly; there is no separate JSON wire contract.

    Streaming remains a host/telemetry concern, not a transcript fact (ADR §2.11).

    StreamMode

    Streaming mode for ModelPort::chat. See trait doc.

    NoStream lets the modelport skip chunk wire-format work entirely (no SSE parsing, no callback construction). Stream(cb) asks it to emit each chunk to cb as a StreamChunk. The ADR §2.11 rationale still holds — streaming is a host/telemetry concern, not a transcript fact — but the chunk contract is now the canonical StreamChunk type, not a private JSON shape.

    SystemPromptContributor

    Returns a section of the system prompt. Empty strings are skipped.

    Stability contract: the returned text MUST be byte-for-byte identical for the entire lifetime of one Agent. Posoco reads each contributor exactly once — during the first turn's first before_model call — and caches the assembled result. Dynamic signals (e.g. plan-mode toggles) MUST NOT be implemented as contributors; inject them through PipelineHook::before_model as ephemeral User messages instead.

    SystemPromptHook

    Combines a fixed base prompt with contributor sections and prepends the result as a single System message at index 0. Empty sections are skipped.

    The assembled prompt is cached lazily on the first call to before_model. Laziness matters because extension Lifecycle::on_start callbacks run inside the first run_single_turn but before the first before_model, so contributors whose state is initialized in on_start must be read after that point. After caching, the text is frozen for the lifetime of the hook.

    SystemPromptSection

    A contributor entry: manifest id (for section header) and the contributor.

    TaskHandle

    Opaque handle for one admitted task. The callbacks are installed by core composition; extensions only receive wait/cancel operations.

    TaskMode

    Execution lifetime for work submitted through the composed task capability.

    TaskOutcome

    A task's terminal result. Background outcomes are retained by session until the owning Agent successfully commits them.

    TaskReceipt

    Stable identity returned at admission. The id is opaque to the caller.

    TaskSpec

    A unit of Agent-owned work. The Agent supplies the structured scope and records the terminal outcome; callers never supply a TaskGroup.

    TaskStatus

    The single terminal state shared by foreground waits and background inbox delivery.

    TaskSubmitError

    Admission failures are explicit so an extension cannot mistake an inactive scope or a closed Agent for accepted work.

    Tasks

    Concrete composed capability. It is deliberately a value rather than a public trait: task admission is Agent-owned and has no replacement port.

    ToolCall

    Tool arguments as parsed JSON. This is the post-parsing representation: if the model stream produced malformed JSON, the reducer has already terminated the Run with ModelParseFailure before this type is constructed. Therefore any ToolCall reaching the scheduler has well-formed arguments, but the arguments may still fail the catalog schema (handled separately as NotExecuted(SchemaMismatch)).

    ToolCallContext

    Effect-scoped execution context handed to ToolProvider::execute. effect_id is the reducer-assigned identity of this ExecuteTool effect — the exact id a later cancellation carries, so a provider owning interruptible work keys its in-flight state by it.

    ToolCancellable

    Cancellation facet a ToolProvider may also implement for effects it owns. Register via ToolCallContext.register_cancel inside execute; the runtime routes a later cancel back through the same effect_id.

    Idempotent per execution. Propagated means only that a signal was sent; Stopped requires confirmation that owned work has stopped (not rollback). Return AlreadySettled for previously completed work. Core bounds cleanup attempts and retains unresolved registrations on failure or timeout so shutdown can retry; never disguise an error as confirmed termination.

    ToolCatalogChange

    Which Agent-owned tool-catalog fact ToolCatalogChanged reports. These are composition/publication facts, not reducer-journal facts: the default InMemoryJournal does not persist them.

    ToolCatalogChangedPayload

    Payload of TurnEvent::ToolCatalogChanged. revision is the owner's contribution revision, while catalog_version is the operation-level catalog version the Agent allocated at capture — the two are deliberately separate numbers. operation_id names the admitting operation for Captured facts. The payload never carries provider objects, credentials, arguments or connection details.

    ToolContributionCommit

    Acknowledgement of one accepted contribution commit. revision is core-owned and monotonic per owner; it is distinct from the catalog version an operation captures at admission. state is the owner slot's state after the commit.

    ToolContributionError

    ToolContributionState

    Directory state of one owner slot, as reported by an ACK or a query.

    ToolContributions

    ToolDecision

    The hook's decision for a pending tool call.

    Chain semantics: every registered hook is evaluated (no short-circuit). Precedence: a raise aborts the run; Defer terminal-rejects; ApproveAfterConsent beats Reject; Reject skips the call and feeds the joined reasons back to the model as a NotExecuted(RejectedByHook) tool result; otherwise the call executes with the chained rewrite. The complete chain is finalized before catalog validation and scheduling, so the final call determines its schema, owner, and execution policy. Consent binds to the exact call_id, name, and arguments returned by that consent decision; a later mutation cannot reuse it, and call_id is never rewriteable.

    ToolDef

    A single tool's definition as the model and the executor see it. Owned by the catalog snapshot (T05), not by individual providers.

    ToolExposure

    Exposure is captured once per operation. Hooks may only narrow it.

    ToolName

    A tool name as it appears in the catalog (call-visible name). The Kernel uses this only for lookup in the catalog snapshot; it never re-parses or re-normalizes it.

    ToolOutcome

    Outcome of a single tool invocation (ADR §2.4, §2.11).

    Variants:
    • Success: tool ran and returned structured data. content is the plain-text result sent to the model by the built-in adapters and used for transcripts. structured is an optional machine-readable payload preserved in the kernel transcript, TurnEvent::ToolCallSucceeded, and session snapshots for observers/UIs; the built-in adapters do not send it to the model. Structured objects follow a reserved summary : String field convention for one-line UI summaries.
    • SuccessWithAttachments: like Success, plus media attachments (images today) carried as content blocks. Adapters that cannot express attachments on the wire downgrade them explicitly — never a silent drop.
    • ToolReportedError: tool ran and reported a business-level failure. The Run continues; the failure is fed back to the model as a normal tool message.
    • RuntimeFailure: tool did not run to completion — adapter raised a RuntimeError. Still non-terminal for the Run.
    • NotExecuted: model requested a tool that was not executed (unknown name, parseable-but-schema-invalid args, or a before_tool hook rejection). The original call id is preserved so the model can correlate the feedback.

    There is deliberately no boolean is_error field here. The variant tag IS the error status.

    ToolProvider

    ToolProvider: declare tools via list_tools(), execute via execute().

    Agent binds each definition's owner to the declaring manifest. Providers declare their own execution policy and exposure; these are not inferred from names or provenance. execute returns the canonical ToolOutcome.

    A composing tool must reserve a policy strong enough for its children: Parallel cannot invoke Sequential/Exclusive; Sequential cannot invoke Exclusive. Such requests are rejected before starting, not upgraded at runtime. The parent's scheduling slot covers the whole joined subtree.

    ToolRegistry

    Dynamic runtime tool registration. Implements ToolProvider.

    ToolRetire

    type ToolRetire = async () -> Result[Unit, String]

    Optional version cleanup callback supplied by the publishing owner. Core calls it once the version is neither active nor referenced by any operation or unconfirmed execution. Return Err(detail) for a failed cleanup; core also bounds the call. Either failure keeps the version pending for a later retry, so the callback must be safe to run again.

    ToolWithdrawMode

    What a withdraw request means for still-live versions of the owner.

    Transcript

    Canonical conversation transcript. Every transition produces a fresh array (the reducer never mutates caller-owned arrays, ADR §2.10). The reducer is the only writer.

    TurnEvent

    Turn lifecycle events observed via Observer::on_event. Payload types are canonical kernel types (ToolCall, ToolOutcome, Message, Usage). Tool events form an explicit per-call state machine — every observable node has its own variant, none doubles as another:

    ToolCallPending (entered the pipeline, awaiting hook chain) ├─ ToolCallApproved (chain finalized executable; consent audit) │ └─ ToolCallStarted (wave launched) │ ├─ ToolCallSucceeded / ToolCallFailed ├─ ToolCallRejected (a decision resolved the call: hook reject, │ validation reject; run continues) └─ ToolCallAbandoned (no outcome exists: cancelled mid-wave, terminal hook failure, run ended)

    Pre-pass validation rejects (unknown tool / schema mismatch) surface as ToolCallRejected directly — the call never entered the pipeline, so no ToolCallPending precedes them.

    TurnId

    Identifier for a single turn within a Run. A Run may contain multiple turns (initial prompt + follow-ups); turns never outlive their Run. follow_up allocates a new TurnId under the same RunId.

    TurnResult

    Return value of Agent::run_turn.

    UiBody

    Structured payload a render intent carries. Host decides layout and color.

    UiDescriptor

    Collected at composition time: the host aggregates every UiPort's descriptors to know which autocomplete triggers to honor.

    UiEntry

    One keyed entry of an Entries body. color carries a semantic role name from the host-negotiated vocabulary, never a raw color value.

    UiError

    Errors raised by UiPort implementations. UiPort::request MUST raise one of these typed variants instead of an untyped error, so that callers can distinguish "user cancelled" from "this UI doesn't support requests".

    UiHint

    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.

    UiPort

    The UI extension contract. Implemented by host packages (cetas-js, cetas-native) or testkit.

    UiRender

    One render intent. Same key replaces a prior intent in the same UiSlot.

    UiRenderHook

    UiRenderHook: PipelineHook (on_post_event) that projects events into UI render intents. Non-raising by contract (both PipelineHook::on_post_event and UiPort::render are non-raising), so there is nothing to propagate.

    UiRequest

    User interaction primitives. Hosts that lack a variant raise UiError::Unsupported.

    UiResponse

    Response to a UiRequest. Cancelled = user dismissed; no UI = UiError::Unsupported.

    UiSlot

    Where a render intent lands: Status (single-line bar), Notice (transient toast), Widget (persistent region).

    Usage

    Token usage reported by the model for a single step. The Kernel only counts what is actually reported; absent dimensions are None, never silently coerced to 0 (ADR §2.9 — unknown usage is never reported as 0).

    WakeupError

    Admission failures are explicit so an extension cannot mistake a closed scope, a malformed tag, or an invalid session for accepted work.

    WakeupPort

    Queue one ordinary turn for the session. Synchronous by contract: callable from task bodies, observer callbacks, and hook tails — never awaits. The session must be the Agent's currently bound active window session (set by the host through Agent::set_active_session); pending tickets die when that binding changes or the process exits, and are not revived by binding the same session again. Coalescing key is (session, tag): a pending entry returns the same ticket (the requested event fires once), while every request updates the entry's latest requirement generation. A request that lands after an input snapshot is never satisfied by that snapshot.

    WakeupTicket

    Opaque admission ticket. id is stable per pending (session, tag) entry; a coalesced re-request returns the same ticket. enqueued_at is an observation timestamp read from the async runtime's timer clock, for display and diagnostics only — it is never part of ticket identity or correctness, and carries no monotonicity guarantee.

    Agent

    pub struct Agent {
    // private fields
    }

    Public deep module for agent developers. The runtime representation is a private handle so canonical execution machinery never enters the interface.

    Agent::Agent

    Construct an Agent from an array of extensions. Aggregation order is the array order; collisions fail-fast. At least one extension must contribute a ModelPort.

    ui_projection (default false): when true, Posoco's built-in render policy (UiRenderHook over the aggregated UiPort) is appended to the hook chain. Hosts with their own UI policy leave it off and register their own PipelineHook instead.

    Agent::commands

    Agent::compact_session

    Agent::context_state

    async fn Agent::context_state(self : Agent, session_id : String) ->
    ContextState
    ?

    Agent::control

    fn Agent::control(self : Agent) -> AgentControl

    The Agent's control handle (experimental runtime seam). Advanced hosts use it to abort the active run or to submit follow-up messages that the Agent consumes at turn boundaries. The handle is identity-guarded: submissions against a stale or absent run are rejected at enqueue time.

    Agent::resume_turn

    async fn Agent::resume_turn(self : Agent, session_id : String, continuation? : String) ->
    TurnResult
    raise
    AgentError

    Resume an interrupted turn from its persisted transcript. If the transcript ends with a user message, that message is replayed as the model input without appending a duplicate. Otherwise continuation is appended as a new user message.

    Agent::run_scoped

    async fn[X] Agent::run_scoped(self : Agent, body : async (Agent) -> X) -> X

    Own an Agent and all of its managed work for one structured lifetime. The body receives the same Agent value that extensions use for turns; returning, raising, or being cancelled closes the task scope and then shuts down the Agent.

    Agent::set_active_session

    fn Agent::set_active_session(self : Agent, session_id : String?) -> Unit

    Bind the host's currently active window session (None clears the binding; the initial state is None). Synchronous and usable while the host is idle; it never raises, never releases the operation guard, and never cancels a running user turn — an already admitted operation settles through its own lifecycle.

    Wakeup admission only accepts this session: request for any other session, or while no session is bound, fails with InvalidSession, and a request never binds on its own. Changing the binding drops the previous window's pending tickets with one wakeup_dropped terminal each; binding the same session again does not drop valid tickets, and binding a previously bound session does not revive its dropped tickets.

    Agent::shutdown

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

    Agent::with_runtime

    Advanced constructor (experimental runtime seam): same Agent, same default assembly, but the caller supplies the effect-execution runtime. runtime is typically a wrapper around @runtime.PortRuntime that overrides only the methods it needs (e.g. execute_tool + cancel_effects for cancellation propagation). Plain extension authors do not need this — see docs/RUNTIME.md.

    AgentConfig

    pub struct AgentConfig {
    max_tool_rounds : Int?
    max_tool_calls : Int?
    max_tool_depth : Int?
    temperature : Double?
    max_output_tokens : Int?
    model_context_window : Int?
    compact_threshold : Double?
    system_prompt : String?
    memory_inbound_timeout_ms : Int?
    }

    Agent-level runtime config: provider-agnostic tuning and run budget. max_tool_rounds bounds the tool rounds of one turn: Some(n) allows n full rounds and rejects the (n+1)-th batch atomically; Some(0) forbids tool execution; None is unbounded (the recommended product default — the human abort and compaction govern loop length, matching peer coding agents).

    AgentControl

    pub struct AgentControl {
    // private fields
    }

    AgentControl::abort_active

    Request cancellation of the active run. Repeated requests return the displayable AbortAlreadyRequested outcome.

    AgentControl::active_run_id

    Currently-active run id, or None while the Agent is idle.

    AgentControl::active_turn_id

    Currently-active turn id, or None while the Agent is idle.

    AgentControl::enqueue_follow_up

    Submit a follow-up consumed by the Agent at a turn boundary. A stale or absent run is rejected so a late task cannot target a later run.

    AgentControl::pending_follow_ups

    fn AgentControl::pending_follow_ups(self : AgentControl) -> Int

    Number of follow-ups waiting for the Agent's next turn boundary.

    NoopCommandPort

    pub(all) struct NoopCommandPort {
    }

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

    NoopCommandPort::commands

    MEMORY_INBOUND_LEAD

    let MEMORY_INBOUND_LEAD : String

    Stable memory-inbound lead used by existing consumers.

    SESSION_TITLE_METADATA_KEY

    let SESSION_TITLE_METADATA_KEY : String

    Stable metadata key for Agent-derived automatic session titles. Product UIs and SessionPort adapters may read this without inspecting transcript roles or extension-specific injected messages.

    agent_config

    fn agent_config(max_tool_rounds? : Int?, max_tool_calls? : Int?, max_tool_depth? : Int?, temperature? : Double?, max_output_tokens? : Int?, model_context_window? : Int?, compact_threshold? : Double?, system_prompt? : String?, memory_inbound_timeout_ms? : Int?) -> AgentConfig

    Construct an AgentConfig naming only the knobs you set; every omitted knob is None. Prefer this over record literals so new optional fields never break construction sites.

    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).

    validate_args

    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).

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io