Skip to content

Semantic Kernel

Verified 2026-08-19 — PASS, 6s. Session persistence: ChatHistory serialize / restore.

Tools are kernel plugins, and FunctionChoiceBehavior.Auto() runs the call loop for you.

.NET alternative: Semantic Kernel is .NET-native, so a C# agent can be driven directly as a process engine without the Python adapter. The Python path is documented here because it is what was verified.


import sys, os, pathlib, asyncio
sys.path.insert(0, os.path.expanduser("~/agenttests"))
import azcfg, hetool
from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents import ChatHistory
from openai import AsyncOpenAI
SESS = pathlib.Path(os.path.expanduser("~/agenttests/semantickernel/sessions"))
SESS.mkdir(parents=True, exist_ok=True)
class OsTools:
@kernel_function(description="Return the agent's current working folder (absolute path).")
def get_working_folder(self) -> str:
return hetool.get_working_folder()
@kernel_function(description="Run a shell command in the working folder and return its output.")
def run_shell(self, command: str) -> str:
return hetool.run_shell(command)
async def invoke(sid, message):
c = azcfg.load()
client = AsyncOpenAI(base_url=c["base_url"], api_key=c["api_key"])
svc = OpenAIChatCompletion(ai_model_id=c["model"], async_client=client)
kernel = Kernel()
kernel.add_service(svc)
kernel.add_plugin(OsTools(), plugin_name="os")
settings = svc.instantiate_prompt_execution_settings()
settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
f = SESS / f"{sid}.json"
history = (ChatHistory.restore_chat_history(f.read_text()) if f.exists()
else ChatHistory(system_message="Be concise. Use tools for operational questions."))
history.add_user_message(message)
resp = await svc.get_chat_message_content(history, settings, kernel=kernel)
history.add_message(resp)
f.write_text(history.serialize())
return str(resp).strip()

{
"type": "process",
"file": "/home/you/semantickernel/.venv/bin/python3",
"nativeSession": true,
"argsNew": ["/home/you/he_adapter.py", "/home/you/semantickernel_session.py", "", "{message}"],
"argsResume": ["/home/you/he_adapter.py", "/home/you/semantickernel_session.py", "{sessionId}", "{message}"],
"replyField": "result",
"sessionIdField": "session_id",
"approve": false,
"workdir": "/home/you/work",
"proxy": { "shape": "openai", "baseUrlEnv": "OPENAI_BASE_URL", "keyEnv": "OPENAI_API_KEY",
"modelEnv": "OPENAI_MODEL", "model": "<route>|<provider model>" }
}

  1. Keep your kernel, plugins and prompts as they are.
  2. Serialise ChatHistory keyed on sid; restore it when the file exists.
  3. Build OpenAIChatCompletion with an AsyncOpenAI client from azcfg.load().
  4. Set FunctionChoiceBehavior.Auto() if you want tools invoked automatically.
  5. Seal the engine, point external.engine at it, restart.

FunctionChoiceBehavior.Auto() is required for tool calling. Without it the plugin is registered and never used — the model has no way to invoke it, so you get an agent that describes what it would do instead of doing it.

Pass kernel=kernel to get_chat_message_content. The service needs the kernel reference to resolve plugins; omit it and tools are invisible even with Auto() set.

async_client=, not a base URL. Construct AsyncOpenAI yourself with the injected route.

The system message belongs in the ChatHistory constructor, and only on first creation — a restored history already carries it. Adding it again each turn duplicates it.

Add the response back to the history before serialising, or the assistant’s turn is lost and the conversation reads as a series of unanswered user messages.