Skip to content

Adds Valkey to chat message history#2

Open
MatthiasHowellYopp wants to merge 1 commit into
mainfrom
issue-5445
Open

Adds Valkey to chat message history#2
MatthiasHowellYopp wants to merge 1 commit into
mainfrom
issue-5445

Conversation

@MatthiasHowellYopp
Copy link
Copy Markdown
Owner

Motivation and Context

The .NET Agent Framework currently has no dedicated Valkey integration package. Teams running Valkey in production — whether self-hosted or through managed cloud services like AWS ElastiCache for Valkey or GCP Memorystore for Valkey — have no first-party way to use Valkey for persistent chat history or long-term memory context. This has become increasingly common since the Redis license change.

The Microsoft.Agents.AI.Valkey package was scaffolded with working ValkeyChatHistoryProvider and ValkeyContextProvider implementations. This PR completes the package by adding missing pieces identified during review, fixing bugs in the existing sample, and adding a Bedrock-powered sample for AWS users.

Fixes issue microsoft#5445

Description

###Valkey package improvements:

  • Added ValkeyProviderScope copy constructor matching the Mem0ProviderScope pattern, with null-guard via Throw.IfNull
  • Added XML doc tags to all ValkeyProviderScope properties for consistency with Mem0ProviderScope
  • Kept the package dependency lean — only references Microsoft.Agents.AI.Abstractions (no dependency on Microsoft.Agents.AI)

###Bug fix in existing sample:

  • Fixed Program.cs — the stateInitializer referenced session?.Id which doesn't exist on AgentSession. Replaced with Guid.NewGuid():N using a discard parameter to make intent clear

###New Bedrock sample (AgentWithMemory_Step03_MemoryUsingValkey_Bedrock):

  • Demonstrates both ValkeyChatHistoryProvider and ValkeyContextProvider powered by Amazon Bedrock via AWSSDK.Extensions.Bedrock.MEAI
  • Uses IAmazonBedrockRuntime.AsIChatClient() which provides an IChatClient that plugs directly into the Agent Framework's ChatClientAgent
  • Uses the standard AWS credential chain (env vars, profile, IAM role)
  • Includes README with prerequisites, environment variables, and run instructions

###Infrastructure:

Added AWSSDK.Extensions.Bedrock.MEAI v4.0.6.10 to Directory.Packages.props
Registered the Bedrock sample in agent-framework-dotnet.slnx
Unit tests:

  • Added tests for ValkeyProviderScope copy constructor (clone all properties, null source throws)
  • 23 tests total, all passing

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Copy link
Copy Markdown

@Jonathan-Improving Jonathan-Improving left a comment

Choose a reason for hiding this comment

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

Consolidated Code Review: PR #2 — Adds Valkey to Chat Message History

PR: issue-5445main | Reviewed: 2026-04-27 | Scope: 16 changed files, +1,427 lines
Reviewers: GLIDE/Domain, DRY/Abstraction, Security (×2 rounds, consolidated)


🔒 Security Review

[VULN-001] Cross-Scope Data Leak — ThreadId Stored but Not Filtered (High) — See inline comment on ValkeyContextProvider.cs:178-193.

[VULN-002] Incomplete Query/Tag Escaping (Medium) — See inline comment on ValkeyContextProvider.cs:330-340.

Needs Verification:

  • [VERIFY-001] No TTL on stored data — messages persist indefinitely. PII accumulates without bound. Intentional?
  • [VERIFY-002] No try/catch around JsonSerializer.Deserialize<ChatMessage> in ValkeyChatHistoryProvider.ProvideChatHistoryAsync — see inline comment. Malformed JSON crashes the session.

Cleared: No arbitrary Valkey command injection (RESP parameterization). Indirect prompt injection risk documented accurately. No hardcoded secrets in samples. Connection security is a deployment concern, correctly documented.


🔴 Critical — Must Fix

