feat: expand parity workflows and harden agent runtime#4
Conversation
…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>
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
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.
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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); | ||
| } |
There was a problem hiding this comment.
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.
| // 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); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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).
| 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; |
There was a problem hiding this comment.
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.
| // 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); | ||
|
|
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
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>
Summary
Verification