22 KiB
VolcanicAgenticCore Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a headless, extensible C# agent harness (a pi clone) with pluggable LLM providers, an extension API, persistent sessions, and a Claude Code subscription provider extension.
Architecture: Two core projects (Abstractions contracts, Core runtime organised as vertical feature slices) plus provider extensions that use only the public extension API. One AgentSession per conversation is the facade a UI consumes; everything the UI needs is an event stream plus a handful of commands.
Tech Stack: .NET 10, C# 14, System.Text.Json, Microsoft.Extensions.DependencyInjection/Logging abstractions, xunit, claude CLI (for the ClaudeCode extension).
Spec: docs/superpowers/specs/2026-09-25-volcanic-agentic-core-design.md
File map
Directory.Build.props
VolcanicAgenticCore.sln
src/Volcanic.AgenticCore.Abstractions/
Messages/{Message.cs, ContentBlock.cs, Usage.cs, StopReason.cs, MessageJson.cs}
Providers/{Model.cs, ThinkingLevel.cs, LlmContext.cs, StreamOptions.cs, AssistantMessageEvent.cs,
AssistantMessageBuilder.cs, IProvider.cs, ProviderRegistration.cs, IModelRegistry.cs}
Tools/{ITool.cs, Tool.cs, ToolResult.cs, ToolCall.cs, JsonSchema.cs}
Extensions/{IExtension.cs, IExtensionApi.cs, IExtensionContext.cs, ExtensionEvent.cs, Events/*.cs,
CommandDefinition.cs, IEventBus.cs, MessageDelivery.cs, ExecResult.cs}
Sessions/{SessionEntry.cs, IReadOnlySessionManager.cs, ISessionStore.cs}
Ui/{IUserInterface.cs, NullUserInterface.cs}
Events/AgentSessionEvent.cs
src/Volcanic.AgenticCore/
Hosting/{AgenticCoreOptions.cs, ServiceCollectionExtensions.cs, VolcanicPaths.cs, AgentSessionOptions.cs}
Features/Models/{ModelRegistry.cs, ApiKeyResolver.cs, TranscriptNormalizer.cs, ThinkingLevels.cs}
Features/Sessions/{SessionManager.cs, FileSessionStore.cs, InMemorySessionStore.cs, SessionContextBuilder.cs}
Features/Tools/{ToolRegistry.cs, ToolExecutor.cs, ArgumentValidator.cs, OutputTruncation.cs,
BuiltIn/{ReadTool.cs, WriteTool.cs, EditTool.cs, BashTool.cs, LsTool.cs, GrepTool.cs, FindTool.cs}}
Features/Extensions/{ExtensionRunner.cs, ExtensionApi.cs, ExtensionContext.cs, ExtensionLoader.cs,
ExtensionLoadContext.cs, EventBus.cs, CombinePolicies.cs, ExtensionRegistration.cs}
Features/Commands/{CommandRegistry.cs}
Features/SystemPrompt/{SystemPromptBuilder.cs, DefaultPrompts.cs}
Features/Compaction/{Compactor.cs, CompactionPrompts.cs, TokenEstimator.cs}
Features/Agent/{AgentLoop.cs, AgentSession.cs, AgentSessionFactory.cs, PendingMessageQueue.cs, RetryPolicy.cs}
extensions/Volcanic.AgenticCore.Extensions.Anthropic/{AnthropicProviderExtension.cs, AnthropicApi.cs, AnthropicModels.cs, Sse.cs}
extensions/Volcanic.AgenticCore.Extensions.OpenAI/{OpenAIProviderExtension.cs, OpenAICompletionsApi.cs, OpenAIModels.cs}
extensions/Volcanic.AgenticCore.Extensions.ClaudeCode/{ClaudeCodeProviderExtension.cs, ClaudeCodeApi.cs, ClaudeArgs.cs,
ClaudeProcess.cs, StreamEventMapper.cs, ContextSerializer.cs, ToolCatalog.cs, SessionRegistry.cs,
ClaudeCodeModels.cs, ClaudeInstallation.cs, ClaudeCodeException.cs, JsonlReader.cs, DoctorCommand.cs}
extensions/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge/Program.cs
samples/Volcanic.AgenticCore.Sample.Headless/Program.cs
tests/Volcanic.AgenticCore.Tests/{Support/FauxProvider.cs, Support/TestHost.cs, ...}
tests/Volcanic.AgenticCore.Extensions.Tests/{Anthropic/*, OpenAI/*, ClaudeCode/*, Fixtures/*}
docs/{architecture.md, extensions.md, providers.md, ui-integration.md, claude-code-provider.md}
README.md
Conventions
Directory.Build.props:net10.0,Nullable=enable,ImplicitUsings=enable,LangVersion=latest,TreatWarningsAsErrors=trueforsrc/andextensions/.- JSON:
System.Text.JsonwithJsonSerializerDefaults.Web, camelCase, polymorphism via[JsonPolymorphic(TypeDiscriminatorPropertyName="role")]for messages and"type"for content blocks and session entries. - Tests: xunit +
Xunit.Assertonly (no FluentAssertions). One test class per feature. - Commits after each task, explicit paths, message format
feat|fix|docs|test(scope): message.
Task 1: Solution scaffold
Files: Directory.Build.props, VolcanicAgenticCore.sln, all .csproj files, tests/Volcanic.AgenticCore.Tests/SmokeTests.cs.
- Create projects with
dotnet new classlib|console|xunit, add references: Core → Abstractions; each extension → Abstractions (ClaudeCode also → Bridge as aProjectReferencewithReferenceOutputAssembly=falseand copy of output); Sample → Core + all extensions; tests → Core + extensions. - Add packages: Core:
Microsoft.Extensions.DependencyInjection.Abstractions,Microsoft.Extensions.Logging.Abstractions,Microsoft.Extensions.Options. Tests:Microsoft.NET.Test.Sdk,xunit,xunit.runner.visualstudio,Microsoft.Extensions.DependencyInjection,Microsoft.Extensions.Logging. SmokeTests.ProjectCompilesassertstypeof(Message)is non-null; rundotnet test; commitchore: scaffold solution.
Task 2: Abstractions — messages and JSON
Files: Messages/*.cs, test MessageJsonTests.cs.
- Tests: round-trip
UserMessage(text + image),AssistantMessage(text, thinking, toolCall, usage, stopReason),ToolResultMessage,CustomMessagethroughMessageJson.Options; discriminator isrole; content discriminator istypewith valuestext|image|thinking|toolCall. - Implement records per spec §6.1.
ToolCallContent.ArgumentsisJsonElement.Usagehas staticZeroandoperator +.StopReasonserialises as camelCase strings (JsonStringEnumConverterwith camelCase policy). - Commit
feat(abstractions): message model.
Task 3: Abstractions — providers, streaming, tools
Files: Providers/*.cs, Tools/*.cs, tests AssistantMessageBuilderTests.cs, ToolSchemaTests.cs.
AssistantMessageBuildertests:StartText/AppendText/EndTextproduceTextStart/TextDelta/TextEndevents with correctContentIndexand a cumulativePartial;StartToolCall/AppendToolCallDelta/EndToolCallparse the accumulated JSON;Finish(StopReason.Stop)yieldsDonewith the final message;Fail("boom", aborted:false)yieldsErrorwithStopReason.ErrorandErrorMessage.Tool<TArgs>test: arecord HelloArgs(string Name, int? Times)produces schema{type:object, properties:{name:{type:string}, times:{type:integer}}, required:[name]}viaJsonSchema.For<T>()(reflection: strings, bool, int/long → integer, double/decimal → number, arrays, nested records, nullable → not required,[Description]→ description).- Implement
Model,ModelCost,ThinkingLevel,LlmContext,ToolDescriptor,StreamOptions,CacheRetention,ProviderResponseInfo,AssistantMessageEventhierarchy,IProvider,ProviderRegistration(withStreamDelegate),IModelRegistry,ITool,Tool<TArgs>,ToolResult,ToolExecutionMode. - Commit
feat(abstractions): provider and tool contracts.
Task 4: Abstractions — extension API, sessions, UI, session events
Files: Extensions/**, Sessions/*.cs, Ui/*.cs, Events/AgentSessionEvent.cs, test SessionEntryJsonTests.cs.
- Tests: session entries round-trip with
typediscriminator (session|message|model_change|thinking_level_change|compaction|custom|custom_message|label|session_info). - Implement
IExtension,IExtensionApi,IExtensionContext,IExtensionCommandContext,ExtensionEventbase withabstract record ExtensionEvent<TResult>, all event records and result records from spec §6.5 table,CommandDefinition,IEventBus,MessageDelivery,ExecOptions/ExecResult,ToolInfo,CommandInfo,ContextUsage,CompactOptions,SwitchResult,NewSessionOptions,SessionEntryfamily,ISessionStore,SessionData,SessionSummary,IReadOnlySessionManager,IUserInterface,NullUserInterface,NotifyKind,UiRequestOptions,AgentSessionEventfamily. - Commit
feat(abstractions): extension, session, ui and event contracts.
Task 5: Core — Models slice
Files: Features/Models/*.cs, tests ModelRegistryTests.cs, ApiKeyResolverTests.cs, TranscriptNormalizerTests.cs.
- Tests: register provider →
Find("p/m")returns model; unregister removes;ListModels()ordered by provider then id.ApiKeyResolver: literal,$VAR,${VAR},!echo secret,auth.jsonlookup,<PROVIDER>_API_KEYenv fallback, null when nothing.TranscriptNormalizer: drops error/aborted assistant messages; inserts synthetic tool result; converts foreign thinking to text; replaces images for text-only models; converts custom → user. - Implement
ModelRegistry : IModelRegistry(thread-safe dictionary ofProviderRegistration,StreamAsync(model, ctx, options, ct)dispatch that injects the resolved API key),ApiKeyResolver,TranscriptNormalizer,ThinkingLevels.Supported(model),Clamp(model, level). - Commit
feat(core): models slice.
Task 6: Core — Sessions slice
Files: Features/Sessions/*.cs, tests SessionManagerTests.cs, FileSessionStoreTests.cs.
- Tests:
SessionManager.Create(cwd, store)appends header;AppendMessagechainsParentIdto previous leaf;GetBranch()returns root→leaf;Open(id)reloads and continues appending;BuildContext()after aCompactionEntryyields summary user message + kept messages;FileSessionStorewrites<dir>/<scope>/<id>.jsonlwith one JSON per line andListAsync(scope)returns summaries sorted by timestamp desc. - Implement per spec §6.6. Entry ids: 8 hex chars from
Guid.NewGuid(). - Commit
feat(core): sessions slice.
Task 7: Core — Tools slice
Files: Features/Tools/**, tests ToolRegistryTests.cs, ArgumentValidatorTests.cs, BuiltInToolTests.cs.
- Tests: registry first-registration-wins and
SetActive; validator rejects missing required and wrong primitive types with a readable message;readreturns numbered content and truncation details;writecreates parent dirs;editreplaces exactly-once text and errors on 0 or >1 matches;bashstreams output viaIProgressand returns exit code in details, honours timeout;lsmarks dirs with/;grepfinds regex matches withfile:line:text;findmatches globs and skips.git. - Implement
ToolRegistry,ArgumentValidator,OutputTruncation(2000 lines / 50 KB), built-ins per spec §7.4 withPromptSnippetandPromptGuidelines. - Commit
feat(core): tools slice with built-in tools.
Task 8: Core — Extensions slice
Files: Features/Extensions/*.cs, tests ExtensionRunnerTests.cs, ExtensionLoaderTests.cs.
- Tests: activation queues tool/command/provider registrations and commits on success, discards on throw (and records error); action methods throw
ExtensionNotBoundExceptionbeforeBind();EmitAsync(ToolCallEvent)returns first block; handler exception inToolCallEventblocks;InputEventchain transform then handled short-circuits;ContextEventchain;BeforeAgentStartEventcollects messages and last system prompt;SessionBeforeSwitchEventfirst cancel; notify events swallow exceptions and raiseExtensionErrorEvent;EventBusdelivers across two extensions;Invalidate()makes old api throw. Loader: builds an extension assembly into a temp dir (compile a tiny extension with Roslyn is too heavy — instead test with the test assembly itself:ExtensionLoader.LoadFromAssembly(typeof(TestExtension).Assembly)discovers publicIExtensiontypes) andDiscoverDirectoriesignores missing dirs. - Implement
ExtensionRunner(handler table keyed by event type, ordered),CombinePoliciesper event type,ExtensionApi,ExtensionContext,EventBus,ExtensionLoader(+ExtensionLoadContext : AssemblyLoadContextusingAssemblyDependencyResolver, unifyingVolcanic.AgenticCore.AbstractionsandMicrosoft.Extensions.*to the default context),ExtensionRegistration(Name, Func<IServiceProvider, IExtension>). - Commit
feat(core): extensions slice.
Task 9: Core — Commands, SystemPrompt, Compaction slices
Files: Features/Commands/CommandRegistry.cs, Features/SystemPrompt/*.cs, Features/Compaction/*.cs, tests CommandRegistryTests.cs, SystemPromptBuilderTests.cs, CompactorTests.cs.
- Tests: command lookup splits at first space and passes raw args; duplicate names get
name:2; prompt builder includes tools section, guidelines, AGENTS.md content from cwd and cwd line; compactorShouldCompactthreshold;FindCutPointnever cuts before a tool result;Serializeformats roles;CompactAsyncwith FauxProvider producesCompactionEntrywithFirstKeptEntryIdand summary text. - Implement per spec §7.2, §7.3.
- Commit
feat(core): commands, system prompt and compaction slices.
Task 10: Core — Agent slice (loop, session facade, factory, hosting)
Files: Features/Agent/*.cs, Hosting/*.cs, tests AgentLoopTests.cs, AgentSessionTests.cs, Support/FauxProvider.cs, Support/TestHost.cs.
FauxProvider: registered as providerfauxwith modelfaux/echo;Script(params FauxTurn[])whereFauxTurn.Text(".."),FauxTurn.ToolCall("name", args),FauxTurn.Error("msg", retryable),FauxTurn.Length(..); emits events throughAssistantMessageBuilderin token-sized chunks; honoursct.- Loop tests: single text turn emits
AgentStart, TurnStart, MessageStart, MessageUpdate*, MessageEnd, TurnEnd, AgentEnd, AgentSettled; tool round trip executes the tool and makes a second provider call whose context contains theToolResultMessage; parallel tools end in completion order and results in source order; steering during a tool turn is delivered before the next call; follow-up runs as an extra turn; abort yieldsStopReason.Aborted; retryable error retries withAutoRetryStart/End;Lengthfails tool calls; overflow error triggers compaction then retry. - Session tests:
/cmd argsdispatches even while streaming;PromptAsyncwhile streaming without behavior throws;SetModelAsyncappendsModelChangeEntryand emitsModelChanged; messages persist and reload throughSwitchSessionAsync;SessionStart/SessionShutdownextension events fire;IUserInterfacereachesctx.Uiin a tool. - Implement
PendingMessageQueue,RetryPolicy(retryable classification: contains429,5xx,overloaded,rate limit,stream ended before,ECONNRESET, timeouts; overflow:context_length_exceeded,prompt is too long,maximum context length),AgentLoop,AgentSession,AgentSessionFactory : IAgentSessionFactory,AgenticCoreOptions,AgentSessionOptions,VolcanicPaths,ServiceCollectionExtensions.AddAgenticCore. - Commit
feat(core): agent loop and session facade.
Task 11: Anthropic provider extension
Files: extensions/Volcanic.AgenticCore.Extensions.Anthropic/*.cs, tests Anthropic/AnthropicApiTests.cs with a fake HttpMessageHandler replaying an SSE fixture.
- Tests: request body has
systemtop-level,messageswith tool_use/tool_result mapping,tools,max_tokens,thinkingwhen reasoning level set,cache_controlon system and last user block; SSE fixture with text + thinking + tool_use maps to the correct event sequence andUsage(input, output, cache read/write) andStopReason.ToolUse; HTTP 529 body yieldsErrorevent with message containing529. - Implement
Sseline parser,AnthropicApi.StreamAsync,AnthropicModels(claude-opus-4-1, claude-sonnet-4-5, claude-haiku-4-5 with costs/context 200k,ThinkingLevelMap),AnthropicProviderExtension : IExtensioncallingapi.RegisterProvider(...)withApiKey = "$ANTHROPIC_API_KEY". - Commit
feat(anthropic): messages api provider extension.
Task 12: OpenAI-compatible provider extension
Files: extensions/Volcanic.AgenticCore.Extensions.OpenAI/*.cs, tests OpenAI/OpenAICompletionsApiTests.cs.
- Tests: request maps system →
systemrole message, tool calls →assistant.tool_calls, tool results →toolrole withtool_call_id,stream: true,stream_options.include_usage,reasoning_effortwhen reasoning; SSE fixture withdelta.content,delta.reasoning_content, chunkeddelta.tool_callsmaps to correct events andfinish_reason: tool_calls→ToolUse;[DONE]terminates; usage chunk populatesUsage. - Implement
OpenAICompletionsApi,OpenAIModels(gpt-5, gpt-5-mini, gpt-4.1 defaults),OpenAIProviderExtensionwithOptions { BaseUrl, ProviderId, Models }so hosts can instantiate it for OpenRouter/Ollama. - Commit
feat(openai): chat completions provider extension.
Task 13: ClaudeCode bridge
Files: extensions/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge/Program.cs, tests ClaudeCode/BridgeTests.cs (runs the bridge in-process via Bridge.Run(TextReader, TextWriter, env)).
- Tests:
initialize→ result withprotocolVersion,capabilities.tools;tools/list→ catalogue and creates ready marker;tools/call→ error-32001and creates violation marker; unknown method →-32601; invalid JSON →-32700; missing catalogue env → exit 2. - Implement
Bridgestatic class withRun(...)andMainthat wires stdin/stdout; 1 MB request cap. - Commit
feat(claude-code): mcp proposal bridge.
Task 14: ClaudeCode provider extension
Files: extensions/Volcanic.AgenticCore.Extensions.ClaudeCode/*.cs, fixtures copied from pi-claude-code-provider (tests/.../Fixtures/claude-2.1.281-*.jsonl, MIT, attributed in Fixtures/README.md), tests ClaudeCode/*Tests.cs.
- Tests:
ClaudeArgs.Buildyields the exact argv from spec §8.3 (order-sensitive);ToolCatalogaliases invalid names totool_<sha16>and mapsmcp__volcanic__<alias>back;ContextSerializeremits header record + one record per message, escapes@, putscache_controlon the last block, and appends image attachment block;JsonlReadersplits on LF, strips CR, rejects > 8 MB;StreamEventMapperonclaude-2.1.281-stream-tool-ok.jsonlyieldsStart, TextStart, TextDelta("calling"), TextEnd, ToolCallStart, ToolCallDelta, ToolCallEnd(name "probe", args {path:"probe"}), Done(ToolUse)and ignores everything after the latch;stream-max-tokens→Done(Length);stream-http529→Errorretryable (stream ended before message_stop);stream-context-window-exceeded→ error message starting withcontext_length_exceeded:;stream-prompt-too-longlikewise; init validation fails whenapiKeySource != "none"or tools mismatch;SessionRegistry.Resolveprefers registered session, else<cwd>in prompt, else newest session for tool-free requests. - Implement per spec §8.3:
ClaudeInstallation.InspectAsync(path,--version,auth statusJSON,--helpflag check),ClaudeProcess(spawn with allow-listed env, process group kill viakill -TERM -pidthen-KILL, Windowstaskkill /T /F, stderr tail 64 KB, idle 5 min / total 30 min timeouts),ClaudeCodeApi.StreamAsync,ClaudeCodeModels,DoctorCommand(/claude-code-doctor),ClaudeCodeProviderExtension(registers providerclaude-code,SessionStartEvent→ registry,SessionShutdownEvent→ remove,MessageEndEvent→ prefix overflow errors). Bridge location:Path.Combine(AppContext.BaseDirectory, "Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.dll")run with thedotnetmuxer resolved fromEnvironment.ProcessPathor PATH. - Commit
feat(claude-code): provider extension.
Task 15: Sample host and end-to-end smoke
Files: samples/Volcanic.AgenticCore.Sample.Headless/Program.cs, test EndToEndTests.cs.
- Sample: builds a host with all extensions, reads
--model,--cwd, prompt from args, prints everyAgentSessionEventas one JSON line, exits onAgentSettled. With--fauxit registers the FauxProvider (moved toVolcanic.AgenticCore/Testing/FauxProvider.csso the sample can use it). - E2E test: DI host + FauxProvider + built-in
bashtool round trip throughAgentSession. - Commit
feat(sample): headless reference host.
Task 16: Documentation
Files: README.md, docs/architecture.md, docs/extensions.md, docs/providers.md, docs/ui-integration.md, docs/claude-code-provider.md.
- Write each per spec §9 (ui-integration includes a complete Telegram.Bot sketch: session-per-chat map, streaming edit throttle, inline-keyboard
IUserInterface,ISessionStoreover a DB), extension authoring walkthrough with a fullIExtensionexample (tool + command + tool_call hook), provider authoring walkthrough, ClaudeCode requirements/limits/doctor. - Commit
docs: user documentation.
Task 17: Verification and push
dotnet build -warnaserrorclean;dotnet testall green;dotnet run --project samples/... -- --faux "hello"prints events; optional--model claude-code/sonnetsmoke ifclaude auth statusreports a subscription.- Push
mainto origin.
Self-review
- Spec coverage: §6.1–6.8 → Tasks 2–4; §7.1 → Task 10; §7.1.1 → Task 5; §7.2/7.3 → Task 9; §7.4 → Task 7; §7.5 → Task 5; §7.6 → Task 10; §8.1 → 11; §8.2 → 12; §8.3 → 13–14; §9 → 16; §11 → each task's tests.
- Names used consistently:
AgentSession,IAgentSessionFactory,AgentSessionOptions,AgenticCoreOptions,ModelRegistry,ExtensionRunner,SessionManager,ToolRegistry,Compactor,SystemPromptBuilder,AssistantMessageBuilder,FauxProvider,ClaudeArgs,StreamEventMapper,ContextSerializer,ToolCatalog,SessionRegistry,Bridge.