We’re examining how Microsoft’s Semantic Kernel controls AI-requested tool execution. Semantic Kernel is an orchestration framework that connects models to plugins, services, embeddings, and MCP transports; its Python kernel.py is the public Kernel façade that coordinates those subsystems. I’m Mahmoud Zalt, an AI solutions architect, and the important observation is that this class does far more than route calls: it decides which requests may proceed, how failures become model feedback, and which mutable values become history. We’ll see why an AI orchestrator earns trust by making refusal explicit—and by controlling the state, memory, and data exposure that surround execution.
The Kernel Is a Policy Boundary
The Kernel is not the model, plugin, embedding provider, or MCP server. It is the control desk in front of them: a single public interface that resolves functions, selects services, applies filters, and records outcomes while delegating specialized work to collaborators.
semantic-kernel/
└── python/semantic_kernel/
├── kernel.py ← public orchestration façade
│ ├── KernelFilterExtension → filters and call stacks
│ ├── KernelFunctionExtension → plugins and function lookup
│ ├── KernelServicesExtension → registered AI services
│ ├── KernelReliabilityExtension
│ ├── KernelFunction → tool and AI execution
│ ├── EmbeddingGeneratorBase → vector generation
│ ├── ChatHistory ← recorded tool results
│ └── connectors.mcp → MCP server adapter
└── functions/
├── kernel_function_from_prompt.py
└── kernel_plugin.py
Its mixin-based declaration makes that division visible:
class Kernel(
KernelFilterExtension,
KernelFunctionExtension,
KernelServicesExtension,
KernelReliabilityExtension,
):
"""The Kernel of Semantic Kernel."""
This design gives callers a stable entry point without embedding provider-specific behavior in one class. It also creates a review requirement: kernel.py cannot be understood in isolation, because function registries, filters, reliability behavior, and service selection live in inherited extensions. A façade should coordinate decisions; when it begins owning every implementation detail, it becomes impossible to reason about its boundary.
Refusal Is Part of Execution
Once a façade coordinates potentially unsafe capabilities, forwarding is no longer enough. The ordinary invoke path resolves a function and passes it KernelArguments, but it also establishes caller-visible semantics: supplied arguments are updated in place with keyword arguments, and OperationCancelledException becomes None. Callers must not treat every unsuccessful invocation as an exception.
Model-requested function calls need a stricter gate. A model proposing a tool name and JSON arguments is not authorization to run that tool. When FunctionChoiceBehavior.filters are present, the kernel derives allowed fully qualified names and rejects a proposed name outside that set:
allowed_functions = [
func.fully_qualified_name
for func in self.get_list_of_function_metadata(
function_behavior.filters
)
]
if function_call.name not in allowed_functions:
raise FunctionExecutionException(
f"Only functions: {allowed_functions} are allowed, "
f"{function_call.name} is not allowed."
)
This is an allowlist, not user authorization. It limits the tools a model may choose; it does not establish caller identity or prove that a caller may modify a tenant’s data. Identity and business permissions remain responsibilities of the server or function layer, particularly when functions are exposed through the experimental as_mcp_server adapter.
After name validation, invoke_function_call parses arguments, checks required and unexpected parameters, runs filter interception, invokes the function, snapshots the result, and adds tool content to ChatHistory. Invalid calls can become corrective history messages instead of escaping as exceptions, allowing the model to retry. That recovery is useful only if the application also retains a machine-readable outcome category; a conversational correction must not be mistaken for successful execution.
| Checkpoint | Question answered | Outcome |
|---|---|---|
| Name and allowlist | Is this tool available and permitted for model selection? | Corrective tool result |
| Argument parsing | Is the payload structurally valid? | Feedback to retry with valid JSON |
| Signature validation | Are required fields present and unknown fields absent? | Precise correction message |
| Filter stack | Which cross-cutting policies wrap execution? | Defined by filters and handlers |
| Result snapshot | Can later mutation rewrite recorded history? | Deep-copy returned value |
Mutable State Defines Isolation
The refusal boundary is only reliable when its state is predictable. The kernel mutates caller-owned KernelArguments, appends validation and result messages to ChatHistory, and writes embeddings into supplied records. Sharing any of those objects across concurrent invocations can mix argument values, reorder history, or expose partially updated embedding batches.
Deep-copying a tool result before converting it to history is an intentional correctness trade-off. If a tool returns a nested dictionary and later mutates it, the recorded conversation remains a snapshot rather than changing silently. For large object graphs, however, that snapshot consumes CPU and memory proportional to the copied graph.
clone makes a different trade-off: it copies plugin containers, selectors, and filter collections, while service-client objects remain shared. A cloned kernel therefore isolates configuration containers, not credentials, connection pools, or runtime identity. In a multi-tenant system, cloning is not a security sandbox; tenants that require separate credentials, quotas, or network identities need separately configured service clients.
Production Pressure Exposes Hidden Contracts
Small implementation seams become resource and security policies under load. add_embedding_to_object inspects inputs[0] to infer a list’s shape, so an empty list raises IndexError before any embedding work occurs. An early return should make the batch invariant explicit: zero records require zero work.
Streaming has a similar hidden cost. invoke_stream and invoke_prompt_stream retain streamed messages so they can merge content by choice_index into a final FunctionResult. With final aggregation enabled, local memory grows as O(s + a), where s is stream size and a is accumulated content. Incremental delivery does not guarantee bounded retention; centralizing the duplicated merge logic is useful, but incremental aggregation is the improvement that can reduce retained chunks.
Logging is another boundary. invoke_function_call logs raw function_call.arguments, which may contain personal data, credentials, customer text, or proprietary identifiers. Observability does not require raw payloads: function name, argument keys, payload size, request ID, duration, and outcome preserve operational value without turning logs into an uncontrolled data store.
The complexity of the tool-call path explains why these contracts are easy to miss: invoke_function_call spans 105 source lines, with cyclomatic complexity 16 and cognitive complexity 24. Its changes must be reviewed across validation, recovery, filters, execution, copying, and history mutation. Streaming metadata deserves the same scrutiny: invoke forwards metadata, while invoke_stream accepts it without passing it to function.invoke_stream; if metadata carries trace correlation, streaming may lose context.
| Signal | What it reveals | Use |
|---|---|---|
kernel.invoke_stream.buffered_chunks | Memory exposure from aggregation | Track p95 and maximum |
kernel.tool_call.validation_failures_total | Malformed or disallowed requests | Alert on sudden changes relative to call volume |
kernel.tool_call.duration_ms | Slow tools and filter overhead | Track p50, p95, and p99 by function and outcome |
kernel.embedding.batch_size | Payload-driven latency and memory pressure | Set limits from connector constraints |
Make the Boundary Explicit
The primary lesson is that an AI kernel is a policy boundary, not merely a router: it must explicitly decide what to accept, refuse, mutate, retain, and reveal between probabilistic model requests and deterministic software capabilities. Semantic Kernel demonstrates the shape of that boundary through function allowlists, signature validation, filter interception, corrective history, and result snapshots. The same mechanisms create operational obligations around authorization, shared state, memory retention, and sensitive logs.
- Separate model tool selection from authorization. Keep allowlists at the orchestration layer, but enforce identity and business permissions in servers and functions.
- Test state transitions, not only return values. Assert mutations to
KernelArguments,ChatHistory, embedding records, clone containers, and copied tool results. - Measure and minimize retained or exposed data. Guard empty batches, redact tool arguments by default, and track buffered streaming chunks before scale makes retention expensive.
The next improvement is not a thinner kernel at any cost. It is a kernel whose acceptance, refusal, state ownership, and resource trade-offs are visible enough to test and operate. That is how an orchestrator can safely connect increasingly capable models to real software actions.







