> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognisafe.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Semantic Kernel Agent Runtime

> Securing .NET and Python Semantic Kernel agents with Cognisafe runtime interception

Semantic Kernel's planner executes multi-step reasoning chains autonomously. Each step is an LLM call with its own prompt, context window, and potential for prompt injection, PII leakage, or runaway function invocation. Without observability at the call level, you cannot tell whether your SK agent is behaving as designed, being manipulated, or silently accruing cost.

Cognisafe intercepts every ChatCompletion call that SK makes — including planner steps, plugin-routed function calls, and memory retrievals that go through an LLM re-ranker. Because Cognisafe presents an OpenAI-compatible proxy endpoint, SK requires zero code changes beyond setting the base URL.

## What gets captured

| SK operation                            | How it appears in Cognisafe                                                                    |
| --------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `kernel.invoke_async` (single prompt)   | Single `llm_requests` row; prompt + completion logged                                          |
| Sequential planner step                 | One row per step; `agent_name` tag identifies the plan                                         |
| Stepwise planner reasoning step         | One row per reasoning iteration                                                                |
| Plugin/function call routed through LLM | Logged as a standard completion; function arguments visible in `request_body`                  |
| Semantic memory retrieval (embedding)   | Logged if the embedding call goes through the proxy; embeddings model appears in `model` field |
| Streaming completions                   | Proxy buffers the stream and logs the reconstructed completion                                 |

## Architecture

```text theme={null}
┌─────────────────────────────────────────────────────────────┐
│  Your application                                            │
│                                                              │
│  sk.Kernel  (Python or .NET)                                 │
│    ├─ SequentialPlanner / StepwisePlanner                    │
│    ├─ KernelPlugin (functions)                               │
│    └─ SemanticMemory (optional)                              │
└────────────────────────┬────────────────────────────────────┘
                         │ OpenAI-compatible HTTP
                         │ base_url = Cognisafe proxy
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  Cognisafe Proxy  (:8080)                                    │
│  - Authenticates PROXY_API_KEY                               │
│  - Forwards to real LLM provider                             │
│  - POSTs /internal/log async → Cognisafe API                 │
└──────────┬──────────────────────────┬───────────────────────┘
           │                          │ async log
           ▼                          ▼
┌──────────────────┐    ┌─────────────────────────────────────┐
│  LLM Provider    │    │  Cognisafe API + Safety Worker       │
│  (OpenAI /       │    │  - Persists to TimescaleDB           │
│   Azure OpenAI)  │    │  - Runs LLM-as-judge scorers                │
└──────────────────┘    │  - Fires alerts on threat_detected   │
                        └─────────────────────────────────────┘
```

## Python implementation

<Steps>
  <Step title="Install dependencies">
    ```bash theme={null}
    pip install semantic-kernel cognisafe
    ```
  </Step>

  <Step title="Configure Cognisafe and patch OpenAI">
    Call `cognisafe.configure` and `cognisafe.patch_openai` before constructing the SK kernel. The patch rewrites the OpenAI client's `base_url` — SK's `OpenAIChatCompletion` connector inherits this automatically.

    ```python theme={null}
    import semantic_kernel as sk
    from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
    from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbedding
    import cognisafe

    cognisafe.configure(
        api_key="csk_...",
        project_id="sk-agent",
        proxy_url="http://localhost:8080",   # or your deployed proxy URL
        api_url="http://localhost:8000",
    )
    cognisafe.patch_openai()

    # Build the kernel — base_url is already rewritten by the patch
    kernel = sk.Kernel()

    # Name the kernel so Cognisafe can attribute calls to this agent
    kernel.name = "customer-support-agent"

    kernel.add_service(
        OpenAIChatCompletion(
            service_id="default",
            ai_model_id="gpt-4o",
            # No base_url override needed — patch_openai() handled it
        )
    )

    # Embeddings also route through the proxy when patched
    kernel.add_service(
        OpenAITextEmbedding(
            service_id="embedding",
            ai_model_id="text-embedding-3-small",
        )
    )
    ```
  </Step>

  <Step title="Add plugins and invoke the planner">
    Plugins and function invocations are captured as part of the LLM calls that SK makes to orchestrate them. The arguments and results are visible in `request_body` and `response_body` in the Cognisafe dashboard.

    ```python theme={null}
    from semantic_kernel.planners import SequentialPlanner
    from semantic_kernel.core_plugins import MathPlugin, TextPlugin

    kernel.add_plugin(MathPlugin(), plugin_name="math")
    kernel.add_plugin(TextPlugin(), plugin_name="text")

    planner = SequentialPlanner(kernel, service_id="default")

    # Each step in the plan generates a separate logged LLM call
    plan = await planner.create_plan_async(
        "Calculate the 15% VAT on £349.99 and format it as a currency string"
    )
    result = await plan.invoke_async(kernel=kernel)
    print(result)
    ```
  </Step>
