Skip to content

feat: expand parity workflows and harden agent runtime#4

Merged
Telli merged 19 commits intomainfrom
codex/opencode-parity
Apr 14, 2026
Merged

feat: expand parity workflows and harden agent runtime#4
Telli merged 19 commits intomainfrom
codex/opencode-parity

Conversation

@Telli
Copy link
Copy Markdown
Contributor

@Telli Telli commented Apr 13, 2026

Summary

  • add layered SharpClaw JSONC config, typed agent catalog defaults, and diagnostics-aware prompt/status integration
  • add self-hosted session sharing, compaction, runtime hooks, and an embedded HTTP/SSE server surface
  • add parity-oriented CLI and REPL commands for models, connect, agents, share, unshare, compact, serve, and sessions alias

Verification

  • dotnet build SharpClawCode.sln --no-restore
  • dotnet test tests/SharpClaw.Code.UnitTests/SharpClaw.Code.UnitTests.csproj --no-restore
  • dotnet test tests/SharpClaw.Code.IntegrationTests/SharpClaw.Code.IntegrationTests.csproj --no-restore
  • dotnet test SharpClawCode.sln --no-restore

Telli and others added 16 commits April 10, 2026 21:48
…ion history

Introduces ContentBlock (with ContentBlockKind enum) and ChatMessage sealed records, registers them in ProtocolJsonContext, and adds three roundtrip serialization tests covering text, tool-use, and tool-result blocks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…vent with tool-use fields

Add ProviderToolDefinition to Protocol (decoupled from Tools), extend
ProviderRequest with Messages/Tools/MaxTokens, extend ProviderEvent with
BlockType/ToolUseId/ToolName/ToolInputJson, and add InputSchemaJson to
ToolDefinition. All new fields are optional with null defaults so existing
call sites compile without modification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ts message history

- Add ToolUse factory method to ProviderStreamEventFactory
- Update AnthropicSdkStreamAdapter to detect ContentBlockStart/Stop and accumulate InputJsonDelta into tool_use events
- Create AnthropicMessageBuilder to map ChatMessage[]/ProviderToolDefinition[] to Anthropic SDK params (MessageParam[], ToolUnion[])
- Update AnthropicProvider.StartStreamAsync to use message history, tools, and MaxTokens from ProviderRequest when Messages is set
- Add ToolUseStreamAdapterTests covering factory, stream adapter, and builder behaviors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ge history

- Update OpenAiMeaiStreamAdapter to detect FunctionCallContent in stream updates and emit ToolUse provider events
- Add OpenAiMessageBuilder to map ProtocolChatMessage[]/ProviderToolDefinition[] to MEAI types (uses AIFunctionFactory.CreateDeclaration for tool schemas)
- Update OpenAiCompatibleProvider to use message history and tools from ProviderRequest when present, falling back to single-prompt path
- Add 5 new unit tests covering role mapping, FunctionCallContent, FunctionResultContent, tool building, and null-schema handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…olExecutor

Implements ToolCallDispatcher that dispatches ProviderEvent tool-use requests
through IToolExecutor, publishes ToolStartedEvent and ToolCompletedEvent runtime
events, and returns a ContentBlock for feeding results back to the provider.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add multi-iteration tool-calling loop that streams provider responses,
detects tool-use events, dispatches them via ToolCallDispatcher, feeds
results back as conversation messages, and resumes until the model stops
requesting tools or MaxToolIterations is reached.

- Create AgentLoopOptions for loop configuration (max iterations, max tokens)
- Update ProviderBackedAgentKernel with tool-calling loop and backward-compat overload
- Update AgentFrameworkBridge to accept IToolRegistry, build ToolExecutionContext,
  map tool definitions, and collect tool results
- Add tool_call_roundtrip mock scenario to DeterministicMockModelProvider
- Add parity test verifying the loop executes end-to-end

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… context window management

Introduces ConversationHistoryAssembler and ContextWindowManager to enable
multi-turn conversations across session resume. Prior completed turns are
read from the event store, assembled into ChatMessage[] pairs, truncated to
a 100k-token budget, and threaded through PromptExecutionContext →
AgentRunContext → ProviderBackedAgentKernel so the provider receives full
context on every request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add NuGet package metadata including author, company, license, repository,
and per-package descriptions to all publishable source projects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SharpClawActivitySource and SharpClawMeterSource as central OTel
instrumentation points; TurnActivityScope and ProviderActivityScope wrap
turn/provider execution in Activity spans; NdjsonTraceFileSink writes
completed spans as NDJSON for offline analysis. DefaultTurnRunner and
ProviderBackedAgentKernel are wired to emit spans and record metrics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wraps IModelProvider registrations in ResilientProviderDecorator which
retries transient failures (HttpRequestException 5xx, TaskCanceledException,
IOException) with exponential backoff and jitter, honours Retry-After on
429 responses, and opens a circuit breaker after a configurable consecutive-
failure threshold.  All behaviour is driven by ProviderResilienceOptions and
can be disabled via Enabled=false.  Four unit tests cover retry-then-succeed,
non-transient pass-through, circuit open, and probe-after-break-duration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code fixes:
- Remove duplicate system prompt ChatMessage from tool-calling loop
  (providers apply SystemPrompt via ProviderRequest, not as a message)
