docs: user documentation and headless sample host
This commit is contained in:
117
docs/architecture.md
Normal file
117
docs/architecture.md
Normal 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.
|
||||
Reference in New Issue
Block a user