Skip to content

Commit f3f9c23

Browse files
committed
feat: expand parity workflows and harden agent runtime
1 parent a25732f commit f3f9c23

54 files changed

Lines changed: 2940 additions & 46 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: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,14 @@ Built-in REPL slash commands include `/help`, `/status`, `/doctor`, `/session`,
7474
Parity-oriented commands now include:
7575

7676
- `models` / `/models`
77+
- `usage` / `/usage`
78+
- `cost` / `/cost`
79+
- `stats` / `/stats`
7780
- `connect` / `/connect`
81+
- `hooks` / `/hooks`
82+
- `skills` / `/skills`
7883
- `agents` / `/agents`
84+
- `todo` / `/todo`
7985
- `share` / `/share`
8086
- `unshare` / `/unshare`
8187
- `compact` / `/compact`
@@ -162,7 +168,7 @@ dotnet test SharpClawCode.sln --filter "FullyQualifiedName~ParityScenarioTests"
162168
| `--session <id>` | Reuse a specific SharpClaw session id for prompt execution |
163169
| `--agent <id>` | Select the active agent for prompt execution |
164170

165-
Subcommands include `prompt`, `repl`, `doctor`, `status`, `session`, `models`, `connect`, `agents`, `share`, `unshare`, `compact`, `serve`, `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`.
166172

167173
## Documentation Map
168174

@@ -218,7 +224,8 @@ All options are validated at startup via `IValidateOptions` implementations.
218224
## Current Scope
219225

220226
- The shared tooling layer is permission-aware across the runtime.
221-
- The current runtime includes multi-turn provider-backed tool execution with durable conversation history.
227+
- The current runtime includes multi-turn provider-backed tool execution with durable conversation history and session-backed prompt replay.
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.
222229
- Operational commands support stable JSON output via `--output-format json`, which makes them useful in scripts and automation.
223230
- The embedded server exposes local JSON and SSE endpoints for prompts, sessions, sharing, status, and doctor flows.
224231

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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The agent stack is described in [agents.md](agents.md).
3131

3232
## Context assembly
3333

