Skip to content

Commit da9bf00

Browse files
authored
Merge pull request #4 from clawdotnet/codex/harden-agent-runtime
feat: expand parity workflows and harden agent runtime
2 parents 5e5cb46 + ef727c6 commit da9bf00

89 files changed

Lines changed: 5847 additions & 133 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
SharpClaw Code is a C# and .NET-native coding agent runtime for teams building AI developer tools, agentic CLIs, and MCP-enabled workflows.
99

10-
It combines durable sessions, permission-aware tool execution, provider abstraction, structured telemetry, and an automation-friendly command-line surface in a codebase designed for production-quality .NET systems, not toy demos.
10+
It combines durable sessions, permission-aware tool execution, provider abstraction, structured telemetry, and an automation-friendly command-line surface in a runtime shaped for real .NET systems: explicit, testable, and operationally legible.
1111

12-
## What SharpClaw Code Is
12+
## What It Is
1313

1414
SharpClaw Code is an open-source runtime for building and operating coding-agent experiences in the .NET ecosystem.
1515

@@ -27,7 +27,7 @@ It is designed for:
2727
- **Durable runtime model**: sessions, checkpoints, append-only event logs, export/import flows, and recovery-aware orchestration
2828
- **Safety by default**: permission modes, approval gates, workspace-boundary enforcement, and mediated tool execution
2929
- **Extensible surface**: providers, MCP servers, plugins, skills, ACP hosting, and runtime commands integrate through explicit seams
30-
- **Good fit for automation**: JSON-friendly command output, stable operational commands, and a clean CLI-first workflow
30+
- **Automation-ready interface**: stable JSON output, operational commands, and a clean CLI-first workflow that works for both humans and scripts
3131

3232
## Quick Start
3333

@@ -71,6 +71,23 @@ dotnet run --project src/SharpClaw.Code.Cli -- --output-format json doctor
7171

7272
Built-in REPL slash commands include `/help`, `/status`, `/doctor`, `/session`, `/commands`, `/mode`, `/editor`, `/export`, `/undo`, `/redo`, and `/version`. Use `/help` to see the active command set, including discovered workspace custom commands.
7373

74+
Parity-oriented commands now include:
75+
76+
- `models` / `/models`
77+
- `usage` / `/usage`
78+
- `cost` / `/cost`
79+
- `stats` / `/stats`
80+
- `connect` / `/connect`
81+
- `hooks` / `/hooks`
82+
- `skills` / `/skills`
83+
- `agents` / `/agents`
84+
- `todo` / `/todo`
85+
- `share` / `/share`
86+
- `unshare` / `/unshare`
87+
- `compact` / `/compact`
88+
- `serve` / `/serve`
89+
- `/sessions` as a friendlier alias over `/session list`
90+
7491
Primary workflow modes:
7592

7693
- `build`: normal coding-agent execution
@@ -89,14 +106,18 @@ Primary workflow modes:
89106
| Structured telemetry | Emit runtime events and usage signals that support diagnostics, replay, and automation |
90107
| JSON-friendly CLI | Use the same runtime through human-readable terminal flows or machine-readable command output |
91108
| Spec workflow mode | Turn prompts into structured requirements, technical design, and task documents for feature proposals |
109+
| Embedded local server | Expose prompt, session, status, doctor, and share endpoints for editor or automation clients |
110+
| Config + agent catalog | Layer user/workspace JSONC config with typed agent defaults, tool allowlists, and runtime hooks |
111+
| Session sharing | Create self-hosted share links and durable sanitized share snapshots under `.sharpclaw/` |
112+
| Diagnostics context | Surface configured diagnostics sources into prompt context, status, and machine-readable output |
92113

93114
## Good Fit For
94115

95116
- building a **C# AI coding assistant**
96117
- running a **local or hosted coding-agent CLI**
97118
- creating a **.NET MCP client/runtime**
98119
- adding **session persistence and auditability** to agent workflows
99-
- experimenting with **Agent Framework-backed orchestration** without pushing core runtime logic into the framework layer
120+
- building on **Microsoft Agent Framework** without pushing your application core into framework-specific code
100121

101122
## Solution Layout
102123

@@ -145,8 +166,9 @@ dotnet test SharpClawCode.sln --filter "FullyQualifiedName~ParityScenarioTests"
145166
| `--output-format text\|json` | Human-readable or structured output |
146167
| `--primary-mode <mode>` | Workflow bias for prompts: `build`, `plan`, or `spec` |
147168
| `--session <id>` | Reuse a specific SharpClaw session id for prompt execution |
169+
| `--agent <id>` | Select the active agent for prompt execution |
148170

