Skip to content

MAF middleware authorization + IawMemoryProvider refactor#44

Merged
LeftTwixWand merged 2 commits into
masterfrom
approval-and-memory-agents-refactoring
Apr 20, 2026
Merged

MAF middleware authorization + IawMemoryProvider refactor#44
LeftTwixWand merged 2 commits into
masterfrom
approval-and-memory-agents-refactoring

Conversation

@LeftTwixWand
Copy link
Copy Markdown
Contributor

Summary

  • Replace discovery-time GatedAIFunction tool 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.
  • Delete the five over-partitioned memory agents + MemoryAgentBase + PreferenceAgent + KnowledgeAgent, introduce IawMemoryProvider : MessageAIContextProvider, IMemoryLookup backed by QdrantClient directly (no new NuGet packages). Agents get automatic per-user memory recall via ChatClientAgentOptions.AIContextProviders.
  • Add a Thread Explain tool + ForwardMessageHint UI part so the Telegram layer can forward the original user message back via the new TelegramMessageSender.ForwardMessageAsync.
  • Migrate every remaining context provider (UserContextProvider, RAGContextProvider, AgentRoutingContextProvider, PolicyContextProvider) to MAF's AIContextProvider pipeline. Delete IAgentContextProvider and the GetContextProviders / BuildContextBlock plumbing.
  • Agent.OnActivateAsync stamps session.StateBag["iaw.userId"] / iaw.threadId right after CreateSessionAsync; all providers read identity from the shared ContextProviderIdentity helper instead of parsing grain keys.
  • Precursor: ApproverAgent authorization loop + ProposeOptions interactive UX #36.

Locked decisions honored

  1. Middleware + context.Terminate = true on deny; no marker tools, no hardcoded exempt lists.
  2. Memo keyed by SHA256(toolName + "|" + argsJson)[..16]; memo hits only on Allow + Thread/User.
  3. Any Approver exception = Deny + ApproverFailures counter; never silently allow.
  4. DelayDeactivation(5min) before await tcs.Task.
  5. ResolveApproval rejects pending entries whose stored UserId doesn't match the grain key.
  6. QdrantClient direct, same pattern as RAGContextProvider / Agent.IngestChunksAsync.
  7. Per-user collection user-memory-{userId} with the exact payload schema specified.
  8. Explain tool pushes a ForwardMessageHint so Telegram calls forwardMessage.
  9. Generation preferences → memory, authorization rules → Approver policies. Two stores, not one.
  10. KnowledgeAgent deleted.
  11. IAgentContextProvider deleted. Single pipeline everywhere.
  12. Existing memory data dropped; only the live project-* RAG collections are preserved.
  13. Identity lives on AgentSession.StateBag.
  14. Source Telegram message id flows via ChatMessage.AdditionalProperties["iaw.sourceTelegramMsgId"].

Telemetry added

  • agents.approver.failures / .denies / .memo_hits / .llm_judgments
  • ToolAuthorizationRequested / ToolAuthorized event constants removed (only ToolDenied is 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 own bin during builds).
  • dotnet test test/Core.Tests — 411 passed, 1 skipped (pre-existing CodeOrchestratorTests.ExecuteCodeOrchestration_CreatesWorkspaceFiles).
  • dotnet test test/Integration.Tests — 4 passed.
  • New ApproverAgentTests — 8 passed covering AddPolicy, thread-scope, ask-flow, memo-hit round trip, cross-user ResolveApproval rejection, empty listing, no-op resolve, empty remove.
  • Aspire verified: assistant, mcp, devui, telegram rebuilt via the Aspire MCP and all report Healthy against Qdrant + Azurite.
  • Live iaw-MCP behavioral scenarios: Memory store/recall, explainability with forward, memo hit, fail-closed deny, ask-flow resolve. The iaw MCP client wasn't in this session's tool snapshot (it attached to an earlier PID), so these were not driven from this session. Run after checkout:
    • 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?" → verify Explain tool response cites the date and Telegram forwards the original message.
    • assistant_chat "run dotnet build" twice → verify second call increments agents.approver.memo_hits.
    • Simulate Approver LLM failure → verify ToolDenied + agents.approver.failures bump (not a silent allow).
    • Approver ask flow: risky shell command → ApprovalRequested event → user taps a button → tool proceeds.
  • Playwright DevUI smoke — not driven from this session.

🤖 Generated with Claude Code

LeftTwixWand and others added 2 commits April 9, 2026 18:33
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>
@LeftTwixWand LeftTwixWand merged commit a04ea9e into master Apr 20, 2026
1 check passed
@LeftTwixWand LeftTwixWand deleted the approval-and-memory-agents-refactoring branch April 20, 2026 16:47
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.

1 participant