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.10.2
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    3K

    #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.9.0

    #Ports

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

    PortPurpose
    ModelPortModel chat + context compaction (chat, compact, provider_config)
    ToolProviderTool discovery and execution
    SessionStoreLoad / 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; 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: [],
    requires: [],
    }
    }

    ///|
    /// 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 and leave tools: [].

    #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)
    • EmptyPort("SessionStore") — no extension contributes a required port

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

    #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 --target native --output-json moon test --target native --output-json moon fmt moon info

    #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. Trimmed in R3 M3.7 to just 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.

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

    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. All fields default to empty arrays; 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.

    Hook

    using @colmugx/posoco/port { trait Hook }

    Pipeline interception points. 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.

    • 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. Approve (with optional rewrite), Defer, or Reject. First non-Approve decision wins.
    • on_post_event: invoked after every effect completes. Read-only and non-raising — all registered hooks run in registration order; a handler cannot fail in a typed way, and a contract violation aborts loudly by design. For interception or abortion use before_model / before_tool.
    • on_post_event_at: scoped variant of on_post_event, additionally carrying the EventScope attribution of the run that committed the effect. The core dispatches ONLY this variant; its default delegates to on_post_event, so pre-scope hooks keep working unchanged.

    HookAbort

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

    HookStage

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

    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.

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

    MemoryEntry

    MemoryError

    MemoryPort

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

    MemoryQuery

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

    NotExecutedReason

    Discriminator for the reason a tool call was NOT executed even though the model requested it (ADR §2.7).

    • UnknownTool: the requested tool name is not present in the catalog snapshot that was handed to the CallModel effect.
    • SchemaMismatch: the model's 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.

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

    Observer

    Observer: read-only event listener. One event channel:

    • Agent-level (on_event / on_event_at): TurnStarted / model response / tool pending and result / TurnCompleted / TurnFailed. Agent projects these directly from committed runtime events; it never reconstructs them by scanning a terminal transcript. on_event_at additionally carries the EventScope attribution (session/run/turn); the core dispatches only the scoped variant and its default delegates here.

    The channel is read-only — never blocks or mutates. For interception or abortion, implement Hook::before_model / Hook::before_tool (they carry block capability via HookAbort / ToolHookDecision); for real-time per-effect telemetry implement Hook::on_post_event — the former effect-level observer channel was folded into Hook (D5/BR-7).

    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.

    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.

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

    R3 M3.7: messages carries canonical @kernel.Message values directly. There is no longer a legacy @types.Message form — the kernel ADT is the single protocol.

    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.

    SessionStore

    SessionStore: persist and load conversation sessions by id.

    Methods use the raise style (matching ModelPort/CommandPort) instead of returning Result. This eliminates the Result-plus-async redundancy and lets js-target adapters (e.g. JsonlSessionStore) implement the trait directly instead of going through load_async/save_async side-paths.

    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.

    R3 M3.7: the previous to_response() -> ModelResponse has been replaced by to_completion() -> @kernel.Completion. The old ModelResponse type is deleted — chat now returns ModelCallResult whose completion field is the canonical Completion.

    StreamChunk

    Streaming chunk shape emitted by modelports inside the Stream(cb) callback of ModelPort::chat. This is the modelport's own representation of provider chunks; posoco does NOT define a canonical chunk type (ADR §2.11 — streaming is host/telemetry concern, not transcript fact).

    StreamChunk exists as a public type because it is useful for modelports that want a shared wire vocabulary (ext-llm and ext-deepseek both emit these). Modelports that prefer their own chunk type are free to use it; the only contract is "what you pass to the Stream(cb) callback, the HostChunkCallback consumer must be able to decode".

    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 raw JSON — posoco does NOT define a canonical chunk type (ADR §2.11: streaming is host/telemetry concern, not transcript fact), so the JSON shape is a private contract between the modelport and the HostChunkCallback consumer.

    SystemPromptContributor

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

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

    ToolDef

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

    ToolHookDecision

    The hook's decision for a pending tool call.

    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. structured is the model-facing JSON payload; content is the human-readable mirror used by the transcript.
    • 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 or parseable-but-schema-invalid args). 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().

    R3 M3.7: signatures use canonical kernel types directly. ToolDef carries owner + policy at the catalog level; list_tools returns the kernel ToolDef minus those (they are filled in by the catalog builder, since they depend on the agent's tool-routing configuration, not on the tool itself). execute returns ToolOutcome so business errors, runtime errors, and not-executed cases are all distinguishable without a boolean.

    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. R3 M3.7: payload types are now canonical kernel types (ToolCall, ToolOutcome, Message, Usage). is_error on ToolCallResult is preserved for legacy observer compatibility — it is derived from the ToolOutcome variant (is_failure).

    TurnResult

    Return value of Agent::run_turn. R3 M3.7: payload types are canonical.

    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.

    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.

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

    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, BR-5): 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 Hook instead.

    Agent::commands

    Agent::control

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

    catalog_source (optional): when supplied, the source owns the tool catalog — Posoco reads it at construction and re-reads it at each prompt boundary whose revision() changed, swapping the refreshed snapshot in for subsequent runs while in-flight runs keep their version. Definitions are taken verbatim (owner/policy respected). Without it, the catalog is the static snapshot of the aggregated ToolProvider declarations.

    AgentConfig

    pub(all) struct AgentConfig {
    max_tool_rounds : Int?
    temperature : Double?
    max_output_tokens : Int?
    model_context_window : 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).

    CompositeToolProvider

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

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

    CompositeUiPort

    pub(all) struct CompositeUiPort {
    contributors : Array[&
    UiPort
    ]
    }

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

    CompositeUiPort::CompositeUiPort

    CompositeUiPort::contributors

    CompositeUiPort::render

    CompositeUiPort::ui_descriptor

    HookRecord

    pub(all) struct HookRecord {
    label : String
    outcome : RecordedHookOutcome2
    }

    A recorded hook invocation: which hook function ran and its decision.

    ManifestOnly

    pub(all) struct ManifestOnly {
    cached :
    ExtensionManifest

    }

    Anonymous Extension wrapper around a pre-built ExtensionManifest.

    Real extensions implement Extension directly on their struct (so the compiler knows the concrete type satisfies every port it contributes). But tests, prototypes, and one-off agents often want to compose ports without defining a fresh named struct. ManifestOnly fills that role: build a manifest with tk_ext, wrap it, and pass it to Agent::new as a &Extension.

    This is a testkit-only convenience. Production agents should expose a typed <ext>_extension() factory that returns an ExtensionManifest from a real struct, not use ManifestOnly.

    ManifestOnly::ManifestOnly

    Construct an anonymous Extension from a pre-built manifest.

    ManifestOnly::extension_id

    fn ManifestOnly::extension_id(self : ManifestOnly) -> String

    Expose Extension methods on ManifestOnly for dot-syntax callers and so &ManifestOnly can be coerced to &Extension.

    ManifestOnly::manifest

    Expose Extension methods on ManifestOnly for dot-syntax callers and so &ManifestOnly can be coerced to &Extension.

    MemoryRetrievalHook

    pub(all) struct MemoryRetrievalHook {
    memories : Array[&
    MemoryPort
    ]
    top_k : Int
    on_failure : (String) -> Unit
    }

    MemoryRetrievalHook: Hook (before_model) that retrieves durable memory and injects as a SystemMessage (after the system-prompt). Best-effort, non-fatal — a search failure never aborts the turn; messages pass through unchanged and the failure is reported through on_failure (Agent wires this to the Custom(source="posoco.core",label="secondary_failure") observer event).

    MemoryRetrievalHook::MemoryRetrievalHook

    fn MemoryRetrievalHook::MemoryRetrievalHook(memories~ : Array[&
    MemoryPort
    ], top_k? : Int, on_failure? : (String) -> Unit) -> MemoryRetrievalHook

    NoopCommandPort

    pub(all) struct NoopCommandPort {
    }

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

    NoopCommandPort::commands

    NoopLifecycle

    pub(all) struct NoopLifecycle {
    }

    NoopUiPort

    pub(all) struct NoopUiPort {
    }

    Default UiPort for headless hosts. request raises Unsupported.

    NoopUiPort::NoopUiPort

    fn NoopUiPort::NoopUiPort() -> NoopUiPort

    NoopUiPort::render

    fn NoopUiPort::render(_self : NoopUiPort, _intent :
    UiRender
    ) -> Unit

    NoopUiPort::ui_descriptor

    RecordedHookOutcome2

    pub(all) enum RecordedHookOutcome2 {
    PassedThrough
    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]
    }

    Multi-hook fake that implements Hook at all three interception points. By default it is a pass-through (returns messages unchanged, approves tool calls). Records every call.

    RecordingHook::RecordingHook

    fn RecordingHook::RecordingHook() -> RecordingHook

    RecordingHook::abort_when

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

    Configure the hook to abort/reject when the label contains trigger.

    RecordingHook::defer_when

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

    Configure the hook to defer when the label contains trigger.

    RecordingHook::records

    RecordingLifecycle

    pub(all) struct RecordingLifecycle {
    id : String
    log : Array[String]
    seen_model : Bool
    seen_ui : Bool
    }

    RecordingLifecycle — records Lifecycle phase invocations for post-hoc assertions. Log entries are "<id>:compose(model=<Bool>,ui=<Bool>)", "<id>:start", "<id>:shutdown" in call order. Share one Array[String] across several fakes (and other recorders writing the same log) to assert interleaving and ordering.

    RecordingLifecycle::RecordingLifecycle

    fn RecordingLifecycle::RecordingLifecycle(id~ : String, log~ : Array[String]) -> RecordingLifecycle

    RecordingLifecycle::on_compose

    Expose Lifecycle methods on RecordingLifecycle for dot-syntax callers.

    RecordingLifecycle::on_shutdown

    async fn RecordingLifecycle::on_shutdown(self : RecordingLifecycle) -> Unit

    Expose Lifecycle methods on RecordingLifecycle for dot-syntax callers.

    RecordingLifecycle::on_start

    async fn RecordingLifecycle::on_start(self : RecordingLifecycle) -> Unit

    Expose Lifecycle methods on RecordingLifecycle for dot-syntax callers.

    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::RecordingObserver

    fn RecordingObserver::RecordingObserver() -> RecordingObserver

    RecordingObserver::events

    Snapshot of all recorded events in order.

    RecordingObserver::on_event

    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::RecordingSessionStore

    fn RecordingSessionStore::RecordingSessionStore() -> RecordingSessionStore

    RecordingSessionStore::fail_load

    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::ops

    Recorded operations in order.

    RecordingSessionStore::save

    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

    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::RecordingToolProvider

    RecordingToolProvider::exec_records

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

    RecordingToolProvider::execute_direct

    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::list_tools

    RecordingToolProvider::ops

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

    RecordingUiPort

    pub(all) struct RecordingUiPort {
    rendered : Array[
    UiRender
    ]
    scripted_responses : Array[
    UiResponse
    ]
    unsupported_mode : Bool
    descriptor :
    UiDescriptor

    }

    RecordingUiPort::RecordingUiPort

    Construct with a list of scripted responses. They will be returned in order, one per request call.

    RecordingUiPort::new_unsupported

    fn RecordingUiPort::new_unsupported() -> RecordingUiPort

    Construct a UiPort that always raises Unsupported from request.

    RecordingUiPort::render

    Expose UiPort methods on RecordingUiPort for dot-syntax callers.

    RecordingUiPort::render_count

    fn RecordingUiPort::render_count(self : RecordingUiPort) -> Int

    Number of render calls received.

    RecordingUiPort::rendered_in_slot

    Filter rendered intents by slot. Useful for asserting "exactly one Notice was emitted" without caring about other slots.

    RecordingUiPort::rendered_intents

    All render intents received, in arrival order. The returned array is a copy; mutating it does not affect the recording.

    RecordingUiPort::request

    Expose UiPort methods on RecordingUiPort for dot-syntax callers.

    RecordingUiPort::set_descriptor

    fn RecordingUiPort::set_descriptor(self : RecordingUiPort, descriptor :
    UiDescriptor
    ) -> Unit

    Set the descriptor returned by ui_descriptor.

    RecordingUiPort::ui_descriptor

    Expose UiPort methods on RecordingUiPort for dot-syntax callers.

    RecordingUiPort::unconsumed_response_count

    fn RecordingUiPort::unconsumed_response_count(self : RecordingUiPort) -> Int

    Number of request calls that have NOT yet been answered (i.e. consumed a scripted response). Useful to assert that no extra requests happened.

    ScopeRecordingModel

    ModelPort fake that records the InvocationScope received by every chat/compact call. Chat behaviour is scripted like ScriptedModel; compact returns compact_result when set, otherwise raises ResponseParse (same contract as ScriptedModel). Use it to pin the scope-flow contract end to end.

    ScopeRecordingModel::ScopeRecordingModel

    ScopeRecordingModel::chat_scopes

    Scopes observed by chat, in call order.

    ScopeRecordingModel::compact_scopes

    Scopes observed by compact, in call order.

    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::ScriptedModel

    fn ScriptedModel::ScriptedModel(steps : Array[ScriptedModelStep]) -> ScriptedModel

    ScriptedModel::call_count

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

    Number of chat calls observed so far.

    ScriptedModel::chat_direct

    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::options_received

    Snapshot of all chat options received, in call order.

    ScriptedModelStep

    A single scripted model interaction. R3 M3.7: each step produces a ModelCallResult (the canonical chat return type) instead of the deleted ModelResponse. Stream carries the chunk sequence plus the final completion; chunks are emitted to the callback during the call.

    ScriptedToolOutcome

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

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

    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.

    SystemPromptHook

    pub(all) struct SystemPromptHook {
    base_prompt : String
    contributors : Array[SystemPromptSection]
    }

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

    SystemPromptHook::SystemPromptHook

    fn SystemPromptHook::SystemPromptHook(base_prompt~ : String, contributors~ : Array[SystemPromptSection]) -> SystemPromptHook

    SystemPromptHook::assemble

    fn SystemPromptHook::assemble(self : SystemPromptHook) -> String

    Assemble base + non-empty contributor sections with id: headers.

    SystemPromptSection

    pub(all) struct SystemPromptSection {
    id : String
    contributor : &
    SystemPromptContributor

    }

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

    SystemPromptSection::SystemPromptSection

    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.

    ToolProviderOp

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

    Unified provider operation trace, preserving list/execute interleaving.

    ToolRegistry

    Dynamic runtime tool registration. Implements ToolProvider.

    ToolRegistry::ToolRegistry

    fn ToolRegistry::ToolRegistry() -> ToolRegistry

    ToolRegistry::register

    Register (or deliberately replace) a tool at runtime. Re-registering an existing name overwrites both the definition and the executor — that is intentional hot-replacement semantics for dynamic registries. Callers that did NOT mean to replace should use register_strict, which fails fast on a name collision instead of silently shadowing the prior tool.

    ToolRegistry::register_strict

    Register a tool, failing fast with RuntimeError::ToolAlreadyRegistered when the name is already taken. This mirrors the composition-time rule (M0-T06-B): a name collision is never silently resolved by last-wins.

    ToolRegistry::unregister

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

    UiRenderHook

    pub(all) struct UiRenderHook {
    ui : &
    UiPort

    }

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

    UiRenderHook::UiRenderHook

    UiRenderHook::on_post_event

    fn UiRenderHook::on_post_event(self : UiRenderHook, stage :
    HookStage
    ) -> Unit

    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

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

    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 universal AgentConfig for scripted tests.

    tk_error_result

    fn tk_error_result(content : String) ->
    ToolOutcome

    Build a business-error ToolOutcome (provider reports a tool-level failure the model should see, but the run continues).

    tk_ext

    Build a ManifestOnly extension from labeled optional arguments.

    Every port argument defaults to empty; pick the ones the test needs. The model parameter is &ModelPort? because at most one model is allowed per agent — Some(m) puts it in the manifest's models array, None leaves models empty (use a different extension to contribute the model).

    Example:
    let 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(),
    )

    tk_ok_result

    fn tk_ok_result(content : String) ->
    ToolOutcome

    Build a successful ToolOutcome.

    tk_scope

    fn tk_scope(session_id? : String, run_id? : String) ->
    InvocationScope

    Fabricated InvocationScope for tests that drive a ModelPort directly (no reducer in play, so effect_id is None).

    tk_stop_response

    fn tk_stop_response(text : String) ->
    ModelCallResult

    Build a ModelCallResult whose completion is a stop-finish text response with no tool calls. The processed_messages is None, signalling to the pump that the modelport did no preprocessing and the transcript should be left as-is.

    Important: because ModelCallResult requires processed_messages to be a concrete array (not Optional), the default @runtime.PortRuntime is responsible for filling it with the input messages when the modelport did not preprocess. Tests that drive ScriptedModel directly through the agent pipeline go through PortRuntime, which substitutes the actual input messages when processed_messages is empty. See PortRuntime::call_model for the substitution logic.

    tk_system_msg

    fn tk_system_msg(text : String) ->
    Message

    Build a system message.

    tk_tool_call_response

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

    Build a ModelCallResult whose completion 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. Owner is a placeholder — Agent::build_catalog derives the catalog owner at composition time. Policy is Parallel (the declared default; composition honors it as-is). Provenance is None.

    tk_user_msg

    fn tk_user_msg(text : String) ->
    Message

    Build a user message.

    tk_view

    Build a CompositionView for unit-testing Lifecycle::on_compose implementations: requires plays the role of the fake manifest's declaration, and the composed ports stand in for what the composition would have delivered.

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