</Steps>

## .NET / C# implementation

SK for .NET uses `HttpClient` under the hood. Configure the proxy base URL on the `HttpClient` that the kernel's OpenAI connector uses.

<Steps>
  <Step title="Install packages">
    ```bash theme={null}
    dotnet add package Microsoft.SemanticKernel
    dotnet add package Azure.Identity
    ```
  </Step>

  <Step title="Configure the kernel with Cognisafe proxy">
    ```csharp theme={null}
    using Microsoft.SemanticKernel;
    using Microsoft.SemanticKernel.Connectors.OpenAI;
    using System.Net.Http;

    // Build an HttpClient that routes through the Cognisafe proxy.
    // The proxy authenticates via the Authorization header (PROXY_API_KEY).
    var httpClient = new HttpClient(new HttpClientHandler())
    {
        BaseAddress = new Uri("http://your-cognisafe-proxy:8080/"),
        DefaultRequestHeaders =
        {
            { "Authorization", $"Bearer {Environment.GetEnvironmentVariable("COGNISAFE_API_KEY")}" },
            { "X-Cognisafe-Project-ID", "sk-dotnet-agent" },
        },
    };

    var kernel = Kernel.CreateBuilder()
        .AddOpenAIChatCompletion(
            modelId: "gpt-4o",
            apiKey: "placeholder",          // proxy ignores this; uses PROXY_API_KEY
            httpClient: httpClient
        )
        .Build();

    // Tag the agent for attribution in the Cognisafe dashboard
    kernel.Data["agent_name"] = "claims-processing-agent";
    ```
  </Step>

  <Step title="Invoke with execution settings">
    ```csharp theme={null}
    using Microsoft.SemanticKernel.Connectors.OpenAI;

    var settings = new OpenAIPromptExecutionSettings
    {
        MaxTokens = 1024,
        Temperature = 0.2,
        // Tool call behaviour for function/plugin invocation
        ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
    };

    var result = await kernel.InvokePromptAsync(
        "Summarise the following claims and flag any that exceed £10,000: {{$input}}",
        new KernelArguments(settings) { ["input"] = claimsText }
    );
    Console.WriteLine(result);
    ```
  </Step>
</Steps>

## Plugin and function call security

SK's function-calling loop can invoke plugins repeatedly within a single plan. Each LLM call that includes function invocation instructions is logged separately. In the Cognisafe dashboard, filter by `agent_name = "customer-support-agent"` to see the full invocation chain for a single user request.

To detect dangerous function call patterns, configure a custom scorer in `evals/scorers.yaml`:

```yaml theme={null}
- name: sk_function_abuse
  type: keyword_list
  severity: high
  description: "Detects function names associated with data exfiltration or destructive operations"
  keywords:
    - send_email
    - delete_record
    - export_data
    - execute_sql
    - write_file
  match_on: request_body
```

This scorer runs on every SK LLM call and fires a `threat_detected` webhook if any of the listed function names appear in the tool call arguments.

## Planner patterns and observability

**SequentialPlanner** generates a complete plan before execution. In Cognisafe, you will see:

