Skip to content

fix(dotnet): forward Context and ForwardedProperties from RawRepresentationFactory input (#2151)#2177

Merged
BenTaylorDev merged 3 commits into
mainfrom
fix/2151-dotnet-context
Jul 15, 2026
Merged

fix(dotnet): forward Context and ForwardedProperties from RawRepresentationFactory input (#2151)#2177
BenTaylorDev merged 3 commits into
mainfrom
fix/2151-dotnet-context

Conversation

@BenTaylorDev

Copy link
Copy Markdown
Contributor

Fixes #2151.

Problem

When a caller seeds the outgoing request via ChatOptions.RawRepresentationFactory (a RunAgentInput), AGUIChatClientHandler.BuildRunAgentInput copies Messages, Tools, State and ParentRunId onto the request it actually sends — but never reads Context or ForwardedProperties. Both are silently dropped, so caller-supplied run context/props never reach the AG-UI server.

Fix

Forward the two missing fields inside the existing providedInput is not null block, mirroring the surrounding pattern:

if (providedInput.Context is { Count: > 0 })
{
    input.Context = providedInput.Context;
}

if (providedInput.ForwardedProperties.ValueKind != JsonValueKind.Undefined)
{
    input.ForwardedProperties = providedInput.ForwardedProperties;
}

Context is IList<AGUIContext>? (guarded on non-empty) and ForwardedProperties is a non-nullable JsonElement (default ValueKind is Undefined, so the guard skips an unset value).

Testing

  • New unit test in AGUIChatClientTest using the existing CapturingTransport to assert both Context and ForwardedProperties survive onto the RunAgentInput sent to the transport.
  • TDD-verified: the test fails on the pre-fix code (Assert.NotNull on Context → null) and passes after the fix.
  • Full AGUI.Client.UnitTests suite green: 111/111 (net10.0). Ran locally with .NET 10.0.100 (dotnet test … /p:SignAssembly=false, since the test-only InternalsVisibleTo is gated on unsigned builds).

🤖 Generated with Claude Code

…pplied RunAgentInput (#2151)

`AGUIChatClientHandler.BuildRunAgentInput` copied Messages/Tools/State/ParentRunId
from a `ChatOptions.RawRepresentationFactory`-supplied `RunAgentInput` onto the
request actually sent, but silently dropped `Context` and `ForwardedProperties` —
so caller-supplied run context/props never reached the AG-UI server.

Forward both, mirroring the existing field-copy pattern (`Context` when non-empty,
`ForwardedProperties` when its JsonElement is not Undefined).

Adds a unit test (CapturingTransport) asserting both survive onto the sent input;
verified it fails on the pre-fix code (Context null) and passes after. Full
AGUI.Client.UnitTests suite green (111/111, net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BenTaylorDev
BenTaylorDev requested a review from a team as a code owner July 13, 2026 22:17
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1784125344 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then install the packages you need:

# Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1784125344' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1784125344' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1784125344' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1784125344' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1784125344' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1784125344' --index testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1784125344

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: 84fc630

@pkg-pr-new

pkg-pr-new Bot commented Jul 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@2177

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@2177

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@2177

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@2177

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@2177

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@2177

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@2177

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@2177

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@2177

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@2177

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@2177

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@2177

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@2177

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@2177

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@2177

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@2177

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@2177

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@2177

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@2177

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@2177

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@2177

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@2177

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@2177

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@2177

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@2177

commit: a2b545d

@contextablemark contextablemark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BenTaylorDev Did you mean to leave this untouched? Seems like it would have the same issue, but only if RawRepresentationFactory is used rather than resume.

@contextablemark

Copy link
Copy Markdown
Contributor

this

My explanation is wrong, but I poked at the critique and it appears to be valid.

@BenTaylorDev

Copy link
Copy Markdown
Contributor Author

@contextablemark that link resolves to main (it has no commit SHA), so it's showing the pre-fix method — this PR actually modifies exactly that block: input.Context and input.ForwardedProperties are added right after ParentRunId (permalink).

On resume: it's the same BuildRunAgentInput (resume just sets input.Resume inside it, and the result is sent directly via _transport.SendAsync), so the forwarding covers resume turns too — it's gated on RawRepresentationFactory supplying the fields, same as Messages/Tools/State/ParentRunId. So nothing separate is left untouched, but let me know if you're seeing a path I'm missing.

@contextablemark contextablemark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See review... the agent still thinks there's an issue with Resume not being copied from providedInput. If dropping Resume is intentional, then the critique falls apart.

if (providedInput.ForwardedProperties.ValueKind != JsonValueKind.Undefined)
{
input.ForwardedProperties = providedInput.ForwardedProperties;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to add something here like :

Suggested change
}
}
if (providedInput.Resume is { Count: > 0 })
{
input.Resume = providedInput.Resume;
}

or is the Resume not relevant here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The analysis indicates that, until the change above is made, input.Resume will always be null, so this check will be dead, but if the change above is made then it could then be skipped. It all boils down to whether it is intentional not to copy providedInput.Resume into input.Resume. If not, you would need something like this.

// Convert ToolApprovalResponseContent list (passed from AGUIChatClient) to resume payload
if (options?.AdditionalProperties?.TryGetValue(AGUIClientInternalKeys.ApprovalResponses, out List<ToolApprovalResponseContent>? approvalResponses) is true
    && approvalResponses is { Count: > 0 })
{
    var resumeList = input.Resume is { Count: > 0 } existing
        ? new List<AGUIResume>(existing)
        : new List<AGUIResume>(approvalResponses.Count);

    // ... unchanged foreach ...

    input.Resume = resumeList;
}

And then adding a new test :

public async Task GetStreamingResponse_RawRepresentationFactory_ForwardsResume()
{
    var transport = new CapturingTransport();
    using var client = new AGUIChatClient(new() { Transport = transport });

    var options = new ChatOptions
    {
        RawRepresentationFactory = _ => new RunAgentInput
        {
            Resume = new List<AGUIResume>
            {
                new() { InterruptId = "int-1", Status = ResumeStatus.Resolved },
            },
        },
    };

    await DrainAsync(client.GetStreamingResponseAsync(
        new List<ChatMessage> { new(ChatRole.User, "Hello") }, options));

    var sent = transport.LastInput!;
    Assert.NotNull(sent.Resume);
    Assert.Equal("int-1", Assert.Single(sent.Resume!).InterruptId);
}

…#2151 review)

Follow-up to the Context/ForwardedProperties fix: BuildRunAgentInput also dropped
providedInput.Resume. The approval-response translation's `if (input.Resume is
null && ...)` guard was effectively dead (nothing set input.Resume first),
confirming it was meant to be copied. Forward it in the same block; a
caller-supplied Resume now takes precedence over the approval translation, and
interrupt responses still append. Tests for both (forwarded + precedence); full
client suite green (113).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BenTaylorDev

Copy link
Copy Markdown
Contributor Author

Confirmed — providedInput.Resume was being dropped, same class as Context/ForwardedProperties. The approval path's if (input.Resume is null && …) guard was effectively dead (nothing set input.Resume beforehand), which is a good signal it was meant to be copied.

Pushed the forwarding in the same block (permalink) so a caller-supplied Resume is forwarded and takes precedence over the approval-response translation; interrupt responses still append. Added tests for both (forwarded + precedence) — confirmed each fails on the pre-fix code and passes after — and the full client suite is green (113).

@BenTaylorDev

Copy link
Copy Markdown
Contributor Author

@NathanTarbert apologies — I missed your outstanding #2164 when I opened this, so #2177 duplicates the same Context/ForwardedProperties fix (identical guarded pattern to yours).

The one difference: this PR additionally forwards Resume, which is also dropped from a caller-supplied RunAgentInput — with precedence over the approval-response translation (making the previously-dead input.Resume is null guard meaningful). @contextablemark caught that in review here, and it's covered by tests (forwarded + precedence).

Since they overlap, would you be OK closing #2164 in favor of this one as the superset? Equally happy to do it the other way — port the Resume delta onto #2164 and close this — whichever you'd prefer. Sorry again for the dup.

@contextablemark

contextablemark commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@BenTaylorDev
The agent has found another discrepancy - an apparent asymmetry between how Resume is handled between tool-approval responses and interrupt responses :

Caller Resume set, last message also carries… Outcome
a tool-approval response dropped — approval block skips on input.Resume is null (L370)
an interrupt response appended — interrupt block is ungated (L414)

So if this asymmetry is intended... fine - otherwise something else to look at

@BenTaylorDev

Copy link
Copy Markdown
Contributor Author

It's not. It was seemingly always present but no code hit it until I added the additional cases for the resume handling. Adding a fix for it.

…oval and interrupt responses

Previously a caller-supplied Resume (via RawRepresentationFactory) dropped
approval responses (the approval block was gated on `input.Resume is null`)
but interrupt responses were still appended unconditionally. That asymmetry
was dormant until Resume forwarding was added in this PR, which is the only
path that populates input.Resume before the translation blocks run.

Gate both translations on a single callerSuppliedResume flag so a caller who
hand-builds Resume owns it entirely, keeping the two resume paths symmetric.
Since the dropped approval/interrupt responses were already stripped from the
outgoing messages, emit an ActivityEvent so the drop is observable instead of
silent. Adds a mirror test for the interrupt precedence path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BenTaylorDev

Copy link
Copy Markdown
Contributor Author

Good catch @contextablemark — the asymmetry was real, and not intended. Fixed in a2b545d.

Root cause: the two blocks used different merge strategies. Approval replaced input.Resume, so it had to be gated on input.Resume is null to avoid clobbering a caller value; interrupt appended, so it was never gated. That difference was dormant until this PR — nothing ever populated input.Resume before those blocks ran, so the approval gate was effectively always-true and both blocks always executed. Forwarding a caller-supplied Resume is the first path that makes input.Resume non-null beforehand, which is exactly what flips the approval gate off while interrupts kept appending. So this PR is what turned a latent inconsistency into an observable one.

Fix (caller-wins, symmetric):

  • Both the approval and interrupt translations now yield to a single callerSuppliedResume flag, so a caller who hand-builds Resume owns it entirely. This matches the documented "caller-supplied Resume takes precedence" intent and the existing approval-precedence test.
  • The approval+interrupt auto-merge (both present in the same turn, no caller Resume) is preserved — interrupt still appends onto approval-derived entries in that path.
  • Since the dropped approval/interrupt responses are already stripped from the outgoing messages, the previous behavior would have silently discarded them. To avoid a silent drop, we now emit an ActivityEvent (agui.resume.caller_override_dropped_responses) with counts when a caller Resume overrides pending responses.
  • Added GetStreamingResponse_CallerResume_TakesPrecedenceOverInterruptResponses mirroring the approval test.

All 114 AGUI.Client unit tests pass locally (net10.0).

@contextablemark contextablemark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@BenTaylorDev
BenTaylorDev merged commit 828e444 into main Jul 15, 2026
59 of 60 checks passed
@BenTaylorDev
BenTaylorDev deleted the fix/2151-dotnet-context branch July 15, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: AGUIChatClient.BuildRunAgentInput drops RunAgentInput.Context (and ForwardedProperties) from a caller-supplied RawRepresentationFactory input

2 participants