- Track usage per iteration in ProviderBackedAgentKernel so
  ProviderActivityScope tags reflect correct per-call token counts
- Validate ToolName/ToolUseId in ToolCallDispatcher before execution;
  return error tool-result blocks when metadata is missing
- Use envelope.Request from IToolExecutor for ToolStartedEvent so
  persisted events reflect real approval scope and destructive flags
- Add lock-based thread safety to NdjsonTraceFileSink writer
- Fix ContextWindowManager XML docs to match actual behavior
  (preserves most recent non-system message, not specifically user)

Documentation fixes:
- Fix env var names in getting-started.md to use .NET double-underscore
  path format (SharpClaw__Providers__Anthropic__ApiKey)
- Fix OpenAI config section name to OpenAiCompatible
- Fix MaxToolIterations default from ~10 to 25 in integration guide
- Fix ISharpClawTool interface snippet to match actual contract
  (ToolDefinition + ExecuteAsync with context/request)
- Fix all 3 example appsettings.json to use correct config paths
  (SharpClaw:Providers:Catalog:DefaultProvider, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 13, 2026 22:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands SharpClaw Code’s runtime “parity” surface by adding layered JSONC configuration, agent catalog defaults, diagnostics-aware prompt/status output, self-hosted session sharing + compaction workflows, an embedded HTTP/SSE server, and provider/tooling updates to support multi-turn + tool-calling behavior end-to-end.

Changes:

  • Add runtime services for layered config, agent catalog resolution, workspace diagnostics, sharing/unsharing, compaction, and hook dispatch.
  • Add embedded HTTP server endpoints (JSON + SSE) and new CLI/REPL commands for parity workflows (models/connect/agents/share/unshare/compact/serve).
  • Add provider-side tool-call/message mapping, provider resilience decorator, and telemetry Activity+metrics infrastructure; extend protocol models/events accordingly.

Reviewed changes

Copilot reviewed 129 out of 129 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/SharpClaw.Code.UnitTests/SharpClaw.Code.UnitTests.csproj Adds Agents project reference for new unit tests.
tests/SharpClaw.Code.UnitTests/Runtime/SharpClawConfigServiceTests.cs Tests JSONC user/workspace config merge precedence.
tests/SharpClaw.Code.UnitTests/Runtime/ShareAndCompactionServicesTests.cs Tests share + compaction persistence and hook triggers.
tests/SharpClaw.Code.UnitTests/Runtime/ConversationHistoryAssemblerTests.cs Tests event→chat-history assembly behavior.
tests/SharpClaw.Code.UnitTests/Runtime/ContextWindowManagerTests.cs Tests context-window truncation rules.
tests/SharpClaw.Code.UnitTests/Providers/ResilienceTests.cs Tests retry/circuit-breaker behavior of provider decorator.
tests/SharpClaw.Code.UnitTests/Protocol/ChatMessageSerializationTests.cs Tests ChatMessage/ContentBlock JSON round-tripping.
tests/SharpClaw.Code.UnitTests/Operational/OperationalReportsJsonTests.cs Extends RuntimeStatusReport JSON round-trip coverage.
tests/SharpClaw.Code.UnitTests/Agents/ToolCallDispatcherTests.cs Tests provider tool-use→tool execution bridge.
tests/SharpClaw.Code.ParityHarness/ParityScenarioTests.cs Adds tool-call roundtrip parity scenario.
tests/SharpClaw.Code.ParityHarness/ParityScenarioIds.cs Registers new parity scenario id.
tests/SharpClaw.Code.MockProvider/ParityProviderScenario.cs Adds mock provider scenario constant for tool-call flow.
tests/SharpClaw.Code.MockProvider/DeterministicMockModelProvider.cs Emits tool_use events and recognizes tool_result messages.
tests/SharpClaw.Code.IntegrationTests/Smoke/CliCommandSurfaceTests.cs Expands expected CLI command surface/options.
src/SharpClaw.Code.Web/SharpClaw.Code.Web.csproj Adds package metadata description.
src/SharpClaw.Code.Tools/SharpClaw.Code.Tools.csproj Adds package metadata description.
src/SharpClaw.Code.Tools/Models/ToolDefinition.cs Extends tool definition with optional JSON schema.
src/SharpClaw.Code.Telemetry/SharpClaw.Code.Telemetry.csproj Adds package metadata description.
src/SharpClaw.Code.Telemetry/Metrics/SharpClawMeterSource.cs Adds central Meter + counters/histograms.
src/SharpClaw.Code.Telemetry/Export/NdjsonTraceFileSink.cs Adds NDJSON activity span sink.
src/SharpClaw.Code.Telemetry/Diagnostics/TurnActivityScope.cs Adds turn Activity span helper with tags/errors.
src/SharpClaw.Code.Telemetry/Diagnostics/SharpClawActivitySource.cs Adds shared ActivitySource definition.
src/SharpClaw.Code.Telemetry/Diagnostics/ProviderActivityScope.cs Adds provider call Activity span helper.
src/SharpClaw.Code.Skills/SharpClaw.Code.Skills.csproj Adds package metadata description.
src/SharpClaw.Code.Sessions/Storage/SessionStorageLayout.cs Adds share snapshot storage paths.
src/SharpClaw.Code.Sessions/SharpClaw.Code.Sessions.csproj Adds package metadata description.
src/SharpClaw.Code.Runtime/Workflow/ShareSessionService.cs Implements self-hosted share snapshot create/remove/load.
src/SharpClaw.Code.Runtime/Workflow/HookDispatcher.cs Executes configured external hooks per trigger.
src/SharpClaw.Code.Runtime/Workflow/ConversationCompactionService.cs Compacts sessions into title + summary metadata.
src/SharpClaw.Code.Runtime/Workflow/AgentCatalogService.cs Overlays configured agents on built-ins + resolves defaults.
src/SharpClaw.Code.Runtime/Turns/DefaultTurnRunner.cs Adds turn Activity + metrics; passes conversation history to agent.
src/SharpClaw.Code.Runtime/SharpClaw.Code.Runtime.csproj Adds package metadata description.
src/SharpClaw.Code.Runtime/Server/WorkspaceHttpServer.cs Adds embedded HTTP JSON/SSE surface for runtime commands + shares.
src/SharpClaw.Code.Runtime/Orchestration/ConversationRuntime.cs Adds agent/config defaults, auto-share, new commands, hook dispatch, status enhancements.
src/SharpClaw.Code.Runtime/Diagnostics/WorkspaceDiagnosticsService.cs Adds cached diagnostics snapshot, including dotnet build parsing.
src/SharpClaw.Code.Runtime/Diagnostics/OperationalDiagnosticsCoordinator.cs Surfaces diagnostics counts into RuntimeStatusReport.
src/SharpClaw.Code.Runtime/Context/PromptExecutionContext.cs Adds conversation history field.
src/SharpClaw.Code.Runtime/Context/PromptContextAssembler.cs Adds diagnostics section + conversation history assembly/truncation.
src/SharpClaw.Code.Runtime/Context/ConversationHistoryAssembler.cs Converts persisted events into ChatMessage pairs.
src/SharpClaw.Code.Runtime/Context/ContextWindowManager.cs Truncates chat history to token budget estimate.
src/SharpClaw.Code.Runtime/Configuration/SharpClawConfigService.cs Loads and merges user/workspace JSONC config by precedence.
src/SharpClaw.Code.Runtime/Composition/RuntimeServiceCollectionExtensions.cs Registers new runtime services (config, catalog, diagnostics, share, compact, hooks, server).
src/SharpClaw.Code.Runtime/Abstractions/IWorkspaceHttpServer.cs Adds embedded server abstraction.
src/SharpClaw.Code.Runtime/Abstractions/IWorkspaceDiagnosticsService.cs Adds diagnostics snapshot abstraction.
src/SharpClaw.Code.Runtime/Abstractions/ISharpClawConfigService.cs Adds layered config abstraction.
src/SharpClaw.Code.Runtime/Abstractions/IShareSessionService.cs Adds session share abstraction.
src/SharpClaw.Code.Runtime/Abstractions/IRuntimeCommandService.cs Adds share/unshare/compact commands + agent id in context.
src/SharpClaw.Code.Runtime/Abstractions/IHookDispatcher.cs Adds hook dispatcher abstraction.
src/SharpClaw.Code.Runtime/Abstractions/IConversationCompactionService.cs Adds compaction abstraction.
src/SharpClaw.Code.Runtime/Abstractions/IAgentCatalogService.cs Adds agent catalog abstraction.
src/SharpClaw.Code.Providers/SharpClaw.Code.Providers.csproj Adds package metadata description.
src/SharpClaw.Code.Providers/Resilience/ResilientProviderDecorator.cs Adds retry/rate-limit/circuit-breaker wrapper.
src/SharpClaw.Code.Providers/ProvidersServiceCollectionExtensions.cs Adds resilience options binding + provider wrapping.
src/SharpClaw.Code.Providers/OpenAiCompatibleProvider.cs Accepts conversation messages, tools, and max tokens.
src/SharpClaw.Code.Providers/Internal/ProviderStreamEventFactory.cs Adds factory for tool-use provider events.
src/SharpClaw.Code.Providers/Internal/OpenAiMessageBuilder.cs Maps protocol messages/tools to MEAI types.
src/SharpClaw.Code.Providers/Internal/OpenAiMeaiStreamAdapter.cs Emits tool-use ProviderEvents from function calls.
src/SharpClaw.Code.Providers/Internal/AnthropicSdkStreamAdapter.cs Accumulates tool_use JSON and emits tool-use ProviderEvents.
src/SharpClaw.Code.Providers/Internal/AnthropicMessageBuilder.cs Maps protocol messages/tools to Anthropic SDK params.
src/SharpClaw.Code.Providers/Configuration/ProviderResilienceOptions.cs Adds resilience options model.
src/SharpClaw.Code.Providers/AnthropicProvider.cs Accepts conversation messages/tools/max tokens.
src/SharpClaw.Code.Protocol/SharpClaw.Code.Protocol.csproj Adds package metadata description.
src/SharpClaw.Code.Protocol/Serialization/ProtocolJsonContext.cs Adds JSON context entries for new protocol types.
src/SharpClaw.Code.Protocol/Operational/RuntimeStatusReport.cs Adds diagnostics/LSP counts to status report.
src/SharpClaw.Code.Protocol/Models/SharpClawWorkflowMetadataKeys.cs Adds metadata keys for agent/share/compaction/tool allowlists.
src/SharpClaw.Code.Protocol/Models/ProviderToolDefinition.cs Adds lightweight provider tool definition type.
src/SharpClaw.Code.Protocol/Models/ProviderRequest.cs Adds Messages/Tools/MaxTokens for provider calls.
src/SharpClaw.Code.Protocol/Models/ProviderEvent.cs Adds tool-use structured fields to provider events.
src/SharpClaw.Code.Protocol/Models/OpenCodeParityModels.cs Adds config/agent/diagnostics/share/server protocol models.
src/SharpClaw.Code.Protocol/Models/ContentBlock.cs Adds content block model + enum discriminator.
src/SharpClaw.Code.Protocol/Models/ChatMessage.cs Adds chat message model.
src/SharpClaw.Code.Protocol/Events/ShareRemovedEvent.cs Adds runtime event for share removal.
src/SharpClaw.Code.Protocol/Events/ShareCreatedEvent.cs Adds runtime event for share creation.
src/SharpClaw.Code.Protocol/Events/RuntimeEvent.cs Registers share events for polymorphic serialization.
src/SharpClaw.Code.Plugins/SharpClaw.Code.Plugins.csproj Adds package metadata description.
src/SharpClaw.Code.Permissions/SharpClaw.Code.Permissions.csproj Adds package metadata description.
src/SharpClaw.Code.Memory/SharpClaw.Code.Memory.csproj Adds package metadata description.
src/SharpClaw.Code.Memory/Services/SessionSummaryService.cs Prefers compacted summary from session metadata.
src/SharpClaw.Code.Mcp/SharpClaw.Code.Mcp.csproj Adds package metadata description.
src/SharpClaw.Code.Infrastructure/SharpClaw.Code.Infrastructure.csproj Adds package metadata description.
src/SharpClaw.Code.Git/SharpClaw.Code.Git.csproj Adds package metadata description.
src/SharpClaw.Code.Commands/SharpClaw.Code.Commands.csproj Adds package metadata description.
src/SharpClaw.Code.Commands/Repl/ReplInteractionState.cs Adds REPL agent override state.
src/SharpClaw.Code.Commands/Repl/ReplHost.cs Threads agent override into runtime context.
src/SharpClaw.Code.Commands/Options/GlobalCliOptions.cs Adds global --agent option.
src/SharpClaw.Code.Commands/Models/CommandExecutionContext.cs Adds AgentId field.
src/SharpClaw.Code.Commands/Handlers/UnshareCommandHandler.cs Adds unshare CLI + slash command.
src/SharpClaw.Code.Commands/Handlers/ShareCommandHandler.cs Adds share CLI + slash command.
src/SharpClaw.Code.Commands/Handlers/SessionsSlashCommandHandler.cs Adds /sessions alias for /session list/show.
src/SharpClaw.Code.Commands/Handlers/ServeCommandHandler.cs Adds serve CLI + slash command.
src/SharpClaw.Code.Commands/Handlers/PromptCommandHandler.cs Threads agent option into runtime context.
src/SharpClaw.Code.Commands/Handlers/ModelsCommandHandler.cs Adds models CLI + slash command.
src/SharpClaw.Code.Commands/Handlers/ConnectCommandHandler.cs Adds connect list/open CLI + slash command.
src/SharpClaw.Code.Commands/Handlers/CompactCommandHandler.cs Adds compact CLI + slash command.
src/SharpClaw.Code.Commands/Handlers/AgentsCommandHandler.cs Adds agents list/use CLI + slash command.
src/SharpClaw.Code.Commands/CliCommandFactory.cs Threads agent option into runtime context.
src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj Adds package metadata description.
src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs Registers new command handlers and slash command handlers.
src/SharpClaw.Code.Agents/SharpClaw.Code.Agents.csproj Adds Options package, InternalsVisibleTo, metadata description.
src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs Adds tool registry mapping + tool loop event/tool result integration.
src/SharpClaw.Code.Agents/Models/AgentRunContext.cs Adds conversation history to agent context.
src/SharpClaw.Code.Agents/Internal/ToolCallDispatcher.cs Adds tool-use→tool execution dispatcher.
src/SharpClaw.Code.Agents/Internal/ProviderInvocationResult.cs Adds tool results + tool events to provider invocation result.
src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs Adds multi-iteration tool-calling loop + provider telemetry metrics.
src/SharpClaw.Code.Agents/Configuration/AgentLoopOptions.cs Adds tool-loop options model.
src/SharpClaw.Code.Agents/AgentsServiceCollectionExtensions.cs Registers loop options + tool dispatcher + kernel.
src/SharpClaw.Code.Agents/Agents/SharpClawAgentBase.cs Applies configured instruction appendix.
src/SharpClaw.Code.Acp/SharpClaw.Code.Acp.csproj Adds package metadata description.
examples/WebApiAgent/appsettings.json Adds minimal example config.
examples/WebApiAgent/WebApiAgent.csproj Adds Web API example project.
examples/WebApiAgent/Program.cs Adds minimal API example usage of runtime.
examples/MinimalConsoleAgent/appsettings.json Adds minimal console example config.
examples/MinimalConsoleAgent/Program.cs Adds minimal console example usage of runtime.
examples/MinimalConsoleAgent/MinimalConsoleAgent.csproj Adds minimal console example project.
examples/McpToolAgent/appsettings.json Adds custom tool agent example config.
examples/McpToolAgent/Program.cs Adds custom tool registration example.
examples/McpToolAgent/McpToolAgent.csproj Adds custom tool agent example project.
examples/McpToolAgent/EchoTool.cs Adds example custom tool implementation.
docs/runtime.md Documents new runtime services + embedded server surface.
docs/getting-started.md Adds onboarding guide and usage documentation.
README.md Updates feature list, CLI surface, and config description.
Directory.Build.props Adds package metadata defaults (authors/license/repo).
.github/workflows/release.yml Adds NuGet packaging/publish workflow.
.github/workflows/ci.yml Adds CI build/test + code coverage workflow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +41 to +55
var baseAgentId = string.IsNullOrWhiteSpace(definition.BaseAgentId)
? "primary-coding-agent"
: definition.BaseAgentId!;
var baseEntry = entries.TryGetValue(baseAgentId, out var found)
? found
: entries["primary-coding-agent"];

entries[definition.Id] = new AgentCatalogEntry(
definition.Id,
string.IsNullOrWhiteSpace(definition.Name) ? definition.Id : definition.Name,
string.IsNullOrWhiteSpace(definition.Description) ? baseEntry.Description : definition.Description!,
baseEntry.BaseAgentId,
definition.Model,
definition.PrimaryMode,
definition.AllowedTools,
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When building a derived AgentCatalogEntry, BaseAgentId is set from baseEntry.BaseAgentId rather than the immediate baseAgentId resolved for this definition. If a configured agent inherits from another configured agent, this will lose the direct parent relationship (it will point at the base agent’s base). Use the resolved baseAgentId when populating BaseAgentId so multi-level inheritance is represented correctly.

Copilot uses AI. Check for mistakes.
Comment on lines 129 to 253
for (var iteration = 0; iteration < options.MaxToolIterations; iteration++)
{
providerEvents.Add(providerEvent);
UsageSnapshot? iterationUsage = null;

if (!providerEvent.IsTerminal && !string.IsNullOrWhiteSpace(providerEvent.Content))
var providerRequest = providerRequestPreflight.Prepare(new ProviderRequest(
Id: $"provider-request-{Guid.NewGuid():N}",
SessionId: request.Context.SessionId,
TurnId: request.Context.TurnId,
ProviderName: resolvedProviderName,
Model: requestedModel,
Prompt: request.Context.Prompt,
SystemPrompt: request.Instructions,
OutputFormat: request.Context.OutputFormat,
Temperature: 0.1m,
Metadata: baseMetadata,
Messages: messages,
Tools: availableTools,
MaxTokens: options.MaxTokensPerRequest));

lastProviderRequest = providerRequest;

var iterationTextSegments = new List<string>();
var toolUseEvents = new List<ProviderEvent>();

using var providerScope = new ProviderActivityScope(resolvedProviderName, requestedModel, providerRequest.Id);
var providerSw = Stopwatch.StartNew();
try
{
outputSegments.Add(providerEvent.Content);
var stream = await provider.StartStreamAsync(providerRequest, cancellationToken).ConfigureAwait(false);

await foreach (var providerEvent in stream.Events.WithCancellation(cancellationToken))
{
allProviderEvents.Add(providerEvent);

if (!providerEvent.IsTerminal && !string.IsNullOrWhiteSpace(providerEvent.Content))
{
iterationTextSegments.Add(providerEvent.Content);
}

if (!string.IsNullOrEmpty(providerEvent.ToolUseId) && !string.IsNullOrEmpty(providerEvent.ToolName))
{
toolUseEvents.Add(providerEvent);
}

if (providerEvent.IsTerminal && providerEvent.Usage is not null)
{
iterationUsage = providerEvent.Usage;
terminalUsage = providerEvent.Usage;
}
}

providerSw.Stop();
providerScope.SetCompleted(iterationUsage?.InputTokens, iterationUsage?.OutputTokens);
SharpClawMeterSource.ProviderDuration.Record(providerSw.Elapsed.TotalMilliseconds);
}
catch (Exception ex)
{
providerSw.Stop();
providerScope.SetError(ex.Message);
throw;
}

// If no tool-use events, accumulate text and break
if (toolUseEvents.Count == 0)
{
outputSegments.AddRange(iterationTextSegments);
break;
}

// Build assistant message with text + tool-use content blocks
var assistantBlocks = new List<ContentBlock>();
var iterationText = string.Concat(iterationTextSegments);
if (!string.IsNullOrEmpty(iterationText))
{
assistantBlocks.Add(new ContentBlock(ContentBlockKind.Text, iterationText, null, null, null, null));
}

foreach (var toolUseEvent in toolUseEvents)
{
assistantBlocks.Add(new ContentBlock(
ContentBlockKind.ToolUse,
null,
toolUseEvent.ToolUseId,
toolUseEvent.ToolName,
toolUseEvent.ToolInputJson,
null));
}

messages.Add(new ChatMessage("assistant", assistantBlocks));

if (providerEvent.IsTerminal && providerEvent.Usage is not null)
// Dispatch each tool call and collect results
var toolResultBlocks = new List<ContentBlock>();
foreach (var toolUseEvent in toolUseEvents)
{
terminalUsage = providerEvent.Usage;
if (toolExecutionContext is null)
{
// No tool execution context means we cannot dispatch tools
toolResultBlocks.Add(new ContentBlock(
ContentBlockKind.ToolResult,
"Tool execution is not available in this context.",
toolUseEvent.ToolUseId,
null,
null,
true));
continue;
}

var (resultBlock, toolResult, events) = await toolCallDispatcher.DispatchAsync(
toolUseEvent,
toolExecutionContext,
cancellationToken).ConfigureAwait(false);

toolResultBlocks.Add(resultBlock);
allToolResults.Add(toolResult);
allToolEvents.AddRange(events);
}

messages.Add(new ChatMessage("user", toolResultBlocks));

// Accumulate partial text from tool-calling iterations
if (!string.IsNullOrEmpty(iterationText))
{
outputSegments.Add(iterationText);
}
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tool-calling loop runs up to options.MaxToolIterations, but if the provider keeps requesting tools on every iteration the loop will exit due to the iteration limit and then proceed to construct a "successful" ProviderInvocationResult without indicating truncation. This can silently return partial/incorrect outputs. Consider detecting when the iteration limit is reached and returning a failure (or a clear summary/error) so callers can distinguish an incomplete tool loop.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +71
// Drop oldest messages until budget is satisfied.
// Always preserve at least the last message (most recent user turn).
while (working.Count > 1)
{
var totalEstimate = EstimateTokens(working);
if (systemMessage is not null)
{
totalEstimate += EstimateTokens(systemMessage);
}

if (totalEstimate <= maxTokenBudget)
{
break;
}

working.RemoveAt(0);
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContextWindowManager.Truncate recomputes EstimateTokens(working) from scratch on each loop iteration while removing messages one by one. In worst-case (many messages to drop) this becomes O(n^2). Consider precomputing per-message token estimates and maintaining a running total while popping from the front to keep truncation linear.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +66
// 1. Execute the tool (this builds the real ToolExecutionRequest internally with correct approval/destructive metadata)
var envelope = await toolExecutor.ExecuteAsync(toolName, toolInputJson, context, cancellationToken);

// 2. Publish ToolStartedEvent using the real request from the executor (has correct approval scope, destructive flag, etc.)
var startedEvent = new ToolStartedEvent(
EventId: $"event-{Guid.NewGuid():N}",
SessionId: context.SessionId,
TurnId: context.TurnId,
OccurredAtUtc: DateTimeOffset.UtcNow,
Request: envelope.Request);

await eventPublisher.PublishAsync(startedEvent, cancellationToken: cancellationToken);
collectedEvents.Add(startedEvent);

// 3. Publish ToolCompletedEvent
var completedEvent = new ToolCompletedEvent(
EventId: $"event-{Guid.NewGuid():N}",
SessionId: context.SessionId,
TurnId: context.TurnId,
OccurredAtUtc: DateTimeOffset.UtcNow,
Result: envelope.Result);

await eventPublisher.PublishAsync(completedEvent, cancellationToken: cancellationToken);
collectedEvents.Add(completedEvent);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToolCallDispatcher executes the tool via IToolExecutor and then publishes ToolStartedEvent/ToolCompletedEvent itself. The built-in ToolExecutor already publishes these events (and persists them) when an event publisher is configured, so this will result in duplicate tool events and duplicated persistence once ConversationRuntime also appends the returned events. Additionally, the ToolStartedEvent is emitted after the tool has already completed, so event ordering is incorrect. Consider having ToolCallDispatcher return the events without publishing them (or suppress ToolExecutor publishing in this call path) and emit Started before execution.

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +124
share.Url.Should().Be("http://127.0.0.1:7345/s/" + share.ShareId);
fileSystem.FileExists(SessionStorageLayout.GetShareSnapshotPath(pathService, workspaceRoot, share.ShareId)).Should().BeFalse();
sharedSession!.Metadata.Should().ContainKey(SharpClawWorkflowMetadataKeys.ShareId);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion expects the share snapshot file to not exist, but ShareSessionService.CreateShareAsync writes the snapshot to SessionStorageLayout.GetShareSnapshotPath(...). As written, this test should fail once CreateShareAsync is exercised. Update the assertion to match the intended behavior (e.g., the snapshot file exists and/or its contents are sanitized).

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +61
private static readonly ConcurrentDictionary<string, WorkspaceDiagnosticsSnapshot> Cache = new(StringComparer.Ordinal);
private static readonly TimeSpan CacheLifetime = TimeSpan.FromSeconds(15);

/// <inheritdoc />
public async Task<WorkspaceDiagnosticsSnapshot> BuildSnapshotAsync(string workspaceRoot, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(workspaceRoot);
if (Cache.TryGetValue(workspaceRoot, out var cached)
&& systemClock.UtcNow - cached.GeneratedAtUtc < CacheLifetime)
{
return cached;
}

var config = await configService.GetConfigAsync(workspaceRoot, cancellationToken).ConfigureAwait(false);
var configuredServers = (IReadOnlyList<ConfiguredLspServerDefinition>)(config.Document.LspServers ?? []);
var diagnostics = new List<WorkspaceDiagnosticItem>();

var buildTarget = FindBuildTarget(workspaceRoot);
if (!string.IsNullOrWhiteSpace(buildTarget))
{
try
{
var result = await processRunner.RunAsync(
new ProcessRunRequest(
"dotnet",
["build", buildTarget, "--nologo", "--no-restore", "-consolelogger:NoSummary"],
workspaceRoot,
null),
cancellationToken).ConfigureAwait(false);

diagnostics.AddRange(ParseDotnetDiagnostics(result.StandardOutput, "dotnet-build"));
diagnostics.AddRange(ParseDotnetDiagnostics(result.StandardError, "dotnet-build"));
}
catch (Exception ex) when (ex is InvalidOperationException or IOException)
{
logger.LogDebug(ex, "Skipping build-backed diagnostics for workspace {WorkspaceRoot}.", workspaceRoot);
}
}

var snapshot = new WorkspaceDiagnosticsSnapshot(workspaceRoot, systemClock.UtcNow, configuredServers, diagnostics);
Cache[workspaceRoot] = snapshot;
return snapshot;
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WorkspaceDiagnosticsService uses a static ConcurrentDictionary cache keyed by workspaceRoot, but entries are never evicted—CacheLifetime only gates reuse, not removal. Over time (e.g., many workspaces in a long-lived process), this can grow unbounded. Consider using MemoryCache with expiration, or periodically removing stale entries.

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +145
// Assemble prior-turn conversation history for multi-turn context.
const int MaxHistoryTokenBudget = 100_000;
var sessionEvents = await eventStore
.ReadAllAsync(workspaceRoot, session.Id, cancellationToken)
.ConfigureAwait(false);
var rawHistory = ConversationHistoryAssembler.Assemble(sessionEvents);
var conversationHistory = ContextWindowManager.Truncate(rawHistory, MaxHistoryTokenBudget);

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PromptContextAssembler reads the full session event log on every prompt to assemble conversation history (eventStore.ReadAllAsync + Assemble + Truncate). For long-running sessions this can become a significant per-turn cost and IO hotspot. Consider a more incremental approach (e.g., read only events since last turn, or persist a compacted message history in session metadata) so prompt execution doesn’t scale linearly with total session history.

Copilot uses AI. Check for mistakes.
Comment on lines +259 to +268
private static async Task WriteJsonAsync(HttpListenerResponse response, int statusCode, object payload, CancellationToken cancellationToken)
{
response.StatusCode = statusCode;
response.ContentType = "application/json";
response.ContentEncoding = Encoding.UTF8;
var json = JsonSerializer.Serialize(payload);
await using var writer = new StreamWriter(response.OutputStream, new UTF8Encoding(false), leaveOpen: true);
await writer.WriteAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WriteJsonAsync serializes using JsonSerializer.Serialize(payload) without ProtocolJsonContext / configured JsonSerializerOptions. Most of the runtime uses ProtocolJsonContext for stable enum/string handling and polymorphic event payloads; using the default serializer here can change JSON shape (e.g., enums as numbers) and make the embedded server responses inconsistent with CLI/other JSON outputs. Consider serializing with the appropriate ProtocolJsonContext (or a shared options instance) for each payload type.

Copilot uses AI. Check for mistakes.
Comment on lines +228 to +236
private static async Task<int> WriteCommandResultAsync(HttpListenerResponse response, CommandResult result, CancellationToken cancellationToken)
{
var envelope = new ServerCommandEnvelope(
result.Succeeded,
result.ExitCode,
result.Message,
TryParseData(result.DataJson),
result.DataJson is not null && TryParseData(result.DataJson) is null ? result.DataJson : null);
var statusCode = result.Succeeded ? 200 : 400;
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WriteCommandResultAsync calls TryParseData(result.DataJson) twice, which parses the same JSON twice. Parse once and reuse the JsonElement? (or parsed-null result) to avoid redundant work and double exception handling.

Copilot uses AI. Check for mistakes.
Telli and others added 2 commits April 13, 2026 17:05
Code fixes:
- AgentCatalogService: use resolved baseAgentId (not base's base) for
  correct multi-level inheritance
- ProviderBackedAgentKernel: detect iteration-limit exhaustion and
  append truncation warning to output; hoist loop variable for post-loop
  access
- ContextWindowManager: replace O(n^2) truncation with O(n)
  precomputed-total approach using running subtraction
- ToolCallDispatcher: remove duplicate event publishing (ToolExecutor
  already publishes ToolStarted/ToolCompleted); validate ToolName and
  ToolUseId upfront, returning error blocks when missing; remove unused
  eventPublisher parameter
- NdjsonTraceFileSink thread safety carried forward from prior commit
- WorkspaceDiagnosticsService: add cache eviction (remove stale entries
  when cache exceeds 50 items)
- PromptContextAssembler: skip event log read on first turn
  (SequenceNumber == 1) since there's no prior history; add comment
  documenting linear scaling concern for long sessions
- WorkspaceHttpServer: use shared JsonSerializerOptions with camelCase
  and string enums for consistent JSON output; fix double
  TryParseData call in WriteCommandResultAsync

Test fixes:
- ShareAndCompactionServicesTests: add comment explaining raw vs
  normalized path assertion (macOS /var → /private/var)
- ToolCallDispatcherTests: update for removed event publishing; add
  test for missing ToolName validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Telli Telli changed the title new-features feat: expand parity workflows and harden agent runtime Apr 14, 2026
@Telli Telli merged commit da9bf00 into main Apr 14, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants