telegram-bot.md: what you can send, files from the agent, speech settings, security notes. ui-integration.md: document/voice input rows and a new section 5 on IChatChannel. architecture, extensions and CLAUDE.md list IChatChannel. Drop the interim paragraph from the message-context spec; mark the attachments spec implemented with resolved questions.
8.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
VolcanicAgenticCore is a headless, extensible agent harness for .NET 10, ported from
earendil-works/pi without the terminal UI. It also ships a C# port of
chem/pi-claude-code-provider, which drives the local claude
CLI on a Claude subscription as a model provider. A Telegram bot is the first front end.
When porting behaviour, pi and pi-claude-code-provider are the reference: event names, JSON shapes and invariants deliberately match them.
Commands
dotnet build # whole solution (VolcanicAgenticCore.slnx)
dotnet test # all three test projects; no test calls a paid API
dotnet test tests/Volcanic.AgenticCore.Tests # one project
dotnet test --filter "FullyQualifiedName~AgentLoopTests" # one class
dotnet test --filter "FullyQualifiedName~AgentLoopTests.SomeTest" # one test
# reference host with the scripted faux provider (no keys)
dotnet run --project samples/Volcanic.AgenticCore.Sample.Headless -- --faux "Say hello"
dotnet run --project samples/Volcanic.AgenticCore.Sample.Headless -- --model claude-code/sonnet "List the files here"
# Telegram bot
TELEGRAM_BOT_TOKEN=123:abc TELEGRAM_ALLOWED_USER_IDS=111111111 dotnet run --project hosts/Volcanic.AgenticCore.Telegram
Directory.Build.props sets TreatWarningsAsErrors=true, nullable and implicit usings for every non-test project,
so a new warning fails the build. Test projects turn warnings-as-errors off. Tests use xunit 2.
Architecture
Read docs/architecture.md first. Key points:
src/Volcanic.AgenticCore.Abstractionsholds the contracts: messages,Model/IProvider, the streaming event protocol,ITool/Tool<TArgs>,IExtension/IExtensionApiand extension events, session entries/stores,IUserInterface,IChatChannel, andAgentSessionEvent. Extensions reference only this assembly, so it stays small and stable. Don't let runtime types leak into it.src/Volcanic.AgenticCoreis organised as vertical slices underFeatures/(Agent,Sessions,Models,Tools,Extensions,Commands,SystemPrompt,Compaction,Processes). Each slice exposes one entry type (AgentSession,SessionManager,ModelRegistry,ToolRegistry,ExtensionRunner,CommandRegistry,SystemPromptBuilder,Compactor,ProcessSpawnPolicy). Slices talk only through those entry types and the Abstractions.Hosting/hasAddAgenticCore(...)andAgenticCoreOptions;Testing/FauxProvider.csis the scripted provider that tests and--fauxuse.- One conversation = one
AgentSession, built byIAgentSessionFactory.CreateAsync. Extensions are instantiated per session and each session has its ownModelRegistry, so provider registrations and extension fields are per conversation. Process-wide state has to be explicit (e.g.ClaudeLoginCoordinator.Shared). - Two event systems.
AgentSessionEvents go to hosts viasession.Subscribeand are invoked synchronously on the loop.ExtensionEvents go to extensions viaapi.On<T>: they are mutable event args (Block,Cancel,MarkHandled, setText/Message/...), run sequentially in load order, and some short-circuit. - Loop invariants (from pi): providers never throw, they emit
Errorevents. An error or abort ends the run. Tool preparation runs in source order, execution runs in parallel by default, results are appended in source order. Failed assistant messages are persisted but dropped from model context. Context overflow triggers one compaction and a retry. - Processes: every agent-requested process gets its environment from
IProcessSpawnPolicy(AgenticCoreOptions.ProcessEnvironment: deny-list,Allow,SecretValues, host hooks);AgentSessionredactsSecretValuesfrom tool output;HostProcessHardeningmakes the host non-dumpable on Linux, once per process. Test projects apply it withHardenHostProcess = falsefrom a module initializer (NoHostHardening.cs). - Persistence: a session is a tree of JSONL entries (
id/parentId+ leaf pointer) under~/.volcanic/agent/sessions/<scope>/(VOLCANIC_AGENT_DIRoverrides the root). Compaction entries replace older messages when context is built. - System prompt pulls project context from the working directory:
AGENTS.md, orCLAUDE.mdif there is noAGENTS.md(never both), plusPERSONA.mdandMEMORY.md. File names match case-insensitively. - Extension discovery: host code, then
<cwd>/.volcanic/settings.json/.volcanic/extensions/*.dll, then the same under~/.volcanic/agent. Earlier sources win name collisions. Each assembly loads in its ownAssemblyLoadContextand sharesVolcanic.AgenticCore.*,Microsoft.Extensions.*andSystem.*with the host.
Extensions (extensions/)
Extensions built on the public API only: providers Anthropic (Messages API), OpenAI (chat-completions compatible),
ClaudeCode + ClaudeCode.Bridge; Mcp (an MCP client: stdio servers as tools, plus the Playwright browser
preset; docs/mcp.md); and the GoogleCalendar feature extension (calendar tool,
/calendar_connect|status|disconnect, one Google account per chat keyed by session scope, tokens under
<agentDir>/google-calendar/; docs/google-calendar.md). The Claude Code provider is the most intricate piece
(docs/claude-code-provider.md):
- Every model call spawns a fresh
claude -pwith built-in tools off and replays the whole transcript as stream-json. Core tools are exposed as MCP toolsmcp__volcanic__<alias>through the Bridge, a tiny MCP server that never executes tools. Ontool_usethe provider kills the process and returnsDone(ToolUse), and the core runs the tool. system/initis validated strictly (fail closed). The extension refuses to run ifANTHROPIC_API_KEYor a similar env var is set, because it is subscription-only.- Session expiry →
ClaudeLoginCoordinatorruns one login flow at a time throughClaudeCodeProviderOptions.LoginUi, then replays the paused request exactly once./claude-code-doctorand/claude-code-loginare its commands. - Tests replay recorded CLI output from
tests/Volcanic.AgenticCore.Extensions.Tests/ClaudeCode/Fixtures/*.jsonl. Mcp: the tool catalogue is discovered once per host process (McpCatalogCache.Shared), each session starts its own server process on the first call, and tests runtests/Volcanic.AgenticCore.Extensions.Tests.FakeMcpServer.
Telegram host (hosts/Volcanic.AgenticCore.Telegram)
Long polling (Polling/, with ReconnectPolicy backoff). Each private chat gets its own AgentSession
(Chats/ChatRegistry), with workspace <WorkspaceRoot>/<chatId> and session scope telegram-<chatId>. Access is
limited by AllowedUserIds. Incoming messages go through Inbound/: the router takes a ChatInbox lease per
message in update order, InboundComposer builds its part (reply/forward headers from MessageContext, text and
photo) in the background, and the inbox flushes a batch InputDebounce (600 ms) after the last message as one prompt
with a local-time header. Files, audio and voice are saved to <workspace>/inbox/ (voice transcribed by
Speech/OpenAiSpeechToText when OPENAI_API_KEY is set) through InboundComposer's content steps
(Inbound/InboundMedia), and Chats/TelegramChatChannel implements IChatChannel for the core send tool through
ChatRenderer.RunInOrderAsync. The first allowed id is the "owner": OwnerLoginChannel sends Claude Code re-login
prompts to that chat. It resolves lazily to break the DI cycle ChatRegistry → session factory → extension → ChatRegistry.
Answers go out complete, not streamed (Rendering/). Tests drive the host through Support/FakeBotClient and ChatHost.
Conventions
- Commits follow Conventional Commits scoped by area:
feat(claude-code): ...,fix(telegram): ...,docs(spec): ...,test(...),chore: .... - Design specs and implementation plans live in
docs/superpowers/specs/anddocs/superpowers/plans/. Update the relevant spec and the user docs indocs/when behaviour changes. - By design, the port leaves out pi's terminal UI, themes, keybindings, skills/prompt templates, OAuth logins and session-tree navigation commands.