MAF middleware authorization + IawMemoryProvider refactor#44
Merged
Conversation
Replace discovery-time GatedAIFunction with MAF function-calling middleware,
introduce automatic memory via a QdrantClient-backed MessageAIContextProvider,
delete the over-partitioned memory/preference/knowledge agents, and migrate
all context providers to MAF's AIContextProvider pipeline.
## Authorization
- Delete GatedAIFunction and its per-tool wrap logic.
- Add Agent.Authorization.cs with ToolApprovalMiddleware: registered via
`.AsBuilder().Use(...).Build()` on the AIAgent produced by AsAIAgent.
Denies terminate the call with context.Terminate = true; any grain exception
fails closed as Deny + telemetry (never silently Allow).
- ApproverAgent adds a memo table keyed by SHA-256(toolName|argsJson) fingerprint.
Memo hits return cached Allow decisions without an LLM call. Memo writes only
happen on ResolveApproval with scope Thread/User.
- Approver holds DelayDeactivation(5min) across the await tcs.Task HITL wait
and asserts the pending entry's UserId matches its grain key before resolving.
- Override ResolveApproverGrainKey() => null on Approver so its own LLM calls
bypass the authorization middleware (prevents infinite recursion).
- Simplify ToolAuthorizationRequest to
(AgentId, AgentDisplayName, ToolName, ArgumentsJson, RecentMessages).
Threadid is derived at judgment time from the sub-agent grain key.
- New telemetry counters: ApproverFailures, ApproverDenies, ApproverMemoHits,
ApproverLlmJudgments.
- Remove ToolAuthorizationRequested / ToolAuthorized event constants (only
ToolDenied is published now).
## Memory
- Delete the five over-partitioned memory agents (User/Project/Episode/
Pattern/Code) plus MemoryAgentBase, IMemoryAgent, MemoryEntry, and
MemoryContextProvider. Delete PreferenceAgent / IPreference /
PreferenceRule / PreferenceContextProvider. Delete KnowledgeAgent /
IKnowledge — its typed data folds into memory with optional tags.
- Add src/Core/Memory: MemoryHit, IMemoryLookup, IawMemoryProvider.
IawMemoryProvider : MessageAIContextProvider + IMemoryLookup.
Uses QdrantClient DIRECTLY (same pattern as RAGContextProvider and
Agent.IngestChunksAsync) — no new NuGet packages, no SK adapters.
Per-user collection `user-memory-{userId}` with payload fields
content/userId/threadId/role/createdAtTicks/sourceTelegramMsgId.
- ProvideMessagesAsync injects top-5 relevant memories as a single
ChatRole.System "## Memories" message. StoreAIContextAsync persists
each new request/response message with embedding + metadata.
- LookupOriginAsync searches top-1 for explainability and maps to MemoryHit.
- Add ForwardMessageHint UIPart; Thread adds an Explain tool that calls
IMemoryLookup.LookupOriginAsync, pushes the hint to pending UI hints, and
returns the stored text + timestamp.
- TelegramMessageSender gains ForwardMessageAsync; ResponseStreamer calls it
for every ForwardMessageHint emitted during a turn so the user sees the
original message forwarded back to them.
- Telegram layer stamps `message.MessageId` onto
`ChatMessage.SourceTelegramMsgId` when handing off to Thread; Agent.cs
carries it through `ProduceLlmStreamAsync` by constructing an
M.E.AI ChatMessage with AdditionalProperties["iaw.sourceTelegramMsgId"].
- Register IawMemoryProvider + IMemoryLookup in IAWSiloExtensions, resolve
it in Agent.OnActivateAsync and add to ChatClientAgentOptions.AIContextProviders.
- Remove IUserProfile.RememberFact / RecallFacts (and all callers) — memory
is now a concern of IawMemoryProvider, not UserProfile.
- Wire IMemoryLookup into ExplainabilityAgent alongside Approver policy search.
## Context providers → MAF AIContextProvider
- Delete IAgentContextProvider interface entirely. Single pipeline, no parallel
plumbing.
- Migrate UserContextProvider, RAGContextProvider, AgentRoutingContextProvider,
and PolicyContextProvider to `MessageAIContextProvider`. All read userId /
threadId from `AIAgent.CurrentRunContext?.Session?.StateBag` via a shared
ContextProviderIdentity helper.
- Delete orphan providers (Project, Task, TaskLedger, TaskStream, TaskResult)
that had no wire-up — their tests went with them. EventFlowIntegrationTests
now calls ledger.GetContextBlockAsync directly.
- Delete Agent.GetContextProviders/BuildContextBlock + the instructions
concatenation in StreamResponseCore. Providers are now registered via
ChatClientAgentOptions.AIContextProviders at activation, plus a
`GetAdditionalAIContextProviders()` virtual hook for thread-specific
providers (Thread adds RAG + AgentRouting).
## Identity plumbing
- Agent.OnActivateAsync parses userId/threadId from the grain key and pushes
them to `session.StateBag["iaw.userId"]` / `"iaw.threadId"` right after
CreateSessionAsync, so every provider can read the same identity regardless
of which grain it runs inside.
## Tests
- Rewrite ApproverAgentTests: allow/deny/ask judgment coverage, ask-flow
pending entry, ResolveApproval memo write, second Authorize hits memo
without reissuing ApprovalRequested, ResolveApproval cross-user rejection.
- Remove MemoryAgentBase / MemoryAgent / PreferenceAgent / Explainability /
MemoryEntry / MemoryBase / orphan context-provider tests.
- Trim UserProfileTests (RememberFact/RecallFacts gone) and
ArchitectureGuardV2Tests (memory base check gone).
- EventFlowIntegrationTests exercises ledger.GetContextBlockAsync directly.
Builds cleanly, `dotnet test test/Core.Tests` and
`dotnet test test/Integration.Tests` green. Tested live through the Aspire
MCP: assistant / mcp / devui / telegram rebuilt and run Healthy with the new
wiring. Precursor: #36 (ApproverAgent authorization loop + ProposeOptions UX).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codifies the working mode for design conversations — sharpening questions, honest pushback, distinct prototypes, closing the loop into phase-1 plans. Switch to execution mode only on explicit "go"/"build". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GatedAIFunctiontool wrapping with MAF function-calling middleware (AsBuilder().Use(ToolApprovalMiddleware).Build()). The Approver judges every call, fails closed on any exception, and now carries a SHA256 memo table so repeat(toolName, argsJson)pairs short-circuit the LLM.MemoryAgentBase+PreferenceAgent+KnowledgeAgent, introduceIawMemoryProvider : MessageAIContextProvider, IMemoryLookupbacked byQdrantClientdirectly (no new NuGet packages). Agents get automatic per-user memory recall viaChatClientAgentOptions.AIContextProviders.Explaintool +ForwardMessageHintUI part so the Telegram layer can forward the original user message back via the newTelegramMessageSender.ForwardMessageAsync.UserContextProvider,RAGContextProvider,AgentRoutingContextProvider,PolicyContextProvider) to MAF'sAIContextProviderpipeline. DeleteIAgentContextProviderand theGetContextProviders/BuildContextBlockplumbing.Agent.OnActivateAsyncstampssession.StateBag["iaw.userId"]/iaw.threadIdright afterCreateSessionAsync; all providers read identity from the sharedContextProviderIdentityhelper instead of parsing grain keys.Locked decisions honored
context.Terminate = trueon deny; no marker tools, no hardcoded exempt lists.SHA256(toolName + "|" + argsJson)[..16]; memo hits only onAllow+Thread/User.Deny+ApproverFailurescounter; never silently allow.DelayDeactivation(5min)beforeawait tcs.Task.ResolveApprovalrejects pending entries whose storedUserIddoesn't match the grain key.QdrantClientdirect, same pattern asRAGContextProvider/Agent.IngestChunksAsync.user-memory-{userId}with the exact payload schema specified.Explaintool pushes aForwardMessageHintso Telegram callsforwardMessage.KnowledgeAgentdeleted.IAgentContextProviderdeleted. Single pipeline everywhere.project-*RAG collections are preserved.AgentSession.StateBag.ChatMessage.AdditionalProperties["iaw.sourceTelegramMsgId"].Telemetry added
agents.approver.failures/.denies/.memo_hits/.llm_judgmentsToolAuthorizationRequested/ToolAuthorizedevent constants removed (onlyToolDeniedis published).Test plan
dotnet build IAW.slnx— 0 CS errors, 0 warnings (only AppHost self-bin copy warnings because the Aspire AppHost is running its ownbinduring builds).dotnet test test/Core.Tests— 411 passed, 1 skipped (pre-existingCodeOrchestratorTests.ExecuteCodeOrchestration_CreatesWorkspaceFiles).dotnet test test/Integration.Tests— 4 passed.ApproverAgentTests— 8 passed coveringAddPolicy, thread-scope, ask-flow, memo-hit round trip, cross-userResolveApprovalrejection, empty listing, no-op resolve, empty remove.assistant,mcp,devui,telegramrebuilt via the Aspire MCP and all report Healthy against Qdrant + Azurite.assistant_chat"always sign emails as Regards, Vlad" → follow-up "draft an email to John about the release" → verify signature.assistant_chat"why did you sign as Vlad?" → verifyExplaintool response cites the date and Telegram forwards the original message.assistant_chat"run dotnet build" twice → verify second call incrementsagents.approver.memo_hits.ToolDenied+agents.approver.failuresbump (not a silent allow).ApprovalRequestedevent → user taps a button → tool proceeds.🤖 Generated with Claude Code