Semantic Kernel Adapter¶
The SemanticKernelAdapter registers an AUTO_FUNCTION_INVOCATION filter on a
Semantic Kernel Kernel instance. The filter intercepts every auto-invoked
tool call and enforces Edictum contracts around it.
When to use this¶
Add Edictum to your Semantic Kernel project when you need contract enforcement on auto-invoked functions. The register(kernel) method installs an AUTO_FUNCTION_INVOCATION filter that evaluates every tool call the kernel makes -- plugin methods, planner steps, any auto-invoked function -- without per-function wiring. The filter can replace context.function_result, so the on_postcondition_warn callback supports PII redaction. By default, denied calls terminate the current turn (terminate_on_deny=True), but you can set terminate_on_deny=False to let remaining tool calls proceed.
Installation¶
Integration¶
from edictum import Edictum
from edictum.adapters.semantic_kernel import SemanticKernelAdapter
from semantic_kernel import Kernel
kernel = Kernel()
guard = Edictum.from_yaml("contracts.yaml")
adapter = SemanticKernelAdapter(guard=guard)
adapter.register(kernel)
After calling register(kernel), every auto-invoked tool call on that
kernel passes through Edictum contract enforcement. No further wiring is needed.
Filter Behavior¶
The adapter registers a filter using
@kernel.filter(FilterTypes.AUTO_FUNCTION_INVOCATION). Inside the filter:
- Extracts the function name and arguments from the invocation context.
- Evaluates preconditions.
- On allow: calls
await next(context)to let Semantic Kernel execute the function, then evaluates postconditions againstcontext.function_result. - On deny: sets
context.function_resultto the denial string. The function is never executed. By default (terminate_on_deny=True), the kernel also stops further auto-invocations in the current turn. Withterminate_on_deny=False, remaining tool calls continue through contract enforcement normally.
PII Redaction Callback¶
Use on_postcondition_warn to transform tool output when postconditions flag
issues. The callback's return value replaces context.function_result:
import re
def redact_pii(result, findings):
text = str(result)
text = re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "[SSN REDACTED]", text)
text = re.sub(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b", "[EMAIL REDACTED]", text)
return text
adapter.register(kernel, on_postcondition_warn=redact_pii)
Controlling Termination on Denial¶
By default, when a tool call is denied, the adapter sets context.terminate = True,
which stops the kernel from making additional function calls in the same turn. This
is safe but aggressive — one denied tool prevents all remaining tools from executing.
Set terminate_on_deny=False to let the kernel continue with remaining tool calls
after a denial:
adapter = SemanticKernelAdapter(
guard=guard,
terminate_on_deny=False, # denied tools don't stop remaining calls
)
adapter.register(kernel)
The denied tool still receives a denial message and is never executed. Only the termination signal changes — subsequent tool calls proceed through contract enforcement as normal.
Known Limitations¶
-
Registration timing:
adapter.register(kernel)must be called before invoking prompts that trigger auto function calls. The filter is permanently registered on the kernel instance. -
Error detection: Beyond standard string-based error checking, the adapter also inspects Semantic Kernel
FunctionResultobjects for error metadata viaresult.metadata.get("error").
Full Working Example¶
import asyncio
from edictum import Edictum, Principal
from edictum.adapters.semantic_kernel import SemanticKernelAdapter
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.functions import kernel_function
# Build kernel
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion(service_id="chat", ai_model_id="gpt-4o-mini"))
# Define a plugin with tools
class FileOpsPlugin:
@kernel_function(name="read_file", description="Read a file")
def read_file(self, path: str) -> str:
with open(path) as f:
return f.read()
@kernel_function(name="list_files", description="List files in a directory")
def list_files(self, directory: str) -> str:
import os
return "\n".join(os.listdir(directory))
kernel.add_plugin(FileOpsPlugin(), "FileOps")
# Load contracts
guard = Edictum.from_yaml("contracts.yaml")
adapter = SemanticKernelAdapter(
guard=guard,
session_id="sk-session-01",
principal=Principal(user_id="analyst", role="data-team"),
)
adapter.register(kernel)
# Use the kernel -- contracts are enforced on all auto-invoked functions
async def main():
settings = kernel.get_prompt_execution_settings_from_service_id("chat")
settings.function_choice_behavior = "auto"
result = await kernel.invoke_prompt(
"List the files in the current directory",
settings=settings,
)
print(result)
asyncio.run(main())
Observe Mode¶
Deploy contracts without enforcement to see what would be denied:
guard = Edictum.from_yaml("contracts.yaml", mode="observe")
adapter = SemanticKernelAdapter(guard=guard)
adapter.register(kernel)
In observe mode, the filter always calls await next(context) to allow tool
execution, even for calls that would be denied. CALL_WOULD_DENY audit events
are emitted so you can review enforcement behavior before enabling it.