vsl-maf 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vsl_maf-0.2.2/LICENSE +21 -0
- vsl_maf-0.2.2/PKG-INFO +239 -0
- vsl_maf-0.2.2/README.md +215 -0
- vsl_maf-0.2.2/pyproject.toml +43 -0
- vsl_maf-0.2.2/setup.cfg +4 -0
- vsl_maf-0.2.2/src/vsl_maf/__init__.py +23 -0
- vsl_maf-0.2.2/src/vsl_maf/adapter.py +180 -0
- vsl_maf-0.2.2/src/vsl_maf/integration.py +167 -0
- vsl_maf-0.2.2/src/vsl_maf.egg-info/PKG-INFO +239 -0
- vsl_maf-0.2.2/src/vsl_maf.egg-info/SOURCES.txt +14 -0
- vsl_maf-0.2.2/src/vsl_maf.egg-info/dependency_links.txt +1 -0
- vsl_maf-0.2.2/src/vsl_maf.egg-info/requires.txt +6 -0
- vsl_maf-0.2.2/src/vsl_maf.egg-info/top_level.txt +1 -0
- vsl_maf-0.2.2/tests/test_conformance.py +7 -0
- vsl_maf-0.2.2/tests/test_integration.py +223 -0
- vsl_maf-0.2.2/tests/test_real_agent.py +186 -0
vsl_maf-0.2.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Super Semantics
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
vsl_maf-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vsl-maf
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Microsoft Agent Framework (agent-framework) adapter for VSL (VERBA Specification Language) governance primitives -- implements vsl-core's VSLAdapter conformance contract.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/supersemantics/VSL-Microsoft-agent-framework
|
|
7
|
+
Project-URL: Repository, https://github.com/supersemantics/VSL-Microsoft-agent-framework
|
|
8
|
+
Project-URL: Changelog, https://github.com/supersemantics/VSL-Microsoft-agent-framework/blob/HEAD/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/supersemantics/VSL-Microsoft-agent-framework/issues
|
|
10
|
+
Keywords: governance,ai-safety,guardrails,agent-framework,vsl,verba,microsoft-agent-framework
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: super-semantics-vsl<0.4,>=0.3
|
|
19
|
+
Requires-Dist: agent-framework-core>=1.25.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# vsl-maf
|
|
26
|
+
|
|
27
|
+
A [Microsoft Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/) (`agent-framework`) adapter for [vsl-core](https://github.com/supersemantics/VSL-Core): implements `vsl_core.conformance.protocol.VSLAdapter` and provides `VSLFunctionMiddleware`, a `FunctionMiddleware` subclass that gates every tool call an `Agent` makes before it fires.
|
|
28
|
+
|
|
29
|
+
Requires vsl-core 0.2.0 or later and agent-framework-core 1.25 or later. Handover records (below) need vsl-core 0.3.0.
|
|
30
|
+
|
|
31
|
+
Is:
|
|
32
|
+
|
|
33
|
+
- A `VSLAdapter` implementation (`MAFAdapter`) that passes `vsl_core.conformance.suite.run_conformance_suite` with an empty result.
|
|
34
|
+
- `VSLFunctionMiddleware`, which calls a compiled gate inside `process()` before `call_next()`, so the gate completes strictly before the guarded tool's side effect (F1 pre-commitment), and makes a denial abort the run fail-closed.
|
|
35
|
+
|
|
36
|
+
Is not:
|
|
37
|
+
|
|
38
|
+
- A replacement for `vsl-core`: the PreNode/Invariant/Fallback/TerminalState constructs, the ledger and the governance vocabulary all live there. This package only compiles and wires them into MAF.
|
|
39
|
+
- Automatic interception. `VSLFunctionMiddleware` must be passed in the `middleware=[...]` list when creating an `Agent`.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install vsl-maf
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This installs vsl-core (`super-semantics-vsl`, imported as `vsl_core`) and `agent-framework-core`
|
|
48
|
+
with it. From a local clone of this repo: `pip install -e .`
|
|
49
|
+
|
|
50
|
+
Add the MAF connector for your model service as well, for example `agent-framework-foundry`.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from typing import Annotated
|
|
56
|
+
|
|
57
|
+
from agent_framework import Agent, tool
|
|
58
|
+
from agent_framework.foundry import FoundryChatClient
|
|
59
|
+
from azure.identity.aio import AzureCliCredential
|
|
60
|
+
from pydantic import Field
|
|
61
|
+
|
|
62
|
+
from vsl_core.constructs import PreNode
|
|
63
|
+
from vsl_core.ledger import VerbaLedger
|
|
64
|
+
from vsl_core.metrics import ROBUST_GAMMA_DEFAULT_THRESHOLD, AssuranceBasis, F2Modification, GammaEstimate
|
|
65
|
+
from vsl_maf import MAFAdapter, VSLFunctionMiddleware, VSLGateDenied, tool_arguments
|
|
66
|
+
|
|
67
|
+
REFUND_LIMIT = 500.0
|
|
68
|
+
REFUND_GATE = "refund-within-policy"
|
|
69
|
+
ledger = VerbaLedger()
|
|
70
|
+
|
|
71
|
+
@tool(approval_mode="never_require")
|
|
72
|
+
def issue_refund(amount: Annotated[float, Field(description="Refund amount")]) -> str:
|
|
73
|
+
return f"Refunded {amount}"
|
|
74
|
+
|
|
75
|
+
async def refund_headroom(candidate) -> GammaEstimate:
|
|
76
|
+
amount = float(candidate.get("amount", 0.0))
|
|
77
|
+
# A ratio where 1.0 is break-even; 0.0 for a missing amount, so the gate fails closed.
|
|
78
|
+
estimate = GammaEstimate(gamma_hat=REFUND_LIMIT / amount if amount > 0 else 0.0)
|
|
79
|
+
ledger.write_gate_decision(
|
|
80
|
+
identity_key="refund-agent",
|
|
81
|
+
primitive_name=REFUND_GATE,
|
|
82
|
+
estimate=estimate,
|
|
83
|
+
gamma_threshold=ROBUST_GAMMA_DEFAULT_THRESHOLD,
|
|
84
|
+
)
|
|
85
|
+
return estimate
|
|
86
|
+
|
|
87
|
+
gate = MAFAdapter(identity_key="refund-agent").compile_pre_node(PreNode(
|
|
88
|
+
name=REFUND_GATE,
|
|
89
|
+
description="Refunds must stay comfortably inside the policy limit.",
|
|
90
|
+
monitor=refund_headroom,
|
|
91
|
+
# No model at the guarded call: the gate only allows or blocks.
|
|
92
|
+
assurance_basis=AssuranceBasis(f1_pre_commitment=True, f2_modification=F2Modification.NONE),
|
|
93
|
+
gamma_threshold=ROBUST_GAMMA_DEFAULT_THRESHOLD,
|
|
94
|
+
))
|
|
95
|
+
|
|
96
|
+
async with Agent(
|
|
97
|
+
client=FoundryChatClient(project_endpoint="...", model="gpt-4o-mini", credential=AzureCliCredential()),
|
|
98
|
+
name="RefundAgent",
|
|
99
|
+
instructions="Use issue_refund to process refund requests.",
|
|
100
|
+
tools=issue_refund,
|
|
101
|
+
middleware=[VSLFunctionMiddleware(gate, candidate_input_fn=tool_arguments, function_name="issue_refund")],
|
|
102
|
+
) as agent:
|
|
103
|
+
try:
|
|
104
|
+
result = await agent.run("Refund 900 to order 1234")
|
|
105
|
+
except VSLGateDenied as denied:
|
|
106
|
+
print(f"Refund denied before it ran: {denied.denial.reason}")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`compile_invariant` works the same way with an `Invariant`. Its denial carries the more specific `InvariantViolation` as `denied.denial`.
|
|
110
|
+
|
|
111
|
+
## How a denial reaches your code
|
|
112
|
+
|
|
113
|
+
MAF's function-invocation loop turns an ordinary exception from function middleware into a tool-error result and keeps running. The tool does not run, but the model is only told "Error: Function failed.", may retry or carry on, and `agent.run()` returns normally, so your code never learns about the denial. For a governance gate that is fail-open at the level of the run.
|
|
114
|
+
|
|
115
|
+
So by default (`on_denial="abort"`) a denial raises `VSLGateDenied`, a subclass of MAF's `MiddlewareFailure`. MAF's documented fail-closed signal is never turned into a tool result: it cancels the current batch of tool calls and propagates to the caller of `agent.run()` (for streaming runs, when the stream is consumed). `denied.denial` is the gate's `AutomationDeniedException` or `InvariantViolation` (also the exception's `__cause__`), and `denied.function_name` is the tool that did not run.
|
|
116
|
+
|
|
117
|
+
`on_denial="return_to_model"` keeps MAF's default: the model gets a tool error and the run continues. The tool still never runs. Use it only when you want the model to explain the refusal or take another route, knowing your code is not told.
|
|
118
|
+
|
|
119
|
+
A gate that fails for another reason (a monitor raising `RuntimeError`, say) also aborts with `MiddlewareFailure` in `"abort"` mode, so the tool never runs unguarded.
|
|
120
|
+
|
|
121
|
+
A caveat from MAF itself: when the model asks for several tools at once, an abort cancels the rest of the batch cooperatively. A synchronous tool body already running in a worker thread cannot be interrupted, though its result is discarded. Each gated tool's own gate still runs before that tool.
|
|
122
|
+
|
|
123
|
+
## How it works in MAF
|
|
124
|
+
|
|
125
|
+
MAF calls `process(context, call_next)` before every tool invocation:
|
|
126
|
+
|
|
127
|
+
- `context.function.name`: the tool being called.
|
|
128
|
+
- `context.arguments`: the arguments, as a mapping or a pydantic model. `tool_arguments(context)` returns a plain dict either way.
|
|
129
|
+
- `call_next()`: continues to the next middleware or the tool. It takes no arguments.
|
|
130
|
+
|
|
131
|
+
If the gate raises, `call_next()` is never reached and the tool never runs.
|
|
132
|
+
|
|
133
|
+
Register VSL middleware after any argument-repair middleware: MAF fails closed if arguments change after a policy middleware has seen them.
|
|
134
|
+
|
|
135
|
+
## Chaining multiple gates
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
middleware=[
|
|
139
|
+
VSLFunctionMiddleware(cap_gate, candidate_input_fn=tool_arguments, function_name="approve_purchase"),
|
|
140
|
+
VSLFunctionMiddleware(budget_gate, candidate_input_fn=tool_arguments, function_name="approve_purchase"),
|
|
141
|
+
]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
MAF runs middleware in registration order. If the first gate denies, the second gate and the tool never run.
|
|
145
|
+
|
|
146
|
+
## Writing gate values honestly
|
|
147
|
+
|
|
148
|
+
vsl-core never measures Gamma: your monitor computes `gamma_hat` and the gate only compares it, passing when it is strictly above `max(threshold, 1 + delta_estimation_error)` (vsl-core 0.2.0 rules, version `2026.10`).
|
|
149
|
+
|
|
150
|
+
- Build it as a ratio where 1.0 is break-even: a limit divided by the requested amount, a judge's score divided by its break-even score.
|
|
151
|
+
- Return `0.0` when the input is missing or unreadable, so the gate fails closed.
|
|
152
|
+
- Never pass a raw 0–1 confidence, or a confidence the model supplies in the tool's own arguments.
|
|
153
|
+
- Keep thresholds above 1.0 (`ROBUST_GAMMA_HIGH_STAKES_THRESHOLD`, 1.2, for payments, medical and legal actions). For a rule that must never be broken, use an `Invariant`.
|
|
154
|
+
|
|
155
|
+
## `VSLFunctionMiddleware` doesn't consume `Fallback`
|
|
156
|
+
|
|
157
|
+
A `PreNode`'s `Fallback` is policy data only here: on denial the middleware raises once, immediately, with no retry loop. `compile_pre_node` warns (`UserWarning`) if a `Fallback` is non-default, so this can't be silently assumed. vsl-core's own reference adapter doesn't consume `Fallback` either.
|
|
158
|
+
|
|
159
|
+
## Recording decisions: the ledger doesn't write itself
|
|
160
|
+
|
|
161
|
+
Nothing in this package writes to a `VerbaLedger`. That is the application's choice, and vsl-core's conformance contract forbids an adapter from making it. Record each gate's decision from inside its monitor or rule with `write_gate_decision()` (vsl-core 0.2.0), as in the usage example above:
|
|
162
|
+
|
|
163
|
+
- For a PreNode, pass the monitor's `GammaEstimate` and the PreNode's `gamma_threshold`. `allowed` is then computed with the same check the gate makes, and a contradiction is refused.
|
|
164
|
+
- For an Invariant, pass `allowed=holds`.
|
|
165
|
+
- Pass a `decision_id` to group a decision's records.
|
|
166
|
+
|
|
167
|
+
A denied gate's record flags drift, so follow it with the decision's next entry (`PRE_NODE`, then `VERIFICATION`, or `TERMINAL`) from your `except VSLGateDenied` block, as `examples/financial-agents/purchase_approval_agent.py` does. An agent with no ledger calls produces working gates and no audit trail, and this package won't warn you.
|
|
168
|
+
|
|
169
|
+
`VerbaLedger` accepts any `LedgerStore`: `JsonlLedgerStore` for a local append-only file, or the optional immudb store in vsl-core.
|
|
170
|
+
|
|
171
|
+
## Multi-agent workflows: handovers and Cluster Gamma
|
|
172
|
+
|
|
173
|
+
MAF composes agents: one agent can call another as a tool (`agent.as_tool()`), or a workflow can hand a conversation from one agent to the next. Each agent's own gates can pass while the hand-off between them goes unchecked.
|
|
174
|
+
|
|
175
|
+
With vsl-core 0.3.0, record the hand-off before the receiving agent acts, and have the receiver name it in its first gate decision:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
handover = ledger.write_handover(
|
|
179
|
+
identity_key="intake-agent",
|
|
180
|
+
to_identity_key="refund-agent",
|
|
181
|
+
decision_id=decision_id,
|
|
182
|
+
reason="customer asked for a refund",
|
|
183
|
+
)
|
|
184
|
+
# ...inside the refund agent's monitor:
|
|
185
|
+
ledger.write_gate_decision(
|
|
186
|
+
identity_key="refund-agent",
|
|
187
|
+
primitive_name=REFUND_GATE,
|
|
188
|
+
estimate=estimate,
|
|
189
|
+
gamma_threshold=ROBUST_GAMMA_DEFAULT_THRESHOLD,
|
|
190
|
+
decision_id=decision_id,
|
|
191
|
+
caused_by=handover.entry_id, # shows the receiver checked what it was handed
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
When the sub-agent is called as a tool, the calling agent's `VSLFunctionMiddleware` sees that call like any other, so the same gating applies to the hand-off itself.
|
|
196
|
+
|
|
197
|
+
Gating every tool call does not make the composed system safe. vsl-core's motivating example is the 2010 Flash Crash: every participant's Gamma was above 1, and the Cluster-level Gamma was still 0. If several agents coordinate on one outcome, declare them as a `vsl_core.cluster.Cluster` and pass its `cluster_key` on their records. The Cluster's `invariants` are plain `Invariant` objects, compiled and wired through `MAFAdapter`/`VSLFunctionMiddleware` like any other.
|
|
198
|
+
|
|
199
|
+
## The F1/F2 assurance distinction
|
|
200
|
+
|
|
201
|
+
Every `PreNode`/`Invariant` requires an `AssuranceBasis`. F2 is judged at the call the gate guards and is never earned by the gate itself:
|
|
202
|
+
|
|
203
|
+
| Gate | F1 | F2 | Level |
|
|
204
|
+
|---|---|---|---|
|
|
205
|
+
| Amount cap or budget check before a tool (no model at the call) | True | `NONE` | LOW |
|
|
206
|
+
| Gate before a hosted-model call whose system prompt the application writes | True | `PARTIAL` | MEDIUM |
|
|
207
|
+
| Gate before a self-hosted model where the application steers logits or activations | True | `FULL` | HIGH |
|
|
208
|
+
|
|
209
|
+
The level measures influence on the model, not enforcement: a LOW gate still blocks every tool call it denies. Report "blocks before the action: yes" separately rather than raising F2.
|
|
210
|
+
|
|
211
|
+
The basis is self-declared, not verified. `MAFAdapter` carries one heuristic check: `compile_pre_node`/`compile_invariant` warn (`UserWarning`) when a monitor or rule claims `F2Modification.FULL` but its source appears to call a hosted model API (`openai`, `anthropic`, `google.genai`, `cohere`, `mistralai`, Azure AI inference, or a MAF chat client such as `FoundryChatClient`). A hosted, black-box call cannot modify the energy function, so the honest ceiling is `PARTIAL`. The check can miss indirect calls, and passing it doesn't confirm a `FULL` claim.
|
|
212
|
+
|
|
213
|
+
## Sample agents
|
|
214
|
+
|
|
215
|
+
`examples/financial-agents/purchase_approval_agent.py` chains an Invariant (hard cap) and a PreNode (budget headroom) on a real MAF `Agent` with `FoundryChatClient`, records every decision, and handles denials. It needs Azure/Foundry credentials; see `examples/README.md`.
|
|
216
|
+
|
|
217
|
+
## Layout
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
src/vsl_maf/
|
|
221
|
+
├── adapter.py MAFAdapter (VSLAdapter conformance contract)
|
|
222
|
+
└── integration.py VSLFunctionMiddleware, VSLGateDenied, tool_arguments
|
|
223
|
+
examples/
|
|
224
|
+
└── financial-agents/purchase_approval_agent.py
|
|
225
|
+
tests/
|
|
226
|
+
├── test_conformance.py run_conformance_suite(MAFAdapter()) == []
|
|
227
|
+
├── test_integration.py VSLFunctionMiddleware.process() against a mock context
|
|
228
|
+
└── test_real_agent.py a real MAF Agent with a scripted chat client (no credentials)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Status
|
|
232
|
+
|
|
233
|
+
Alpha. Conformance-suite green. Tested against agent-framework-core 1.25.0, including end-to-end runs through a real `Agent` and MAF's own function-invocation loop with a scripted chat client: allowed calls, denials aborting the run, `return_to_model`, Invariant denials and recorded gate decisions. The Foundry example has not been run against a live Foundry endpoint.
|
|
234
|
+
|
|
235
|
+
See [CHANGELOG.md](https://github.com/supersemantics/VSL-Microsoft-agent-framework/blob/HEAD/CHANGELOG.md).
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT -- see LICENSE. Copyright Super Semantics.
|
vsl_maf-0.2.2/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# vsl-maf
|
|
2
|
+
|
|
3
|
+
A [Microsoft Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/) (`agent-framework`) adapter for [vsl-core](https://github.com/supersemantics/VSL-Core): implements `vsl_core.conformance.protocol.VSLAdapter` and provides `VSLFunctionMiddleware`, a `FunctionMiddleware` subclass that gates every tool call an `Agent` makes before it fires.
|
|
4
|
+
|
|
5
|
+
Requires vsl-core 0.2.0 or later and agent-framework-core 1.25 or later. Handover records (below) need vsl-core 0.3.0.
|
|
6
|
+
|
|
7
|
+
Is:
|
|
8
|
+
|
|
9
|
+
- A `VSLAdapter` implementation (`MAFAdapter`) that passes `vsl_core.conformance.suite.run_conformance_suite` with an empty result.
|
|
10
|
+
- `VSLFunctionMiddleware`, which calls a compiled gate inside `process()` before `call_next()`, so the gate completes strictly before the guarded tool's side effect (F1 pre-commitment), and makes a denial abort the run fail-closed.
|
|
11
|
+
|
|
12
|
+
Is not:
|
|
13
|
+
|
|
14
|
+
- A replacement for `vsl-core`: the PreNode/Invariant/Fallback/TerminalState constructs, the ledger and the governance vocabulary all live there. This package only compiles and wires them into MAF.
|
|
15
|
+
- Automatic interception. `VSLFunctionMiddleware` must be passed in the `middleware=[...]` list when creating an `Agent`.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install vsl-maf
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This installs vsl-core (`super-semantics-vsl`, imported as `vsl_core`) and `agent-framework-core`
|
|
24
|
+
with it. From a local clone of this repo: `pip install -e .`
|
|
25
|
+
|
|
26
|
+
Add the MAF connector for your model service as well, for example `agent-framework-foundry`.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from typing import Annotated
|
|
32
|
+
|
|
33
|
+
from agent_framework import Agent, tool
|
|
34
|
+
from agent_framework.foundry import FoundryChatClient
|
|
35
|
+
from azure.identity.aio import AzureCliCredential
|
|
36
|
+
from pydantic import Field
|
|
37
|
+
|
|
38
|
+
from vsl_core.constructs import PreNode
|
|
39
|
+
from vsl_core.ledger import VerbaLedger
|
|
40
|
+
from vsl_core.metrics import ROBUST_GAMMA_DEFAULT_THRESHOLD, AssuranceBasis, F2Modification, GammaEstimate
|
|
41
|
+
from vsl_maf import MAFAdapter, VSLFunctionMiddleware, VSLGateDenied, tool_arguments
|
|
42
|
+
|
|
43
|
+
REFUND_LIMIT = 500.0
|
|
44
|
+
REFUND_GATE = "refund-within-policy"
|
|
45
|
+
ledger = VerbaLedger()
|
|
46
|
+
|
|
47
|
+
@tool(approval_mode="never_require")
|
|
48
|
+
def issue_refund(amount: Annotated[float, Field(description="Refund amount")]) -> str:
|
|
49
|
+
return f"Refunded {amount}"
|
|
50
|
+
|
|
51
|
+
async def refund_headroom(candidate) -> GammaEstimate:
|
|
52
|
+
amount = float(candidate.get("amount", 0.0))
|
|
53
|
+
# A ratio where 1.0 is break-even; 0.0 for a missing amount, so the gate fails closed.
|
|
54
|
+
estimate = GammaEstimate(gamma_hat=REFUND_LIMIT / amount if amount > 0 else 0.0)
|
|
55
|
+
ledger.write_gate_decision(
|
|
56
|
+
identity_key="refund-agent",
|
|
57
|
+
primitive_name=REFUND_GATE,
|
|
58
|
+
estimate=estimate,
|
|
59
|
+
gamma_threshold=ROBUST_GAMMA_DEFAULT_THRESHOLD,
|
|
60
|
+
)
|
|
61
|
+
return estimate
|
|
62
|
+
|
|
63
|
+
gate = MAFAdapter(identity_key="refund-agent").compile_pre_node(PreNode(
|
|
64
|
+
name=REFUND_GATE,
|
|
65
|
+
description="Refunds must stay comfortably inside the policy limit.",
|
|
66
|
+
monitor=refund_headroom,
|
|
67
|
+
# No model at the guarded call: the gate only allows or blocks.
|
|
68
|
+
assurance_basis=AssuranceBasis(f1_pre_commitment=True, f2_modification=F2Modification.NONE),
|
|
69
|
+
gamma_threshold=ROBUST_GAMMA_DEFAULT_THRESHOLD,
|
|
70
|
+
))
|
|
71
|
+
|
|
72
|
+
async with Agent(
|
|
73
|
+
client=FoundryChatClient(project_endpoint="...", model="gpt-4o-mini", credential=AzureCliCredential()),
|
|
74
|
+
name="RefundAgent",
|
|
75
|
+
instructions="Use issue_refund to process refund requests.",
|
|
76
|
+
tools=issue_refund,
|
|
77
|
+
middleware=[VSLFunctionMiddleware(gate, candidate_input_fn=tool_arguments, function_name="issue_refund")],
|
|
78
|
+
) as agent:
|
|
79
|
+
try:
|
|
80
|
+
result = await agent.run("Refund 900 to order 1234")
|
|
81
|
+
except VSLGateDenied as denied:
|
|
82
|
+
print(f"Refund denied before it ran: {denied.denial.reason}")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`compile_invariant` works the same way with an `Invariant`. Its denial carries the more specific `InvariantViolation` as `denied.denial`.
|
|
86
|
+
|
|
87
|
+
## How a denial reaches your code
|
|
88
|
+
|
|
89
|
+
MAF's function-invocation loop turns an ordinary exception from function middleware into a tool-error result and keeps running. The tool does not run, but the model is only told "Error: Function failed.", may retry or carry on, and `agent.run()` returns normally, so your code never learns about the denial. For a governance gate that is fail-open at the level of the run.
|
|
90
|
+
|
|
91
|
+
So by default (`on_denial="abort"`) a denial raises `VSLGateDenied`, a subclass of MAF's `MiddlewareFailure`. MAF's documented fail-closed signal is never turned into a tool result: it cancels the current batch of tool calls and propagates to the caller of `agent.run()` (for streaming runs, when the stream is consumed). `denied.denial` is the gate's `AutomationDeniedException` or `InvariantViolation` (also the exception's `__cause__`), and `denied.function_name` is the tool that did not run.
|
|
92
|
+
|
|
93
|
+
`on_denial="return_to_model"` keeps MAF's default: the model gets a tool error and the run continues. The tool still never runs. Use it only when you want the model to explain the refusal or take another route, knowing your code is not told.
|
|
94
|
+
|
|
95
|
+
A gate that fails for another reason (a monitor raising `RuntimeError`, say) also aborts with `MiddlewareFailure` in `"abort"` mode, so the tool never runs unguarded.
|
|
96
|
+
|
|
97
|
+
A caveat from MAF itself: when the model asks for several tools at once, an abort cancels the rest of the batch cooperatively. A synchronous tool body already running in a worker thread cannot be interrupted, though its result is discarded. Each gated tool's own gate still runs before that tool.
|
|
98
|
+
|
|
99
|
+
## How it works in MAF
|
|
100
|
+
|
|
101
|
+
MAF calls `process(context, call_next)` before every tool invocation:
|
|
102
|
+
|
|
103
|
+
- `context.function.name`: the tool being called.
|
|
104
|
+
- `context.arguments`: the arguments, as a mapping or a pydantic model. `tool_arguments(context)` returns a plain dict either way.
|
|
105
|
+
- `call_next()`: continues to the next middleware or the tool. It takes no arguments.
|
|
106
|
+
|
|
107
|
+
If the gate raises, `call_next()` is never reached and the tool never runs.
|
|
108
|
+
|
|
109
|
+
Register VSL middleware after any argument-repair middleware: MAF fails closed if arguments change after a policy middleware has seen them.
|
|
110
|
+
|
|
111
|
+
## Chaining multiple gates
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
middleware=[
|
|
115
|
+
VSLFunctionMiddleware(cap_gate, candidate_input_fn=tool_arguments, function_name="approve_purchase"),
|
|
116
|
+
VSLFunctionMiddleware(budget_gate, candidate_input_fn=tool_arguments, function_name="approve_purchase"),
|
|
117
|
+
]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
MAF runs middleware in registration order. If the first gate denies, the second gate and the tool never run.
|
|
121
|
+
|
|
122
|
+
## Writing gate values honestly
|
|
123
|
+
|
|
124
|
+
vsl-core never measures Gamma: your monitor computes `gamma_hat` and the gate only compares it, passing when it is strictly above `max(threshold, 1 + delta_estimation_error)` (vsl-core 0.2.0 rules, version `2026.10`).
|
|
125
|
+
|
|
126
|
+
- Build it as a ratio where 1.0 is break-even: a limit divided by the requested amount, a judge's score divided by its break-even score.
|
|
127
|
+
- Return `0.0` when the input is missing or unreadable, so the gate fails closed.
|
|
128
|
+
- Never pass a raw 0–1 confidence, or a confidence the model supplies in the tool's own arguments.
|
|
129
|
+
- Keep thresholds above 1.0 (`ROBUST_GAMMA_HIGH_STAKES_THRESHOLD`, 1.2, for payments, medical and legal actions). For a rule that must never be broken, use an `Invariant`.
|
|
130
|
+
|
|
131
|
+
## `VSLFunctionMiddleware` doesn't consume `Fallback`
|
|
132
|
+
|
|
133
|
+
A `PreNode`'s `Fallback` is policy data only here: on denial the middleware raises once, immediately, with no retry loop. `compile_pre_node` warns (`UserWarning`) if a `Fallback` is non-default, so this can't be silently assumed. vsl-core's own reference adapter doesn't consume `Fallback` either.
|
|
134
|
+
|
|
135
|
+
## Recording decisions: the ledger doesn't write itself
|
|
136
|
+
|
|
137
|
+
Nothing in this package writes to a `VerbaLedger`. That is the application's choice, and vsl-core's conformance contract forbids an adapter from making it. Record each gate's decision from inside its monitor or rule with `write_gate_decision()` (vsl-core 0.2.0), as in the usage example above:
|
|
138
|
+
|
|
139
|
+
- For a PreNode, pass the monitor's `GammaEstimate` and the PreNode's `gamma_threshold`. `allowed` is then computed with the same check the gate makes, and a contradiction is refused.
|
|
140
|
+
- For an Invariant, pass `allowed=holds`.
|
|
141
|
+
- Pass a `decision_id` to group a decision's records.
|
|
142
|
+
|
|
143
|
+
A denied gate's record flags drift, so follow it with the decision's next entry (`PRE_NODE`, then `VERIFICATION`, or `TERMINAL`) from your `except VSLGateDenied` block, as `examples/financial-agents/purchase_approval_agent.py` does. An agent with no ledger calls produces working gates and no audit trail, and this package won't warn you.
|
|
144
|
+
|
|
145
|
+
`VerbaLedger` accepts any `LedgerStore`: `JsonlLedgerStore` for a local append-only file, or the optional immudb store in vsl-core.
|
|
146
|
+
|
|
147
|
+
## Multi-agent workflows: handovers and Cluster Gamma
|
|
148
|
+
|
|
149
|
+
MAF composes agents: one agent can call another as a tool (`agent.as_tool()`), or a workflow can hand a conversation from one agent to the next. Each agent's own gates can pass while the hand-off between them goes unchecked.
|
|
150
|
+
|
|
151
|
+
With vsl-core 0.3.0, record the hand-off before the receiving agent acts, and have the receiver name it in its first gate decision:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
handover = ledger.write_handover(
|
|
155
|
+
identity_key="intake-agent",
|
|
156
|
+
to_identity_key="refund-agent",
|
|
157
|
+
decision_id=decision_id,
|
|
158
|
+
reason="customer asked for a refund",
|
|
159
|
+
)
|
|
160
|
+
# ...inside the refund agent's monitor:
|
|
161
|
+
ledger.write_gate_decision(
|
|
162
|
+
identity_key="refund-agent",
|
|
163
|
+
primitive_name=REFUND_GATE,
|
|
164
|
+
estimate=estimate,
|
|
165
|
+
gamma_threshold=ROBUST_GAMMA_DEFAULT_THRESHOLD,
|
|
166
|
+
decision_id=decision_id,
|
|
167
|
+
caused_by=handover.entry_id, # shows the receiver checked what it was handed
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
When the sub-agent is called as a tool, the calling agent's `VSLFunctionMiddleware` sees that call like any other, so the same gating applies to the hand-off itself.
|
|
172
|
+
|
|
173
|
+
Gating every tool call does not make the composed system safe. vsl-core's motivating example is the 2010 Flash Crash: every participant's Gamma was above 1, and the Cluster-level Gamma was still 0. If several agents coordinate on one outcome, declare them as a `vsl_core.cluster.Cluster` and pass its `cluster_key` on their records. The Cluster's `invariants` are plain `Invariant` objects, compiled and wired through `MAFAdapter`/`VSLFunctionMiddleware` like any other.
|
|
174
|
+
|
|
175
|
+
## The F1/F2 assurance distinction
|
|
176
|
+
|
|
177
|
+
Every `PreNode`/`Invariant` requires an `AssuranceBasis`. F2 is judged at the call the gate guards and is never earned by the gate itself:
|
|
178
|
+
|
|
179
|
+
| Gate | F1 | F2 | Level |
|
|
180
|
+
|---|---|---|---|
|
|
181
|
+
| Amount cap or budget check before a tool (no model at the call) | True | `NONE` | LOW |
|
|
182
|
+
| Gate before a hosted-model call whose system prompt the application writes | True | `PARTIAL` | MEDIUM |
|
|
183
|
+
| Gate before a self-hosted model where the application steers logits or activations | True | `FULL` | HIGH |
|
|
184
|
+
|
|
185
|
+
The level measures influence on the model, not enforcement: a LOW gate still blocks every tool call it denies. Report "blocks before the action: yes" separately rather than raising F2.
|
|
186
|
+
|
|
187
|
+
The basis is self-declared, not verified. `MAFAdapter` carries one heuristic check: `compile_pre_node`/`compile_invariant` warn (`UserWarning`) when a monitor or rule claims `F2Modification.FULL` but its source appears to call a hosted model API (`openai`, `anthropic`, `google.genai`, `cohere`, `mistralai`, Azure AI inference, or a MAF chat client such as `FoundryChatClient`). A hosted, black-box call cannot modify the energy function, so the honest ceiling is `PARTIAL`. The check can miss indirect calls, and passing it doesn't confirm a `FULL` claim.
|
|
188
|
+
|
|
189
|
+
## Sample agents
|
|
190
|
+
|
|
191
|
+
`examples/financial-agents/purchase_approval_agent.py` chains an Invariant (hard cap) and a PreNode (budget headroom) on a real MAF `Agent` with `FoundryChatClient`, records every decision, and handles denials. It needs Azure/Foundry credentials; see `examples/README.md`.
|
|
192
|
+
|
|
193
|
+
## Layout
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
src/vsl_maf/
|
|
197
|
+
├── adapter.py MAFAdapter (VSLAdapter conformance contract)
|
|
198
|
+
└── integration.py VSLFunctionMiddleware, VSLGateDenied, tool_arguments
|
|
199
|
+
examples/
|
|
200
|
+
└── financial-agents/purchase_approval_agent.py
|
|
201
|
+
tests/
|
|
202
|
+
├── test_conformance.py run_conformance_suite(MAFAdapter()) == []
|
|
203
|
+
├── test_integration.py VSLFunctionMiddleware.process() against a mock context
|
|
204
|
+
└── test_real_agent.py a real MAF Agent with a scripted chat client (no credentials)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Status
|
|
208
|
+
|
|
209
|
+
Alpha. Conformance-suite green. Tested against agent-framework-core 1.25.0, including end-to-end runs through a real `Agent` and MAF's own function-invocation loop with a scripted chat client: allowed calls, denials aborting the run, `return_to_model`, Invariant denials and recorded gate decisions. The Foundry example has not been run against a live Foundry endpoint.
|
|
210
|
+
|
|
211
|
+
See [CHANGELOG.md](https://github.com/supersemantics/VSL-Microsoft-agent-framework/blob/HEAD/CHANGELOG.md).
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT -- see LICENSE. Copyright Super Semantics.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vsl-maf"
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
description = "Microsoft Agent Framework (agent-framework) adapter for VSL (VERBA Specification Language) governance primitives -- implements vsl-core's VSLAdapter conformance contract."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
# vsl-core, published on PyPI as super-semantics-vsl (imported as vsl_core).
|
|
14
|
+
# vsl_maf also checks the installed version on import (needs >= 0.2.0).
|
|
15
|
+
"super-semantics-vsl>=0.3,<0.4",
|
|
16
|
+
# Core only: the adapter needs FunctionMiddleware and MiddlewareFailure, not
|
|
17
|
+
# every model connector. Tested against 1.25.0.
|
|
18
|
+
"agent-framework-core>=1.25.0",
|
|
19
|
+
]
|
|
20
|
+
keywords = ["governance", "ai-safety", "guardrails", "agent-framework", "vsl", "verba", "microsoft-agent-framework"]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/supersemantics/VSL-Microsoft-agent-framework"
|
|
30
|
+
Repository = "https://github.com/supersemantics/VSL-Microsoft-agent-framework"
|
|
31
|
+
Changelog = "https://github.com/supersemantics/VSL-Microsoft-agent-framework/blob/HEAD/CHANGELOG.md"
|
|
32
|
+
Issues = "https://github.com/supersemantics/VSL-Microsoft-agent-framework/issues"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["pytest>=7", "pytest-asyncio>=0.23"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
asyncio_mode = "auto"
|
|
43
|
+
asyncio_default_fixture_loop_scope = "function"
|
vsl_maf-0.2.2/setup.cfg
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import vsl_core as _vsl_core
|
|
2
|
+
|
|
3
|
+
from .adapter import MAFAdapter
|
|
4
|
+
from .integration import VSLFunctionMiddleware, VSLGateDenied, tool_arguments
|
|
5
|
+
|
|
6
|
+
__version__ = "0.2.0"
|
|
7
|
+
|
|
8
|
+
# Gate-decision records (VerbaLedger.write_gate_decision) and the 2026.10 Gamma
|
|
9
|
+
# rule arrived in vsl-core 0.2.0; this package's docs and examples rely on both.
|
|
10
|
+
_MIN_VSL_CORE = (0, 2, 0)
|
|
11
|
+
_found = tuple(int(part) for part in _vsl_core.__version__.split(".")[:3])
|
|
12
|
+
if _found < _MIN_VSL_CORE:
|
|
13
|
+
raise ImportError(
|
|
14
|
+
f"vsl-maf {__version__} needs vsl-core >= 0.2.0, found {_vsl_core.__version__}. "
|
|
15
|
+
"Upgrade: pip install --upgrade git+https://github.com/supersemantics/VSL-Core.git"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"MAFAdapter",
|
|
20
|
+
"VSLFunctionMiddleware",
|
|
21
|
+
"VSLGateDenied",
|
|
22
|
+
"tool_arguments",
|
|
23
|
+
]
|