
# Posoco Agent Guide

1. 先加载 moonbit-orientation
2. 实现之前，请仔细思考：你的实现是否符合函数式思想

This is a [MoonBit](https://docs.moonbitlang.com) project: **`colmugx/posoco`** — an LLM agent framework using hexagonal (ports-and-adapters) architecture. Depends on `moonbitlang/async`.

## One-Line Summary

Posoco defines 9 traits + an Agent struct with a `run_turn` loop; no generics, uses MoonBit `&Trait` references throughout.

## Architecture

```
Layer 3 — Agent (program/composition)
  Agent struct holds &Trait references; run_turn() orchestrates the loop

Layer 2 — Interpreters (runtime/injection)
  Users provide concrete trait implementations (mocks in tests, real in prod)

Layer 1 — Traits (effect interfaces)
  ModelPort, ToolProvider, SessionStore, Observer, Compressor,
  PipelineHook, MemoryPort, Lifecycle, CommandPort
```

### Source Files

| File | Purpose |
|------|---------|
| `port_*.mbt` | 9 `pub(open)` traits split across `port_model.mbt`, `port_tool.mbt`, `port_session.mbt`, `port_observer.mbt`, `port_compressor.mbt`, `port_hook.mbt`, `port_memory.mbt`, `port_lifecycle.mbt`, `port_command.mbt` |
| `types_*.mbt` | Shared types: `types_message.mbt`, `types_model.mbt`, `types_session.mbt`, `types_chat.mbt`, `types_memory.mbt` |
| `agent.mbt` | Agent struct + AgentConfig + run_turn loop + tool routing |
| `errors.mbt` | All typed errors: ModelError, SessionError, RuntimeError, AgentError, CompositionError, etc. |
| `builtin.mbt` | NoopCompressor, NoopHook, HookChain, CompositeToolProvider, ToolRegistry, helpers |
| `testkit.mbt` | Conformance testkit: ScriptedModel, Recording* fakes, trace assertions (M0-T04) |
| `posoco_test.mbt` | 9 blackbox tests (uses @posoco prefix) |
| `posoco_wbtest.mbt` | 30 whitebox tests (direct symbol access) |
| `*_test.mbt` | M0 regression suites: `session_metadata_test.mbt` (T05), `tool_outcome_test.mbt` (T06), `error_transparency_test.mbt` (T07), `testkit_conformance_test.mbt` (T04) |
| `quickstart_fixture_test.mbt` | Compiled blackbox contract for README/docs quickstart public API |
| `pkg.generated.mbti` | Generated by `moon info` — commit when public API changes |

## Commands

```bash
rtk moon check --target native --output-json   # type check (also runs in pre-commit hook)
rtk moon test  --target native --output-json   # all root tests (native: 98/98 at M0 implementation sign-off)
rtk moon test  --target native --output-json -f 't05*'  # filter by glob
rtk moon test --update                         # refresh snapshots
rtk moon fmt                                   # format all code
rtk moon info                                  # update .mbti generated interface
rtk moon coverage analyze                      # coverage report (redirect explicitly if needed)
```

> **IMPORTANT**: All `moon check`/`test`/`build` commands MUST use `--output-json`
> so diagnostics are machine-readable and test counts are captured. Always pass
> `--target native` for the primary gate (default target runs sync tests only).

**Pre-commit checklist** (the hook only runs `moon check`, but run all 4):
```bash
rtk moon check --target native --output-json
rtk moon test --target native --output-json
rtk moon fmt
rtk moon info
```

## Key Architecture Rules

- **Split your pre-commit**: The pre-commit hook only runs `moon check`. The full 4-step checklist above is for _before pushing_.
- **Two hook layers**: `Observer` (read-only, returns Unit, never blocks) vs `PipelineHook` (can mutate stages, returns Result, can abort turn).
- **Tool failure is non-fatal**: tool errors become messages for the LLM; only model errors abort the turn.
- **Compressor is pure**: returns instructions (None/Replace/NewThread) — never saves sessions or emits events directly. Agent and Observer handle side effects.
- **Compression runs every LLM round** (before each model call), not once per turn.
- **All traits are `pub(open)`** so external packages can implement them.
- **Tool collision is fail-fast (M0-T06-B)**: `Agent::new` raises `CompositionError::ToolCollision` if two providers register the same tool name. There is no last-wins fallback.
- **Session metadata fidelity (M0-T05)**: loaded session metadata (unknown keys) survives a turn unchanged; NewThread lineage (`parent_session_id`) is preserved through the final save.
- **Tool outcome is single-sourced (M0-T06-A)**: the `ToolCallResult` event's `is_error` is derived from `ToolResult.is_error`, never hardcoded.
- **Malformed stream JSON is loud (M0-T07)**: `StreamAccumulator::to_response` raises `ModelError::ResponseParse` on bad tool-call JSON; it never silently becomes `{}`.
- **Terminal events are single-sourced (M0-T07)**: after `TurnStarted`, success emits exactly one `TurnCompleted`; every primary failure emits exactly one sanitized `TurnFailed`. A `BeforeTurn` abort happens before start and emits neither.
- **Secondary failures remain observable (M0-T07)**: After/Error hook failures emit `Custom(source="posoco.core", label="secondary_failure", ...)` without replacing the primary outcome or exposing hook payloads.
- **Shutdown is loud (M0-T07)**: `Agent::shutdown` does not catch lifecycle panics; cleanup failure propagates to the caller.
- **`pub impl` required for direct calls**: trait methods on concrete types need `pub impl Trait for Type` (not bare `impl`) to be callable as `value.method()` within the same package.

## MoonBit Syntax Traps (Common Mistakes)

| Trap | Wrong | Correct |
|------|-------|---------|
| Trait impl | `impl Trait for Type { fn method(...) }` | `pub impl Trait for Type with method_name(self, ...) { }` |
| Try operator | `func()?` | Match `Ok`/`Err` explicitly |
| Optional chain | `self.hook?.call()` | `match self.hook { Some(h) => ..., None => () }` |
| Map literal | `{ key: value }` | `Map::from_array([("key", value)])` |
| Trait object | `Box<dyn Trait>` | `&Trait` — Agent has no generics |
| Named enum variant | `Variant { field: Type }` | `Variant(field~ : Type)` — note `~` label |
| Infinite loop | `loop { }` (Rust) | `while true { }` |
| Struct fields `mut` | Needs `mut` on Map/Array fields | Only `mut` on scalar index counters, not on Array/Map fields |
| Map access | `map[key]` without guard | Guard with `.contains()` first (returns V, not Option[V]) |
| Json null | `Json::Null` | `Json::null()` |

## Mock Pattern for Tests

Tests use an array-based composition setup (blackbox uses `@posoco` prefix, whitebox uses direct access). `Agent::new` takes labeled array args and **raises `CompositionError`** on tool-name collision:

```moonbit
let model = MockModelPort::{ responses: [stop_response("hi")], index: 0 }
let tools = MockToolProvider::{ tools: [], results: Map::from_array([]) }
let store = MockSessionStore::{ store: Map::from_array([]) }
let observer = MockObserver::{ events: [] }
let config = AgentConfig::{ max_tool_rounds: 10, temperature: None, max_output_tokens: None, tool_choice: None, reasoning_effort: None, model_context_window: None }
let agent = Agent::new(
  model,
  tools=[tools],
  hooks=[],
  memory=[],
  compressors=[NoopCompressor::{}],
  observers=[observer],
  sessions=[store],
  lifecycle=None,
  config~,
)
let result = agent.run_turn(user_msg("hello"), "session_1")
```

Key mock conventions:
- `MockModelPort` — `mut index : Int`, cycles through `responses` array, fails when exhausted
- `MockToolProvider` — unified: declares `tools` AND holds `results` map for execute; guard with `.contains()` first
- `MockSessionStore` — in-memory map; returns empty session for unknown IDs
- `MockObserver` — records all events for post-turn inspection
- Always use `Map::from_array([])` for empty maps and session metadata
- Always use `Json::null()` for ToolCall arguments
- For new regression tests, prefer the testkit fakes (`ScriptedModel`, `RecordingToolProvider`, `RecordingSessionStore`, `RecordingObserver`) in `testkit.mbt`

## Useful Context from Docs

See `docs/` for detailed guidance:
- `docs/support-matrix.md` — Supported/experimental package matrix + per-target truth (M0-T01/T03)
- `docs/workspace.md` — Workspace & local-resolution entry (M0-T02)
- `docs/design/01-error-transparency-adr.md` — Core catch inventory + hook failure semantics (M0-T07)
- `docs/architecture.md` — Full design rationale, type definitions, loop pseudocode
- `docs/DEVELOPING.md` — MoonBit syntax traps and coding conventions (essential reading for newcomers)
- `docs/EXTENSIONS.md` — How to create extension packages that implement Posoco traits
- `docs/TESTING.md` — Mock patterns, test coverage map, common pitfalls
- `docs/questions.md` — Open MoonBit syntax/toolchain questions (checked by User)

## Rules

- Before start work, YOU MUST LOAD `moonbit-orientation` skill

## MoonBit Syntax Uncertainty

遇到 MoonBit 语法不确定时，不要自信地给出方案。把问题记录到 `docs/questions.md`，每个工作阶段结束后（或关键代码卡住时），User 会检查列表并帮助解决。
