Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ jobs:
dotnet build examples/McpToolAgent/McpToolAgent.csproj --no-restore --configuration Release
- name: Test
run: dotnet test SharpClawCode.sln --no-build --configuration Release --collect:"XPlat Code Coverage" --results-directory ./coverage
- name: Agent scenario harness
run: |
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj --no-build --configuration Release -- test run
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj --no-build --configuration Release -- test gates
- name: Upload coverage
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" />
<PackageVersion Include="Spectre.Console" Version="0.55.0" />
<PackageVersion Include="System.Security.Cryptography.ProtectedData" Version="10.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.5" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
Expand Down
60 changes: 60 additions & 0 deletions SharpClawCode.sln
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalConsoleAgent", "exam
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkerServiceHost", "examples\WorkerServiceHost\WorkerServiceHost.csproj", "{2E8A9F4F-8161-4E49-9F04-533D972C11CB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpClaw.Testing.Abstractions", "src\SharpClaw.Testing.Abstractions\SharpClaw.Testing.Abstractions.csproj", "{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpClaw.Testing.Harness", "src\SharpClaw.Testing.Harness\SharpClaw.Testing.Harness.csproj", "{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpClaw.Testing.Cli", "src\SharpClaw.Testing.Cli\SharpClaw.Testing.Cli.csproj", "{425E2495-940F-46A6-9F3E-ED05301504BD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpClaw.Testing.Xunit", "src\SharpClaw.Testing.Xunit\SharpClaw.Testing.Xunit.csproj", "{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -411,6 +419,54 @@ Global
{2E8A9F4F-8161-4E49-9F04-533D972C11CB}.Release|x64.Build.0 = Release|Any CPU
{2E8A9F4F-8161-4E49-9F04-533D972C11CB}.Release|x86.ActiveCfg = Release|Any CPU
{2E8A9F4F-8161-4E49-9F04-533D972C11CB}.Release|x86.Build.0 = Release|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Debug|x64.ActiveCfg = Debug|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Debug|x64.Build.0 = Debug|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Debug|x86.ActiveCfg = Debug|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Debug|x86.Build.0 = Debug|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Release|Any CPU.Build.0 = Release|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Release|x64.ActiveCfg = Release|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Release|x64.Build.0 = Release|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Release|x86.ActiveCfg = Release|Any CPU
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF}.Release|x86.Build.0 = Release|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Debug|x64.ActiveCfg = Debug|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Debug|x64.Build.0 = Debug|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Debug|x86.ActiveCfg = Debug|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Debug|x86.Build.0 = Debug|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Release|Any CPU.Build.0 = Release|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Release|x64.ActiveCfg = Release|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Release|x64.Build.0 = Release|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Release|x86.ActiveCfg = Release|Any CPU
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E}.Release|x86.Build.0 = Release|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Debug|x64.ActiveCfg = Debug|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Debug|x64.Build.0 = Debug|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Debug|x86.ActiveCfg = Debug|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Debug|x86.Build.0 = Debug|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Release|Any CPU.Build.0 = Release|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Release|x64.ActiveCfg = Release|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Release|x64.Build.0 = Release|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Release|x86.ActiveCfg = Release|Any CPU
{425E2495-940F-46A6-9F3E-ED05301504BD}.Release|x86.Build.0 = Release|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Debug|x64.ActiveCfg = Debug|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Debug|x64.Build.0 = Debug|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Debug|x86.ActiveCfg = Debug|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Debug|x86.Build.0 = Debug|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Release|Any CPU.Build.0 = Release|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Release|x64.ActiveCfg = Release|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Release|x64.Build.0 = Release|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Release|x86.ActiveCfg = Release|Any CPU
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -444,5 +500,9 @@ Global
{963C636F-2096-45B1-8101-B8345967F197} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
{7BA2E64A-B330-4783-9330-AEF46B91929A} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
{2E8A9F4F-8161-4E49-9F04-533D972C11CB} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
{A4E45F2B-9118-41EC-8AF2-08EBF0F9B3EF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A78CD9D6-54CF-422C-B5D8-B3BC4D99323E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{425E2495-940F-46A6-9F3E-ED05301504BD} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C45BBEA7-5970-40BB-AE6D-B8F09D1E2EE1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
14 changes: 13 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@
| **SharpClaw.Code.IntegrationTests** | Runtime + provider flows with real composition |
| **SharpClaw.Code.MockProvider** | **`DeterministicMockModelProvider`**, **`AddDeterministicMockModelProvider`**, **`ParityMetadataKeys`**, **`ParityProviderScenario`** |
| **SharpClaw.Code.ParityHarness** | End-to-end scenarios over real **`AddSharpClawRuntime`** + mock LLM |
| **SharpClaw.Testing.\*** | JSON scenario contracts, oracle runner, CLI commands, and xUnit adapter for explicit agent testing |

Run all tests:

```bash
dotnet test SharpClawCode.sln
```

Run the explicit agent scenario harness:

```bash
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj -- test run
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj -- test gates
```

Build the example hosts as part of normal validation:

```bash
Expand Down Expand Up @@ -59,6 +67,10 @@ Stable scenario **ids** are listed in **`ParityScenarioIds`** (e.g. `streaming_t

**Note:** Many scenarios exercise **`IToolExecutor`** directly rather than going through the LLM agent loop (which matches current **`AgentFrameworkBridge`** behavior).

## Agent scenario harness

The scenario harness lives in **`SharpClaw.Testing.Abstractions`**, **`SharpClaw.Testing.Harness`**, **`SharpClaw.Testing.Cli`**, and **`SharpClaw.Testing.Xunit`**. Scenario files live in **`tests/agent-scenarios`** and use JSON with explicit oracles. See **`docs/testing/agent-testing-harness.md`** for the contract, CLI usage, xUnit adapter, and gate model.

## CI

CI restores and builds the full solution, explicitly builds every example host project, and then runs `dotnet test` on the solution. Parity tests use temp directories under **`Path.GetTempPath()`** and avoid network.
CI restores and builds the full solution, explicitly builds every example host project, runs `dotnet test`, then runs the explicit agent scenario harness through `sharpclaw test run` and `sharpclaw test gates`. Parity tests use temp directories under **`Path.GetTempPath()`** and avoid network.
140 changes: 140 additions & 0 deletions docs/testing/agent-testing-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Agent Testing Harness

## Purpose

The agent testing harness is a disciplined scenario runner for SharpClaw agent behavior. It is not a generic AI test generator. Each scenario declares the prompt, the trace source, risk level, and explicit oracles that must pass.

Every run produces a structured trace and evaluates that trace against named oracles. The first implementation uses a `scripted` executor so the model, loader, trace writer, report writer, gates, CLI, and xUnit adapter can stabilize before wiring the harness to the live runtime/gateway.

## Scenario Format

Scenarios live under `tests/agent-scenarios` as JSON files:

```json
{
"id": "basic-tool-call",
"risk": "Low",
"input": {
"prompt": "Read the project README.",
"executor": "scripted",
"scriptedTrace": [
{
"kind": "ToolCall",
"toolCall": {
"toolName": "read_file",
"argumentsJson": "{\"path\":\"README.md\"}"
}
}
],
"scriptedFinalAnswer": "README starts with SharpClaw Code."
},
"expected": {
"oracles": [
{ "type": "ToolCalled", "toolName": "read_file" },
{ "type": "FinalAnswerContains", "text": "SharpClaw Code" }
]
}
}
```

The JSON contracts are defined in `SharpClaw.Testing.Abstractions` and serialized with `System.Text.Json`. The shape avoids runtime reflection-heavy polymorphic JSON: `TraceStep` has explicit optional payloads such as `toolCall`, `toolResult`, and `stateChange`.

## Oracle Model

Built-in oracles:

- `ToolCalled`
- `ToolNotCalled`
- `FinalAnswerContains`
- `MaxToolCalls`
- `StateEquals`
- `ApprovalRequired`
- `NoUnsafeTool`

Failed oracles include a clear message plus expected and actual summaries. Scenarios with no explicit oracles fail the explicit-oracle gate.

## CLI Usage

Initialize example scenarios:

```bash
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj -- test init
```

Run scenarios, write traces, evaluate oracles, and generate `docs/testing/test-run-report.md`:

```bash
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj -- test run
```

Regenerate a markdown report from the latest result file:

```bash
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj -- test report
```

Run gate checks:

```bash
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj -- test gates
```

Defaults:

- Scenarios: `tests/agent-scenarios`
- Markdown report: `docs/testing/test-run-report.md`
- Machine-readable results: `artifacts/testing/test-run-results.json`
- Trace files: `artifacts/testing/traces`

## xUnit Usage

`SharpClaw.Testing.Xunit` exposes data and assertion helpers:

```csharp
public static IEnumerable<object[]> Scenarios
=> XunitScenarioData.LoadDirectory("tests/agent-scenarios");

[Theory]
[MemberData(nameof(Scenarios))]
public Task Scenario_passes(AgentScenario scenario)
=> XunitScenarioAssert.PassesAsync(scenario);
```

The adapter uses explicit `MemberData`; it does not scan assemblies for tests.

## CI Integration

Recommended CI commands:

```bash
dotnet test SharpClawCode.sln --configuration Release
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj --configuration Release --no-build -- test run
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj --configuration Release --no-build -- test gates
```

`test run` and `test gates` return non-zero when gates fail. Gates currently require scenario discovery, explicit oracles, passing high/critical risk scenarios, passing scenarios marked `requiredForGates`, and non-empty traces.

## Avoiding Shallow AI-Generated Tests

Generated or hand-authored scenarios are not accepted just because they execute. They must include explicit oracles tied to observable trace behavior, final answers, state transitions, approvals, and tool safety. A scenario with no oracle fails. A high-risk scenario with a failed oracle fails the gate.

Before accepting AI-assisted scenarios, review:

- whether the prompt maps to a real product invariant,
- whether every expected outcome is represented by an oracle,
- whether the trace captures enough evidence for replay,
- whether safety-sensitive tool behavior is checked explicitly,
- whether the risk level is accurate.

## Future Extension Points

The first executor is `scripted`, which acts as a replay foundation. Future runtime integration should add an executor that adapts the real SharpClaw runtime/gateway and emits the same `AgentRunTrace` model.

Likely extensions:

- runtime-backed scenario executor,
- trace replay from captured production traces,
- richer approval and permission trace payloads,
- scenario filters by tag or risk,
- golden trace comparison,
- additional oracles for sessions, provider retries, MCP/plugin lifecycle, and telemetry.
65 changes: 65 additions & 0 deletions docs/testing/test-run-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Agent Testing Run Report

Generated: `2026-05-10T09:13:50.5670530+00:00`
Gate status: **PASS**

## Gates

| Gate | Status | Message |
|------|--------|---------|
| scenario-discovery | PASS | Discovered 4 scenario(s). |
| explicit-oracles | PASS | Every scenario defines at least one explicit oracle. |
| high-risk-pass | PASS | All high and critical risk scenarios passed. |
| required-scenarios-pass | PASS | All scenarios marked required for gates passed. |
| trace-presence | PASS | Every scenario produced at least one trace step. |

## Scenarios

| Scenario | Risk | Status | Trace |
|----------|------|--------|-------|
| approval-required | High | PASS | ../../artifacts/testing/traces/approval-required-38827d895786449094bbd28d8b640055.trace.json |
| basic-tool-call | Low | PASS | ../../artifacts/testing/traces/basic-tool-call-01ea2af915aa442ea785c520fc90d869.trace.json |
| timeout-retry-placeholder | Medium | PASS | ../../artifacts/testing/traces/timeout-retry-placeholder-6621dde240b1468a87f98735d6af81cd.trace.json |
| unsafe-tool-blocked | Critical | PASS | ../../artifacts/testing/traces/unsafe-tool-blocked-8554b8c5b4304acfa242c050a835a57a.trace.json |

## Oracle Results

### approval-required

Final answer: `Approval is required before updating protected configuration.`

| Oracle | Status | Message | Expected | Actual |
|--------|--------|---------|----------|--------|
| ApprovalRequired | PASS | Approval was required for write_file. | approval required | 1 |
| StateEquals | PASS | State 'approval.status' matched. | required | required |
| FinalAnswerContains | PASS | Final answer contained 'Approval is required'. | Approval is required | Approval is required before updating protected configuration. |

### basic-tool-call

Final answer: `README starts with SharpClaw Code.`

| Oracle | Status | Message | Expected | Actual |
|--------|--------|---------|----------|--------|
| ToolCalled | PASS | Tool 'read_file' was called. | read_file | read_file |
| MaxToolCalls | PASS | Tool-call count for all tools was 1. | <= 1 | 1 |
| FinalAnswerContains | PASS | Final answer contained 'SharpClaw Code'. | SharpClaw Code | README starts with SharpClaw Code. |

### timeout-retry-placeholder

Final answer: `Timeout captured; retry scheduled for a future runtime adapter.`

| Oracle | Status | Message | Expected | Actual |
|--------|--------|---------|----------|--------|
| StateEquals | PASS | State 'retry.scheduled' matched. | true | true |
| FinalAnswerContains | PASS | Final answer contained 'retry scheduled'. | retry scheduled | Timeout captured; retry scheduled for a future runtime adapter. |
| MaxToolCalls | PASS | Tool-call count for all tools was 0. | <= 0 | 0 |

### unsafe-tool-blocked

Final answer: `The unsafe shell command was blocked by policy.`

| Oracle | Status | Message | Expected | Actual |
|--------|--------|---------|----------|--------|
| ApprovalRequired | PASS | Approval was required for bash. | approval required | 1 |
| NoUnsafeTool | PASS | No unsafe tool call executed. | 0 unsafe executed | 0 |
| FinalAnswerContains | PASS | Final answer contained 'blocked'. | blocked | The unsafe shell command was blocked by policy. |
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 @@ -54,7 +54,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 @@ -108,6 +109,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 @@ -121,7 +132,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 @@ -149,7 +164,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
Loading
Loading