microsoft / microsoft/agent-framework

.NET: Proposal: Integrating Foundgine as a Semantic Execution Layer for AI Agents

Open
#7,834 5 comments 0 reactions 1 assignee View on GitHub

@sphenry is already working on this.

Since Aug 27, 2026.

.NET
Dominant language
Python
Stars
13.6k
Forks
2.3k
Avg merge
2d 45m
Merged PRs (30d)
358

Description

# Foundgine + Microsoft AI: a semantic execution boundary for AI agents

## What is Foundgine?

Foundgine separates what a caller wants from how the application executes it.

A caller submits structured intent. Foundgine resolves that intent against an application-defined semantic model, validates the requested capabilities, applies authorization constraints, builds an execution plan, and sends the plan to a provider such as SQL or InMemory.

The result is a reusable execution boundary that can sit underneath multiple interfaces:

```
Intent Sources

API GraphQL Automation AI Agent
\ | | /
\ | | /
└────────┴──────────────┴───────────┘


┌───────────────┐
│ Foundgine │
│ │
│ Semantic │
│ Authorization │
│ Planning │
│ Execution │
└───────┬───────┘

┌───────────┼───────────┐
▼ ▼ ▼
SQL InMemory Providers
```

Architecture reference: [cristianbarragan.github.io/Foundgine/docs-site/architecture](https://cristianbarragan.github.io/Foundgine/docs-site/architecture/index.html)

## Why is this interesting for AI agents?

Modern applications increasingly have many callers: web and mobile apps, APIs, GraphQL clients, internal services, automation, and now AI agents.

An agent can determine *what* it wants to accomplish, but the application should remain authoritative over:

- what capabilities exist
- what the caller is allowed to do
- which data can be accessed
- which relationships can be traversed
- how mutations are validated
- how the operation is translated into execution
- what actually gets committed

Without an explicit execution boundary, it's easy for an agent integration to collapse into a scattered pattern, where every tool reimplements its own authorization, validation, and data access:

```
Before: AI Agent → Tool → Application code → ORM / SQL / API

After: AI Agent → Structured Intent → Semantic Model → Capability Validation
→ Authorization → Execution Plan → Provider → Result / Evidence
```

**The question this raises:** could Foundgine provide a semantic execution layer underneath an AI agent framework?

Foundgine wouldn't replace the agent framework — the framework stays responsible for reasoning, orchestration and interaction. Foundgine sits underneath it as an application-defined, strongly typed execution boundary.

This also opens up **generated agent capabilities**: instead of exposing arbitrary application methods or database operations to an agent, the application exposes capabilities derived from its semantic model (Application Semantic Model → Capability Graph → Agent-facing tools → Structured Intent → Foundgine).

The agent reasons about application capabilities; the application stays in control of authorization and execution. This matters most for mutations — e.g. `TransferFunds`, which needs tenant isolation, account ownership, account state, amount validation, authorization revalidation, idempotency, atomic execution, and audit/evidence, all enforced by the application rather than by the agent's own judgment.

Benchmark: [Run5SameClient](https://github.com/CristianBarragan/Foundgine/tree/main/benchmarks/AgentEndToEnd/Run5SameClient) · [Run5b results](https://cristianbarragan.github.io/Foundgine/docs-site/agent-benchmark/run-5b/index.html)

**The agent can request the operation. It should not get to redefine the security rules for the operation.** That separation is the architectural idea behind Foundgine.

## Questions for the Microsoft AI / Agent Framework / Semantic Kernel community

- Where should this boundary sit relative to Agent Framework?
- Should agent tools be generated from application capabilities?
- How should authorization be represented between an agent and the application runtime?
- Is a semantic capability graph useful to an agent?
- Could this complement existing Microsoft AI patterns rather than introducing another agent abstraction?

Open source: [github.com/CristianBarragan/Foundgine](https://github.com/CristianBarragan/Foundgine)

## How the boundary works in practice

Since posting the original proposal above, Foundgine has evolved into a fuller semantic-policy stack. It introduces a single, application-controlled semantic execution boundary between caller intent, domain meaning, authorization rules, and the operations actually performed on data, APIs, GraphQL, MCP tools, or backend systems — so agents can *propose* actions, but only the application decides what's allowed, safe, and executed.

**The problem:** as applications expose more functionality to callers, each tool/endpoint tends to grow its own validation, authorization, query logic, and business rules. Without a shared semantic layer, each surface (application code, GraphQL, JSON, AI-generated intent) invents its own rules for what entities/fields exist, which relationships can be traversed, which filters are valid, what's authorized, and how requests become database operations — producing duplicated semantics and inconsistent security boundaries. Foundgine centralizes that into one boundary: retrieval can discover candidates and evidence, but retrieval is not authorization. The application owns identity and policy; providers execute the already-authorized artifact.

### Example: alias resolution

Two callers can ask for the same thing in different words:

- Canonical: *"show me overdue purchase orders from our top supplier in Texas"*
- Paraphrase: *"show me the overdue buys from our top seller in Texas"*

In the Supply Chain semantic contract, `Buy`/`Buys` are declared aliases of `PurchaseOrder`, and `Seller` is a declared alias of `Supplier`. Both sentences ground onto the same canonical semantic identities before authorization or planning ever runs.

| # | Layer | What happens |
|---|-------|--------------|
| 1 | Caller intent | The caller sends either sentence — no SQL or provider instructions. |
| 2 | Intent representation | The request becomes structured intent. |
| 3 | Semantic Model | Exposes canonical meanings and aliases: `PurchaseOrder ← Buy, Buys`, `Supplier ← Vendor, Seller`. |
| 4 | Semantic Operation Graph | Becomes application meaning: overdue purchase-order semantics, ranked "top supplier," Texas constraint. |
| 5 | Retrieval | Relational, fuzzy/full-text, BM25/search, or graph strategies propose candidates and evidence — never grant authority. |
| 6 | Semantic Resolution | `buys → PurchaseOrder`, `seller → Supplier`; aliases normalize to the same canonical identities. |
| 7 | Authorization | Application policy runs against the resolved semantic graph and caller identity. Retrieval can't bypass this. |
| 8 | Plan Binding | The authorized decision binds to a provider-independent execution plan. |
| 9 | ExecutionIR | Carries the resolved plan and its authorization provenance across the boundary. |
| 10 | Provider | Only now does a physical provider (e.g. PostgreSQL) receive the already-authorized artifact. |
| 11 | Execution | The provider executes the constrained plan; it doesn't reinterpret caller vocabulary. |
| 12 | Evidence | The result carries evidence of what was resolved and executed — evidence doesn't grant authority. |

**Invariant:** alias matching changes vocabulary, not authority. "Buys" doesn't create a new capability, and "seller" doesn't create a second supplier meaning.

Tests: `SupplyChainGroundingAliasTests.cs` (advanced Supply Chain sample) · `SemanticAliasSynonymGroundingTests.cs` (core semantics).

### Example: when more than one meaning is legal

Aliases collapse different words onto one meaning; sometimes ambiguity runs the other way — the same word legally matches two different meanings, and neither the graph nor a retrieval score can break the tie alone.

Take "active customers." Both are structurally valid readings:

- a customer whose account is enabled (`Customer.AccountEnabled`)
- a customer who placed a recent order (`Customer.HasRecentOrder`)

A fuzzy/BM25/vector retriever can legitimately return both, with close scores (0.91 vs. 0.89). Foundgine doesn't break the tie by picking the higher score — a higher score isn't evidence of intent, and authorization can't rescue a wrong guess; a request built from the wrong meaning is still a *fully authorized request*, just a perfectly authorized misunderstanding.

| Stage | What happens |
|-------|---------------|
| Retrieval (fuzzy) | Every plausible reading returns as a candidate with its own score and evidence. Retrieval only proposes. |
| Graph-constrained resolution | Both `AccountEnabled` and `HasRecentOrder` form a legal path — a legal path proves an interpretation is *possible*, not that it's *intended*. |
| Grounding decision | `SemanticLexicalResolver.Ground` compares the two paths' signatures. Neither dominates on confidence, so the outcome is `GroundingOutcome.RequiresClarification`: `Committed` stays null, both readings listed in `CompetingInterpretations` with their own steps, confidence, and evidence. |
| Caller chooses | The competing meanings surface as a clarifying question — "Did you mean customers with an enabled account, or customers with a recent order?" — instead of silently executing a guess. |
| Same boundary as everyone else | Once chosen, that interpretation goes through the same Authorization → Planning → Execution path as any other request. |

**Invariant:** a legal semantic path is not proof of intent. The same fail-closed mechanism applies when a resource limit (token count, search budget, timeout) stops the search before proving a single meaning (`GroundingOutcome.BudgetExceeded`), and when no legal interpretation exists at all (`GroundingOutcome.Unresolved`). Neither case falls back to a best-effort guess.

Further reading: *Lexical grounding* (fuzzy retrieval, resolver complexity bounds, adversarial examples) and *Grounding decisions* (full `GroundingDecision` shape, "different evidence for the same meaning" vs. "different meanings," the complete active-customers walkthrough).

## Why the boundary matters

The number of independent execution surfaces is a security and maintenance multiplier. A tool-per-capability design can give an agent dozens of places where authorization, tenant filtering, and query construction are implemented differently. Foundgine centralizes the semantic decision without making a transport or database the center of the architecture.

Deeper rationale: [`docs/WHY-FOUNDGINE.md`](https://github.com/CristianBarragan/Foundgine/blob/main/docs/WHY-FOUNDGINE.md), [`docs/APPLICATION-CATEGORIES.md`](https://github.com/CristianBarragan/Foundgine/blob/main/docs/APPLICATION-CATEGORIES.md), [`docs/ARCHITECTURE.md`](https://github.com/CristianBarragan/Foundgine/blob/main/docs/ARCHITECTURE.md), [`docs/AUTHORIZATION.md`](https://github.com/CristianBarragan/Foundgine/blob/main/docs/AUTHORIZATION.md), [`docs/SECURITY.md`](https://github.com/CristianBarragan/Foundgine/blob/main/docs/SECURITY.md), [`docs/AI-AGENT.md`](https://github.com/CristianBarragan/Foundgine/blob/main/docs/AI-AGENT.md)

## New: exposing MCP with open intent

`Foundgine.Providers.Tools.MCP` is Foundgine's actual Model Context Protocol adapter — it lets an MCP client reach the same semantic execution boundary described above, instead of getting its own bespoke tool-by-tool integration. (The standalone `Foundgine.MCP` NuGet package is legacy and deprecated — MCP support now ships inside `Foundgine.Providers`, installed as `dotnet add package Foundgine.Providers.Tools.MCP`.) It's wired up in the sample at [`samples/Foundgine.SupplyChain.Advanced/MCP.Foundgine`](https://github.com/CristianBarragan/Foundgine/tree/main/samples/Foundgine.SupplyChain.Advanced/MCP.Foundgine), whose `Program.cs` registers it with `builder.Services.AddFoundgineMcp(...)` and `.WithTools()`.

### The tools it actually exposes

`FoundgineMcpTools` (`src/Foundgine.Providers/Tools/MCP/FoundgineMcpTools.cs`) registers three read-side MCP tools:

| Tool | What it does |
|---|---|
| `foundgine_capabilities` | Discovery only — returns the semantic capability contract for the current caller. Per its own description: "Discovery is descriptive; authorization is re-evaluated during execution." |
| `foundgine_query` | **Open intent.** Takes either a natural-language string (its doc example: `"show customer orders"`) or a JSON read intent. Natural language is run through lexical grounding (`SemanticLexicalReadIntentGrounder`) before execution. |
| `foundgine_query_semantic` | Structured intent only — same JSON shape, but for callers that already resolved the semantic model themselves; skips grounding. |

A companion `FoundgineMcpMutationTools` class adds the write-side equivalents (`foundgine_mutation_dry_run`, `foundgine_mutation_approve`, and others) through Foundgine's separate mutation boundary.

### A real MCP call

This is an actual `tools/call` request an MCP client would send to invoke `foundgine_query`. The `intentJson` shape (`rootEntity`, `selections`, `filter`, `order`, `limit`/`offset`/`after`) is the real schema `JsonReadIntentAdapter` parses — not a guess:

```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "foundgine_query",
"arguments": {
"intentJson": "{\"rootEntity\":\"Customer\",\"selections\":[{\"field\":\"Id\"},{\"field\":\"Name\"}],\"limit\":50}"
}
}
}
```

`intentJson` can also just be plain language (`"show customer orders"`, per the tool's own description) — `foundgine_query` grounds that against the semantic contract first; `foundgine_query_semantic` skips grounding and expects the structured JSON directly.

```csharp
// samples/Foundgine.SupplyChain.Advanced/MCP.Foundgine/Program.cs
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments("/mcp"),
branch => branch.Use(async (ctx, next) =>
{
await OpenIntentDemoSecurity.PopulateSecurityContextAsync(ctx);
await next();
}));
app.MapMcp("/mcp");
```

— never from the MCP client's `intentJson` arguments.

### What Foundgine does with it

```csharp
// src/Foundgine.Providers/Tools/MCP/FoundgineMcpTools.cs (real, trimmed)
[McpServerTool(Name = "foundgine_query")]
public async Task ExecuteQueryAsync(string intentJson, CancellationToken cancellationToken)
{
var security = _securityContextProvider.RequireSecurityExecutionContext("MCP", "execution");
var intent = ParseOpenIntent(intentJson, cancellationToken); // JSON or NL -> ReadIntent
intent = intent with { Security = security }; // host-owned, not client-supplied
var result = await _foundgine.ExecuteAsync(intent, _contextFactory(), cancellationToken);
return JsonSerializer.Serialize(new { rows = result.Rows, pageInfo = result.PageInfo,
evidence = result.Evidence, receipt = result.Receipt });
}
```

This calls the same `IFoundgine.ExecuteAsync(ReadIntent, ...)` entry point that Foundgine's in-process fluent query API compiles down to (`src/Foundgine.Runtime/Query/QueryBuilder.cs`):

```csharp
// Typed C# — written by hand in application code
var result = await foundgine
.Query()
.Select(c => new { c.Id, c.Name })
.Take(50)
.ExecuteAsync();

// Dynamic C# — the same shape FoundgineMcpTools builds internally from intentJson
var result = await foundgine
.Query("Customer")
.Where("Id", SemanticFilterOperator.Eq, customerId)
.Take(50)
.ExecuteAsync();
```

Typed C#, dynamic C#, and MCP's `intentJson` are three ways of authoring the same `ReadIntent`. Everything downstream of that — resolution → authorization → planning → execution — doesn't know or care which one produced it.

Full chain for an MCP call: **MCP client → `tools/call` (`foundgine_query`, JSON-RPC) → `FoundgineMcpTools` → `ReadIntent` (host attaches `SecurityExecutionContext`) → `IFoundgine.ExecuteAsync` → resolution → authorization → planning → execution → `{ rows, pageInfo, evidence, receipt }` back to the client.**

This is what keeps an MCP-exposed agent from becoming a tool-surface liability: instead of one authorization/validation implementation per exposed tool (the "50 tools, 50 security surfaces" problem), every MCP call resolves through the one semantic + authorization boundary that GraphQL, JSON, and other callers already share.

## Package shape

| Package | Responsibility |
|---|---|
| `Foundgine.Core` | Semantic model, metadata, intent, planning and provider-independent contracts |
| `Foundgine.Runtime` | Application-facing orchestration, authorization and execution |
| `Foundgine.Providers` | Storage, AI/model, MCP, AOT and other concrete integrations |
| `Foundgine.Extensions` | Optional framework integrations such as Hot Chocolate GraphQL |

The normal application starting point is `Foundgine.Runtime` + `Foundgine.Providers`.

## Get started

The fastest path is the Supply Chain sample pair:

- **Starter:** [`samples/Foundgine.SupplyChain`](https://github.com/CristianBarragan/Foundgine/tree/main/samples/Foundgine.SupplyChain) — the smallest realistic application boundary
- **Advanced:** [`samples/Foundgine.SupplyChain.Advanced`](https://github.com/CristianBarragan/Foundgine/tree/main/samples/Foundgine.SupplyChain.Advanced) — richer semantics, grounding, retrieval, authorization and adversarial testing (start at [`docs/00-Overview-And-Setup.md`](https://github.com/CristianBarragan/Foundgine/blob/main/docs/00-Overview-And-Setup.md), follow 01–05)

Conceptual path: [`docs/README.md`](https://github.com/CristianBarragan/Foundgine/blob/main/docs/README.md) or the [documentation site](https://cristianbarragan.github.io/Foundgine/docs-site/index.html).

## Evidence

The repository contains controlled benchmarks and deterministic security tests, distinguishing measured tool calls, latency, RPS, and success/failure counts from estimated context metrics:

- [Agent benchmark explorer](https://cristianbarragan.github.io/Foundgine/docs-site/agent-benchmark/run-5b/index.html)
- Supply Chain E2E
- Security PenTest
- [`benchmarks/AgentEndToEnd/README.md`](https://github.com/CristianBarragan/Foundgine/blob/main/benchmarks/AgentEndToEnd/README.md)

Benchmark results are workload-specific and should not be generalized beyond the published experiment.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.