.NET SDK
AgentMemory.Client for .NET 10 — an HTTP implementation of IAgentMemory with dependency-injection helpers and an agent context for the common calls.
AgentMemory.Client is the .NET client. It implements the platform's IAgentMemory interface over the /v1 HTTP API, so code written against the interface runs the same whether memory is in-process or remote. It targets .NET 10 and depends on AgentMemory.Contracts (the shared models) and Microsoft.Extensions.Http.
Install
The client is not published to NuGet. Add it to your solution as a project reference from the Agent Memory source repository:
dotnet add reference /path/to/agent-memory/AgentMemory.Client/AgentMemory.Client.csprojAgentMemory.Contracts comes along as a transitive project reference.
Configure
using AgentMemory.Client;
builder.Services.AddAgentMemoryClient(
"https://memory.example.com",
_ => Task.FromResult<string?>(builder.Configuration["AgentMemory:ApiKey"]));AddAgentMemoryClient(baseUrl, tokenProvider) registers IAgentMemory as a typed HttpClient (30-second timeout). The token provider is called per request, so it can return a rotating OAuth token as easily as a static API key.
The bearer token and any Idempotency-Key are set per request, never on the shared HttpClient.
The agent context
Most IAgentMemory methods take a TenantExecutionContext (who is calling) and a MemoryAddress (where the memory lives). AgentContext builds both for you from an API key's workspace and fixes the run ID, so everyday calls are one line:
using AgentMemory.Client;
using AgentMemory.Contracts.Abstractions;
var agent = AgentContext.ForKey(
client,
tenantId: tenantId, // shown when the key/tenant was created
workspaceId: workspaceId,
userId: "alex", // optional: the end user, requires "act for users"
runId: "onboarding-42"); // optional: a new run-… id is minted when omittedForKey names no agent unless you pass agentId: — the key's bound agent acts. Tenant and workspace IDs on the client are only used to address requests; the server derives the tenant from the key and checks the workspace against it. An applicationId: argument scopes the context to an application.
With dependency injection, register a scoped context:
builder.Services.AddAgentContext(
sp => new AgentPrincipal(tenantId, workspaceId, AgentId: "", AgentInstanceId: null, UserId: null,
Roles: [Roles.Agent], Scopes: []));AgentPrincipal and Roles are in AgentMemory.Contracts.Security. An empty AgentId means the key's bound agent acts. Each scope gets a fresh run ID unless you pass the optional runId factory.
Remember, recall, forget
Extension methods on IAgentContext (namespace AgentMemory.Contracts.Abstractions) cover the common lifecycle:
// remember
AppendOutcome stored = await agent.AppendAsync(
"Alex is vegetarian", type: "preference", visibility: MemoryVisibility.User,
tags: new Dictionary<string, string> { ["facet"] = "diet" },
idempotencyKey: "onboarding-42:diet");
// recall
MemoryContext context = await agent.SearchAsync("what can Alex eat?");
Console.WriteLine(context.ToPromptBlock()); // render the context as a prompt section
// read
MemoryEntry? entry = await agent.GetAsync(stored.Id);
// share (needs the share capability)
ShareOutcome shared = await agent.ShareAsync(stored.Id, MemoryVisibility.Workspace);
// end the run
await agent.ConsolidateAsync(goal: "Onboard Alex");
// forget (never cascades unless you pass cascade: true)
DeleteOutcome deleted = await agent.DeleteAsync(stored.Id);| Extension | Calls |
|---|---|
AppendAsync(content, type = "decision", visibility = Workspace, tags, idempotencyKey) | POST /v1/memory/append (layer Workspace) |
SearchAsync(query, options?) | POST /v1/memory/retrieve |
GetAsync(memoryId) | GET /v1/memory/{id} |
ShareAsync(memoryId, visibility) | POST /v1/memory/{id}/share |
DeleteAsync(memoryId, cascade = false) | DELETE /v1/memory/{id} |
ConsolidateAsync(goal?) | POST /v1/memory/consolidate |
Address(), Execution() | Build the MemoryAddress / TenantExecutionContext for direct client calls. |
SearchAsync accepts a RetrieveOptions with every retrieval option: TopN, TokenBudget, Mode, Rerank, MinScore, Raw, IncludeSuperseded, AsOf, AsOfAxis, Temporal, Tags, ExpandSessions, Explain, Overrides.
DeleteOutcome is Deleted, NotFound, ProtectedTenantScope, PolicyDenied or BlockedByDependents. To see whether cleanup is still pending, or which dependents blocked the delete, call the client overload that returns a DeleteResult (below).
Calling the client directly
For everything else, call AgentMemoryHttpClient (or IAgentMemory) with agent.Execution() and agent.Address():
// outcome with the recall's receipt and the same run
await client.ReportOutcomeAsync(agent.Execution(), agent.Address(), new OutcomeReport
{
RunId = agent.RunId,
ReceiptId = context.ReceiptId,
BuildSuccess = true,
TestPassRate = 1.0,
});
// what became of the write
OperationReceipt? receipt = await client.GetOperationAsync(agent.Address(), stored.OperationId);
// delete with the full result
DeleteResult result = await client.DeleteMemoryAsync(
agent.Execution(), agent.Address(), stored.Id, new DeleteOptions(Cascade: false));
// result.Outcome, result.BlockingDependents, result.Cleanup ("complete" | "pending")
ErasureStatus? erasure = await client.GetErasureStatusAsync(agent.Execution(), agent.Address(), stored.Id);
// conditional update
UpdateOutcome updated = await client.UpdateContentAsync(
agent.Execution(), agent.Address(), stored.Id, "Alex is vegan", expectedVersion: entry?.Version);| Method | Endpoint |
|---|---|
RetrieveAsync(ctx, address, agentId, runId, options?) | POST /v1/memory/retrieve |
AppendAsync(ctx, address, entry, idempotencyKey?) | POST /v1/memory/append |
GetMemoryAsync(ctx, address, memoryId[, agentId, userId]) | GET /v1/memory/{id} |
UpdateContentAsync(ctx, address, memoryId, content, expectedVersion[, agentId, userId]) | PUT /v1/memory/{id} |
DeleteMemoryAsync(ctx, address, memoryId, options[, agentId, userId]) | DELETE /v1/memory/{id} |
GetErasureStatusAsync(ctx, address, memoryId[, agentId, userId]) | GET /v1/memory/{id}/erasure |
GetOperationAsync(address, operationId) | GET /v1/memory/operations/{id} |
ExplainMemoryAsync(address, memoryId[, ct, agentId, userId]) | GET /v1/memory/{id}/explain |
ShareAsync(ctx, address, memoryId, visibility[, agentId, userId]) | POST /v1/memory/{id}/share |
ConsolidateAsync(ctx, address, runId, goal?, extractionInstructions?) | POST /v1/memory/consolidate |
GetBlocksAsync(ctx, address, agentId) | GET /v1/blocks/{workspaceId} |
UpdateBlockAsync(ctx, address, label, value, agentScope = "*") | PUT /v1/blocks/{workspaceId} |
CurateBlockAsync(ctx, address, label, action, match?, value?, agentScope = "*") | POST /v1/blocks/{workspaceId}/curate |
ReportOutcomeAsync(ctx, address, report) | POST /v1/outcomes |
RecordRetrievalFeedbackAsync(ctx, address, feedback) | POST /v1/retrieval-feedback |
Errors
Results that are part of a method's contract are returned as values: null for not found, DeleteOutcome / ShareOutcome / UpdateOutcome for refusals and conflicts. Any other non-success status throws HttpRequestException (via EnsureSuccessStatusCode).