Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
SystemPrompt: request.Instructions,
OutputFormat: request.Context.OutputFormat,
Temperature: 0.1m,
Metadata: baseMetadata));
Metadata: baseMetadata,
ContainsImageInput: request.Context.UserContent?.Any(static block => block.Kind == ContentBlockKind.Image) == true));

var resolvedProviderName = resolvedRequest.ProviderName;

Expand Down Expand Up @@ -102,6 +103,16 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
throw CreateMissingProviderException(resolvedProviderName, requestedModel, "provider resolution");
}

if (request.Context.UserContent?.Any(static block => block.Kind == ContentBlockKind.Image) == true
&& !provider.SupportsImageInput)
{
throw new ProviderExecutionException(
resolvedProviderName,
requestedModel,
ProviderFailureKind.StreamFailed,
$"Provider '{resolvedProviderName}' does not support structured image input.");
}

// --- Build initial conversation messages ---
// Do not add request.Instructions as a shared "system" chat message here.
// Provider adapters apply system instructions via ProviderRequest.SystemPrompt
Expand All @@ -115,7 +126,11 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
messages.AddRange(history);
}

messages.Add(new ChatMessage("user", [new ContentBlock(ContentBlockKind.Text, request.Context.Prompt, null, null, null, null)]));
messages.Add(new ChatMessage(
"user",
request.Context.UserContent?.Count > 0
? request.Context.UserContent
: [new ContentBlock(ContentBlockKind.Text, request.Context.Prompt, null, null, null, null)]));

// --- Tool-calling loop ---
var allProviderEvents = new List<ProviderEvent>();
Expand Down Expand Up @@ -143,7 +158,8 @@ internal async Task<ProviderInvocationResult> ExecuteAsync(
Metadata: baseMetadata,
Messages: messages,
Tools: availableTools,
MaxTokens: options.MaxTokensPerRequest));
MaxTokens: options.MaxTokensPerRequest,
ContainsImageInput: messages.Any(static message => message.Content.Any(static block => block.Kind == ContentBlockKind.Image))));

lastProviderRequest = providerRequest;