149-
Subcommands include `prompt`, `repl`, `doctor`, `status`, `session`, `commands`, `mcp`, `plugins`, `acp`, `bridge`, and `version`.
171+
Subcommands include `prompt`, `repl`, `doctor`, `status`, `session`, `models`, `usage`, `cost`, `stats`, `connect`, `hooks`, `skills`, `agents`, `todo`, `share`, `unshare`, `compact`, `serve`, `commands`, `mcp`, `plugins`, `acp`, `bridge`, and `version`.
150172

151173
## Documentation Map
152174

@@ -167,7 +189,17 @@ Subcommands include `prompt`, `repl`, `doctor`, `status`, `session`, `commands`,
167189

168190
## Configuration
169191

170-
SharpClaw Code uses the standard .NET configuration stack (`appsettings.json`, environment variables, CLI args). Key configuration sections:
192+
SharpClaw Code uses both the standard .NET configuration stack (`appsettings.json`, environment variables, CLI args) and layered SharpClaw JSONC config files:
193+
194+
- user config: `~/.config/sharpclaw/config.jsonc` on Unix-like systems
195+
- Windows user config: `%AppData%\\SharpClaw\\config.jsonc`
196+
- workspace config: `<workspace>/sharpclaw.jsonc`
197+
198+
Precedence is:
199+
200+
`CLI args > workspace sharpclaw.jsonc > user config.jsonc > appsettings/environment defaults`
201+
202+
Key runtime configuration sections:
171203