1. One LLM call for plan generation (typically a large `request_body` containing the goal and available plugin descriptions).
2. One LLM call per plan step during execution.

**StepwisePlanner** (ReAct-style) generates one reasoning step at a time. You will see a sequence of short-cycle calls, each containing the intermediate scratchpad. This makes it easy to identify loops, backtracking, or hallucinated tool names.

Filter requests by `model` and `agent_name` in the dashboard and sort by `created_at` to reconstruct the full reasoning chain for any agent execution.

## Semantic Memory (embeddings)

When SK calls a vector store to retrieve memories, the embedding generation call goes through the patched OpenAI client and is therefore logged. The `model` field will be `text-embedding-3-small` (or whichever embedding model you configure). These calls are low-cost but high-frequency — use the Cognisafe cost dashboard to track embedding spend separately from completion spend.

<Tip>
  Filter the Cognisafe requests view by `model LIKE 'text-embedding%'` to isolate memory retrieval activity from planning and completion calls.
</Tip>

## Agent tagging

Set `kernel.name` in Python or `kernel.Data["agent_name"]` in .NET before any invocation. Cognisafe reads the `X-Cognisafe-Agent-Name` header (injected by the SDK) and writes it to `llm_requests.agent_name`. All dashboard groupings, alert rules, and cost breakdowns are keyed on this field.

For multi-agent systems (e.g. an orchestrator kernel that spawns specialist sub-kernels), use distinct names:

```python theme={null}
orchestrator_kernel.name = "orchestrator"
search_kernel.name = "search-specialist"
summariser_kernel.name = "summariser"
```

Each sub-kernel's calls appear as a separate series in the Cognisafe agent breakdown view.

## Recommended alert rules for SK agents

Configure these in the Cognisafe alerting UI or via the API:

| Rule name               | Condition                                                                                   | Severity | Rationale                                                      |
| ----------------------- | ------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------- |
| `sk_excessive_steps`    | `step_count > 20` within a single `session_id`                                              | High     | Runaway planner loop; likely hallucination or prompt injection |
| `sk_system_prompt_leak` | `system_prompt_leakage` scorer triggers                                                     | Critical | Agent's system prompt visible in completion output             |
| `sk_pii_in_tool_args`   | `pii_detection` triggers on a request where `request_body` contains function call arguments | High     | PII being passed to external plugins                           |
| `sk_new_function_name`  | First occurrence of a function name not in the approved plugin registry                     | Medium   | Shadow plugin or confused deputy                               |

## Azure deployment

For production SK agents on Azure, deploy within the same AKS cluster as Cognisafe so all traffic stays within the VNet:

```text theme={null}
AKS namespace: agents
  ├─ Deployment: customer-support-agent  (SK Python app)
  ├─ Deployment: claims-processor        (SK .NET app)
  └─ Service: each app calls cognisafe-proxy.cognisafe.svc.cluster.local:8080
```

```yaml theme={null}
# agent-deployment.yaml (excerpt)
env:
  - name: COGNISAFE_API_KEY
    valueFrom:
      secretKeyRef:
        name: cognisafe-secrets
        key: api-key
  - name: COGNISAFE_PROXY_URL
    value: "http://cognisafe-proxy.cognisafe.svc.cluster.local:8080"
  - name: COGNISAFE_API_URL
    value: "http://cognisafe-api.cognisafe.svc.cluster.local:8000"
  - name: COGNISAFE_PROJECT_ID
    value: "sk-agents-prod"
```

The proxy forwards to Azure OpenAI via the private endpoint configured in the [Azure OpenAI + APIM pattern](/reference-architectures/azure-openai-apim).

<Warning>
  Semantic Kernel's `FunctionChoiceBehavior.Auto` in .NET 1.x will retry failed function calls silently. Each retry is a separate LLM call and will appear as a separate row in Cognisafe. If you see unexpectedly high call counts for an agent, check for retry loops in the planner execution trace.
</Warning>
