docs: user documentation and headless sample host

This commit is contained in:
VolcanicAgenticCore
2026-09-25 12:36:24 +00:00
parent c220428cb7
commit 018c92af49
7 changed files with 879 additions and 1 deletions

117
docs/architecture.md Normal file
View File

@@ -0,0 +1,117 @@
# Architecture
## Projects
```
src/Volcanic.AgenticCore.Abstractions contracts (no runtime code except helpers)
src/Volcanic.AgenticCore runtime, organised as vertical slices
extensions/* provider extensions built on the public API only
samples/* reference host
tests/* xunit
```
`Abstractions` is deliberately small and stable: messages and content blocks, `Model` and `IProvider`,
the streaming event protocol, `ITool`, the extension API and its events, session entries and stores,
`IUserInterface`, and the `AgentSessionEvent` family a UI subscribes to. Extensions reference only this
assembly, which is what allows them to be loaded from separate directories.
## Vertical slices
The runtime is organised by feature, not by layer. Each folder under `src/Volcanic.AgenticCore/Features`
owns everything for that feature and exposes one entry type:
| Slice | Entry type | Responsibility |
|---|---|---|
| `Models` | `ModelRegistry` | Provider registrations, `provider/id` lookup, API key resolution, transcript normalisation before a call |
| `Sessions` | `SessionManager` | The entry tree of one session, branch/context building, `ISessionStore` implementations |
| `Tools` | `ToolRegistry` | Known and active tools, argument validation, output truncation, the built-in tools |
| `Extensions` | `ExtensionRunner` | Activating extensions (two-phase), dispatching events, contexts, the event bus, assembly loading |
| `Commands` | `CommandRegistry` | Slash commands |
| `SystemPrompt` | `SystemPromptBuilder` | Base prompt, tools section, guidelines, project context files, environment |
| `Compaction` | `Compactor` | When to compact, what to summarise, the summary prompt |
| `Agent` | `AgentSession`, `AgentSessionFactory` | The conversation facade and the loop |
Slices talk to each other only through their entry types and the Abstractions contracts. `AgentSession`
is the composition root of one conversation; a host never touches the other slices directly.
## One conversation = one `AgentSession`
`IAgentSessionFactory.CreateAsync(options)` builds a session:
1. open or create the `SessionManager` in the store (scope derived from the working directory, or given);
2. create the tool, command and model registries;
3. activate every extension (host-wide list, per-session extras, discovered assemblies), each getting its
own `IExtensionApi`; registrations are committed only when activation succeeds;
4. bind the runtime so extension actions work, restore model/thinking level from the branch on resume;
5. emit `session_start`.
Extensions are instantiated per session, so extension fields are per conversation. Provider registrations
also live per session (a `ModelRegistry` per session), which keeps sessions independent.
## The agent loop
`PromptAsync(text)`:
1. `/command args` → the command handler runs immediately, even while streaming.
2. `input` event: extensions can transform or swallow the text.
3. Streaming? Queue as steer or follow-up (the caller must say which).
4. Idle? Start a run.
One run (`RunLoopAsync`):
```
threshold compaction check
before_agent_start → system prompt options, injected custom messages
agent_start
loop:
turn_start
append pending messages (prompt, injected, steering, follow-up)
compaction between turns if requested/needed
context → extensions may rewrite the message list
stream the provider → message_start / message_update* / message_end (with retry + overflow recovery)
tool calls? → tool_call hook, execute (parallel by default), tool_result hook, tool result messages
turn_end
more tool calls or steering messages or continue requested? → next turn
follow-up queued? → next turn
agent_end
agent_settled
```
Invariants carried over from pi:
- Providers never throw; failures are `Error` events. The registry also guards against a stream that ends
without a terminal event.
- An error or abort ends the run immediately (after `turn_end`/`agent_end`).
- If the assistant stopped with `Length`, every tool call in that message fails with an error result.
- Tool preparation is sequential in source order; execution is parallel unless the session or any called
tool asks for sequential. `tool_execution_end` fires in completion order, tool result messages are
appended in source order.
- Failed assistant messages are persisted (so the transcript is honest) but the transcript normaliser drops
them from the model context, so retries do not see them.
## Event model
Two independent event systems exist:
- **Session events** (`AgentSessionEvent`) go to the host through `AgentSession.Subscribe`. Names match pi's
JSON event stream (`agent_start`, `message_update`, `tool_execution_end`, `agent_settled`, ...). Handlers are
invoked synchronously on the loop; keep them fast.
- **Extension events** (`ExtensionEvent` subclasses) go to extensions through `IExtensionApi.On<TEvent>`. They
are mutable "event args": a handler influences the runtime by calling methods on the event
(`Block`, `Cancel`, `MarkHandled`) or setting properties (`Text`, `Payload`, `Message`, `Override`).
Handlers run sequentially in load order and see each other's changes; some events short-circuit.
## Persistence
A session is a tree of entries (`message`, `model_change`, `thinking_level_change`, `compaction`, `custom`,
`label`, `session_info`) linked by `id`/`parentId`, with a leaf pointer. `Branch(entryId)` moves the leaf so the
next append forks. Context building walks the branch and, when a compaction entry exists, replaces older
messages with the summary. `FileSessionStore` writes JSONL; implement `ISessionStore` for a database.
## Error isolation
- Extension handler exceptions are logged, reported as `extension_error` session events, and dispatch
continues. A failing `tool_call` handler blocks the tool.
- Tool exceptions become error tool results the model can read.
- Provider exceptions become assistant error messages; transient ones are retried with backoff, context
overflow triggers one compaction and retry.