172204
| Section | Purpose |
173205
|---|---|
@@ -177,13 +209,25 @@ SharpClaw Code uses the standard .NET configuration stack (`appsettings.json`, e
177209
| `SharpClaw:Web` | Web search provider name, endpoint template, user agent |
178210
| `SharpClaw:Telemetry` | Runtime event ring buffer capacity |
179211

212+
Key `sharpclaw.jsonc` capabilities:
213+
214+
- `shareMode`: `manual`, `auto`, or `disabled`
215+
- `server`: host, port, and optional public base URL for share links
216+
- `defaultAgentId`: default prompt agent
217+
- `agents`: typed agent catalog entries with model defaults, tool allowlists, and instruction appendices
218+
- `lspServers`: configured diagnostics sources
219+
- `hooks`: lifecycle hooks for turn/tool/share/server events
220+
- `connectLinks`: browser entry points for provider or external auth flows
221+
180222
All options are validated at startup via `IValidateOptions` implementations.
181223

182224
## Current Scope
183225

184226
- The shared tooling layer is permission-aware across the runtime.
185-
- The current Agent Framework bridge is focused on provider-backed runs rather than a full tool-calling loop inside the framework path.
186-
- Operational commands support stable JSON output via `--output-format json`, which makes them useful in scripts and automation.
227+
- The current runtime includes multi-turn provider-backed tool execution, session-backed prompt replay, and durable conversation history.
228+
- Agent-driven tool calls flow through the same approval and allowlist enforcement path used by direct tool execution, including caller-aware interactive approval behavior.
229+
- Operational commands support stable JSON output via `--output-format json`, which makes them suitable for scripts, editors, and automation.
230+
- The embedded server exposes local JSON and SSE endpoints for prompts, sessions, sharing, status, and doctor flows.
187231

188232
## Contributing
189233

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Test projects: **UnitTests**, **IntegrationTests**, **MockProvider**, **ParityHa
4040
4. **`SharpClawAgentBase`** delegates to **`AgentFrameworkBridge.RunAsync`**, which drives **`ProviderBackedAgentKernel`** (streaming `IModelProvider`, auth checks, **`ProviderExecutionException`** on hard failures).
4141
5. Turn completion updates session, checkpoints as implemented in **`ConversationRuntime`**, publishes events via **`IRuntimeEventPublisher`**.
4242

43-
**Note:** `AgentRunContext` carries **`IToolExecutor`**, but the current **`AgentFrameworkBridge`** path does not attach SharpClaw tools to the Microsoft Agent Framework chat loop; **`AgentRunResult.ToolResults`** is empty in that bridge. Tools are still fully usable via **`IToolExecutor`** (tests and parity harness call it directly).
43+
**Note:** `AgentRunContext` carries **`IToolExecutor`**, and the current **`AgentFrameworkBridge`** path advertises the resolved tool set to the provider, executes tool calls through the permission-aware executor, and records tool results in the agent run result. Prompt references and tool approvals respect the caller's normalized interactivity mode.
4444

4545
### Operational commands
4646

docs/runtime.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
The **runtime** layer is centered on **`SharpClaw.Code.Runtime`** and especially **`ConversationRuntime`**, which implements:
44

55
- Session surface on **`IConversationRuntime`** — create/get session, **`RunPromptAsync`**
6-
- **`IRuntimeCommandService`****`ExecutePromptAsync`**, **`GetStatusAsync`**, **`RunDoctorAsync`**, **`InspectSessionAsync`**
6+
- **`IRuntimeCommandService`**prompt execution plus status, doctor, session inspection, share/unshare, and compaction commands
77

88
Registration: `RuntimeServiceCollectionExtensions.AddSharpClawRuntime`.
99

@@ -15,6 +15,13 @@ Registration: `RuntimeServiceCollectionExtensions.AddSharpClawRuntime`.
1515
2. Maps **`RunPromptRequest`** + session into **`AgentRunContext`** (session/turn ids, working directory, permission mode, output format, **`IToolExecutor`**, metadata).
1616
3. Invokes **`PrimaryCodingAgent.RunAsync`**.
1717

18+
Before the agent runs, **`ConversationRuntime`** also layers in:
19+
20+
- merged SharpClaw JSONC config (`ISharpClawConfigService`)
21+
- resolved agent defaults (`IAgentCatalogService`)
22+
- persisted active-agent metadata from the session, when present
23+
- auto-share policy checks (`ShareMode.Auto`)
24+
1825
The agent stack is described in [agents.md](agents.md).
1926

2027
## Lifecycle and state
@@ -24,7 +31,9 @@ The agent stack is described in [agents.md](agents.md).
2431

2532
## Context assembly
2633

27-
**`PromptContextAssembler`** pulls workspace/session-aware data (skills registry, memory hooks, git context as wired today) into the prompt path before the agent runs.
34+
**`PromptContextAssembler`** pulls workspace/session-aware data (skills registry, todo state, memory hooks, git context as wired today) into the prompt path before the agent runs.
35+
36+
It also includes a compact diagnostics summary from **`IWorkspaceDiagnosticsService`**, which currently surfaces configured diagnostics sources and build-derived findings for .NET workspaces.
2837

2938
When the effective **`PrimaryMode`** is **`Spec`**, the assembler appends a structured output contract that requires the model to return machine-readable requirements, design, and task content.
3039

@@ -50,6 +59,41 @@ Each spec-mode prompt creates a fresh folder. If the same slug already exists, t
5059

5160
Used by **`GetStatusAsync`**, **`RunDoctorAsync`**, and **`InspectSessionAsync`** to build **Protocol** reports (`DoctorReport`, `RuntimeStatusReport`, `SessionInspectionReport`).
5261

62+
`RuntimeStatusReport` now also carries:
63+
64+
- configured diagnostics source count
65+
- current diagnostic error count
66+
- current diagnostic warning count
67+
68+
## Config, agents, and hooks
69+
70+
The parity layer adds several runtime-owned services:
71+
72+
- **`ISharpClawConfigService`** — loads user/workspace `config.jsonc` + `sharpclaw.jsonc` and merges them by precedence
73+
- **`IAgentCatalogService`** — overlays configured specialist agents on top of built-in agents
74+
- **`IConversationCompactionService`** — creates durable session summaries stored in session metadata
75+
- **`IShareSessionService`** — creates and removes self-hosted share snapshots
76+
- **`IHookDispatcher`** — executes configured hook processes for turn/tool/share/server events and exposes hook inspection/testing
77+
- **`ITodoService`** — persists session and workspace todo items under session metadata and `.sharpclaw/tasks.json`
78+
- **`IWorkspaceInsightsService`** — reconstructs durable usage, cost, and execution stats from persisted event logs
79+
80+
These services are intentionally small and runtime-owned rather than separate orchestration subsystems.
81+
82+
## Embedded server
83+
84+
**`IWorkspaceHttpServer`** / **`WorkspaceHttpServer`** expose a minimal local HTTP surface for editor and automation clients:
85+
86+
- `POST /v1/prompt`
87+
- `GET /v1/sessions`
88+
- `GET /v1/sessions/{id}`
89+
- `POST /v1/share/{sessionId}`
90+
- `DELETE /v1/share/{sessionId}`
91+
- `GET /v1/status`
92+
- `GET /v1/doctor`
93+
- `GET /s/{shareId}`
94+
95+
Prompt requests can return JSON or replay the completed runtime event stream as SSE.
96+
5397
## Hosted service
5498

5599
**`RuntimeCoordinatorHostedServiceAdapter`** is registered as **`IHostedService`** and currently logs start/stop only (placeholder for future lifecycle coordination).
@@ -62,3 +106,7 @@ Used by **`GetStatusAsync`**, **`RunDoctorAsync`**, and **`InspectSessionAsync`*
62106
| `DefaultTurnRunner` | `src/SharpClaw.Code.Runtime/Turns/DefaultTurnRunner.cs` |
63107
| `OperationalDiagnosticsCoordinator` | `src/SharpClaw.Code.Runtime/Diagnostics/OperationalDiagnosticsCoordinator.cs` |
64108
| `IRuntimeCommandService` | `src/SharpClaw.Code.Runtime/Abstractions/IRuntimeCommandService.cs` |
109+
| `SharpClawConfigService` | `src/SharpClaw.Code.Runtime/Configuration/SharpClawConfigService.cs` |
110+
| `ShareSessionService` | `src/SharpClaw.Code.Runtime/Workflow/ShareSessionService.cs` |
111+
| `ConversationCompactionService` | `src/SharpClaw.Code.Runtime/Workflow/ConversationCompactionService.cs` |
112+
| `WorkspaceHttpServer` | `src/SharpClaw.Code.Runtime/Server/WorkspaceHttpServer.cs` |

src/SharpClaw.Code.Acp/AcpStdioHost.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ private async Task<JsonObject> HandleSessionPromptAsync(
190190
WorkingDirectory: workspace,
191191
PermissionMode: PermissionMode.WorkspaceWrite,
192192
OutputFormat: OutputFormat.Json,
193-
Metadata: new Dictionary<string, string> { ["acp"] = "true" }),
193+
Metadata: new Dictionary<string, string> { ["acp"] = "true" },
194+
IsInteractive: false),
194195
cancellationToken)
195196
.ConfigureAwait(false);
196197