1. Unit tests cover ~20% of public API surface
All 23 tests are synchronous constructor/property checks. Zero tests for any async method (ProvideChatHistoryAsync, StoreChatHistoryAsync, ProvideMessagesAsync, StoreAIContextAsync, EnsureIndexAsync, ClearMessagesAsync, GetMessageCountAsync, DisposeAsync), zero tests for security-critical methods (EscapeTag, EscapeQuery, ParseSearchResults), and zero tests for business logic (MaxMessages trimming, MaxMessagesToRetrieve limiting). InternalsVisibleTo is configured but unused. The Mem0 unit tests in this repo set a clear standard these tests fall far short of. Additionally, StateInitializer_NoScopeFields_Throws never triggers the lazy validation — it just asserts Assert.NotNull(provider).

2. CancellationToken never forwarded
Both providers accept CancellationToken in every async method but never use it. At minimum add cancellationToken.ThrowIfCancellationRequested() before I/O calls and between loop iterations. The Mem0 provider forwards CT to its HTTP calls.

3. EnsureIndexAsync race condition — See inline comment on ValkeyContextProvider.cs:268-275.


🟡 Medium

4. Constructor/DisposeAsync duplication — extract ValkeyConnectionManager
Both providers have 2 constructors each with ~80% identical bodies and character-for-character identical DisposeAsync implementations. An internal ValkeyConnectionManager (composition, not inheritance) would eliminate 4 duplicated constructor bodies and 2 identical DisposeAsync methods.

5. Error handling inconsistency between providers
ValkeyContextProvider wraps operations in try/catch (matching Mem0). ValkeyChatHistoryProvider does not — exceptions propagate unhandled. Either add try/catch to match, or document the intentional difference.

6. Synchronous ConnectionMultiplexer.Connect() in constructors
Blocking network call in constructor. Can cause thread-pool starvation in ASP.NET Core / DI scenarios. Consider ConnectAsync() via lazy init, or document that callers should prefer the IConnectionMultiplexer overload.

7. No disposed-state guard — Neither provider has a _disposed flag. After DisposeAsync(), subsequent calls throw opaque errors.

8. StoreChatHistoryAsync pushes messages one-by-one — N round-trips. ListRightPushAsync accepts RedisValue[] for batch push.

9. Missing [JsonConstructor] on State classes — Unlike the Mem0 pattern. Could break session state serialization.

10. No PII redaction in log messagesConversationId, UserId, AgentId logged directly. Mem0 uses a Redactor pattern.


🟢 Low

  1. Duplicate <NoWarn> line in csproj (see inline)
  2. NuGet <Description> says "vector search" — should say "full-text search" (see inline)
  3. EscapeQuery allocates char[] array per call — make special a static readonly field
  4. ProvideChatHistoryAsync fetches all messages then trims — use ListRangeAsync(key, -N, -1)
  5. Missing Throw.IfNull(context) inconsistency across methods
  6. Sample default model "gpt-5.4-mini" — not a known Azure OpenAI model name
  7. MaxResults.ToString() uses current culture — use CultureInfo.InvariantCulture
  8. PR claims bug fix not visible in diff — both sample files are new, not modified

✅ Positives

  • Excellent pattern consistency with Mem0Provider — ValkeyProviderScope mirrors Mem0ProviderScope exactly
  • Clean separation — ChatHistoryProvider (lists, no search module) vs ContextProvider (FT.SEARCH)
  • Ownership semantics_ownsConnection correctly tracks disposal responsibility
  • Thorough XML documentation with security considerations (PII, compromised store, indirect prompt injection)
  • Scope validationValidateStateInitializer prevents unbounded queries
  • Idiomatic Valkey usage — FT.CREATE with "Index already exists" catch, RPUSH+LTRIM, TAG filters
  • Two high-quality samples (Azure OpenAI + Bedrock) with clear READMEs
  • ConfigureAwait(false) used consistently — correct for library code
  • Lean dependencies — only Abstractions, StackExchange.Redis, Logging.Abstractions