Expand Down
4 changes: 3 additions & 1 deletion src/SharpClaw.Code.Agents/Models/AgentRunContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace SharpClaw.Code.Agents.Models;
/// </param>
/// <param name="IsInteractive">Whether tool approvals can interact with the caller.</param>
/// <param name="ApprovalSettings">Optional bounded auto-approval settings forwarded to tool execution.</param>
/// <param name="UserContent">Optional structured user content blocks for the current turn.</param>
public sealed record AgentRunContext(
string SessionId,
string TurnId,
Expand All @@ -42,4 +43,5 @@ public sealed record AgentRunContext(
IToolMutationRecorder? ToolMutationRecorder = null,
IReadOnlyList<ChatMessage>? ConversationHistory = null,
bool IsInteractive = true,
ApprovalSettings? ApprovalSettings = null);
ApprovalSettings? ApprovalSettings = null,
IReadOnlyList<ContentBlock>? UserContent = null);
44 changes: 41 additions & 3 deletions src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@ public async Task<AgentRunResult> RunAsync(AgentFrameworkRequest request, Cancel
{
ArgumentNullException.ThrowIfNull(request);
var allowedTools = ResolveAllowedTools(request.Context.Metadata);
var trustedPluginNames = ResolveTrustedNames(request.Context.Metadata, SharpClawWorkflowMetadataKeys.TrustedPluginNamesJson);
var trustedMcpServerNames = ResolveTrustedNames(request.Context.Metadata, SharpClawWorkflowMetadataKeys.TrustedMcpServerNamesJson);
var effectivePermissionMode = ResolvePermissionMode(request.Context.Metadata, request.Context.PermissionMode);

// Build tool execution context from agent run context
var toolExecutionContext = new ToolExecutionContext(
SessionId: request.Context.SessionId,
TurnId: request.Context.TurnId,
WorkspaceRoot: request.Context.WorkingDirectory,
WorkingDirectory: request.Context.WorkingDirectory,
PermissionMode: request.Context.PermissionMode,
PermissionMode: effectivePermissionMode,
OutputFormat: request.Context.OutputFormat,
EnvironmentVariables: null,
Model: request.Context.Model,
Expand All @@ -54,8 +57,8 @@ public async Task<AgentRunResult> RunAsync(AgentFrameworkRequest request, Cancel
&& string.Equals(acp, "true", StringComparison.OrdinalIgnoreCase)
? "acp"
: null,
TrustedPluginNames: null,
TrustedMcpServerNames: null,
TrustedPluginNames: trustedPluginNames,
TrustedMcpServerNames: trustedMcpServerNames,
PrimaryMode: request.Context.PrimaryMode,
MutationRecorder: request.Context.ToolMutationRecorder,
ApprovalSettings: request.Context.ApprovalSettings);
Expand Down Expand Up @@ -168,6 +171,41 @@ public async Task<AgentRunResult> RunAsync(AgentFrameworkRequest request, Cancel
}
}

private static IReadOnlyCollection<string>? ResolveTrustedNames(
IReadOnlyDictionary<string, string>? metadata,
string metadataKey)
{
if (metadata is null
|| !metadata.TryGetValue(metadataKey, out var payload)
|| string.IsNullOrWhiteSpace(payload))
{
return null;
}

try
{
return JsonSerializer.Deserialize(payload, ProtocolJsonContext.Default.StringArray);
}
catch (JsonException)
{
return null;
}
}

private static PermissionMode ResolvePermissionMode(
IReadOnlyDictionary<string, string>? metadata,
PermissionMode fallback)
{
if (metadata is not null
&& metadata.TryGetValue(SharpClawWorkflowMetadataKeys.PreferredPermissionMode, out var payload)
&& Enum.TryParse<PermissionMode>(payload, ignoreCase: true, out var parsed))
{
return parsed;
}

return fallback;
}

private static IEnumerable<ToolDefinition> FilterAdvertisedTools(
IReadOnlyList<ToolDefinition> registryTools,
IReadOnlyCollection<string>? allowedTools)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,23 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
services.AddSingleton<IReplTerminal, Terminal.SpectreReplTerminal>();
services.AddSingleton<ReplCommandHandler>();
services.AddSingleton<SessionCommandHandler>();
services.AddSingleton<PermissionsCommandHandler>();
services.AddSingleton<AuthCommandHandler>();
services.AddSingleton<InitCommandHandler>();
services.AddSingleton<ResearchCommandHandler>();
services.AddSingleton<ScheduleCommandHandler>();
services.AddSingleton<EvolutionCommandHandler>();
services.AddSingleton<ICommandHandler, PromptCommandHandler>();
services.AddSingleton<ICommandHandler, StatusCommandHandler>();
services.AddSingleton<ICommandHandler, DoctorCommandHandler>();
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<SessionCommandHandler>());
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<PermissionsCommandHandler>());
services.AddSingleton<ICommandHandler, ModelsCommandHandler>();
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<AuthCommandHandler>());
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<InitCommandHandler>());
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<ResearchCommandHandler>());
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<ScheduleCommandHandler>());
services.AddSingleton<ICommandHandler>(serviceProvider => serviceProvider.GetRequiredService<EvolutionCommandHandler>());
services.AddSingleton<ICommandHandler, UsageCommandHandler>();
services.AddSingleton<ICommandHandler, CostCommandHandler>();
services.AddSingleton<ICommandHandler, StatsCommandHandler>();
Expand Down Expand Up @@ -62,7 +74,14 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
services.AddSingleton<ISlashCommandHandler, DoctorCommandHandler>();
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<SessionCommandHandler>());
services.AddSingleton<ISlashCommandHandler, SessionsSlashCommandHandler>();
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<PermissionsCommandHandler>());
services.AddSingleton<ISlashCommandHandler, ModelsCommandHandler>();
services.AddSingleton<ISlashCommandHandler, ModelSlashCommandHandler>();
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<AuthCommandHandler>());
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<InitCommandHandler>());
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<ResearchCommandHandler>());
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<ScheduleCommandHandler>());
services.AddSingleton<ISlashCommandHandler>(serviceProvider => serviceProvider.GetRequiredService<EvolutionCommandHandler>());
services.AddSingleton<ISlashCommandHandler, UsageCommandHandler>();
services.AddSingleton<ISlashCommandHandler, CostCommandHandler>();
services.AddSingleton<ISlashCommandHandler, StatsCommandHandler>();
Expand All @@ -86,6 +105,9 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
services.AddSingleton<ISlashCommandHandler, VersionCommandHandler>();
services.AddSingleton<ISlashCommandHandler, UndoCommandHandler>();
services.AddSingleton<ISlashCommandHandler, RedoCommandHandler>();
services.AddSingleton<ISlashCommandHandler, NewSessionSlashCommandHandler>();
services.AddSingleton<ISlashCommandHandler, ResumeSlashCommandHandler>();
services.AddSingleton<ISlashCommandHandler, ClearSlashCommandHandler>();