src/SharpClaw.Code.Agents/Agents/SharpClawAgentBase.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using SharpClaw.Code.Agents.Abstractions;
22
using SharpClaw.Code.Agents.Models;
3+
using SharpClaw.Code.Protocol.Models;
34

45
namespace SharpClaw.Code.Agents.Agents;
56

@@ -31,13 +32,23 @@ public abstract class SharpClawAgentBase(IAgentFrameworkBridge agentFrameworkBri
3132

3233
/// <inheritdoc />
3334
public virtual Task<AgentRunResult> RunAsync(AgentRunContext context, CancellationToken cancellationToken)
34-
=> agentFrameworkBridge.RunAsync(
35+
{
36+
var instructions = Instructions;
37+
if (context.Metadata is not null
38+
&& context.Metadata.TryGetValue(SharpClawWorkflowMetadataKeys.AgentInstructionAppendix, out var appendix)
39+
&& !string.IsNullOrWhiteSpace(appendix))
40+
{
41+
instructions = $"{instructions}{Environment.NewLine}{Environment.NewLine}{appendix.Trim()}";
42+
}
43+
44+
return agentFrameworkBridge.RunAsync(
3545
new AgentFrameworkRequest(
3646
AgentId,
3747
AgentKind,
3848
Name,
3949
Description,
40-
Instructions,
50+
instructions,
4151
context),
4252
cancellationToken);
53+
}
4354
}

src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
126126
UsageSnapshot? terminalUsage = null;
127127
ProviderRequest? lastProviderRequest = null;
128128

129-
for (var iteration = 0; iteration < options.MaxToolIterations; iteration++)
129+
var iteration = 0;
130+
for (; iteration < options.MaxToolIterations; iteration++)
130131
{
131132
UsageSnapshot? iterationUsage = null;
132133

@@ -252,6 +253,16 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
252253
}
253254
}
254255