34-
**`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.
3535

3636
It also includes a compact diagnostics summary from **`IWorkspaceDiagnosticsService`**, which currently surfaces configured diagnostics sources and build-derived findings for .NET workspaces.
3737

@@ -73,7 +73,9 @@ The parity layer adds several runtime-owned services:
7373
- **`IAgentCatalogService`** — overlays configured specialist agents on top of built-in agents
7474
- **`IConversationCompactionService`** — creates durable session summaries stored in session metadata
7575
- **`IShareSessionService`** — creates and removes self-hosted share snapshots
76-
- **`IHookDispatcher`** — executes configured hook processes for turn/tool/share/server events
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
7779

7880
These services are intentionally small and runtime-owned rather than separate orchestration subsystems.
7981

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/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);

src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public async Task<AgentRunResult> RunAsync(AgentFrameworkRequest request, Cancel
4444
EnvironmentVariables: null,
4545
AllowedTools: allowedTools,
4646
AllowDangerousBypass: false,
47-
IsInteractive: false,
47+
IsInteractive: request.Context.IsInteractive,
4848
SourceKind: PermissionRequestSourceKind.Runtime,
4949
SourceName: null,
5050
TrustedPluginNames: null,
@@ -57,7 +57,7 @@ public async Task<AgentRunResult> RunAsync(AgentFrameworkRequest request, Cancel
5757
request.Context.WorkingDirectory,
5858
cancellationToken).ConfigureAwait(false);
5959

60-
var providerTools = registryTools
60+
var providerTools = FilterAdvertisedTools(registryTools, allowedTools)
6161
.Select(t => new ProviderToolDefinition(t.Name, t.Description, t.InputSchemaJson))
6262
.ToList();
6363

@@ -158,4 +158,16 @@ public async Task<AgentRunResult> RunAsync(AgentFrameworkRequest request, Cancel
158158
return null;
159159
}
160160
}
161+
162+
private static IEnumerable<ToolDefinition> FilterAdvertisedTools(
163+
IReadOnlyList<ToolDefinition> registryTools,
164+
IReadOnlyCollection<string>? allowedTools)
165+
{
166+
if (allowedTools is null || allowedTools.Count == 0)
167+
{
168+
return registryTools;
169+
}
170+
171+
return registryTools.Where(tool => allowedTools.Contains(tool.Name, StringComparer.OrdinalIgnoreCase));
172+
}
161173
}

src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
3434
services.AddSingleton<ICommandHandler, DoctorCommandHandler>();
3535
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<SessionCommandHandler>());
3636
services.AddSingleton<ICommandHandler, ModelsCommandHandler>();
37+
services.AddSingleton<ICommandHandler, UsageCommandHandler>();
38+
services.AddSingleton<ICommandHandler, CostCommandHandler>();
39+
services.AddSingleton<ICommandHandler, StatsCommandHandler>();
3740
services.AddSingleton<ICommandHandler, ConnectCommandHandler>();
41+
services.AddSingleton<ICommandHandler, HooksCommandHandler>();
42+
services.AddSingleton<ICommandHandler, SkillsCommandHandler>();
3843
services.AddSingleton<ICommandHandler, AgentsCommandHandler>();
44+
services.AddSingleton<ICommandHandler, TodoCommandHandler>();
3945
services.AddSingleton<ICommandHandler, ShareCommandHandler>();
4046
services.AddSingleton<ICommandHandler, UnshareCommandHandler>();
4147
services.AddSingleton<ICommandHandler, CompactCommandHandler>();
@@ -52,8 +58,14 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
5258
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<SessionCommandHandler>());
5359
services.AddSingleton<ISlashCommandHandler, SessionsSlashCommandHandler>();
5460
services.AddSingleton<ISlashCommandHandler, ModelsCommandHandler>();
61+
services.AddSingleton<ISlashCommandHandler, UsageCommandHandler>();
62+
services.AddSingleton<ISlashCommandHandler, CostCommandHandler>();
63+
services.AddSingleton<ISlashCommandHandler, StatsCommandHandler>();
5564
services.AddSingleton<ISlashCommandHandler, ConnectCommandHandler>();
65+
services.AddSingleton<ISlashCommandHandler, HooksCommandHandler>();
66+
services.AddSingleton<ISlashCommandHandler, SkillsCommandHandler>();
5667
services.AddSingleton<ISlashCommandHandler, AgentsCommandHandler>();
68+
services.AddSingleton<ISlashCommandHandler, TodoCommandHandler>();
5769
services.AddSingleton<ISlashCommandHandler, ShareCommandHandler>();
5870
services.AddSingleton<ISlashCommandHandler, UnshareCommandHandler>();
5971
services.AddSingleton<ISlashCommandHandler, CompactCommandHandler>();

src/SharpClaw.Code.Commands/Handlers/AgentsCommandHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private async Task<int> ExecuteListAsync(CommandExecutionContext context, Cancel
7272
0,
7373
context.OutputFormat,
7474
message,
75-
JsonSerializer.Serialize(agents, ProtocolJsonContext.Default.ListAgentCatalogEntry));
75+
JsonSerializer.Serialize(agents.ToList(), ProtocolJsonContext.Default.ListAgentCatalogEntry));
7676
await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
7777
return 0;
7878
}

src/SharpClaw.Code.Commands/Handlers/ConnectCommandHandler.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,14 @@ private async Task<List<ConnectTargetStatus>> BuildStatusesAsync(string workspac
110110
{
111111
var auth = await authFlowService.GetStatusAsync(provider.ProviderName, cancellationToken).ConfigureAwait(false);
112112
var configuredUrl = config.Document.ConnectLinks?.FirstOrDefault(link => string.Equals(link.Target, provider.ProviderName, StringComparison.OrdinalIgnoreCase))?.Url;
113-
results.Add(new ConnectTargetStatus(provider.ProviderName, provider.ProviderName, "provider", auth.IsAuthenticated, configuredUrl));
113+
var detail = auth.IsAuthenticated
114+
? string.IsNullOrWhiteSpace(auth.SubjectId)
115+
? "authenticated"
116+
: $"authenticated as {auth.SubjectId}"
117+
: auth.ExpiresAtUtc is { } expiresAt && expiresAt <= DateTimeOffset.UtcNow
118+
? "authentication expired"
119+
: "not authenticated";
120+
results.Add(new ConnectTargetStatus(provider.ProviderName, provider.ProviderName, "provider", auth.IsAuthenticated, configuredUrl, auth.ExpiresAtUtc, detail));
114121
}
115122

116123
foreach (var link in config.Document.ConnectLinks ?? [])
@@ -120,7 +127,7 @@ private async Task<List<ConnectTargetStatus>> BuildStatusesAsync(string workspac
120127
continue;
121128
}
122129

123-
results.Add(new ConnectTargetStatus(link.Target, link.DisplayName, "external", false, link.Url));
130+
results.Add(new ConnectTargetStatus(link.Target, link.DisplayName, "external", false, link.Url, null, "manual browser flow"));
124131
}
125132

126133
return results.OrderBy(static item => item.Target, StringComparer.OrdinalIgnoreCase).ToList();
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.CommandLine;
2+
using System.Globalization;
3+
using System.Text.Json;
4+
using SharpClaw.Code.Commands.Models;
5+
using SharpClaw.Code.Commands.Options;
6+
using SharpClaw.Code.Protocol.Commands;
7+
using SharpClaw.Code.Protocol.Serialization;
8+
using SharpClaw.Code.Runtime.Abstractions;
9+
10+
namespace SharpClaw.Code.Commands;
11+
12+
/// <summary>
13+
/// Surfaces estimated workspace cost based on persisted usage snapshots.
14+
/// </summary>
15+
public sealed class CostCommandHandler(
16+
IWorkspaceInsightsService workspaceInsightsService,
17+
OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
18+
{
19+
/// <inheritdoc />
20+
public string Name => "cost";
21+
22+
/// <inheritdoc />
23+
public string Description => "Shows estimated usage cost for the current workspace.";
24+
25+
/// <inheritdoc />
26+
public string CommandName => Name;
27+
28+
/// <inheritdoc />
29+
public Command BuildCommand(GlobalCliOptions globalOptions)
30+
{
31+
var command = new Command(Name, Description);
32+
command.SetAction((parseResult, cancellationToken) => ExecuteAsync(globalOptions.Resolve(parseResult), cancellationToken));
33+
return command;
34+
}
35+
36+
/// <inheritdoc />
37+
public Task<int> ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
38+
=> ExecuteAsync(context, cancellationToken);
39+
40+
private async Task<int> ExecuteAsync(CommandExecutionContext context, CancellationToken cancellationToken)
41+
{
42+
var report = await workspaceInsightsService
43+
.BuildCostReportAsync(context.WorkingDirectory, context.SessionId, cancellationToken)
44+
.ConfigureAwait(false);
45+
var total = report.WorkspaceEstimatedCostUsd.HasValue
46+
? report.WorkspaceEstimatedCostUsd.Value.ToString("0.0000", CultureInfo.InvariantCulture)
47+
: "n/a";
48+
var result = new CommandResult(
49+
true,
50+
0,
51+
context.OutputFormat,
52+
$"Workspace estimated cost: ${total} across {report.Sessions.Count} session(s).",
53+
JsonSerializer.Serialize(report, ProtocolJsonContext.Default.WorkspaceCostReport));
54+
await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
55+
return 0;
56+
}
57+
}

0 commit comments

Comments
 (0)