Skip to main content

The Kernel That Must Say No

A kernel cannot approve every request. “The Kernel That Must Say No” asks a practical engineering question: where should software draw the line between proceeding and refusing?

Code Cracking
15m read
#SoftwareEngineering#SystemDesign
The Kernel That Must Say No - Featured blog post image
Mahmoud Zalt

1:1 Mentor

Are you a software engineer moving into AI?

Let's have a call. I'll help you modernize your skills and learn the tools, systems, and architecture behind reliable AI products. One session or ongoing.

Vibe Coding
with Confidence

The Vibecoder's Handbook, from idea to production

4.8

Everything you need to know about shipping software with AI, from the App idea to production.

What it covers

  • 0IntroductionWhat this book is & how to read it
  • 1Set UpGet your tools and a running app ready
  • 2PlanStructure your idea into a clear specification
  • 3ArchitectLay out a modular codebase for your AI
Start Reading Free

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
The kernel coordinates specialized collaborators rather than implementing their work.

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.

CheckpointQuestion answeredOutcome
Name and allowlistIs this tool available and permitted for model selection?Corrective tool result
Argument parsingIs the payload structurally valid?Feedback to retry with valid JSON
Signature validationAre required fields present and unknown fields absent?Precise correction message
Filter stackWhich cross-cutting policies wrap execution?Defined by filters and handlers
Result snapshotCan 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.

SignalWhat it revealsUse
kernel.invoke_stream.buffered_chunksMemory exposure from aggregationTrack p95 and maximum
kernel.tool_call.validation_failures_totalMalformed or disallowed requestsAlert on sudden changes relative to call volume
kernel.tool_call.duration_msSlow tools and filter overheadTrack p50, p95, and p99 by function and outcome
kernel.embedding.batch_sizePayload-driven latency and memory pressureSet 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.

  1. Separate model tool selection from authorization. Keep allowlists at the orchestration layer, but enforce identity and business permissions in servers and functions.
  2. Test state transitions, not only return values. Assert mutations to KernelArguments, ChatHistory, embedding records, clone containers, and copied tool results.
  3. 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.

Full Source Code

Direct source from the upstream repository. Preview it inline or open it on GitHub.

heads/main/python/semantic_kernel/kernel.py

microsoft/semantic-kernel • refs

Read Code on GitHub

Thanks for reading! I hope this was useful. If you have questions or thoughts, feel free to reach out.

Content Creation Process: This article was generated via a semi-automated workflow using AI tools. I prepared the strategic framework, including specific prompts and data sources. From there, the automation system conducted the research, analysis, and writing. The content passed through automated verification steps before being finalized and published without manual intervention.

Mahmoud Zalt

About the Author

I’m Zalt, a technologist with 16+ years of experience, passionate about designing and building AI systems that move us closer to a world where machines handle everything and humans reclaim wonder.

Let's connect if you're working on interesting AI projects, looking for technical advice or want to discuss anything.

Support this content

Share this article

Stay in touch

An occasional note when I build or write something new. Leave anytime.

Hire AI Employees

Hire AI Employees that work 24/7. No code.