256+
// Detect if the loop was exhausted (provider kept requesting tools every iteration).
257+
if (iteration >= options.MaxToolIterations)
258+
{
259+
logger.LogWarning(
260+
"Tool-calling loop reached maximum iterations ({MaxIterations}) for session {SessionId}; output may be incomplete.",
261+
options.MaxToolIterations,
262+
request.Context.SessionId);
263+
outputSegments.Add($"\n\n[Tool-calling loop reached the maximum of {options.MaxToolIterations} iterations. Output may be incomplete.]");
264+
}
265+
255266
var output = string.Concat(outputSegments);
256267
if (string.IsNullOrWhiteSpace(output))
257268
{

src/SharpClaw.Code.Agents/Internal/ToolCallDispatcher.cs

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using SharpClaw.Code.Protocol.Events;
22
using SharpClaw.Code.Protocol.Models;
3-
using SharpClaw.Code.Telemetry.Abstractions;
43
using SharpClaw.Code.Tools.Abstractions;
54
using SharpClaw.Code.Tools.Models;
65

@@ -11,8 +10,7 @@ namespace SharpClaw.Code.Agents.Internal;
1110
/// and returns content blocks for the provider conversation.
1211
/// </summary>
1312
public sealed class ToolCallDispatcher(
14-
IToolExecutor toolExecutor,
15-
IRuntimeEventPublisher eventPublisher)
13+
IToolExecutor toolExecutor)
1614
{
1715
/// <summary>
1816
/// Executes a tool call and returns a tool-result content block.
@@ -38,34 +36,11 @@ public sealed class ToolCallDispatcher(
3836
var toolInputJson = toolUseEvent.ToolInputJson ?? "{}";
3937
var toolUseId = toolUseEvent.ToolUseId;
4038

41-
var collectedEvents = new List<RuntimeEvent>();
42-
43-
// 1. Execute the tool (this builds the real ToolExecutionRequest internally with correct approval/destructive metadata)
39+
// Execute the tool — ToolExecutor already publishes ToolStartedEvent and ToolCompletedEvent
40+
// via IRuntimeEventPublisher, so we do NOT re-publish here to avoid duplicates.
4441
var envelope = await toolExecutor.ExecuteAsync(toolName, toolInputJson, context, cancellationToken);
4542

46-
// 2. Publish ToolStartedEvent using the real request from the executor (has correct approval scope, destructive flag, etc.)
47-
var startedEvent = new ToolStartedEvent(
48-
EventId: $"event-{Guid.NewGuid():N}",
49-
SessionId: context.SessionId,
50-
TurnId: context.TurnId,
51-
OccurredAtUtc: DateTimeOffset.UtcNow,
52-
Request: envelope.Request);
53-
54-
await eventPublisher.PublishAsync(startedEvent, cancellationToken: cancellationToken);
55-
collectedEvents.Add(startedEvent);
56-
57-
// 3. Publish ToolCompletedEvent
58-
var completedEvent = new ToolCompletedEvent(
59-
EventId: $"event-{Guid.NewGuid():N}",
60-
SessionId: context.SessionId,
61-
TurnId: context.TurnId,
62-
OccurredAtUtc: DateTimeOffset.UtcNow,
63-
Result: envelope.Result);
64-
65-
await eventPublisher.PublishAsync(completedEvent, cancellationToken: cancellationToken);
66-
collectedEvents.Add(completedEvent);
67-
68-
// 4. Convert ToolResult to ContentBlock
43+
// Convert ToolResult to ContentBlock
6944
ContentBlock resultBlock;
7045
if (envelope.Result.Succeeded)
7146
{
@@ -88,7 +63,7 @@ public sealed class ToolCallDispatcher(
8863
true);
8964
}
9065

91-
// 5. Return
92-
return (resultBlock, envelope.Result, collectedEvents);
66+
// Events are already published by ToolExecutor; return empty list to avoid duplicates.
67+
return (resultBlock, envelope.Result, []);
9368
}
9469
}

src/SharpClaw.Code.Agents/Models/AgentRunContext.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace SharpClaw.Code.Agents.Models;
2424
/// Prior-turn messages assembled from session events. When non-empty these are prepended
2525
/// to the provider request so the model has multi-turn context.
2626
/// </param>
27+
/// <param name="IsInteractive">Whether tool approvals can interact with the caller.</param>
2728
public sealed record AgentRunContext(
2829
string SessionId,
2930
string TurnId,
@@ -38,4 +39,5 @@ public sealed record AgentRunContext(
3839
DelegatedTaskContract? DelegatedTask = null,
3940
PrimaryMode PrimaryMode = PrimaryMode.Build,
4041
IToolMutationRecorder? ToolMutationRecorder = null,
41-
IReadOnlyList<ChatMessage>? ConversationHistory = null);
42+
IReadOnlyList<ChatMessage>? ConversationHistory = null,
43+
bool IsInteractive = true);

0 commit comments

Comments
 (0)