services.AddSingleton<IOutputRenderer, Rendering.TextOutputRenderer>();
services.AddSingleton<IOutputRenderer, Rendering.JsonOutputRenderer>();
Expand Down
4 changes: 4 additions & 0 deletions src/SharpClaw.Code.Cli/Terminal/SpectreReplTerminal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ public void WriteInfo(string message)
/// <inheritdoc />
public void WriteError(string message)
=> AnsiConsole.MarkupLine($"[red]{Markup.Escape(message)}[/]");

/// <inheritdoc />
public void ClearScreen()
=> AnsiConsole.Clear();
}
5 changes: 5 additions & 0 deletions src/SharpClaw.Code.Commands/Abstractions/IReplTerminal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public interface IReplTerminal
/// </summary>
/// <param name="message">The message to write.</param>
void WriteError(string message);

/// <summary>
/// Clears the visible terminal screen.
/// </summary>
void ClearScreen();
}
Original file line number Diff line number Diff line change
@@ -1,73 +1,27 @@
using SharpClaw.Code.Commands.Models;
using SharpClaw.Code.Commands.Options;
using SharpClaw.Code.Protocol.Commands;
using SharpClaw.Code.Protocol.Models;

namespace SharpClaw.Code.Commands;

/// <summary>
/// Shows or adjusts bounded auto-approval settings for REPL-driven prompts.
/// Provides a durable alias over the permissions auto-approval subcommands.
/// </summary>
public sealed class ApprovalsSlashCommandHandler(
ReplInteractionState replState,
OutputRendererDispatcher outputRendererDispatcher) : ISlashCommandHandler
public sealed class ApprovalsSlashCommandHandler(PermissionsCommandHandler permissionsCommandHandler) : ISlashCommandHandler
{
/// <inheritdoc />
public string CommandName => "approvals";

/// <inheritdoc />
public string Description => "Shows or sets auto-approval scopes and budget for REPL prompts.";
public string Description => "Alias for /permissions approvals show|set|clear.";

/// <inheritdoc />
public Task<int> ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
{
if (command.Arguments.Length == 0)
{
var effective = replState.ApprovalSettingsOverride ?? context.ApprovalSettings;
return RenderAsync(
$"Auto-approvals: {ApprovalSettingsText.RenderSummary(effective)} (override: {(replState.ApprovalSettingsOverride is null ? "none" : ApprovalSettingsText.RenderSummary(replState.ApprovalSettingsOverride))}).",
context,
cancellationToken);
}

if (string.Equals(command.Arguments[0], "reset", StringComparison.OrdinalIgnoreCase)
|| string.Equals(command.Arguments[0], "clear", StringComparison.OrdinalIgnoreCase))
{
replState.ApprovalSettingsOverride = null;
return RenderAsync("Auto-approval reset for the next prompt.", context, cancellationToken);
}

if (!string.Equals(command.Arguments[0], "set", StringComparison.OrdinalIgnoreCase) || command.Arguments.Length < 2)
{
return RenderAsync("Usage: /approvals [set <scopes> [budget]|reset]", context, cancellationToken, success: false);
}

var budget = command.Arguments.Length >= 3
? ParseBudget(command.Arguments[2])
: null;
var settings = ApprovalSettingsText.Parse(command.Arguments[1], budget) ?? ApprovalSettings.Empty;
replState.ApprovalSettingsOverride = settings;
return RenderAsync(
$"Auto-approval override set to {ApprovalSettingsText.RenderSummary(settings)}.",
=> permissionsCommandHandler.ExecuteAsync(
new SlashCommandParseResult(
true,
"permissions",
command.Arguments.Length == 0
? ["approvals", "show"]
: ["approvals", .. command.Arguments]),
context,
cancellationToken);
}

private async Task<int> RenderAsync(
string message,
CommandExecutionContext context,
CancellationToken cancellationToken,
bool success = true)
{
await outputRendererDispatcher.RenderCommandResultAsync(
new CommandResult(success, success ? 0 : 1, context.OutputFormat, message, null),
context.OutputFormat,
cancellationToken).ConfigureAwait(false);
return success ? 0 : 1;
}

private static int? ParseBudget(string value)
=> int.TryParse(value, out var parsed) && parsed > 0
? parsed
: throw new InvalidOperationException($"Invalid auto-approve budget '{value}'.");
}
Loading