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.
The runner
Section titled “The runner”import sys, os, pathlib, asynciosys.path.insert(0, os.path.expanduser("~/agenttests"))import azcfg, hetoolfrom semantic_kernel import Kernelfrom semantic_kernel.functions import kernel_functionfrom semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletionfrom semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehaviorfrom semantic_kernel.contents import ChatHistoryfrom 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()Engine definition
Section titled “Engine definition”{ "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>" }}Adoption
Section titled “Adoption”- Keep your kernel, plugins and prompts as they are.
- Serialise
ChatHistorykeyed onsid; restore it when the file exists. - Build
OpenAIChatCompletionwith anAsyncOpenAIclient fromazcfg.load(). - Set
FunctionChoiceBehavior.Auto()if you want tools invoked automatically. - Seal the engine, point
external.engineat it, restart.
Gotchas
Section titled “Gotchas”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.