6.0 KiB
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:
- open or create the
SessionManagerin the store (scope derived from the working directory, or given); - create the tool, command and model registries;
- activate every extension (host-wide list, per-session extras, discovered assemblies), each getting its
own
IExtensionApi; registrations are committed only when activation succeeds; - bind the runtime so extension actions work, restore model/thinking level from the branch on resume;
- 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):
/command args→ the command handler runs immediately, even while streaming.inputevent: extensions can transform or swallow the text.- Streaming? Queue as steer or follow-up (the caller must say which).
- 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
Errorevents. 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_endfires 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 throughAgentSession.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 (
ExtensionEventsubclasses) go to extensions throughIExtensionApi.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_errorsession events, and dispatch continues. A failingtool_callhandler 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.