Checklist Assessment

Area Status Notes
Functionality Providers implement claimed features correctly
Code Quality 🟡 Good structure; thread safety, disposal, DRY issues
Testing 🔴 ~20% API coverage, no async/behavioral tests
Security 🟡 ThreadId leak, incomplete escaping, no TTL
Performance 🟡 One-by-one push, fetch-all-then-trim, per-call allocations
Documentation Excellent XML docs, READMEs; minor NuGet description issue

<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework - Valkey integration</Title>
<Description>Provides Valkey integration for Microsoft Agent Framework, including chat history persistence and context provider with vector search.</Description>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 NuGet description inaccuracy

The <Description> says "context provider with vector search" but ValkeyContextProvider uses full-text search (FT.SEARCH with TEXT fields). There are no VECTOR fields in the schema. Should say "full-text search" instead.

Comment on lines +268 to +275

var db = this._connection.GetDatabase();

try
{
await db.ExecuteAsync(
"FT.CREATE",
this._indexName,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Race condition — _indexCreated not thread-safe

_indexCreated is a plain bool read/written without synchronization. Under concurrent requests, multiple threads can see false and all attempt FT.CREATE. The catch handles "Index already exists", but the flag itself isn't volatile — reads can be stale on other cores.

Fix: Use SemaphoreSlim for full correctness, or at minimum Volatile.Read/Volatile.Write:

private readonly SemaphoreSlim _indexLock = new(1, 1);

private async Task EnsureIndexAsync()
{
    if (Volatile.Read(ref _indexCreated)) return;
    await _indexLock.WaitAsync().ConfigureAwait(false);
    try
    {
        if (_indexCreated) return;
        // ... FT.CREATE ...
        Volatile.Write(ref _indexCreated, true);
    }
    finally { _indexLock.Release(); }
}

<RootNamespace>Microsoft.Agents.AI.Valkey</RootNamespace>
<VersionSuffix>preview</VersionSuffix>
<NoWarn>$(NoWarn);CA1873</NoWarn>
<NoWarn>$(NoWarn);CA1873</NoWarn>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Duplicate <NoWarn> line

<NoWarn>$(NoWarn);CA1873</NoWarn> appears on both line 7 and line 8. Copy-paste error — remove the duplicate.

messages.Add(message);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Inconsistent error handling — no try/catch on deserialization

JsonSerializer.Deserialize<ChatMessage> is called here with no try/catch. If the Valkey store contains malformed JSON (corruption, tampering), this throws JsonException unhandled and crashes the session.

ValkeyContextProvider wraps both ProvideMessagesAsync and StoreAIContextAsync in try/catch (matching the Mem0 pattern). This provider does not — neither ProvideChatHistoryAsync nor StoreChatHistoryAsync have error handling.

Either add try/catch to match, or document the intentional difference in error handling strategy.

Comment on lines +178 to +193
var filterExpr = filterParts.Count > 0 ? string.Join(" ", filterParts) : "*";
var escapedQuery = $"{filterExpr} {EscapeQuery(queryText)}";

var result = await db.ExecuteAsync(
"FT.SEARCH",
this._indexName,
escapedQuery,
"LIMIT", "0", this.MaxResults.ToString()).ConfigureAwait(false);

var memories = ParseSearchResults(result);
var memoryTexts = memories
.Select(m => m.TryGetValue("content", out var c) ? c : null)
.Where(c => !string.IsNullOrEmpty(c))
.ToList();

this._logger?.LogInformation(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 [VULN-001] Cross-Scope Data Leak — ThreadId stored but not filtered (High)

StoreAIContextAsync writes thread_id to every HASH document (line 243), but the search filter here only checks ApplicationId, AgentId, and UserIdThreadId is never included. A developer who sets ThreadId expecting thread-level isolation gets none — all threads for the same user/agent/app share memories.

In multi-thread scenarios (e.g., a support agent handling multiple customer conversations), memories from one thread bleed into another.

Fix:

if (!string.IsNullOrEmpty(scope.ThreadId))
{
    filterParts.Add($"@thread_id:{{{EscapeTag(scope.ThreadId)}}}");
}

Comment on lines +330 to +340
{
return value
.Replace("\\", "\\\\")
.Replace("{", "\\{")
.Replace("}", "\\}")
.Replace("@", "\\@")
.Replace(" ", "\\ ");
}

private static string EscapeQuery(string text)
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 [VULN-002] Incomplete Query/Tag Escaping (Medium)

EscapeQuery is missing <, % (fuzzy match operator), and , from the special characters array. > is escaped but < is not.

EscapeTag (line 330) is also missing | (tag value separator) and ,. A crafted scope value containing | (e.g., UserId = "user1|user2") could match multiple tag values, potentially retrieving another user's data.

No arbitrary Valkey command injection risk (StackExchange.Redis uses parameterized RESP), but query manipulation within FT.SEARCH is possible.

Fix: Add missing characters to both methods. Consider an allowlist approach for tag values.

Jonathan-Improving

This comment was marked as outdated.

MatthiasHowellYopp pushed a commit that referenced this pull request May 21, 2026
…ers and ToPromptAgentAsync converter (microsoft#5940)

* Consolidate Foundry chat client decorators into FoundryChatClient

- Replace AzureAIProjectChatClient and AzureAIProjectResponsesChatClient with a single internal sealed FoundryChatClient that covers three modes (pure responses, server-side agent reference, hosted agent endpoint).
- Rename AzureAIProjectChatClientExtensions to AIProjectClientExtensions to reflect that it extends AIProjectClient.
- All four AsAIAgent extension overloads and both FoundryAgent constructors now construct FoundryChatClient internally so the microsoft.foundry telemetry tag is uniform across paths.
- Introduce AgentFrameworkUserAgentPolicy that stamps agent-framework-dotnet/{version} on outbound requests, mirroring the Python agent-framework-python/{version} contract.
- Delete the Foundry-local MeaiUserAgentPolicy duplicate; rely on MEAI 10.5.1 to stamp MEAI/{version} automatically.
- HostedAgentUserAgentPolicy keeps the combined foundry-hosting/agent-framework-dotnet/{version} segment (Python parity) and upgrades the bare segment in place to avoid duplication.
- Tests reorganized: FoundryChatClientTests, AIProjectClientExtensionsTests, AgentFrameworkUserAgentPolicyTests, MeaiAutoUserAgentVerificationTests, plus in-place upgrade unit tests in HostedOutboundUserAgentTests.

* Promote FoundryChatClient to public; add file/vector-store helpers and ToPromptAgentAsync converter

- Promote FoundryChatClient from internal sealed to public sealed for Python parity, so .NET developers can hold and pass a FoundryChatClient directly the way Python developers do.
- Mode 3 (hosted agent endpoint) now materializes an AIProjectClient from the parsed project root, making GetService<AIProjectClient>() non-null across all three construction modes. This eliminates the per-mode asymmetry that previously hid project-level helpers from agents constructed via an agent endpoint URL.
- Add four new instance methods on FoundryChatClient mirroring Python's spec: UploadFileAsync, DeleteFileAsync, CreateVectorStoreAsync (bundles upload + create + wait), DeleteVectorStoreAsync. Single overload each, path-only inputs to start; additional overloads can be added later without breaking callers. All are Experimental, consistent with the rest of the Foundry package.
- Add ToPromptAgentAsync extension methods on ChatClientAgent and FoundryAgent for the agent-to-prompt-agent converter described in the Foundry spec. Mode 1 (responses API) synthesizes a DeclarativeAgentDefinition from the agent's ChatOptions; mode 2 (server-side agent reference, version, or record) returns the cached or freshly fetched Definition; mode 3 throws InvalidOperationException because no local definition exists to convert.
- Strict AITool to ResponseTool mapping for mode 1: AIFunction becomes CreateFunctionTool with the function's JSON schema; AITool instances that wrap a ResponseTool unwrap via GetService(typeof(ResponseTool)); anything else throws InvalidOperationException naming the offending tool type. Matches the Python spec's unsupported-tools-raise-ValueError contract.
- New unit tests: FoundryChatClientVectorStoreTests (22 tests covering all four helpers across the three FoundryChatClient construction modes plus validation and cancellation), FoundryPromptAgentConverterTests (16 tests covering both extension entry points across mode 1 synthesis, mode 2 cached and fetched paths, all failure modes, and a Python-parity guard asserting both extensions produce equivalent definitions for equivalent inputs), plus four new tests in FoundryChatClientTests for the mode 3 AIProjectClient materialization.

* Stop building duplicate ProjectOpenAIClient in FoundryAgent agent-endpoint ctor

After Plan #2's mode-3 AIProjectClient materialization, the inner FoundryChatClient already exposes a project-level AIProjectClient (via GetService) that internally provides the project-level ProjectOpenAIClient via GetProjectOpenAIClient(). FoundryAgent's agent-endpoint constructor was still independently constructing a second project-level ProjectOpenAIClient via the now-redundant CreateProjectLevelOpenAIClientFromAgentEndpoint helper — two handles to the same logical resource.

Refactor: the agent-endpoint constructor now reads the inner FoundryChatClient's materialized AIProjectClient via base.GetService(typeof(AIProjectClient)) and derives the project-level ProjectOpenAIClient from it. The dead helper on both FoundryAgent (private static wrapper) and FoundryChatClient (the actual implementation) is removed. The user-supplied per-agent ClientPipelineOptions primitives (Transport, RetryPolicy, NetworkTimeout, UserAgentApplicationId) are propagated into the materialized AIProjectClientOptions so test-injected transports and explicit retry / timeout / user-agent settings reach the project-level pipeline — preserving the behavior the dead helper used to provide.

Updated AgentEndpointConstructor_GetServiceAIProjectClient_ReturnsNull to its now-correct counterpart AgentEndpointConstructor_GetServiceAIProjectClient_ReturnsNonNull, since after Plan #2 the agent-endpoint ctor surfaces a non-null AIProjectClient (per user direction in Plan #2 Q2).

* Strip duplicated AIProjectClient/ProjectOpenAIClient state from FoundryAgent

Both _aiProjectClient and _projectOpenAIClient fields on FoundryAgent were redundant:

- _aiProjectClient: FoundryAgent's GetService<AIProjectClient> override returned this field, but DelegatingAIAgent.GetService → ChatClientAgent.GetService → FoundryChatClient.GetService<AIProjectClient> already returns the same instance through the delegating chain. Field + override are pure duplication.

- _projectOpenAIClient: only used by FoundryAgent's own GetService<ProjectOpenAIClient> override and by CreateConversationSessionAsync. Per user direction, ProjectOpenAIClient is no longer exposed via GetService on either FoundryChatClient or FoundryAgent — callers retrieve it from the AIProjectClient themselves (aiProjectClient.GetProjectOpenAIClient()) the same way the framework does internally. This eliminates the mode-3 asymmetry where the chat client's stored ProjectOpenAIClient was per-agent (URL /agents/{name}/endpoint/protocols/openai) while the agent's was project-level.

Refactor:
- Delete both fields on FoundryAgent and the GetService override.
- Delete the ProjectOpenAIClient branch from FoundryChatClient.GetService.
- CreateConversationSessionAsync now resolves AIProjectClient at call time via this.GetService<AIProjectClient>() and derives the conversations client from it.
- Update FoundryChatClient tests that asserted on GetService<ProjectOpenAIClient> to assert Null (deliberate removal).
- Update FoundryAgent tests AgentEndpointConstructor_GetServiceProjectOpenAIClient_ReturnsNonNull and ProjectEndpointConstructor_GetServiceProjectOpenAIClient_ReturnsNonNull to ...ReturnsNull, and rewrite AgentEndpointConstructor_PropagatesUserAgentApplicationId_ToProjectLevelClient to look up AIProjectClient instead.

No production code (only tests) referenced GetService<ProjectOpenAIClient>, so this is a safe surface reduction. Net: 30 insertions, 61 deletions; FoundryAgent shrinks to a pure delegator with only the two convenience methods (CreateSessionAsync, CreateConversationSessionAsync) on top of the delegating chain.

* Rename FoundryChatClient.HostedAgentName to AgentName and populate it for mode 2

The previous name implied a mode 3 only property tied to the hosted-agent endpoint URL. Today only hosted endpoints surface this name, but conceptually an agent name exists for every server-side agent the client talks to. Renaming to AgentName makes the property general-purpose and ready for future modes where the same chat client may target other server-side agent shapes that are not necessarily 'hosted'.

Mode 2 (server-side agent reference) now mirrors AgentReference.Name into AgentName so callers have a uniform handle regardless of construction mode:

* Mode 1 (pure responses): AgentName is null. There is no agent.
* Mode 2 (AgentReference): AgentName == AgentReference.Name.
* Mode 3 (agent endpoint URL): AgentName is parsed from the URL segment as before.

Converter discriminator update: FoundryPromptAgentConverter previously used 'HostedAgentName is not null' to detect mode 3 and reject it. Now that mode 2 also populates AgentName, the mode 3 guard moves to the end of the resolution chain and uses the unambiguous 'AgentName is set AND no AgentReference exists' test. The user-visible error message and behavior are preserved.

Dead-state cleanup spotted during format verify:

* IDE0052 surfaced that FoundryChatClient._projectOpenAIClient is never read since the prior refactor stopped exposing ProjectOpenAIClient via GetService and rewired CreateConversationSessionAsync to resolve the AIProjectClient through the delegating chain. The field is deleted and its three ctor assignments removed.
* HostedAgentEndpointInner.PerAgentClient only existed to plumb the per-agent ProjectOpenAIClient into that now-deleted field, so the property and its ctor parameter are removed. The local 'perAgentClient' variable inside BuildHostedAgentEndpointInner is still needed to derive the inner IChatClient, but no longer escapes the helper.

Tests:

* Mode1_PureResponses_ReturnsNullForAgentSpecificServices now also asserts AgentName is null.
* New Mode2_AgentReference_PopulatesAgentNameFromAgentReference asserts the mode 2 mirror.
* Mode3_HostedAgentEndpoint_ParsesAgentNameFromUrl renamed assertion target HostedAgentName to AgentName.

Verification: 335/335 net10.0, 273/273 net472 Foundry unit; 229/229 Foundry.Hosting unit; format-verify (WSL2 + Docker mcr.microsoft.com/dotnet/sdk:10.0) clean on Microsoft.Agents.AI.Foundry.

* Adopt canonical mode names: Responses Agent, Prompt Agent, Agent Endpoint

Three FoundryChatClient construction modes now have one canonical noun used everywhere.

* Responses Agent (Mode 1): inline ChatClientAgent, project-level Responses API, no server-side def.
* Prompt Agent (Mode 2): server-side ProjectsAgentDefinition invoked by AgentReference.
* Agent Endpoint (Mode 3): per-agent URL /agents/{name}/endpoint/protocols/openai. Hosted-or-not.

'Hosted' stays the kind of agent (Microsoft.Agents.AI.Foundry.Hosting). Not synonym of Mode 3.

Rings:
1. XML docs + error messages use canonical names. en-GB to en-US: centralises, synthesise.
2. HostedAgentEndpointInner -> AgentEndpointInner, BuildHostedAgentEndpointInner -> BuildAgentEndpointInner.
3. Tests: Mode1_PureResponses_* -> Mode1_ResponsesAgent_*, Mode2_AgentReference_* -> Mode2_PromptAgent_*, Mode3_HostedAgentEndpoint_* -> Mode3_AgentEndpoint_*.

Pure rename. No behavior change. 335/335 net10 + 273/273 net472 unit, format clean.

* Address PR microsoft#5940 design feedback (Q-A through Q-F)

Q-A: poll vector store til status leaves InProgress before return. Exp backoff 250ms-2s. Honor cancel.
Q-B: try/catch upload loop. Mid-fail = best-effort DeleteFileAsync on already-uploaded ids. Swallow cleanup errors.
Q-C: pinned AgentReference.Version uses GetAgentVersionAsync. Empty/whitespace/'latest' = GetLatest path.
Q-D: HostedAgentUserAgentPolicy detects existing combined 'foundry-hosting/...' segment. No double prefix.
Q-E: mode-3 vector-store test uses fake transport. No DNS to example.com.
Q-F: no shim. Class always [Experimental] (since 8015e00, before dotnet-1.0.0). No compat contract. Callers rename to AIProjectClientExtensions.

Rebase onto origin/main reconciliation: aad20c2 added public AsAIAgent(this AIProjectClient, Uri agentEndpoint, ...) extension that calls an internal FoundryAgent(AIProjectClient, Uri, ...) ctor. Reintroduced that ctor + a new FoundryChatClient(AIProjectClient, Uri, ProjectOpenAIClientOptions?) overload that reuses the supplied AIProjectClient's pipeline (via GetProjectResponsesClientForAgentEndpoint) instead of stamping a fresh credential.

Verified: 346/346 net10 + 284/284 net472 Foundry unit, 230/230 Foundry.Hosting unit, format clean.

* Add FoundryAgent helper extensions: UploadFile/DeleteFile/CreateVectorStore/DeleteVectorStore

4 thin forwarders on FoundryAgent that route to the inner FoundryChatClient's helpers via agent.GetService<FoundryChatClient>().X(). Live in existing FoundryAgentExtensions.cs alongside ToPromptAgentAsync.

Throws InvalidOperationException when agent does not expose a FoundryChatClient via GetService (same pattern as ToPromptAgentAsync).

Unit tests: FoundryAgentExtensionsTests covers all 4 forwarders + null-agent ArgumentNullException for each. 8 new tests, 354/354 net10 + 292/292 net472.

Integration tests: parallel FoundryAgentExtensionsTests under Foundry.IntegrationTests mirrors the existing CreateAgent_CreatesAgentWithVectorStoresAsync shape (upload -> create vector store -> FileSearch tool answers question -> cleanup), but routes every helper call through the new FoundryAgent extensions. 4 new IT tests, all verified pass live against the real Foundry project (12-30s each). Skipped by default like the existing vector-store IT.

* Address Sergey's PR review comments

#1 (FoundryAgent.cs:139): drop unused aiProjectClient param from internal FoundryAgent(AIProjectClient, ChatClientAgent) ctor. Was discarded after null-check. Inner FoundryChatClient already surfaces AIProjectClient via GetService. 3 call sites in AIProjectClientExtensions updated.

#2 (FoundryChatClient.cs:376): add pollingTimeout param to CreateVectorStoreAsync. Defaults to 5 min, configurable, Timeout.InfiniteTimeSpan disables. Throws TimeoutException with vector store id and elapsed seconds when bound exceeded. CancellationToken still wins. New unit test PollingTimeout_ThrowsTimeoutExceptionAsync. FoundryAgentExtensions forwarder updated to plumb the new param.

Verified: 355/355 net10 + 293/293 net472 Foundry unit, 230/230 Foundry.Hosting unit, format clean.
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.

2 participants