diff --git a/backend/internal/engine/client_test.go b/backend/internal/engine/client_test.go
index 24799a6..a22f32c 100644
--- a/backend/internal/engine/client_test.go
+++ b/backend/internal/engine/client_test.go
@@ -189,6 +189,54 @@ func TestExtractTraceLLMWithParent(t *testing.T) {
}
}
+// TestExtractTraceLLMReasoning covers a client that sends reasoning:
+// it becomes the chain of thought and the output moves to its own
+// labelled line. Both are wrapped, reasoning is model-generated and no
+// more trustworthy than the response.
+func TestExtractTraceLLMReasoning(t *testing.T) {
+ ev := &pb.PairedEvent{
+ PairType: pb.PairType_PAIR_TYPE_LLM,
+ Agent: &pb.AgentContext{AgentId: "billing"},
+ Data: &pb.PairedEvent_Llm{
+ Llm: &pb.LlmPairData{
+ Model: "claude-opus-5",
+ Output: "Refund issued.",
+ Reasoning: "Order is 5 days old so the policy allows it.",
+ },
+ },
+ }
+ const guid = "test-guid"
+ got := extractTrace(ev, guid)
+ wantWrapped := []string{
+ `Chain of Thought: Order is 5 days old so the policy allows it.`,
+ `Response: Refund issued.`,
+ }
+ for _, w := range wantWrapped {
+ if !strings.Contains(got, w) {
+ t.Errorf("extractTrace missing line %q in:\n%s", w, got)
+ }
+ }
+}
+
+// TestExtractTraceLLMReasoningAbsentUnchanged pins the pre-reasoning
+// rendering for clients that never set the field: output stays the
+// chain of thought and no Response line appears.
+func TestExtractTraceLLMReasoningAbsentUnchanged(t *testing.T) {
+ ev := &pb.PairedEvent{
+ PairType: pb.PairType_PAIR_TYPE_LLM,
+ Agent: &pb.AgentContext{AgentId: "billing"},
+ Data: &pb.PairedEvent_Llm{
+ Llm: &pb.LlmPairData{Model: "gpt-4o-mini", Output: "Refund issued."},
+ },
+ }
+ got := extractTrace(ev, "test-guid")
+ want := "\nClassify this agent trace:\n\n" +
+ `Chain of Thought: Refund issued.`
+ if got != want {
+ t.Errorf("rendering changed for a client without reasoning:\ngot: %q\nwant: %q", got, want)
+ }
+}
+
// -----------------------------------------------------------------
// HTTPClient.Classify
// -----------------------------------------------------------------
diff --git a/backend/internal/engine/trace.go b/backend/internal/engine/trace.go
index c55b6f1..75ef716 100644
--- a/backend/internal/engine/trace.go
+++ b/backend/internal/engine/trace.go
@@ -68,11 +68,20 @@ func writeLLMSection(b *strings.Builder, llm *pb.LlmPairData, guid string) {
if llm == nil {
return
}
- writeLabelled(b, "Chain of Thought: ", llm.Output, guid)
+ // Clients that send reasoning put it here; older ones send only output,
+ // which has always been rendered under this label.
+ cot := llm.Reasoning
+ if cot == "" {
+ cot = llm.Output
+ }
+ writeLabelled(b, "Chain of Thought: ", cot, guid)
+ if llm.Reasoning != "" {
+ writeLabelled(b, "Response: ", llm.Output, guid)
+ }
if len(llm.ToolCalls) == 0 {
return
}
- if llm.Output != "" {
+ if cot != "" {
b.WriteString("\n")
}
b.WriteString("Tool Calls:\n")
diff --git a/backend/internal/proto/event.pb.go b/backend/internal/proto/event.pb.go
index fa30119..a6b7252 100644
--- a/backend/internal/proto/event.pb.go
+++ b/backend/internal/proto/event.pb.go
@@ -1,3 +1,6 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (c) 2026 SecureAgentics
+
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
@@ -448,7 +451,9 @@ type LlmPairData struct {
// Tool calls the model decided to make.
ToolCalls []*ToolCall `protobuf:"bytes,4,rep,name=tool_calls,json=toolCalls,proto3" json:"tool_calls,omitempty"`
// Token usage counters.
- Usage *TokenUsage `protobuf:"bytes,5,opt,name=usage,proto3" json:"usage,omitempty"`
+ Usage *TokenUsage `protobuf:"bytes,5,opt,name=usage,proto3" json:"usage,omitempty"`
+ // The model's reasoning / chain of thought.
+ Reasoning string `protobuf:"bytes,6,opt,name=reasoning,proto3" json:"reasoning,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -518,6 +523,13 @@ func (x *LlmPairData) GetUsage() *TokenUsage {
return nil
}
+func (x *LlmPairData) GetReasoning() string {
+ if x != nil {
+ return x.Reasoning
+ }
+ return ""
+}
+
// ToolPairData is the payload for PAIR_TYPE_TOOL events: one tool_start
// folded with its matching tool_end.
type ToolPairData struct {
@@ -1597,14 +1609,15 @@ const file_event_proto_rawDesc = "" +
"\fAgentContext\x12\x19\n" +
"\bagent_id\x18\x01 \x01(\tR\aagentId\x12#\n" +
"\rsystem_prompt\x18\x02 \x01(\tR\fsystemPrompt\x12)\n" +
- "\x10user_instruction\x18\x03 \x01(\tR\x0fuserInstruction\"\xeb\x01\n" +
+ "\x10user_instruction\x18\x03 \x01(\tR\x0fuserInstruction\"\x89\x02\n" +
"\vLlmPairData\x12\x14\n" +
"\x05model\x18\x01 \x01(\tR\x05model\x12;\n" +
"\bmessages\x18\x02 \x03(\v2\x1f.adrian.core_api.v1.ChatMessageR\bmessages\x12\x16\n" +
"\x06output\x18\x03 \x01(\tR\x06output\x12;\n" +
"\n" +
"tool_calls\x18\x04 \x03(\v2\x1c.adrian.core_api.v1.ToolCallR\ttoolCalls\x124\n" +
- "\x05usage\x18\x05 \x01(\v2\x1e.adrian.core_api.v1.TokenUsageR\x05usage\"{\n" +
+ "\x05usage\x18\x05 \x01(\v2\x1e.adrian.core_api.v1.TokenUsageR\x05usage\x12\x1c\n" +
+ "\treasoning\x18\x06 \x01(\tR\treasoning\"{\n" +
"\fToolPairData\x12\x1b\n" +
"\ttool_name\x18\x01 \x01(\tR\btoolName\x12 \n" +
"\ftool_call_id\x18\x02 \x01(\tR\n" +
diff --git a/backend/internal/ws/pairing.go b/backend/internal/ws/pairing.go
index e064919..5b6d6a6 100644
--- a/backend/internal/ws/pairing.go
+++ b/backend/internal/ws/pairing.go
@@ -36,6 +36,7 @@ func pairedEventToJSON(ev *pb.PairedEvent) (string, error) {
view["llm"] = map[string]any{
"model": llm.Model,
"output": llm.Output,
+ "reasoning": llm.Reasoning,
"messages": chatMessagesToJSON(llm.Messages),
"tool_calls": toolCallsToJSON(llm.ToolCalls),
"usage": tokenUsageToJSON(llm.Usage),
diff --git a/backend/internal/ws/pairing_test.go b/backend/internal/ws/pairing_test.go
new file mode 100644
index 0000000..e010ca8
--- /dev/null
+++ b/backend/internal/ws/pairing_test.go
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (c) 2026 SecureAgentics
+
+package ws
+
+import (
+ "encoding/json"
+ "testing"
+
+ pb "github.com/secureagentics/Adrian/backend/internal/proto"
+)
+
+// TestPairedEventToJSONReasoning checks the reasoning field survives the
+// proto to JSON conversion, the dashboard and the engine read the
+// persisted payload rather than the wire message.
+func TestPairedEventToJSONReasoning(t *testing.T) {
+ ev := &pb.PairedEvent{
+ EventId: "ev-1",
+ PairType: pb.PairType_PAIR_TYPE_LLM,
+ Data: &pb.PairedEvent_Llm{
+ Llm: &pb.LlmPairData{
+ Model: "claude-opus-5",
+ Output: "Refund issued.",
+ Reasoning: "Order is within the refund window.",
+ },
+ },
+ }
+ payload, err := pairedEventToJSON(ev)
+ if err != nil {
+ t.Fatalf("pairedEventToJSON: %v", err)
+ }
+ var got struct {
+ LLM struct {
+ Output string `json:"output"`
+ Reasoning string `json:"reasoning"`
+ } `json:"llm"`
+ }
+ if err := json.Unmarshal([]byte(payload), &got); err != nil {
+ t.Fatalf("unmarshal payload: %v", err)
+ }
+ if got.LLM.Reasoning != "Order is within the refund window." {
+ t.Errorf("reasoning = %q, want it persisted", got.LLM.Reasoning)
+ }
+ if got.LLM.Output != "Refund issued." {
+ t.Errorf("output = %q, want it left alone", got.LLM.Output)
+ }
+}
diff --git a/backend/proto/event.proto b/backend/proto/event.proto
index daf7e59..34bf842 100644
--- a/backend/proto/event.proto
+++ b/backend/proto/event.proto
@@ -70,6 +70,8 @@ message LlmPairData {
repeated ToolCall tool_calls = 4;
// Token usage counters.
TokenUsage usage = 5;
+ // The model's reasoning / chain of thought.
+ string reasoning = 6;
}
// ToolPairData is the payload for PAIR_TYPE_TOOL events: one tool_start
diff --git a/integrations/claude-code/adrian_cc/agent.py b/integrations/claude-code/adrian_cc/agent.py
index b87a3e0..a3c857c 100644
--- a/integrations/claude-code/adrian_cc/agent.py
+++ b/integrations/claude-code/adrian_cc/agent.py
@@ -656,7 +656,6 @@ async def _ws_send_event(
"policy_m3": policy.policy_m3,
"policy_m4": policy.policy_m4,
}
- result["source_ack"] = sf.login_ack.source
# --- Send event ---
batch = pb.PairedEventBatch(events=[event])
@@ -754,8 +753,8 @@ async def _verify_connection() -> dict[str, Any]:
"""Connect and SessionLogin only, with no event and no verdict.
Confirms the URL is reachable and the API key authenticates. Returns
- {ok, source_ack, mode_name} or {ok: False, error}. Never touches the key
- value beyond the auth header.
+ {ok, mode_name} or {ok: False, error}. Never touches the key value
+ beyond the auth header.
"""
headers: dict[str, str] = {}
if ADRIAN_API_KEY:
@@ -782,7 +781,6 @@ async def _verify_connection() -> dict[str, Any]:
if sf.WhichOneof("frame") == "login_ack":
return {
"ok": True,
- "source_ack": sf.login_ack.source,
"mode_name": _mode_name(sf.login_ack.policy.mode),
}
return {"ok": False, "error": "server did not send a LoginAck"}
@@ -802,10 +800,7 @@ def _handle_verify() -> None:
sys.exit(1)
res = asyncio.run(_verify_connection())
if res.get("ok"):
- print(
- f"OK: connected to {ADRIAN_WS_URL} "
- f"(source_ack={res.get('source_ack')!r}, backend mode={res.get('mode_name')})"
- )
+ print(f"OK: connected to {ADRIAN_WS_URL} (backend mode={res.get('mode_name')})")
sys.exit(0)
print(
f"FAIL: {res.get('error')} (check ADRIAN_WS_URL and ADRIAN_API_KEY in ~/.adrian/.env)"
diff --git a/integrations/claude-code/adrian_cc/proto/event_pb2.py b/integrations/claude-code/adrian_cc/proto/event_pb2.py
index d03816d..8792ed6 100644
--- a/integrations/claude-code/adrian_cc/proto/event_pb2.py
+++ b/integrations/claude-code/adrian_cc/proto/event_pb2.py
@@ -1,12 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
# source: event.proto
-# Protobuf Python Version: 4.25.3
+# Protobuf Python Version: 6.33.5
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 6,
+ 33,
+ 5,
+ '',
+ 'event.proto'
+)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
@@ -15,76 +25,77 @@
from .buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x65vent.proto\x12\x12\x61\x64rian.core_api.v1\x1a\x1b\x62uf/validate/validate.proto\";\n\x0b\x43hatMessage\x12\x12\n\x04role\x18\x01 \x01(\tR\x04role\x12\x18\n\x07\x63ontent\x18\x02 \x01(\tR\x07\x63ontent\"K\n\x08ToolCall\x12\x1b\n\x04name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x04name\x12\x12\n\x04\x61rgs\x18\x02 \x01(\tR\x04\x61rgs\x12\x0e\n\x02id\x18\x03 \x01(\tR\x02id\"\x9c\x01\n\nTokenUsage\x12,\n\rprompt_tokens\x18\x01 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00R\x0cpromptTokens\x12\x34\n\x11\x63ompletion_tokens\x18\x02 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00R\x10\x63ompletionTokens\x12*\n\x0ctotal_tokens\x18\x03 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00R\x0btotalTokens\"y\n\x0c\x41gentContext\x12\x19\n\x08\x61gent_id\x18\x01 \x01(\tR\x07\x61gentId\x12#\n\rsystem_prompt\x18\x02 \x01(\tR\x0csystemPrompt\x12)\n\x10user_instruction\x18\x03 \x01(\tR\x0fuserInstruction\"\xeb\x01\n\x0bLlmPairData\x12\x14\n\x05model\x18\x01 \x01(\tR\x05model\x12;\n\x08messages\x18\x02 \x03(\x0b\x32\x1f.adrian.core_api.v1.ChatMessageR\x08messages\x12\x16\n\x06output\x18\x03 \x01(\tR\x06output\x12;\n\ntool_calls\x18\x04 \x03(\x0b\x32\x1c.adrian.core_api.v1.ToolCallR\ttoolCalls\x12\x34\n\x05usage\x18\x05 \x01(\x0b\x32\x1e.adrian.core_api.v1.TokenUsageR\x05usage\"\x84\x01\n\x0cToolPairData\x12$\n\ttool_name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x08toolName\x12 \n\x0ctool_call_id\x18\x02 \x01(\tR\ntoolCallId\x12\x14\n\x05input\x18\x03 \x01(\tR\x05input\x12\x16\n\x06output\x18\x04 \x01(\tR\x06output\"\xe3\x04\n\x0bPairedEvent\x12\"\n\x08\x65vent_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x07\x65ventId\x12#\n\rinvocation_id\x18\x02 \x01(\tR\x0cinvocationId\x12&\n\nsession_id\x18\x03 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\tsessionId\x12\x15\n\x06run_id\x18\x04 \x01(\tR\x05runId\x12\"\n\rparent_run_id\x18\x05 \x01(\tR\x0bparentRunId\x12\x1c\n\ttimestamp\x18\x06 \x01(\tR\ttimestamp\x12\x41\n\tpair_type\x18\x07 \x01(\x0e\x32\x1c.adrian.core_api.v1.PairTypeB\x06\xbaH\x03\xc8\x01\x01R\x08pairType\x12\x36\n\x05\x61gent\x18\x08 \x01(\x0b\x32 .adrian.core_api.v1.AgentContextR\x05\x61gent\x12\x38\n\x06parent\x18\t \x01(\x0b\x32 .adrian.core_api.v1.AgentContextR\x06parent\x12\x33\n\x03llm\x18\n \x01(\x0b\x32\x1f.adrian.core_api.v1.LlmPairDataH\x00R\x03llm\x12\x36\n\x04tool\x18\x0b \x01(\x0b\x32 .adrian.core_api.v1.ToolPairDataH\x00R\x04tool\x12#\n\rmetadata_json\x18\x14 \x01(\x0cR\x0cmetadataJson\x12#\n\rconnection_id\x18\x0c \x01(\tR\x0c\x63onnectionId\x12\x16\n\x06source\x18\x15 \x01(\tR\x06sourceB\x06\n\x04\x64\x61ta\"K\n\x10PairedEventBatch\x12\x37\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1f.adrian.core_api.v1.PairedEventR\x06\x65vents\"b\n\tMcpServer\x12\x1b\n\x04name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x04name\x12\x1c\n\ttransport\x18\x02 \x01(\tR\ttransport\x12\x1a\n\x08\x65ndpoint\x18\x03 \x01(\tR\x08\x65ndpoint\"G\n\x0cMcpInventory\x12\x37\n\x07servers\x18\x01 \x03(\x0b\x32\x1d.adrian.core_api.v1.McpServerR\x07servers\"<\n\x08LLMStack\x12\x1a\n\x08provider\x18\x01 \x01(\tR\x08provider\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\"\xe7\x01\n\x0cSessionLogin\x12&\n\nsession_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\tsessionId\x12\x39\n\tllm_stack\x18\x02 \x01(\x0b\x32\x1c.adrian.core_api.v1.LLMStackR\x08llmStack\x12%\n\x0eschema_version\x18\x04 \x01(\rR\rschemaVersion\x12\x16\n\x06source\x18\x05 \x01(\tR\x06source\x12#\n\rconnection_id\x18\x06 \x01(\tR\x0c\x63onnectionIdJ\x04\x08\x03\x10\x04R\nblock_mode\"\xf1\x01\n\x0b\x43lientFrame\x12\x38\n\x05login\x18\x01 \x01(\x0b\x32 .adrian.core_api.v1.SessionLoginH\x00R\x05login\x12I\n\x0cpaired_batch\x18\x03 \x01(\x0b\x32$.adrian.core_api.v1.PairedEventBatchH\x00R\x0bpairedBatch\x12G\n\rmcp_inventory\x18\x04 \x01(\x0b\x32 .adrian.core_api.v1.McpInventoryH\x00R\x0cmcpInventoryB\x07\n\x05\x66rameJ\x04\x08\x02\x10\x03R\x05\x62\x61tch\"\xb2\x01\n\x0ePolicySnapshot\x12,\n\x04mode\x18\x01 \x01(\x0e\x32\x18.adrian.core_api.v1.ModeR\x04mode\x12\x1b\n\tpolicy_m0\x18\x02 \x01(\x08R\x08policyM0\x12\x1b\n\tpolicy_m2\x18\x03 \x01(\x08R\x08policyM2\x12\x1b\n\tpolicy_m3\x18\x04 \x01(\x08R\x08policyM3\x12\x1b\n\tpolicy_m4\x18\x05 \x01(\x08R\x08policyM4\"=\n\x0cHitlResponse\x12-\n\x12\x63ontinue_execution\x18\x01 \x01(\x08R\x11\x63ontinueExecution\"^\n\x08LoginAck\x12:\n\x06policy\x18\x01 \x01(\x0b\x32\".adrian.core_api.v1.PolicySnapshotR\x06policy\x12\x16\n\x06source\x18\x02 \x01(\tR\x06source\"\x8c\x01\n\x0bServerFrame\x12;\n\tlogin_ack\x18\x01 \x01(\x0b\x32\x1c.adrian.core_api.v1.LoginAckH\x00R\x08loginAck\x12\x37\n\x07verdict\x18\x02 \x01(\x0b\x32\x1b.adrian.core_api.v1.VerdictH\x00R\x07verdictB\x07\n\x05\x66rame\"\x88\x02\n\x07Verdict\x12\"\n\x08\x65vent_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x07\x65ventId\x12&\n\nsession_id\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\tsessionId\x12\x19\n\x08mad_code\x18\x04 \x01(\tR\x07madCode\x12:\n\x06policy\x18\x06 \x01(\x0b\x32\".adrian.core_api.v1.PolicySnapshotR\x06policy\x12\x34\n\x04hitl\x18\x07 \x01(\x0b\x32 .adrian.core_api.v1.HitlResponseR\x04hitlJ\x04\x08\x03\x10\x04J\x04\x08\x05\x10\x06R\x0e\x63lassificationR\x08\x65scalate*L\n\x08PairType\x12\x19\n\x15PAIR_TYPE_UNSPECIFIED\x10\x00\x12\x11\n\rPAIR_TYPE_LLM\x10\x01\x12\x12\n\x0ePAIR_TYPE_TOOL\x10\x02*K\n\x04Mode\x12\x14\n\x10MODE_UNSPECIFIED\x10\x00\x12\x0e\n\nMODE_ALERT\x10\x01\x12\r\n\tMODE_HITL\x10\x02\x12\x0e\n\nMODE_BLOCK\x10\x03\x42\x31Z/github.com/secure-agentics/adrian/server/pkg/pbb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x65vent.proto\x12\x12\x61\x64rian.core_api.v1\x1a\x1b\x62uf/validate/validate.proto\",\n\x0b\x43hatMessage\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\";\n\x08ToolCall\x12\x15\n\x04name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x0c\n\x04\x61rgs\x18\x02 \x01(\t\x12\n\n\x02id\x18\x03 \x01(\t\"o\n\nTokenUsage\x12\x1e\n\rprompt_tokens\x18\x01 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00\x12\"\n\x11\x63ompletion_tokens\x18\x02 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00\x12\x1d\n\x0ctotal_tokens\x18\x03 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00\"Q\n\x0c\x41gentContext\x12\x10\n\x08\x61gent_id\x18\x01 \x01(\t\x12\x15\n\rsystem_prompt\x18\x02 \x01(\t\x12\x18\n\x10user_instruction\x18\x03 \x01(\t\"\xd3\x01\n\x0bLlmPairData\x12\r\n\x05model\x18\x01 \x01(\t\x12\x31\n\x08messages\x18\x02 \x03(\x0b\x32\x1f.adrian.core_api.v1.ChatMessage\x12\x0e\n\x06output\x18\x03 \x01(\t\x12\x30\n\ntool_calls\x18\x04 \x03(\x0b\x32\x1c.adrian.core_api.v1.ToolCall\x12-\n\x05usage\x18\x05 \x01(\x0b\x32\x1e.adrian.core_api.v1.TokenUsage\x12\x11\n\treasoning\x18\x06 \x01(\t\"_\n\x0cToolPairData\x12\x1a\n\ttool_name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x14\n\x0ctool_call_id\x18\x02 \x01(\t\x12\r\n\x05input\x18\x03 \x01(\t\x12\x0e\n\x06output\x18\x04 \x01(\t\"\xda\x03\n\x0bPairedEvent\x12\x19\n\x08\x65vent_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x15\n\rinvocation_id\x18\x02 \x01(\t\x12\x1b\n\nsession_id\x18\x03 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x0e\n\x06run_id\x18\x04 \x01(\t\x12\x15\n\rparent_run_id\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\t\x12\x37\n\tpair_type\x18\x07 \x01(\x0e\x32\x1c.adrian.core_api.v1.PairTypeB\x06\xbaH\x03\xc8\x01\x01\x12/\n\x05\x61gent\x18\x08 \x01(\x0b\x32 .adrian.core_api.v1.AgentContext\x12\x30\n\x06parent\x18\t \x01(\x0b\x32 .adrian.core_api.v1.AgentContext\x12.\n\x03llm\x18\n \x01(\x0b\x32\x1f.adrian.core_api.v1.LlmPairDataH\x00\x12\x30\n\x04tool\x18\x0b \x01(\x0b\x32 .adrian.core_api.v1.ToolPairDataH\x00\x12\x15\n\rconnection_id\x18\x0c \x01(\t\x12\x15\n\rmetadata_json\x18\x14 \x01(\x0c\x12\x0e\n\x06source\x18\x15 \x01(\tB\x06\n\x04\x64\x61ta\"C\n\x10PairedEventBatch\x12/\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1f.adrian.core_api.v1.PairedEvent\"G\n\tMcpServer\x12\x15\n\x04name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x11\n\ttransport\x18\x02 \x01(\t\x12\x10\n\x08\x65ndpoint\x18\x03 \x01(\t\">\n\x0cMcpInventory\x12.\n\x07servers\x18\x01 \x03(\x0b\x32\x1d.adrian.core_api.v1.McpServer\"+\n\x08LLMStack\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\"\xad\x01\n\x0cSessionLogin\x12\x1b\n\nsession_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12/\n\tllm_stack\x18\x02 \x01(\x0b\x32\x1c.adrian.core_api.v1.LLMStack\x12\x16\n\x0eschema_version\x18\x04 \x01(\r\x12\x0e\n\x06source\x18\x05 \x01(\t\x12\x15\n\rconnection_id\x18\x06 \x01(\tJ\x04\x08\x03\x10\x04R\nblock_mode\"\xcf\x01\n\x0b\x43lientFrame\x12\x31\n\x05login\x18\x01 \x01(\x0b\x32 .adrian.core_api.v1.SessionLoginH\x00\x12<\n\x0cpaired_batch\x18\x03 \x01(\x0b\x32$.adrian.core_api.v1.PairedEventBatchH\x00\x12\x39\n\rmcp_inventory\x18\x04 \x01(\x0b\x32 .adrian.core_api.v1.McpInventoryH\x00\x42\x07\n\x05\x66rameJ\x04\x08\x02\x10\x03R\x05\x62\x61tch\"\xad\x01\n\x0ePolicySnapshot\x12&\n\x04mode\x18\x01 \x01(\x0e\x32\x18.adrian.core_api.v1.Mode\x12\x11\n\tpolicy_m0\x18\x02 \x01(\x08\x12\x11\n\tpolicy_m2\x18\x03 \x01(\x08\x12\x11\n\tpolicy_m3\x18\x04 \x01(\x08\x12\x11\n\tpolicy_m4\x18\x05 \x01(\x08\x12\'\n\x1f\x66\x61il_closed_on_classifier_error\x18\x06 \x01(\x08\"*\n\x0cHitlResponse\x12\x1a\n\x12\x63ontinue_execution\x18\x01 \x01(\x08\">\n\x08LoginAck\x12\x32\n\x06policy\x18\x01 \x01(\x0b\x32\".adrian.core_api.v1.PolicySnapshot\"y\n\x0bServerFrame\x12\x31\n\tlogin_ack\x18\x01 \x01(\x0b\x32\x1c.adrian.core_api.v1.LoginAckH\x00\x12.\n\x07verdict\x18\x02 \x01(\x0b\x32\x1b.adrian.core_api.v1.VerdictH\x00\x42\x07\n\x05\x66rame\"\x90\x02\n\x07Verdict\x12\x19\n\x08\x65vent_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x1b\n\nsession_id\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x10\n\x08mad_code\x18\x04 \x01(\t\x12\x32\n\x06policy\x18\x06 \x01(\x0b\x32\".adrian.core_api.v1.PolicySnapshot\x12.\n\x04hitl\x18\x07 \x01(\x0b\x32 .adrian.core_api.v1.HitlResponse\x12\x31\n\x06status\x18\x08 \x01(\x0e\x32!.adrian.core_api.v1.VerdictStatusJ\x04\x08\x03\x10\x04J\x04\x08\x05\x10\x06R\x0e\x63lassificationR\x08\x65scalate*L\n\x08PairType\x12\x19\n\x15PAIR_TYPE_UNSPECIFIED\x10\x00\x12\x11\n\rPAIR_TYPE_LLM\x10\x01\x12\x12\n\x0ePAIR_TYPE_TOOL\x10\x02*K\n\x04Mode\x12\x14\n\x10MODE_UNSPECIFIED\x10\x00\x12\x0e\n\nMODE_ALERT\x10\x01\x12\r\n\tMODE_HITL\x10\x02\x12\x0e\n\nMODE_BLOCK\x10\x03*`\n\rVerdictStatus\x12\x1e\n\x1aVERDICT_STATUS_UNSPECIFIED\x10\x00\x12\x15\n\x11VERDICT_STATUS_OK\x10\x01\x12\x18\n\x14VERDICT_STATUS_ERROR\x10\x02\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'event_pb2', _globals)
-if _descriptor._USE_C_DESCRIPTORS == False:
- _globals['DESCRIPTOR']._options = None
- _globals['DESCRIPTOR']._serialized_options = b'Z/github.com/secure-agentics/adrian/server/pkg/pb'
- _globals['_TOOLCALL'].fields_by_name['name']._options = None
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_TOOLCALL'].fields_by_name['name']._loaded_options = None
_globals['_TOOLCALL'].fields_by_name['name']._serialized_options = b'\272H\004r\002\020\001'
- _globals['_TOKENUSAGE'].fields_by_name['prompt_tokens']._options = None
+ _globals['_TOKENUSAGE'].fields_by_name['prompt_tokens']._loaded_options = None
_globals['_TOKENUSAGE'].fields_by_name['prompt_tokens']._serialized_options = b'\272H\004\032\002(\000'
- _globals['_TOKENUSAGE'].fields_by_name['completion_tokens']._options = None
+ _globals['_TOKENUSAGE'].fields_by_name['completion_tokens']._loaded_options = None
_globals['_TOKENUSAGE'].fields_by_name['completion_tokens']._serialized_options = b'\272H\004\032\002(\000'
- _globals['_TOKENUSAGE'].fields_by_name['total_tokens']._options = None
+ _globals['_TOKENUSAGE'].fields_by_name['total_tokens']._loaded_options = None
_globals['_TOKENUSAGE'].fields_by_name['total_tokens']._serialized_options = b'\272H\004\032\002(\000'
- _globals['_TOOLPAIRDATA'].fields_by_name['tool_name']._options = None
+ _globals['_TOOLPAIRDATA'].fields_by_name['tool_name']._loaded_options = None
_globals['_TOOLPAIRDATA'].fields_by_name['tool_name']._serialized_options = b'\272H\004r\002\020\001'
- _globals['_PAIREDEVENT'].fields_by_name['event_id']._options = None
+ _globals['_PAIREDEVENT'].fields_by_name['event_id']._loaded_options = None
_globals['_PAIREDEVENT'].fields_by_name['event_id']._serialized_options = b'\272H\004r\002\020\001'
- _globals['_PAIREDEVENT'].fields_by_name['session_id']._options = None
+ _globals['_PAIREDEVENT'].fields_by_name['session_id']._loaded_options = None
_globals['_PAIREDEVENT'].fields_by_name['session_id']._serialized_options = b'\272H\004r\002\020\001'
- _globals['_PAIREDEVENT'].fields_by_name['pair_type']._options = None
+ _globals['_PAIREDEVENT'].fields_by_name['pair_type']._loaded_options = None
_globals['_PAIREDEVENT'].fields_by_name['pair_type']._serialized_options = b'\272H\003\310\001\001'
- _globals['_MCPSERVER'].fields_by_name['name']._options = None
+ _globals['_MCPSERVER'].fields_by_name['name']._loaded_options = None
_globals['_MCPSERVER'].fields_by_name['name']._serialized_options = b'\272H\004r\002\020\001'
- _globals['_SESSIONLOGIN'].fields_by_name['session_id']._options = None
+ _globals['_SESSIONLOGIN'].fields_by_name['session_id']._loaded_options = None
_globals['_SESSIONLOGIN'].fields_by_name['session_id']._serialized_options = b'\272H\004r\002\020\001'
- _globals['_VERDICT'].fields_by_name['event_id']._options = None
+ _globals['_VERDICT'].fields_by_name['event_id']._loaded_options = None
_globals['_VERDICT'].fields_by_name['event_id']._serialized_options = b'\272H\004r\002\020\001'
- _globals['_VERDICT'].fields_by_name['session_id']._options = None
+ _globals['_VERDICT'].fields_by_name['session_id']._loaded_options = None
_globals['_VERDICT'].fields_by_name['session_id']._serialized_options = b'\272H\004r\002\020\001'
- _globals['_PAIRTYPE']._serialized_start=3011
- _globals['_PAIRTYPE']._serialized_end=3087
- _globals['_MODE']._serialized_start=3089
- _globals['_MODE']._serialized_end=3164
+ _globals['_PAIRTYPE']._serialized_start=2474
+ _globals['_PAIRTYPE']._serialized_end=2550
+ _globals['_MODE']._serialized_start=2552
+ _globals['_MODE']._serialized_end=2627
+ _globals['_VERDICTSTATUS']._serialized_start=2629
+ _globals['_VERDICTSTATUS']._serialized_end=2725
_globals['_CHATMESSAGE']._serialized_start=64
- _globals['_CHATMESSAGE']._serialized_end=123
- _globals['_TOOLCALL']._serialized_start=125
- _globals['_TOOLCALL']._serialized_end=200
- _globals['_TOKENUSAGE']._serialized_start=203
- _globals['_TOKENUSAGE']._serialized_end=359
- _globals['_AGENTCONTEXT']._serialized_start=361
- _globals['_AGENTCONTEXT']._serialized_end=482
- _globals['_LLMPAIRDATA']._serialized_start=485
- _globals['_LLMPAIRDATA']._serialized_end=720
- _globals['_TOOLPAIRDATA']._serialized_start=723
- _globals['_TOOLPAIRDATA']._serialized_end=855
- _globals['_PAIREDEVENT']._serialized_start=858
- _globals['_PAIREDEVENT']._serialized_end=1469
- _globals['_PAIREDEVENTBATCH']._serialized_start=1471
- _globals['_PAIREDEVENTBATCH']._serialized_end=1546
- _globals['_MCPSERVER']._serialized_start=1548
- _globals['_MCPSERVER']._serialized_end=1646
- _globals['_MCPINVENTORY']._serialized_start=1648
- _globals['_MCPINVENTORY']._serialized_end=1719
- _globals['_LLMSTACK']._serialized_start=1721
- _globals['_LLMSTACK']._serialized_end=1781
- _globals['_SESSIONLOGIN']._serialized_start=1784
- _globals['_SESSIONLOGIN']._serialized_end=2015
- _globals['_CLIENTFRAME']._serialized_start=2018
- _globals['_CLIENTFRAME']._serialized_end=2259
- _globals['_POLICYSNAPSHOT']._serialized_start=2262
- _globals['_POLICYSNAPSHOT']._serialized_end=2440
- _globals['_HITLRESPONSE']._serialized_start=2442
- _globals['_HITLRESPONSE']._serialized_end=2503
- _globals['_LOGINACK']._serialized_start=2505
- _globals['_LOGINACK']._serialized_end=2599
- _globals['_SERVERFRAME']._serialized_start=2602
- _globals['_SERVERFRAME']._serialized_end=2742
- _globals['_VERDICT']._serialized_start=2745
- _globals['_VERDICT']._serialized_end=3009
+ _globals['_CHATMESSAGE']._serialized_end=108
+ _globals['_TOOLCALL']._serialized_start=110
+ _globals['_TOOLCALL']._serialized_end=169
+ _globals['_TOKENUSAGE']._serialized_start=171
+ _globals['_TOKENUSAGE']._serialized_end=282
+ _globals['_AGENTCONTEXT']._serialized_start=284
+ _globals['_AGENTCONTEXT']._serialized_end=365
+ _globals['_LLMPAIRDATA']._serialized_start=368
+ _globals['_LLMPAIRDATA']._serialized_end=579
+ _globals['_TOOLPAIRDATA']._serialized_start=581
+ _globals['_TOOLPAIRDATA']._serialized_end=676
+ _globals['_PAIREDEVENT']._serialized_start=679
+ _globals['_PAIREDEVENT']._serialized_end=1153
+ _globals['_PAIREDEVENTBATCH']._serialized_start=1155
+ _globals['_PAIREDEVENTBATCH']._serialized_end=1222
+ _globals['_MCPSERVER']._serialized_start=1224
+ _globals['_MCPSERVER']._serialized_end=1295
+ _globals['_MCPINVENTORY']._serialized_start=1297
+ _globals['_MCPINVENTORY']._serialized_end=1359
+ _globals['_LLMSTACK']._serialized_start=1361
+ _globals['_LLMSTACK']._serialized_end=1404
+ _globals['_SESSIONLOGIN']._serialized_start=1407
+ _globals['_SESSIONLOGIN']._serialized_end=1580
+ _globals['_CLIENTFRAME']._serialized_start=1583
+ _globals['_CLIENTFRAME']._serialized_end=1790
+ _globals['_POLICYSNAPSHOT']._serialized_start=1793
+ _globals['_POLICYSNAPSHOT']._serialized_end=1966
+ _globals['_HITLRESPONSE']._serialized_start=1968
+ _globals['_HITLRESPONSE']._serialized_end=2010
+ _globals['_LOGINACK']._serialized_start=2012
+ _globals['_LOGINACK']._serialized_end=2074
+ _globals['_SERVERFRAME']._serialized_start=2076
+ _globals['_SERVERFRAME']._serialized_end=2197
+ _globals['_VERDICT']._serialized_start=2200
+ _globals['_VERDICT']._serialized_end=2472
# @@protoc_insertion_point(module_scope)
diff --git a/integrations/claude-code/adrian_cc/proto/event_pb2.pyi b/integrations/claude-code/adrian_cc/proto/event_pb2.pyi
index e295646..5ae9a22 100644
--- a/integrations/claude-code/adrian_cc/proto/event_pb2.pyi
+++ b/integrations/claude-code/adrian_cc/proto/event_pb2.pyi
@@ -1,228 +1,753 @@
-from .buf.validate import validate_pb2 as _validate_pb2
-from google.protobuf.internal import containers as _containers
-from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
+"""
+@generated by mypy-protobuf. Do not edit manually!
+isort:skip_file
+SPDX-License-Identifier: Apache-2.0
+Copyright (c) 2026 SecureAgentics
+"""
+
+from collections import abc as _abc
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
-from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
+from google.protobuf.internal import containers as _containers
+from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
+import builtins as _builtins
+import sys
+import typing as _typing
+
+if sys.version_info >= (3, 10):
+ from typing import TypeAlias as _TypeAlias
+else:
+ from typing_extensions import TypeAlias as _TypeAlias
DESCRIPTOR: _descriptor.FileDescriptor
-class PairType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
- __slots__ = ()
- PAIR_TYPE_UNSPECIFIED: _ClassVar[PairType]
- PAIR_TYPE_LLM: _ClassVar[PairType]
- PAIR_TYPE_TOOL: _ClassVar[PairType]
-
-class Mode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
- __slots__ = ()
- MODE_UNSPECIFIED: _ClassVar[Mode]
- MODE_ALERT: _ClassVar[Mode]
- MODE_HITL: _ClassVar[Mode]
- MODE_BLOCK: _ClassVar[Mode]
-PAIR_TYPE_UNSPECIFIED: PairType
-PAIR_TYPE_LLM: PairType
-PAIR_TYPE_TOOL: PairType
-MODE_UNSPECIFIED: Mode
-MODE_ALERT: Mode
-MODE_HITL: Mode
-MODE_BLOCK: Mode
+class _PairType:
+ ValueType = _typing.NewType("ValueType", _builtins.int)
+ V: _TypeAlias = ValueType # noqa: Y015
+
+class _PairTypeEnumTypeWrapper(_enum_type_wrapper._EnumTypeWrapper[_PairType.ValueType], _builtins.type):
+ DESCRIPTOR: _descriptor.EnumDescriptor
+ PAIR_TYPE_UNSPECIFIED: _PairType.ValueType # 0
+ PAIR_TYPE_LLM: _PairType.ValueType # 1
+ """LLM pair: chat_model_start + llm_end folded together."""
+ PAIR_TYPE_TOOL: _PairType.ValueType # 2
+ """Tool pair: tool_start + tool_end folded together."""
+
+class PairType(_PairType, metaclass=_PairTypeEnumTypeWrapper):
+ """PairType identifies what an event pair represents."""
+
+PAIR_TYPE_UNSPECIFIED: PairType.ValueType # 0
+PAIR_TYPE_LLM: PairType.ValueType # 1
+"""LLM pair: chat_model_start + llm_end folded together."""
+PAIR_TYPE_TOOL: PairType.ValueType # 2
+"""Tool pair: tool_start + tool_end folded together."""
+Global___PairType: _TypeAlias = PairType # noqa: Y015
+
+class _Mode:
+ ValueType = _typing.NewType("ValueType", _builtins.int)
+ V: _TypeAlias = ValueType # noqa: Y015
+
+class _ModeEnumTypeWrapper(_enum_type_wrapper._EnumTypeWrapper[_Mode.ValueType], _builtins.type):
+ DESCRIPTOR: _descriptor.EnumDescriptor
+ MODE_UNSPECIFIED: _Mode.ValueType # 0
+ MODE_ALERT: _Mode.ValueType # 1
+ """Verdicts are dashboard-only, the server never pushes them to the SDK."""
+ MODE_HITL: _Mode.ValueType # 2
+ """Server holds in-policy-scope verdicts pending dashboard approve/reject.
+ Out-of-scope verdicts are forwarded immediately so the SDK doesn't stall.
+ """
+ MODE_BLOCK: _Mode.ValueType # 3
+ """Server forwards every verdict; the SDK enforces per the policy snapshot."""
+class Mode(_Mode, metaclass=_ModeEnumTypeWrapper):
+ """Mode of execution governs how the server handles verdicts for a session.
+ Set via policies.mode and delivered to the SDK via PolicySnapshot
+ on every Verdict.
+ """
+
+MODE_UNSPECIFIED: Mode.ValueType # 0
+MODE_ALERT: Mode.ValueType # 1
+"""Verdicts are dashboard-only, the server never pushes them to the SDK."""
+MODE_HITL: Mode.ValueType # 2
+"""Server holds in-policy-scope verdicts pending dashboard approve/reject.
+Out-of-scope verdicts are forwarded immediately so the SDK doesn't stall.
+"""
+MODE_BLOCK: Mode.ValueType # 3
+"""Server forwards every verdict; the SDK enforces per the policy snapshot."""
+Global___Mode: _TypeAlias = Mode # noqa: Y015
+
+class _VerdictStatus:
+ ValueType = _typing.NewType("ValueType", _builtins.int)
+ V: _TypeAlias = ValueType # noqa: Y015
+
+class _VerdictStatusEnumTypeWrapper(_enum_type_wrapper._EnumTypeWrapper[_VerdictStatus.ValueType], _builtins.type):
+ DESCRIPTOR: _descriptor.EnumDescriptor
+ VERDICT_STATUS_UNSPECIFIED: _VerdictStatus.ValueType # 0
+ VERDICT_STATUS_OK: _VerdictStatus.ValueType # 1
+ VERDICT_STATUS_ERROR: _VerdictStatus.ValueType # 2
+
+class VerdictStatus(_VerdictStatus, metaclass=_VerdictStatusEnumTypeWrapper):
+ """VerdictStatus says whether a Verdict came from a completed classifier
+ decision or represents a classifier failure. ERROR verdicts carry no
+ classifier-produced MAD code; policy decides whether they fail open
+ or fail closed.
+ """
+
+VERDICT_STATUS_UNSPECIFIED: VerdictStatus.ValueType # 0
+VERDICT_STATUS_OK: VerdictStatus.ValueType # 1
+VERDICT_STATUS_ERROR: VerdictStatus.ValueType # 2
+Global___VerdictStatus: _TypeAlias = VerdictStatus # noqa: Y015
+
+@_typing.final
class ChatMessage(_message.Message):
- __slots__ = ("role", "content")
- ROLE_FIELD_NUMBER: _ClassVar[int]
- CONTENT_FIELD_NUMBER: _ClassVar[int]
- role: str
- content: str
- def __init__(self, role: _Optional[str] = ..., content: _Optional[str] = ...) -> None: ...
+ """ChatMessage represents a conversation message with a string role."""
+ DESCRIPTOR: _descriptor.Descriptor
+
+ ROLE_FIELD_NUMBER: _builtins.int
+ CONTENT_FIELD_NUMBER: _builtins.int
+ role: _builtins.str
+ """Role of this message (e.g. "system", "human", "ai", "tool")."""
+ content: _builtins.str
+ """Text content of the message."""
+ def __init__(
+ self,
+ *,
+ role: _builtins.str = ...,
+ content: _builtins.str = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["content", b"content", "role", b"role"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+
+Global___ChatMessage: _TypeAlias = ChatMessage # noqa: Y015
+
+@_typing.final
class ToolCall(_message.Message):
- __slots__ = ("name", "args", "id")
- NAME_FIELD_NUMBER: _ClassVar[int]
- ARGS_FIELD_NUMBER: _ClassVar[int]
- ID_FIELD_NUMBER: _ClassVar[int]
- name: str
- args: str
- id: str
- def __init__(self, name: _Optional[str] = ..., args: _Optional[str] = ..., id: _Optional[str] = ...) -> None: ...
+ """ToolCall represents a tool invocation the model decided to make."""
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ NAME_FIELD_NUMBER: _builtins.int
+ ARGS_FIELD_NUMBER: _builtins.int
+ ID_FIELD_NUMBER: _builtins.int
+ name: _builtins.str
+ """Tool name."""
+ args: _builtins.str
+ """JSON-encoded arguments object."""
+ id: _builtins.str
+ """Provider-assigned tool call ID (may be empty for non-LLM-originated tools)."""
+ def __init__(
+ self,
+ *,
+ name: _builtins.str = ...,
+ args: _builtins.str = ...,
+ id: _builtins.str = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["args", b"args", "id", b"id", "name", b"name"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+Global___ToolCall: _TypeAlias = ToolCall # noqa: Y015
+
+@_typing.final
class TokenUsage(_message.Message):
- __slots__ = ("prompt_tokens", "completion_tokens", "total_tokens")
- PROMPT_TOKENS_FIELD_NUMBER: _ClassVar[int]
- COMPLETION_TOKENS_FIELD_NUMBER: _ClassVar[int]
- TOTAL_TOKENS_FIELD_NUMBER: _ClassVar[int]
- prompt_tokens: int
- completion_tokens: int
- total_tokens: int
- def __init__(self, prompt_tokens: _Optional[int] = ..., completion_tokens: _Optional[int] = ..., total_tokens: _Optional[int] = ...) -> None: ...
+ """TokenUsage holds token consumption counters."""
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ PROMPT_TOKENS_FIELD_NUMBER: _builtins.int
+ COMPLETION_TOKENS_FIELD_NUMBER: _builtins.int
+ TOTAL_TOKENS_FIELD_NUMBER: _builtins.int
+ prompt_tokens: _builtins.int
+ """Number of tokens in the prompt."""
+ completion_tokens: _builtins.int
+ """Number of tokens in the completion."""
+ total_tokens: _builtins.int
+ """Total tokens consumed."""
+ def __init__(
+ self,
+ *,
+ prompt_tokens: _builtins.int = ...,
+ completion_tokens: _builtins.int = ...,
+ total_tokens: _builtins.int = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["completion_tokens", b"completion_tokens", "prompt_tokens", b"prompt_tokens", "total_tokens", b"total_tokens"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+Global___TokenUsage: _TypeAlias = TokenUsage # noqa: Y015
+
+@_typing.final
class AgentContext(_message.Message):
- __slots__ = ("agent_id", "system_prompt", "user_instruction")
- AGENT_ID_FIELD_NUMBER: _ClassVar[int]
- SYSTEM_PROMPT_FIELD_NUMBER: _ClassVar[int]
- USER_INSTRUCTION_FIELD_NUMBER: _ClassVar[int]
- agent_id: str
- system_prompt: str
- user_instruction: str
- def __init__(self, agent_id: _Optional[str] = ..., system_prompt: _Optional[str] = ..., user_instruction: _Optional[str] = ...) -> None: ...
+ """AgentContext carries the identity and prompts for an agent at a given
+ point in the call stack. An empty agent_id signals "no agent" and is
+ used as the parent sentinel for top-level events.
+ """
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ AGENT_ID_FIELD_NUMBER: _builtins.int
+ SYSTEM_PROMPT_FIELD_NUMBER: _builtins.int
+ USER_INSTRUCTION_FIELD_NUMBER: _builtins.int
+ agent_id: _builtins.str
+ """Opaque agent identifier derived from the framework's checkpoint
+ namespace (e.g. "tools|reason").
+ """
+ system_prompt: _builtins.str
+ """System prompt the agent is running under."""
+ user_instruction: _builtins.str
+ """User instruction driving the current invocation."""
+ def __init__(
+ self,
+ *,
+ agent_id: _builtins.str = ...,
+ system_prompt: _builtins.str = ...,
+ user_instruction: _builtins.str = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["agent_id", b"agent_id", "system_prompt", b"system_prompt", "user_instruction", b"user_instruction"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+Global___AgentContext: _TypeAlias = AgentContext # noqa: Y015
+
+@_typing.final
class LlmPairData(_message.Message):
- __slots__ = ("model", "messages", "output", "tool_calls", "usage")
- MODEL_FIELD_NUMBER: _ClassVar[int]
- MESSAGES_FIELD_NUMBER: _ClassVar[int]
- OUTPUT_FIELD_NUMBER: _ClassVar[int]
- TOOL_CALLS_FIELD_NUMBER: _ClassVar[int]
- USAGE_FIELD_NUMBER: _ClassVar[int]
- model: str
- messages: _containers.RepeatedCompositeFieldContainer[ChatMessage]
- output: str
- tool_calls: _containers.RepeatedCompositeFieldContainer[ToolCall]
- usage: TokenUsage
- def __init__(self, model: _Optional[str] = ..., messages: _Optional[_Iterable[_Union[ChatMessage, _Mapping]]] = ..., output: _Optional[str] = ..., tool_calls: _Optional[_Iterable[_Union[ToolCall, _Mapping]]] = ..., usage: _Optional[_Union[TokenUsage, _Mapping]] = ...) -> None: ...
+ """LlmPairData is the payload for PAIR_TYPE_LLM events: one chat_model_start
+ folded with its matching llm_end.
+ """
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ MODEL_FIELD_NUMBER: _builtins.int
+ MESSAGES_FIELD_NUMBER: _builtins.int
+ OUTPUT_FIELD_NUMBER: _builtins.int
+ TOOL_CALLS_FIELD_NUMBER: _builtins.int
+ USAGE_FIELD_NUMBER: _builtins.int
+ REASONING_FIELD_NUMBER: _builtins.int
+ model: _builtins.str
+ """Model class name (e.g. "ChatAnthropic", "ChatOpenAI")."""
+ output: _builtins.str
+ """The model's text response."""
+ reasoning: _builtins.str
+ """The model's reasoning / chain of thought."""
+ @_builtins.property
+ def messages(self) -> _containers.RepeatedCompositeFieldContainer[Global___ChatMessage]:
+ """Ordered conversation messages sent to the model."""
+
+ @_builtins.property
+ def tool_calls(self) -> _containers.RepeatedCompositeFieldContainer[Global___ToolCall]:
+ """Tool calls the model decided to make."""
+
+ @_builtins.property
+ def usage(self) -> Global___TokenUsage:
+ """Token usage counters."""
+ def __init__(
+ self,
+ *,
+ model: _builtins.str = ...,
+ messages: _abc.Iterable[Global___ChatMessage] | None = ...,
+ output: _builtins.str = ...,
+ tool_calls: _abc.Iterable[Global___ToolCall] | None = ...,
+ usage: Global___TokenUsage | None = ...,
+ reasoning: _builtins.str = ...,
+ ) -> None: ...
+ _HasFieldArgType: _TypeAlias = _typing.Literal["usage", b"usage"] # noqa: Y015
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["messages", b"messages", "model", b"model", "output", b"output", "reasoning", b"reasoning", "tool_calls", b"tool_calls", "usage", b"usage"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+
+Global___LlmPairData: _TypeAlias = LlmPairData # noqa: Y015
+
+@_typing.final
class ToolPairData(_message.Message):
- __slots__ = ("tool_name", "tool_call_id", "input", "output")
- TOOL_NAME_FIELD_NUMBER: _ClassVar[int]
- TOOL_CALL_ID_FIELD_NUMBER: _ClassVar[int]
- INPUT_FIELD_NUMBER: _ClassVar[int]
- OUTPUT_FIELD_NUMBER: _ClassVar[int]
- tool_name: str
- tool_call_id: str
- input: str
- output: str
- def __init__(self, tool_name: _Optional[str] = ..., tool_call_id: _Optional[str] = ..., input: _Optional[str] = ..., output: _Optional[str] = ...) -> None: ...
+ """ToolPairData is the payload for PAIR_TYPE_TOOL events: one tool_start
+ folded with its matching tool_end.
+ """
+
+ DESCRIPTOR: _descriptor.Descriptor
+ TOOL_NAME_FIELD_NUMBER: _builtins.int
+ TOOL_CALL_ID_FIELD_NUMBER: _builtins.int
+ INPUT_FIELD_NUMBER: _builtins.int
+ OUTPUT_FIELD_NUMBER: _builtins.int
+ tool_name: _builtins.str
+ """Name of the tool that was invoked."""
+ tool_call_id: _builtins.str
+ """ID linking this invocation to the model's tool call. May be empty
+ for tools invoked outside an LLM flow.
+ """
+ input: _builtins.str
+ """JSON-encoded input passed to the tool."""
+ output: _builtins.str
+ """Tool execution output."""
+ def __init__(
+ self,
+ *,
+ tool_name: _builtins.str = ...,
+ tool_call_id: _builtins.str = ...,
+ input: _builtins.str = ...,
+ output: _builtins.str = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["input", b"input", "output", b"output", "tool_call_id", b"tool_call_id", "tool_name", b"tool_name"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+
+Global___ToolPairData: _TypeAlias = ToolPairData # noqa: Y015
+
+@_typing.final
class PairedEvent(_message.Message):
- __slots__ = ("event_id", "invocation_id", "session_id", "run_id", "parent_run_id", "timestamp", "pair_type", "agent", "parent", "llm", "tool", "metadata_json", "connection_id", "source")
- EVENT_ID_FIELD_NUMBER: _ClassVar[int]
- INVOCATION_ID_FIELD_NUMBER: _ClassVar[int]
- SESSION_ID_FIELD_NUMBER: _ClassVar[int]
- RUN_ID_FIELD_NUMBER: _ClassVar[int]
- PARENT_RUN_ID_FIELD_NUMBER: _ClassVar[int]
- TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
- PAIR_TYPE_FIELD_NUMBER: _ClassVar[int]
- AGENT_FIELD_NUMBER: _ClassVar[int]
- PARENT_FIELD_NUMBER: _ClassVar[int]
- LLM_FIELD_NUMBER: _ClassVar[int]
- TOOL_FIELD_NUMBER: _ClassVar[int]
- METADATA_JSON_FIELD_NUMBER: _ClassVar[int]
- CONNECTION_ID_FIELD_NUMBER: _ClassVar[int]
- SOURCE_FIELD_NUMBER: _ClassVar[int]
- event_id: str
- invocation_id: str
- session_id: str
- run_id: str
- parent_run_id: str
- timestamp: str
- pair_type: PairType
- agent: AgentContext
- parent: AgentContext
- llm: LlmPairData
- tool: ToolPairData
- metadata_json: bytes
- connection_id: str
- source: str
- def __init__(self, event_id: _Optional[str] = ..., invocation_id: _Optional[str] = ..., session_id: _Optional[str] = ..., run_id: _Optional[str] = ..., parent_run_id: _Optional[str] = ..., timestamp: _Optional[str] = ..., pair_type: _Optional[_Union[PairType, str]] = ..., agent: _Optional[_Union[AgentContext, _Mapping]] = ..., parent: _Optional[_Union[AgentContext, _Mapping]] = ..., llm: _Optional[_Union[LlmPairData, _Mapping]] = ..., tool: _Optional[_Union[ToolPairData, _Mapping]] = ..., metadata_json: _Optional[bytes] = ..., connection_id: _Optional[str] = ..., source: _Optional[str] = ...) -> None: ...
+ """PairedEvent is a completed event pair with agent context attached."""
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ EVENT_ID_FIELD_NUMBER: _builtins.int
+ INVOCATION_ID_FIELD_NUMBER: _builtins.int
+ SESSION_ID_FIELD_NUMBER: _builtins.int
+ RUN_ID_FIELD_NUMBER: _builtins.int
+ PARENT_RUN_ID_FIELD_NUMBER: _builtins.int
+ TIMESTAMP_FIELD_NUMBER: _builtins.int
+ PAIR_TYPE_FIELD_NUMBER: _builtins.int
+ AGENT_FIELD_NUMBER: _builtins.int
+ PARENT_FIELD_NUMBER: _builtins.int
+ LLM_FIELD_NUMBER: _builtins.int
+ TOOL_FIELD_NUMBER: _builtins.int
+ CONNECTION_ID_FIELD_NUMBER: _builtins.int
+ METADATA_JSON_FIELD_NUMBER: _builtins.int
+ SOURCE_FIELD_NUMBER: _builtins.int
+ event_id: _builtins.str
+ """Unique event identifier."""
+ invocation_id: _builtins.str
+ """Groups all events for one user-prompted invocation of the top-level graph."""
+ session_id: _builtins.str
+ """Session identifier."""
+ run_id: _builtins.str
+ """Framework run ID for this pair (LLM run_id for LLM pairs, tool run_id for tool pairs)."""
+ parent_run_id: _builtins.str
+ """Parent run ID. For tool pairs this points at the producing LLM's run_id
+ and is the key used for block-mode verdict correlation.
+ """
+ timestamp: _builtins.str
+ """ISO 8601 timestamp of the pair's completion (end event)."""
+ pair_type: Global___PairType.ValueType
+ """Whether this is an LLM pair or a tool pair."""
+ connection_id: _builtins.str
+ """Per-connection routing id (mirrors SessionLogin.connection_id). When set,
+ the server routes this connection's verdict on connection_id instead of
+ session_id, so concurrent connections sharing one session_id (parallel
+ Claude Code tool-call hooks) do not evict each other.
+ """
+ metadata_json: _builtins.bytes
+ """Escape hatch for framework metadata not modelled as first-class fields
+ (e.g. LangGraph checkpoint_ns, arbitrary tags).
+ """
+ source: _builtins.str
+ @_builtins.property
+ def agent(self) -> Global___AgentContext:
+ """The agent that produced this pair."""
+
+ @_builtins.property
+ def parent(self) -> Global___AgentContext:
+ """The agent that delegated to this one. agent_id == "" means top-level."""
+
+ @_builtins.property
+ def llm(self) -> Global___LlmPairData:
+ """Payload for PAIR_TYPE_LLM."""
+
+ @_builtins.property
+ def tool(self) -> Global___ToolPairData:
+ """Payload for PAIR_TYPE_TOOL."""
+
+ def __init__(
+ self,
+ *,
+ event_id: _builtins.str = ...,
+ invocation_id: _builtins.str = ...,
+ session_id: _builtins.str = ...,
+ run_id: _builtins.str = ...,
+ parent_run_id: _builtins.str = ...,
+ timestamp: _builtins.str = ...,
+ pair_type: Global___PairType.ValueType = ...,
+ agent: Global___AgentContext | None = ...,
+ parent: Global___AgentContext | None = ...,
+ llm: Global___LlmPairData | None = ...,
+ tool: Global___ToolPairData | None = ...,
+ connection_id: _builtins.str = ...,
+ metadata_json: _builtins.bytes = ...,
+ source: _builtins.str = ...,
+ ) -> None: ...
+ _HasFieldArgType: _TypeAlias = _typing.Literal["agent", b"agent", "data", b"data", "llm", b"llm", "parent", b"parent", "tool", b"tool"] # noqa: Y015
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["agent", b"agent", "connection_id", b"connection_id", "data", b"data", "event_id", b"event_id", "invocation_id", b"invocation_id", "llm", b"llm", "metadata_json", b"metadata_json", "pair_type", b"pair_type", "parent", b"parent", "parent_run_id", b"parent_run_id", "run_id", b"run_id", "session_id", b"session_id", "source", b"source", "timestamp", b"timestamp", "tool", b"tool"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+ _WhichOneofReturnType_data: _TypeAlias = _typing.Literal["llm", "tool"] # noqa: Y015
+ _WhichOneofArgType_data: _TypeAlias = _typing.Literal["data", b"data"] # noqa: Y015
+ def WhichOneof(self, oneof_group: _WhichOneofArgType_data) -> _WhichOneofReturnType_data | None: ...
+Global___PairedEvent: _TypeAlias = PairedEvent # noqa: Y015
+
+@_typing.final
class PairedEventBatch(_message.Message):
- __slots__ = ("events",)
- EVENTS_FIELD_NUMBER: _ClassVar[int]
- events: _containers.RepeatedCompositeFieldContainer[PairedEvent]
- def __init__(self, events: _Optional[_Iterable[_Union[PairedEvent, _Mapping]]] = ...) -> None: ...
+ """PairedEventBatch wraps multiple paired events for batched WebSocket frames."""
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ EVENTS_FIELD_NUMBER: _builtins.int
+ @_builtins.property
+ def events(self) -> _containers.RepeatedCompositeFieldContainer[Global___PairedEvent]: ...
+ def __init__(
+ self,
+ *,
+ events: _abc.Iterable[Global___PairedEvent] | None = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["events", b"events"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+Global___PairedEventBatch: _TypeAlias = PairedEventBatch # noqa: Y015
+
+@_typing.final
class McpServer(_message.Message):
- __slots__ = ("name", "transport", "endpoint")
- NAME_FIELD_NUMBER: _ClassVar[int]
- TRANSPORT_FIELD_NUMBER: _ClassVar[int]
- ENDPOINT_FIELD_NUMBER: _ClassVar[int]
- name: str
- transport: str
- endpoint: str
- def __init__(self, name: _Optional[str] = ..., transport: _Optional[str] = ..., endpoint: _Optional[str] = ...) -> None: ...
+ """McpServer is one MCP server the SDK observed at startup. Mirrors the
+ SDK-side McpServer dataclass.
+ """
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ NAME_FIELD_NUMBER: _builtins.int
+ TRANSPORT_FIELD_NUMBER: _builtins.int
+ ENDPOINT_FIELD_NUMBER: _builtins.int
+ name: _builtins.str
+ """Server identifier. Adapter-layer captures use the connection key
+ from MultiServerMCPClient.connections; raw-transport captures
+ synthesise ":".
+ """
+ transport: _builtins.str
+ """One of "stdio", "sse", "streamable_http", "websocket", or "unknown"."""
+ endpoint: _builtins.str
+ """URL for SSE/HTTP/WebSocket transports, or the joined command line
+ for stdio. Empty string when neither is available.
+ """
+ def __init__(
+ self,
+ *,
+ name: _builtins.str = ...,
+ transport: _builtins.str = ...,
+ endpoint: _builtins.str = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["endpoint", b"endpoint", "name", b"name", "transport", b"transport"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+Global___McpServer: _TypeAlias = McpServer # noqa: Y015
+
+@_typing.final
class McpInventory(_message.Message):
- __slots__ = ("servers",)
- SERVERS_FIELD_NUMBER: _ClassVar[int]
- servers: _containers.RepeatedCompositeFieldContainer[McpServer]
- def __init__(self, servers: _Optional[_Iterable[_Union[McpServer, _Mapping]]] = ...) -> None: ...
+ """McpInventory is the SDK's one-shot dump of its connected MCP servers.
+ Sent once per WS session right after the SDK login. The server replaces
+ the session's entire MCP-server list on receipt, no diff/merge.
+ """
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ SERVERS_FIELD_NUMBER: _builtins.int
+ @_builtins.property
+ def servers(self) -> _containers.RepeatedCompositeFieldContainer[Global___McpServer]: ...
+ def __init__(
+ self,
+ *,
+ servers: _abc.Iterable[Global___McpServer] | None = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["servers", b"servers"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+Global___McpInventory: _TypeAlias = McpInventory # noqa: Y015
+
+@_typing.final
class LLMStack(_message.Message):
- __slots__ = ("provider", "model")
- PROVIDER_FIELD_NUMBER: _ClassVar[int]
- MODEL_FIELD_NUMBER: _ClassVar[int]
- provider: str
- model: str
- def __init__(self, provider: _Optional[str] = ..., model: _Optional[str] = ...) -> None: ...
+ """LLMStack identifies the LLM provider and model for a session."""
+
+ DESCRIPTOR: _descriptor.Descriptor
+ PROVIDER_FIELD_NUMBER: _builtins.int
+ MODEL_FIELD_NUMBER: _builtins.int
+ provider: _builtins.str
+ """Provider name (e.g. "anthropic", "openai")."""
+ model: _builtins.str
+ """Model identifier (e.g. "claude-sonnet-4-20250514")."""
+ def __init__(
+ self,
+ *,
+ provider: _builtins.str = ...,
+ model: _builtins.str = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["model", b"model", "provider", b"provider"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+
+Global___LLMStack: _TypeAlias = LLMStack # noqa: Y015
+
+@_typing.final
class SessionLogin(_message.Message):
- __slots__ = ("session_id", "llm_stack", "schema_version", "source", "connection_id")
- SESSION_ID_FIELD_NUMBER: _ClassVar[int]
- LLM_STACK_FIELD_NUMBER: _ClassVar[int]
- SCHEMA_VERSION_FIELD_NUMBER: _ClassVar[int]
- SOURCE_FIELD_NUMBER: _ClassVar[int]
- CONNECTION_ID_FIELD_NUMBER: _ClassVar[int]
- session_id: str
- llm_stack: LLMStack
- schema_version: int
- source: str
- connection_id: str
- def __init__(self, session_id: _Optional[str] = ..., llm_stack: _Optional[_Union[LLMStack, _Mapping]] = ..., schema_version: _Optional[int] = ..., source: _Optional[str] = ..., connection_id: _Optional[str] = ...) -> None: ...
+ """SessionLogin is sent as the first frame after WebSocket upgrade."""
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ SESSION_ID_FIELD_NUMBER: _builtins.int
+ LLM_STACK_FIELD_NUMBER: _builtins.int
+ SCHEMA_VERSION_FIELD_NUMBER: _builtins.int
+ SOURCE_FIELD_NUMBER: _builtins.int
+ CONNECTION_ID_FIELD_NUMBER: _builtins.int
+ session_id: _builtins.str
+ """Session identifier."""
+ schema_version: _builtins.int
+ """Wire schema version the client is speaking. Server rejects unknown values."""
+ source: _builtins.str
+ """SDK / integration that produced this session (e.g. "claude-code"); the
+ server branches HITL behavior on it. Empty for legacy SDKs.
+ """
+ connection_id: _builtins.str
+ """Optional per-connection routing id, unique per live WebSocket connection.
+ When set, the server routes this connection's verdicts on connection_id
+ instead of session_id (and keys the single-owner guard on it), so multiple
+ concurrent connections sharing one session_id (parallel Claude Code
+ tool-call hooks) no longer evict each other.
+ """
+ @_builtins.property
+ def llm_stack(self) -> Global___LLMStack:
+ """LLM stack information for this session."""
+
+ def __init__(
+ self,
+ *,
+ session_id: _builtins.str = ...,
+ llm_stack: Global___LLMStack | None = ...,
+ schema_version: _builtins.int = ...,
+ source: _builtins.str = ...,
+ connection_id: _builtins.str = ...,
+ ) -> None: ...
+ _HasFieldArgType: _TypeAlias = _typing.Literal["llm_stack", b"llm_stack"] # noqa: Y015
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["connection_id", b"connection_id", "llm_stack", b"llm_stack", "schema_version", b"schema_version", "session_id", b"session_id", "source", b"source"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+
+Global___SessionLogin: _TypeAlias = SessionLogin # noqa: Y015
+@_typing.final
class ClientFrame(_message.Message):
- __slots__ = ("login", "paired_batch", "mcp_inventory")
- LOGIN_FIELD_NUMBER: _ClassVar[int]
- PAIRED_BATCH_FIELD_NUMBER: _ClassVar[int]
- MCP_INVENTORY_FIELD_NUMBER: _ClassVar[int]
- login: SessionLogin
- paired_batch: PairedEventBatch
- mcp_inventory: McpInventory
- def __init__(self, login: _Optional[_Union[SessionLogin, _Mapping]] = ..., paired_batch: _Optional[_Union[PairedEventBatch, _Mapping]] = ..., mcp_inventory: _Optional[_Union[McpInventory, _Mapping]] = ...) -> None: ...
+ """ClientFrame is the top-level envelope for all client WebSocket messages."""
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ LOGIN_FIELD_NUMBER: _builtins.int
+ PAIRED_BATCH_FIELD_NUMBER: _builtins.int
+ MCP_INVENTORY_FIELD_NUMBER: _builtins.int
+ @_builtins.property
+ def login(self) -> Global___SessionLogin:
+ """Session login (must be first frame)."""
+
+ @_builtins.property
+ def paired_batch(self) -> Global___PairedEventBatch:
+ """Paired event batch (requires prior login)."""
+
+ @_builtins.property
+ def mcp_inventory(self) -> Global___McpInventory:
+ """One-shot MCP-server inventory dump from the SDK after login."""
+
+ def __init__(
+ self,
+ *,
+ login: Global___SessionLogin | None = ...,
+ paired_batch: Global___PairedEventBatch | None = ...,
+ mcp_inventory: Global___McpInventory | None = ...,
+ ) -> None: ...
+ _HasFieldArgType: _TypeAlias = _typing.Literal["frame", b"frame", "login", b"login", "mcp_inventory", b"mcp_inventory", "paired_batch", b"paired_batch"] # noqa: Y015
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["frame", b"frame", "login", b"login", "mcp_inventory", b"mcp_inventory", "paired_batch", b"paired_batch"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+ _WhichOneofReturnType_frame: _TypeAlias = _typing.Literal["login", "paired_batch", "mcp_inventory"] # noqa: Y015
+ _WhichOneofArgType_frame: _TypeAlias = _typing.Literal["frame", b"frame"] # noqa: Y015
+ def WhichOneof(self, oneof_group: _WhichOneofArgType_frame) -> _WhichOneofReturnType_frame | None: ...
+Global___ClientFrame: _TypeAlias = ClientFrame # noqa: Y015
+
+@_typing.final
class PolicySnapshot(_message.Message):
- __slots__ = ("mode", "policy_m0", "policy_m2", "policy_m3", "policy_m4")
- MODE_FIELD_NUMBER: _ClassVar[int]
- POLICY_M0_FIELD_NUMBER: _ClassVar[int]
- POLICY_M2_FIELD_NUMBER: _ClassVar[int]
- POLICY_M3_FIELD_NUMBER: _ClassVar[int]
- POLICY_M4_FIELD_NUMBER: _ClassVar[int]
- mode: Mode
- policy_m0: bool
- policy_m2: bool
- policy_m3: bool
- policy_m4: bool
- def __init__(self, mode: _Optional[_Union[Mode, str]] = ..., policy_m0: bool = ..., policy_m2: bool = ..., policy_m3: bool = ..., policy_m4: bool = ...) -> None: ...
+ """PolicySnapshot is the org's effective execution-mode policy at the moment
+ a verdict was decided. Attached by the server to every Verdict it sends
+ so the SDK can apply user-configured behaviour (halt vs continue,
+ review-pending vs proceed) without an extra round-trip to fetch policy.
+
+ Per-MAD-code booleans say whether the active mode's behaviour fires on
+ that code. False means "treat this code as silent regardless of mode".
+ fail_closed_on_classifier_error controls ERROR verdicts and BLOCK-mode
+ SDK verdict timeouts. The default false value preserves fail-open
+ availability when talking to older backends.
+ """
+
+ DESCRIPTOR: _descriptor.Descriptor
+ MODE_FIELD_NUMBER: _builtins.int
+ POLICY_M0_FIELD_NUMBER: _builtins.int
+ POLICY_M2_FIELD_NUMBER: _builtins.int
+ POLICY_M3_FIELD_NUMBER: _builtins.int
+ POLICY_M4_FIELD_NUMBER: _builtins.int
+ FAIL_CLOSED_ON_CLASSIFIER_ERROR_FIELD_NUMBER: _builtins.int
+ mode: Global___Mode.ValueType
+ policy_m0: _builtins.bool
+ policy_m2: _builtins.bool
+ policy_m3: _builtins.bool
+ policy_m4: _builtins.bool
+ fail_closed_on_classifier_error: _builtins.bool
+ def __init__(
+ self,
+ *,
+ mode: Global___Mode.ValueType = ...,
+ policy_m0: _builtins.bool = ...,
+ policy_m2: _builtins.bool = ...,
+ policy_m3: _builtins.bool = ...,
+ policy_m4: _builtins.bool = ...,
+ fail_closed_on_classifier_error: _builtins.bool = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["fail_closed_on_classifier_error", b"fail_closed_on_classifier_error", "mode", b"mode", "policy_m0", b"policy_m0", "policy_m2", b"policy_m2", "policy_m3", b"policy_m3", "policy_m4", b"policy_m4"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+
+Global___PolicySnapshot: _TypeAlias = PolicySnapshot # noqa: Y015
+
+@_typing.final
class HitlResponse(_message.Message):
- __slots__ = ("continue_execution",)
- CONTINUE_EXECUTION_FIELD_NUMBER: _ClassVar[int]
- continue_execution: bool
- def __init__(self, continue_execution: bool = ...) -> None: ...
+ """HitlResponse rides on a Verdict that has been resolved through the
+ human-in-the-loop review queue. Absent on regular (non-HITL or
+ out-of-scope) verdicts.
+ """
+
+ DESCRIPTOR: _descriptor.Descriptor
+ CONTINUE_EXECUTION_FIELD_NUMBER: _builtins.int
+ continue_execution: _builtins.bool
+ """True if the reviewer approved (agent should continue), false if rejected."""
+ def __init__(
+ self,
+ *,
+ continue_execution: _builtins.bool = ...,
+ ) -> None: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["continue_execution", b"continue_execution"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+
+Global___HitlResponse: _TypeAlias = HitlResponse # noqa: Y015
+
+@_typing.final
class LoginAck(_message.Message):
- __slots__ = ("policy", "source")
- POLICY_FIELD_NUMBER: _ClassVar[int]
- SOURCE_FIELD_NUMBER: _ClassVar[int]
- policy: PolicySnapshot
- source: str
- def __init__(self, policy: _Optional[_Union[PolicySnapshot, _Mapping]] = ..., source: _Optional[str] = ...) -> None: ...
+ """LoginAck is the server's first frame after a successful SessionLogin.
+ Carries the org's effective policy so the SDK can apply correct mode +
+ timeout behaviour from the very first event, before any verdict round-trip.
+
+ Policy snapshot is captured at WS login time. Mid-session policy changes
+ do NOT propagate to existing sessions, reconnect for new policy to take
+ effect.
+ """
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ POLICY_FIELD_NUMBER: _builtins.int
+ @_builtins.property
+ def policy(self) -> Global___PolicySnapshot: ...
+ def __init__(
+ self,
+ *,
+ policy: Global___PolicySnapshot | None = ...,
+ ) -> None: ...
+ _HasFieldArgType: _TypeAlias = _typing.Literal["policy", b"policy"] # noqa: Y015
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["policy", b"policy"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+Global___LoginAck: _TypeAlias = LoginAck # noqa: Y015
+
+@_typing.final
class ServerFrame(_message.Message):
- __slots__ = ("login_ack", "verdict")
- LOGIN_ACK_FIELD_NUMBER: _ClassVar[int]
- VERDICT_FIELD_NUMBER: _ClassVar[int]
- login_ack: LoginAck
- verdict: Verdict
- def __init__(self, login_ack: _Optional[_Union[LoginAck, _Mapping]] = ..., verdict: _Optional[_Union[Verdict, _Mapping]] = ...) -> None: ...
+ """ServerFrame is the top-level envelope for all server→SDK WebSocket
+ messages. Multiplexes login-ack, verdicts, and any future server-pushed
+ frame types over the same WebSocket without ambiguity.
+ """
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ LOGIN_ACK_FIELD_NUMBER: _builtins.int
+ VERDICT_FIELD_NUMBER: _builtins.int
+ @_builtins.property
+ def login_ack(self) -> Global___LoginAck:
+ """Sent exactly once, immediately after a successful SessionLogin."""
+
+ @_builtins.property
+ def verdict(self) -> Global___Verdict:
+ """Sent for every classified event the server forwards to the SDK
+ (subject to mode/policy gates).
+ """
+
+ def __init__(
+ self,
+ *,
+ login_ack: Global___LoginAck | None = ...,
+ verdict: Global___Verdict | None = ...,
+ ) -> None: ...
+ _HasFieldArgType: _TypeAlias = _typing.Literal["frame", b"frame", "login_ack", b"login_ack", "verdict", b"verdict"] # noqa: Y015
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["frame", b"frame", "login_ack", b"login_ack", "verdict", b"verdict"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+ _WhichOneofReturnType_frame: _TypeAlias = _typing.Literal["login_ack", "verdict"] # noqa: Y015
+ _WhichOneofArgType_frame: _TypeAlias = _typing.Literal["frame", b"frame"] # noqa: Y015
+ def WhichOneof(self, oneof_group: _WhichOneofArgType_frame) -> _WhichOneofReturnType_frame | None: ...
+
+Global___ServerFrame: _TypeAlias = ServerFrame # noqa: Y015
+@_typing.final
class Verdict(_message.Message):
- __slots__ = ("event_id", "session_id", "mad_code", "policy", "hitl")
- EVENT_ID_FIELD_NUMBER: _ClassVar[int]
- SESSION_ID_FIELD_NUMBER: _ClassVar[int]
- MAD_CODE_FIELD_NUMBER: _ClassVar[int]
- POLICY_FIELD_NUMBER: _ClassVar[int]
- HITL_FIELD_NUMBER: _ClassVar[int]
- event_id: str
- session_id: str
- mad_code: str
- policy: PolicySnapshot
- hitl: HitlResponse
- def __init__(self, event_id: _Optional[str] = ..., session_id: _Optional[str] = ..., mad_code: _Optional[str] = ..., policy: _Optional[_Union[PolicySnapshot, _Mapping]] = ..., hitl: _Optional[_Union[HitlResponse, _Mapping]] = ...) -> None: ...
+ """Verdict is the classification result for a PairedEvent."""
+
+ DESCRIPTOR: _descriptor.Descriptor
+
+ EVENT_ID_FIELD_NUMBER: _builtins.int
+ SESSION_ID_FIELD_NUMBER: _builtins.int
+ MAD_CODE_FIELD_NUMBER: _builtins.int
+ POLICY_FIELD_NUMBER: _builtins.int
+ HITL_FIELD_NUMBER: _builtins.int
+ STATUS_FIELD_NUMBER: _builtins.int
+ event_id: _builtins.str
+ """The event_id of the PairedEvent being classified."""
+ session_id: _builtins.str
+ """Session identifier for routing."""
+ mad_code: _builtins.str
+ """MAD code the classifier returned (e.g. "M0", "M2_C", "M4_a").
+ Empty string means no MAD code was produced, such as for a
+ VerdictStatus.ERROR classifier failure. Benign classifier success
+ is represented by status OK with mad_code "M0".
+ """
+ status: Global___VerdictStatus.ValueType
+ """Status of the classifier result. OK means mad_code carries a normal
+ classifier decision. ERROR means classification did not complete and
+ mad_code is empty; fail-open/fail-closed behaviour comes from policy.
+ """
+ @_builtins.property
+ def policy(self) -> Global___PolicySnapshot:
+ """Org's effective execution-mode policy at the time of this verdict.
+ Always populated by the server; SDK reads this to decide whether to
+ halt, pause, or continue.
+ """
+
+ @_builtins.property
+ def hitl(self) -> Global___HitlResponse:
+ """Present only when this verdict represents a human-in-the-loop review
+ resolution (approve or reject from the dashboard). Absent on auto-
+ classified verdicts and on out-of-scope verdicts forwarded immediately.
+ """
+
+ def __init__(
+ self,
+ *,
+ event_id: _builtins.str = ...,
+ session_id: _builtins.str = ...,
+ mad_code: _builtins.str = ...,
+ policy: Global___PolicySnapshot | None = ...,
+ hitl: Global___HitlResponse | None = ...,
+ status: Global___VerdictStatus.ValueType = ...,
+ ) -> None: ...
+ _HasFieldArgType: _TypeAlias = _typing.Literal["hitl", b"hitl", "policy", b"policy"] # noqa: Y015
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["event_id", b"event_id", "hitl", b"hitl", "mad_code", b"mad_code", "policy", b"policy", "session_id", b"session_id", "status", b"status"] # noqa: Y015
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
+
+Global___Verdict: _TypeAlias = Verdict # noqa: Y015
diff --git a/integrations/claude-code/tests/test_state_and_subagent.py b/integrations/claude-code/tests/test_state_and_subagent.py
index b768218..b189fcc 100644
--- a/integrations/claude-code/tests/test_state_and_subagent.py
+++ b/integrations/claude-code/tests/test_state_and_subagent.py
@@ -443,7 +443,7 @@ def test_verify_reports_ok_from_login_ack(
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
async def _ok(*_a: object, **_k: object) -> dict[str, Any]:
- return {"ok": True, "source_ack": "claude-code", "mode_name": "ALERT"}
+ return {"ok": True, "mode_name": "ALERT"}
monkeypatch.setattr(agent, "ADRIAN_API_KEY", "adr_live_x")
monkeypatch.setattr(agent, "_verify_connection", _ok)
@@ -451,7 +451,7 @@ async def _ok(*_a: object, **_k: object) -> dict[str, Any]:
agent._handle_verify()
assert e.value.code == 0
out = capsys.readouterr().out
- assert out.startswith("OK:") and "claude-code" in out
+ assert out.startswith("OK:") and "ALERT" in out
assert "adr_live_x" not in out # key value never printed
def test_verify_reports_fail(
diff --git a/proto/event.proto b/proto/event.proto
index 1281cbe..a4d40e0 100644
--- a/proto/event.proto
+++ b/proto/event.proto
@@ -70,6 +70,8 @@ message LlmPairData {
repeated ToolCall tool_calls = 4;
// Token usage counters.
TokenUsage usage = 5;
+ // The model's reasoning / chain of thought.
+ string reasoning = 6;
}
// ToolPairData is the payload for PAIR_TYPE_TOOL events: one tool_start
diff --git a/sdk/python/adrian/anthropic_handler.py b/sdk/python/adrian/anthropic_handler.py
index f2f2194..58167fc 100644
--- a/sdk/python/adrian/anthropic_handler.py
+++ b/sdk/python/adrian/anthropic_handler.py
@@ -284,6 +284,53 @@ def _extract_response_text(content: list[Any]) -> str:
return "\n".join(p for p in parts if p)
+def _request_summarised_thinking(kwargs: dict[str, Any]) -> None:
+ """Ask for summarised thinking when the caller enabled it but left it hidden.
+
+ ``thinking.display`` defaults to ``"omitted"`` on current models, so
+ thinking blocks arrive with empty text and there is nothing to capture.
+ ``display`` controls visibility only: thinking still runs and bills the
+ same either way, so opting in costs nothing. Callers who set ``display``
+ themselves are left alone, as are callers who never enabled thinking.
+
+ Args:
+ kwargs: ``messages.create`` keyword arguments, mutated in place.
+ """
+ thinking = kwargs.get("thinking")
+
+ if not isinstance(thinking, dict):
+ return
+
+ if thinking.get("type") not in ("adaptive", "enabled"):
+ return
+
+ if "display" in thinking:
+ return
+
+ kwargs["thinking"] = {**thinking, "display": "summarized"}
+
+
+def _extract_reasoning(content: list[Any]) -> str:
+ """Extract the model's reasoning from an Anthropic response content list.
+
+ Args:
+ content: ``Message.content`` from the Anthropic response.
+
+ Returns:
+ Concatenated text from all ``ThinkingBlock`` entries. Empty when the
+ model produced none, or when ``thinking.display`` left them redacted.
+ """
+ parts: list[str] = []
+
+ for block in content:
+ if hasattr(block, "type") and block.type == "thinking":
+ parts.append(getattr(block, "thinking", ""))
+ elif isinstance(block, dict) and block.get("type") == "thinking":
+ parts.append(str(block.get("thinking", "")))
+
+ return "\n\n".join(p for p in parts if p)
+
+
def _derive_agent_id(messages: list[ChatMessage]) -> str:
"""Derive a stable agent identity from the system prompt.
@@ -349,6 +396,7 @@ def build_anthropic_llm_pair(
content: list[Any] = getattr(response, "content", [])
output_text = _extract_response_text(content)
+ reasoning = _extract_reasoning(content)
tool_calls = _extract_anthropic_tool_calls(content)
usage = _extract_anthropic_usage(response)
@@ -374,6 +422,7 @@ def build_anthropic_llm_pair(
output=output_text,
tool_calls=tool_calls,
usage=usage,
+ reasoning=reasoning,
),
)
@@ -1054,6 +1103,7 @@ def _patched_sync_create(
*args: Any,
**kwargs: Any, # noqa: ANN401
) -> Any: # noqa: ANN401
+ _request_summarised_thinking(kwargs)
response = _original_sync(self, *args, **kwargs)
# ChatAnthropic routes through here, but the LangChain
@@ -1077,6 +1127,7 @@ def _patched_sync_stream(
# Sampled now, not at consumption time: the caller may read the
# final message after leaving the anthropic_invocation() block.
captured = get_invocation_id()
+ _request_summarised_thinking(kwargs)
inner = _original_sync_stream(self, *args, **kwargs)
# Owned by the LangChain callbacks: hand back the SDK's own
@@ -1108,6 +1159,7 @@ async def _patched_async_create(
*args: Any,
**kwargs: Any, # noqa: ANN401
) -> Any: # noqa: ANN401
+ _request_summarised_thinking(kwargs)
response = await _original_async(self, *args, **kwargs)
# See the sync wrapper: the LangChain callbacks own this
@@ -1131,6 +1183,7 @@ def _patched_async_stream(
# Not async: stream() returns an async context manager without
# being awaited itself.
captured = get_invocation_id()
+ _request_summarised_thinking(kwargs)
inner = _original_async_stream(self, *args, **kwargs)
# See the sync stream wrapper.
diff --git a/sdk/python/adrian/format/types.py b/sdk/python/adrian/format/types.py
index 7541b84..1f07504 100644
--- a/sdk/python/adrian/format/types.py
+++ b/sdk/python/adrian/format/types.py
@@ -72,6 +72,7 @@ class LlmPairData:
output: Generated text output from llm_end.
tool_calls: Tool calls requested by the model.
usage: Token usage statistics, or ``None``.
+ reasoning: The model's reasoning, where the provider exposes it.
"""
model: str
@@ -79,6 +80,7 @@ class LlmPairData:
output: str = ""
tool_calls: list[ToolCallRecord] = field(default_factory=list)
usage: TokenUsage | None = None
+ reasoning: str = ""
@dataclass(slots=True)
diff --git a/sdk/python/adrian/handler.py b/sdk/python/adrian/handler.py
index 166b7d2..173aa89 100644
--- a/sdk/python/adrian/handler.py
+++ b/sdk/python/adrian/handler.py
@@ -368,6 +368,7 @@ async def on_llm_end(
**kwargs: Additional LangChain callback kwargs.
"""
output = ""
+ reasoning = ""
typed_tool_calls: list[ToolCallRecord] = []
if response.generations:
@@ -376,6 +377,7 @@ async def on_llm_end(
if isinstance(gen, ChatGeneration):
msg = gen.message
+ reasoning = _extract_reasoning(msg)
raw_calls: list[dict[str, str | dict[str, object]]] = (
getattr(msg, "tool_calls", None) or []
)
@@ -404,6 +406,7 @@ async def on_llm_end(
"output": output,
"tool_calls": typed_tool_calls,
"usage": usage,
+ "reasoning": reasoning,
}
session_id = self._resolve_session_id()
@@ -539,6 +542,100 @@ async def on_tool_end(
# ------------------------------------------------------------------
+def _reasoning_block_text(block: dict[str, Any]) -> str:
+ """Read the human-readable text out of one reasoning block.
+
+ Providers and LangChain output versions disagree on where the text
+ sits, so three layouts are accepted:
+
+ * a ``summary`` list of ``{"type": "summary_text", "text": ...}``
+ entries (OpenAI Responses, LangChain's default and
+ ``responses/v1``),
+ * a bare ``reasoning`` string (LangChain ``output_version="v1"``,
+ which emits one block per reasoning pass),
+ * Anthropic's ``thinking`` string.
+
+ ``encrypted_content`` is skipped: it is an opaque blob for
+ multi-turn reuse, not readable text.
+ """
+ parts: list[str] = []
+ summary: Any = block.get("summary")
+
+ if isinstance(summary, list):
+ for entry in cast(list[object], summary):
+ if not isinstance(entry, dict):
+ continue
+
+ text: Any = cast(dict[str, Any], entry).get("text")
+
+ if text:
+ parts.append(str(text))
+
+ if parts:
+ return "\n\n".join(parts)
+
+ # No summary: fall through to the single-string layouts. Checked
+ # second so a populated summary never gets duplicated by a block
+ # that carries both.
+ for key in ("reasoning", "thinking"):
+ value: Any = block.get(key)
+
+ if isinstance(value, str) and value:
+ return value
+
+ return ""
+
+
+def _extract_reasoning(message: BaseMessage) -> str:
+ """Pull the model's reasoning off an AIMessage.
+
+ ``ChatGeneration.text`` concatenates text blocks only, so reasoning
+ is dropped unless read off the message directly. Content blocks are
+ the usual home; ``output_version="v0"`` instead parks the whole
+ block in ``additional_kwargs``, so that is checked as a fallback.
+
+ Args:
+ message: The AIMessage from the generation.
+
+ Returns:
+ Reasoning text, or ``""`` when the model exposed none.
+ """
+ content: Any = getattr(message, "content", None)
+ parts: list[str] = []
+
+ if isinstance(content, list):
+ for raw in cast(list[object], content):
+ if not isinstance(raw, dict):
+ continue
+
+ block = cast(dict[str, Any], raw)
+
+ # redacted_thinking carries an encrypted blob, never text.
+ if block.get("type") not in ("reasoning", "thinking"):
+ continue
+
+ text = _reasoning_block_text(block)
+
+ if text:
+ parts.append(text)
+
+ if parts:
+ return "\n\n".join(parts)
+
+ extra: Any = getattr(message, "additional_kwargs", None)
+
+ if isinstance(extra, dict):
+ blob: Any = cast(dict[str, Any], extra).get("reasoning")
+
+ if isinstance(blob, dict):
+ return _reasoning_block_text(cast(dict[str, Any], blob))
+
+ if isinstance(blob, str):
+ return blob
+
+ return ""
+
+
def _build_llm_start_data(
model: str,
messages: list[ChatMessage],
diff --git a/sdk/python/adrian/pairing.py b/sdk/python/adrian/pairing.py
index be63728..2a76acb 100644
--- a/sdk/python/adrian/pairing.py
+++ b/sdk/python/adrian/pairing.py
@@ -221,6 +221,7 @@ def _assemble_llm_pair(
output=llm_end.get("output", ""),
tool_calls=tool_calls,
usage=llm_end.get("usage"),
+ reasoning=llm_end.get("reasoning", ""),
),
parent_run_id=start.parent_run_id,
metadata=start.metadata,
diff --git a/sdk/python/adrian/pii/_redactor.py b/sdk/python/adrian/pii/_redactor.py
index 9f99e91..9765b63 100644
--- a/sdk/python/adrian/pii/_redactor.py
+++ b/sdk/python/adrian/pii/_redactor.py
@@ -101,6 +101,7 @@ def _iter_event_text(self, event: PairedEvent) -> list[str]:
for msg in event.data.messages:
texts.append(msg["content"])
texts.append(event.data.output)
+ texts.append(event.data.reasoning)
for tc in event.data.tool_calls:
_collect_strings(tc.get("args", {}), texts)
else:
@@ -142,6 +143,7 @@ def _redact_llm_data(self, data: LlmPairData) -> None:
msg["content"] = self._redact_str(msg["content"])
data.output = self._redact_str(data.output)
+ data.reasoning = self._redact_str(data.reasoning)
for tc in data.tool_calls:
args = tc["args"]
diff --git a/sdk/python/adrian/proto/event_pb2.py b/sdk/python/adrian/proto/event_pb2.py
index e8f2f50..8792ed6 100644
--- a/sdk/python/adrian/proto/event_pb2.py
+++ b/sdk/python/adrian/proto/event_pb2.py
@@ -25,7 +25,7 @@
from .buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x65vent.proto\x12\x12\x61\x64rian.core_api.v1\x1a\x1b\x62uf/validate/validate.proto\",\n\x0b\x43hatMessage\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\";\n\x08ToolCall\x12\x15\n\x04name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x0c\n\x04\x61rgs\x18\x02 \x01(\t\x12\n\n\x02id\x18\x03 \x01(\t\"o\n\nTokenUsage\x12\x1e\n\rprompt_tokens\x18\x01 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00\x12\"\n\x11\x63ompletion_tokens\x18\x02 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00\x12\x1d\n\x0ctotal_tokens\x18\x03 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00\"Q\n\x0c\x41gentContext\x12\x10\n\x08\x61gent_id\x18\x01 \x01(\t\x12\x15\n\rsystem_prompt\x18\x02 \x01(\t\x12\x18\n\x10user_instruction\x18\x03 \x01(\t\"\xc0\x01\n\x0bLlmPairData\x12\r\n\x05model\x18\x01 \x01(\t\x12\x31\n\x08messages\x18\x02 \x03(\x0b\x32\x1f.adrian.core_api.v1.ChatMessage\x12\x0e\n\x06output\x18\x03 \x01(\t\x12\x30\n\ntool_calls\x18\x04 \x03(\x0b\x32\x1c.adrian.core_api.v1.ToolCall\x12-\n\x05usage\x18\x05 \x01(\x0b\x32\x1e.adrian.core_api.v1.TokenUsage\"_\n\x0cToolPairData\x12\x1a\n\ttool_name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x14\n\x0ctool_call_id\x18\x02 \x01(\t\x12\r\n\x05input\x18\x03 \x01(\t\x12\x0e\n\x06output\x18\x04 \x01(\t\"\xc3\x03\n\x0bPairedEvent\x12\x19\n\x08\x65vent_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x15\n\rinvocation_id\x18\x02 \x01(\t\x12\x1b\n\nsession_id\x18\x03 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x0e\n\x06run_id\x18\x04 \x01(\t\x12\x15\n\rparent_run_id\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\t\x12\x37\n\tpair_type\x18\x07 \x01(\x0e\x32\x1c.adrian.core_api.v1.PairTypeB\x06\xbaH\x03\xc8\x01\x01\x12/\n\x05\x61gent\x18\x08 \x01(\x0b\x32 .adrian.core_api.v1.AgentContext\x12\x30\n\x06parent\x18\t \x01(\x0b\x32 .adrian.core_api.v1.AgentContext\x12.\n\x03llm\x18\n \x01(\x0b\x32\x1f.adrian.core_api.v1.LlmPairDataH\x00\x12\x30\n\x04tool\x18\x0b \x01(\x0b\x32 .adrian.core_api.v1.ToolPairDataH\x00\x12\x15\n\rmetadata_json\x18\x14 \x01(\x0c\x12\x0e\n\x06source\x18\x15 \x01(\tB\x06\n\x04\x64\x61ta\"C\n\x10PairedEventBatch\x12/\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1f.adrian.core_api.v1.PairedEvent\"G\n\tMcpServer\x12\x15\n\x04name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x11\n\ttransport\x18\x02 \x01(\t\x12\x10\n\x08\x65ndpoint\x18\x03 \x01(\t\">\n\x0cMcpInventory\x12.\n\x07servers\x18\x01 \x03(\x0b\x32\x1d.adrian.core_api.v1.McpServer\"+\n\x08LLMStack\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\"\x96\x01\n\x0cSessionLogin\x12\x1b\n\nsession_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12/\n\tllm_stack\x18\x02 \x01(\x0b\x32\x1c.adrian.core_api.v1.LLMStack\x12\x16\n\x0eschema_version\x18\x04 \x01(\r\x12\x0e\n\x06source\x18\x05 \x01(\tJ\x04\x08\x03\x10\x04R\nblock_mode\"\xcf\x01\n\x0b\x43lientFrame\x12\x31\n\x05login\x18\x01 \x01(\x0b\x32 .adrian.core_api.v1.SessionLoginH\x00\x12<\n\x0cpaired_batch\x18\x03 \x01(\x0b\x32$.adrian.core_api.v1.PairedEventBatchH\x00\x12\x39\n\rmcp_inventory\x18\x04 \x01(\x0b\x32 .adrian.core_api.v1.McpInventoryH\x00\x42\x07\n\x05\x66rameJ\x04\x08\x02\x10\x03R\x05\x62\x61tch\"\xad\x01\n\x0ePolicySnapshot\x12&\n\x04mode\x18\x01 \x01(\x0e\x32\x18.adrian.core_api.v1.Mode\x12\x11\n\tpolicy_m0\x18\x02 \x01(\x08\x12\x11\n\tpolicy_m2\x18\x03 \x01(\x08\x12\x11\n\tpolicy_m3\x18\x04 \x01(\x08\x12\x11\n\tpolicy_m4\x18\x05 \x01(\x08\x12\'\n\x1f\x66\x61il_closed_on_classifier_error\x18\x06 \x01(\x08\"*\n\x0cHitlResponse\x12\x1a\n\x12\x63ontinue_execution\x18\x01 \x01(\x08\">\n\x08LoginAck\x12\x32\n\x06policy\x18\x01 \x01(\x0b\x32\".adrian.core_api.v1.PolicySnapshot\"y\n\x0bServerFrame\x12\x31\n\tlogin_ack\x18\x01 \x01(\x0b\x32\x1c.adrian.core_api.v1.LoginAckH\x00\x12.\n\x07verdict\x18\x02 \x01(\x0b\x32\x1b.adrian.core_api.v1.VerdictH\x00\x42\x07\n\x05\x66rame\"\x90\x02\n\x07Verdict\x12\x19\n\x08\x65vent_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x1b\n\nsession_id\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x10\n\x08mad_code\x18\x04 \x01(\t\x12\x32\n\x06policy\x18\x06 \x01(\x0b\x32\".adrian.core_api.v1.PolicySnapshot\x12.\n\x04hitl\x18\x07 \x01(\x0b\x32 .adrian.core_api.v1.HitlResponse\x12\x31\n\x06status\x18\x08 \x01(\x0e\x32!.adrian.core_api.v1.VerdictStatusJ\x04\x08\x03\x10\x04J\x04\x08\x05\x10\x06R\x0e\x63lassificationR\x08\x65scalate*L\n\x08PairType\x12\x19\n\x15PAIR_TYPE_UNSPECIFIED\x10\x00\x12\x11\n\rPAIR_TYPE_LLM\x10\x01\x12\x12\n\x0ePAIR_TYPE_TOOL\x10\x02*K\n\x04Mode\x12\x14\n\x10MODE_UNSPECIFIED\x10\x00\x12\x0e\n\nMODE_ALERT\x10\x01\x12\r\n\tMODE_HITL\x10\x02\x12\x0e\n\nMODE_BLOCK\x10\x03*`\n\rVerdictStatus\x12\x1e\n\x1aVERDICT_STATUS_UNSPECIFIED\x10\x00\x12\x15\n\x11VERDICT_STATUS_OK\x10\x01\x12\x18\n\x14VERDICT_STATUS_ERROR\x10\x02\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x65vent.proto\x12\x12\x61\x64rian.core_api.v1\x1a\x1b\x62uf/validate/validate.proto\",\n\x0b\x43hatMessage\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\";\n\x08ToolCall\x12\x15\n\x04name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x0c\n\x04\x61rgs\x18\x02 \x01(\t\x12\n\n\x02id\x18\x03 \x01(\t\"o\n\nTokenUsage\x12\x1e\n\rprompt_tokens\x18\x01 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00\x12\"\n\x11\x63ompletion_tokens\x18\x02 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00\x12\x1d\n\x0ctotal_tokens\x18\x03 \x01(\x05\x42\x07\xbaH\x04\x1a\x02(\x00\"Q\n\x0c\x41gentContext\x12\x10\n\x08\x61gent_id\x18\x01 \x01(\t\x12\x15\n\rsystem_prompt\x18\x02 \x01(\t\x12\x18\n\x10user_instruction\x18\x03 \x01(\t\"\xd3\x01\n\x0bLlmPairData\x12\r\n\x05model\x18\x01 \x01(\t\x12\x31\n\x08messages\x18\x02 \x03(\x0b\x32\x1f.adrian.core_api.v1.ChatMessage\x12\x0e\n\x06output\x18\x03 \x01(\t\x12\x30\n\ntool_calls\x18\x04 \x03(\x0b\x32\x1c.adrian.core_api.v1.ToolCall\x12-\n\x05usage\x18\x05 \x01(\x0b\x32\x1e.adrian.core_api.v1.TokenUsage\x12\x11\n\treasoning\x18\x06 \x01(\t\"_\n\x0cToolPairData\x12\x1a\n\ttool_name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x14\n\x0ctool_call_id\x18\x02 \x01(\t\x12\r\n\x05input\x18\x03 \x01(\t\x12\x0e\n\x06output\x18\x04 \x01(\t\"\xda\x03\n\x0bPairedEvent\x12\x19\n\x08\x65vent_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x15\n\rinvocation_id\x18\x02 \x01(\t\x12\x1b\n\nsession_id\x18\x03 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x0e\n\x06run_id\x18\x04 \x01(\t\x12\x15\n\rparent_run_id\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\t\x12\x37\n\tpair_type\x18\x07 \x01(\x0e\x32\x1c.adrian.core_api.v1.PairTypeB\x06\xbaH\x03\xc8\x01\x01\x12/\n\x05\x61gent\x18\x08 \x01(\x0b\x32 .adrian.core_api.v1.AgentContext\x12\x30\n\x06parent\x18\t \x01(\x0b\x32 .adrian.core_api.v1.AgentContext\x12.\n\x03llm\x18\n \x01(\x0b\x32\x1f.adrian.core_api.v1.LlmPairDataH\x00\x12\x30\n\x04tool\x18\x0b \x01(\x0b\x32 .adrian.core_api.v1.ToolPairDataH\x00\x12\x15\n\rconnection_id\x18\x0c \x01(\t\x12\x15\n\rmetadata_json\x18\x14 \x01(\x0c\x12\x0e\n\x06source\x18\x15 \x01(\tB\x06\n\x04\x64\x61ta\"C\n\x10PairedEventBatch\x12/\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1f.adrian.core_api.v1.PairedEvent\"G\n\tMcpServer\x12\x15\n\x04name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x11\n\ttransport\x18\x02 \x01(\t\x12\x10\n\x08\x65ndpoint\x18\x03 \x01(\t\">\n\x0cMcpInventory\x12.\n\x07servers\x18\x01 \x03(\x0b\x32\x1d.adrian.core_api.v1.McpServer\"+\n\x08LLMStack\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\"\xad\x01\n\x0cSessionLogin\x12\x1b\n\nsession_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12/\n\tllm_stack\x18\x02 \x01(\x0b\x32\x1c.adrian.core_api.v1.LLMStack\x12\x16\n\x0eschema_version\x18\x04 \x01(\r\x12\x0e\n\x06source\x18\x05 \x01(\t\x12\x15\n\rconnection_id\x18\x06 \x01(\tJ\x04\x08\x03\x10\x04R\nblock_mode\"\xcf\x01\n\x0b\x43lientFrame\x12\x31\n\x05login\x18\x01 \x01(\x0b\x32 .adrian.core_api.v1.SessionLoginH\x00\x12<\n\x0cpaired_batch\x18\x03 \x01(\x0b\x32$.adrian.core_api.v1.PairedEventBatchH\x00\x12\x39\n\rmcp_inventory\x18\x04 \x01(\x0b\x32 .adrian.core_api.v1.McpInventoryH\x00\x42\x07\n\x05\x66rameJ\x04\x08\x02\x10\x03R\x05\x62\x61tch\"\xad\x01\n\x0ePolicySnapshot\x12&\n\x04mode\x18\x01 \x01(\x0e\x32\x18.adrian.core_api.v1.Mode\x12\x11\n\tpolicy_m0\x18\x02 \x01(\x08\x12\x11\n\tpolicy_m2\x18\x03 \x01(\x08\x12\x11\n\tpolicy_m3\x18\x04 \x01(\x08\x12\x11\n\tpolicy_m4\x18\x05 \x01(\x08\x12\'\n\x1f\x66\x61il_closed_on_classifier_error\x18\x06 \x01(\x08\"*\n\x0cHitlResponse\x12\x1a\n\x12\x63ontinue_execution\x18\x01 \x01(\x08\">\n\x08LoginAck\x12\x32\n\x06policy\x18\x01 \x01(\x0b\x32\".adrian.core_api.v1.PolicySnapshot\"y\n\x0bServerFrame\x12\x31\n\tlogin_ack\x18\x01 \x01(\x0b\x32\x1c.adrian.core_api.v1.LoginAckH\x00\x12.\n\x07verdict\x18\x02 \x01(\x0b\x32\x1b.adrian.core_api.v1.VerdictH\x00\x42\x07\n\x05\x66rame\"\x90\x02\n\x07Verdict\x12\x19\n\x08\x65vent_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x1b\n\nsession_id\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01\x12\x10\n\x08mad_code\x18\x04 \x01(\t\x12\x32\n\x06policy\x18\x06 \x01(\x0b\x32\".adrian.core_api.v1.PolicySnapshot\x12.\n\x04hitl\x18\x07 \x01(\x0b\x32 .adrian.core_api.v1.HitlResponse\x12\x31\n\x06status\x18\x08 \x01(\x0e\x32!.adrian.core_api.v1.VerdictStatusJ\x04\x08\x03\x10\x04J\x04\x08\x05\x10\x06R\x0e\x63lassificationR\x08\x65scalate*L\n\x08PairType\x12\x19\n\x15PAIR_TYPE_UNSPECIFIED\x10\x00\x12\x11\n\rPAIR_TYPE_LLM\x10\x01\x12\x12\n\x0ePAIR_TYPE_TOOL\x10\x02*K\n\x04Mode\x12\x14\n\x10MODE_UNSPECIFIED\x10\x00\x12\x0e\n\nMODE_ALERT\x10\x01\x12\r\n\tMODE_HITL\x10\x02\x12\x0e\n\nMODE_BLOCK\x10\x03*`\n\rVerdictStatus\x12\x1e\n\x1aVERDICT_STATUS_UNSPECIFIED\x10\x00\x12\x15\n\x11VERDICT_STATUS_OK\x10\x01\x12\x18\n\x14VERDICT_STATUS_ERROR\x10\x02\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -56,12 +56,12 @@
_globals['_VERDICT'].fields_by_name['event_id']._serialized_options = b'\272H\004r\002\020\001'
_globals['_VERDICT'].fields_by_name['session_id']._loaded_options = None
_globals['_VERDICT'].fields_by_name['session_id']._serialized_options = b'\272H\004r\002\020\001'
- _globals['_PAIRTYPE']._serialized_start=2409
- _globals['_PAIRTYPE']._serialized_end=2485
- _globals['_MODE']._serialized_start=2487
- _globals['_MODE']._serialized_end=2562
- _globals['_VERDICTSTATUS']._serialized_start=2564
- _globals['_VERDICTSTATUS']._serialized_end=2660
+ _globals['_PAIRTYPE']._serialized_start=2474
+ _globals['_PAIRTYPE']._serialized_end=2550
+ _globals['_MODE']._serialized_start=2552
+ _globals['_MODE']._serialized_end=2627
+ _globals['_VERDICTSTATUS']._serialized_start=2629
+ _globals['_VERDICTSTATUS']._serialized_end=2725
_globals['_CHATMESSAGE']._serialized_start=64
_globals['_CHATMESSAGE']._serialized_end=108
_globals['_TOOLCALL']._serialized_start=110
@@ -71,31 +71,31 @@
_globals['_AGENTCONTEXT']._serialized_start=284
_globals['_AGENTCONTEXT']._serialized_end=365
_globals['_LLMPAIRDATA']._serialized_start=368
- _globals['_LLMPAIRDATA']._serialized_end=560
- _globals['_TOOLPAIRDATA']._serialized_start=562
- _globals['_TOOLPAIRDATA']._serialized_end=657
- _globals['_PAIREDEVENT']._serialized_start=660
- _globals['_PAIREDEVENT']._serialized_end=1111
- _globals['_PAIREDEVENTBATCH']._serialized_start=1113
- _globals['_PAIREDEVENTBATCH']._serialized_end=1180
- _globals['_MCPSERVER']._serialized_start=1182
- _globals['_MCPSERVER']._serialized_end=1253
- _globals['_MCPINVENTORY']._serialized_start=1255
- _globals['_MCPINVENTORY']._serialized_end=1317
- _globals['_LLMSTACK']._serialized_start=1319
- _globals['_LLMSTACK']._serialized_end=1362
- _globals['_SESSIONLOGIN']._serialized_start=1365
- _globals['_SESSIONLOGIN']._serialized_end=1515
- _globals['_CLIENTFRAME']._serialized_start=1518
- _globals['_CLIENTFRAME']._serialized_end=1725
- _globals['_POLICYSNAPSHOT']._serialized_start=1728
- _globals['_POLICYSNAPSHOT']._serialized_end=1901
- _globals['_HITLRESPONSE']._serialized_start=1903
- _globals['_HITLRESPONSE']._serialized_end=1945
- _globals['_LOGINACK']._serialized_start=1947
- _globals['_LOGINACK']._serialized_end=2009
- _globals['_SERVERFRAME']._serialized_start=2011
- _globals['_SERVERFRAME']._serialized_end=2132
- _globals['_VERDICT']._serialized_start=2135
- _globals['_VERDICT']._serialized_end=2407
+ _globals['_LLMPAIRDATA']._serialized_end=579
+ _globals['_TOOLPAIRDATA']._serialized_start=581
+ _globals['_TOOLPAIRDATA']._serialized_end=676
+ _globals['_PAIREDEVENT']._serialized_start=679
+ _globals['_PAIREDEVENT']._serialized_end=1153
+ _globals['_PAIREDEVENTBATCH']._serialized_start=1155
+ _globals['_PAIREDEVENTBATCH']._serialized_end=1222
+ _globals['_MCPSERVER']._serialized_start=1224
+ _globals['_MCPSERVER']._serialized_end=1295
+ _globals['_MCPINVENTORY']._serialized_start=1297
+ _globals['_MCPINVENTORY']._serialized_end=1359
+ _globals['_LLMSTACK']._serialized_start=1361
+ _globals['_LLMSTACK']._serialized_end=1404
+ _globals['_SESSIONLOGIN']._serialized_start=1407
+ _globals['_SESSIONLOGIN']._serialized_end=1580
+ _globals['_CLIENTFRAME']._serialized_start=1583
+ _globals['_CLIENTFRAME']._serialized_end=1790
+ _globals['_POLICYSNAPSHOT']._serialized_start=1793
+ _globals['_POLICYSNAPSHOT']._serialized_end=1966
+ _globals['_HITLRESPONSE']._serialized_start=1968
+ _globals['_HITLRESPONSE']._serialized_end=2010
+ _globals['_LOGINACK']._serialized_start=2012
+ _globals['_LOGINACK']._serialized_end=2074
+ _globals['_SERVERFRAME']._serialized_start=2076
+ _globals['_SERVERFRAME']._serialized_end=2197
+ _globals['_VERDICT']._serialized_start=2200
+ _globals['_VERDICT']._serialized_end=2472
# @@protoc_insertion_point(module_scope)
diff --git a/sdk/python/adrian/proto/event_pb2.pyi b/sdk/python/adrian/proto/event_pb2.pyi
index 1094342..5ae9a22 100644
--- a/sdk/python/adrian/proto/event_pb2.pyi
+++ b/sdk/python/adrian/proto/event_pb2.pyi
@@ -1,6 +1,8 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
+SPDX-License-Identifier: Apache-2.0
+Copyright (c) 2026 SecureAgentics
"""
from collections import abc as _abc
@@ -12,10 +14,10 @@ import builtins as _builtins
import sys
import typing as _typing
-if sys.version_info >= (3, 11):
- from typing import TypeAlias as _TypeAlias, Never as _Never
+if sys.version_info >= (3, 10):
+ from typing import TypeAlias as _TypeAlias
else:
- from typing_extensions import TypeAlias as _TypeAlias, Never as _Never
+ from typing_extensions import TypeAlias as _TypeAlias
DESCRIPTOR: _descriptor.FileDescriptor
@@ -114,11 +116,8 @@ class ChatMessage(_message.Message):
role: _builtins.str = ...,
content: _builtins.str = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["content", b"content", "role", b"role"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___ChatMessage: _TypeAlias = ChatMessage # noqa: Y015
@@ -144,11 +143,8 @@ class ToolCall(_message.Message):
args: _builtins.str = ...,
id: _builtins.str = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["args", b"args", "id", b"id", "name", b"name"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___ToolCall: _TypeAlias = ToolCall # noqa: Y015
@@ -174,11 +170,8 @@ class TokenUsage(_message.Message):
completion_tokens: _builtins.int = ...,
total_tokens: _builtins.int = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["completion_tokens", b"completion_tokens", "prompt_tokens", b"prompt_tokens", "total_tokens", b"total_tokens"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___TokenUsage: _TypeAlias = TokenUsage # noqa: Y015
@@ -209,11 +202,8 @@ class AgentContext(_message.Message):
system_prompt: _builtins.str = ...,
user_instruction: _builtins.str = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["agent_id", b"agent_id", "system_prompt", b"system_prompt", "user_instruction", b"user_instruction"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___AgentContext: _TypeAlias = AgentContext # noqa: Y015
@@ -230,10 +220,13 @@ class LlmPairData(_message.Message):
OUTPUT_FIELD_NUMBER: _builtins.int
TOOL_CALLS_FIELD_NUMBER: _builtins.int
USAGE_FIELD_NUMBER: _builtins.int
+ REASONING_FIELD_NUMBER: _builtins.int
model: _builtins.str
"""Model class name (e.g. "ChatAnthropic", "ChatOpenAI")."""
output: _builtins.str
"""The model's text response."""
+ reasoning: _builtins.str
+ """The model's reasoning / chain of thought."""
@_builtins.property
def messages(self) -> _containers.RepeatedCompositeFieldContainer[Global___ChatMessage]:
"""Ordered conversation messages sent to the model."""
@@ -254,12 +247,12 @@ class LlmPairData(_message.Message):
output: _builtins.str = ...,
tool_calls: _abc.Iterable[Global___ToolCall] | None = ...,
usage: Global___TokenUsage | None = ...,
+ reasoning: _builtins.str = ...,
) -> None: ...
_HasFieldArgType: _TypeAlias = _typing.Literal["usage", b"usage"] # noqa: Y015
def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
- _ClearFieldArgType: _TypeAlias = _typing.Literal["messages", b"messages", "model", b"model", "output", b"output", "tool_calls", b"tool_calls", "usage", b"usage"] # noqa: Y015
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["messages", b"messages", "model", b"model", "output", b"output", "reasoning", b"reasoning", "tool_calls", b"tool_calls", "usage", b"usage"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___LlmPairData: _TypeAlias = LlmPairData # noqa: Y015
@@ -293,11 +286,8 @@ class ToolPairData(_message.Message):
input: _builtins.str = ...,
output: _builtins.str = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["input", b"input", "output", b"output", "tool_call_id", b"tool_call_id", "tool_name", b"tool_name"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___ToolPairData: _TypeAlias = ToolPairData # noqa: Y015
@@ -318,6 +308,7 @@ class PairedEvent(_message.Message):
PARENT_FIELD_NUMBER: _builtins.int
LLM_FIELD_NUMBER: _builtins.int
TOOL_FIELD_NUMBER: _builtins.int
+ CONNECTION_ID_FIELD_NUMBER: _builtins.int
METADATA_JSON_FIELD_NUMBER: _builtins.int
SOURCE_FIELD_NUMBER: _builtins.int
event_id: _builtins.str
@@ -336,6 +327,12 @@ class PairedEvent(_message.Message):
"""ISO 8601 timestamp of the pair's completion (end event)."""
pair_type: Global___PairType.ValueType
"""Whether this is an LLM pair or a tool pair."""
+ connection_id: _builtins.str
+ """Per-connection routing id (mirrors SessionLogin.connection_id). When set,
+ the server routes this connection's verdict on connection_id instead of
+ session_id, so concurrent connections sharing one session_id (parallel
+ Claude Code tool-call hooks) do not evict each other.
+ """
metadata_json: _builtins.bytes
"""Escape hatch for framework metadata not modelled as first-class fields
(e.g. LangGraph checkpoint_ns, arbitrary tags).
@@ -371,12 +368,13 @@ class PairedEvent(_message.Message):
parent: Global___AgentContext | None = ...,
llm: Global___LlmPairData | None = ...,
tool: Global___ToolPairData | None = ...,
+ connection_id: _builtins.str = ...,
metadata_json: _builtins.bytes = ...,
source: _builtins.str = ...,
) -> None: ...
_HasFieldArgType: _TypeAlias = _typing.Literal["agent", b"agent", "data", b"data", "llm", b"llm", "parent", b"parent", "tool", b"tool"] # noqa: Y015
def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
- _ClearFieldArgType: _TypeAlias = _typing.Literal["agent", b"agent", "data", b"data", "event_id", b"event_id", "invocation_id", b"invocation_id", "llm", b"llm", "metadata_json", b"metadata_json", "pair_type", b"pair_type", "parent", b"parent", "parent_run_id", b"parent_run_id", "run_id", b"run_id", "session_id", b"session_id", "source", b"source", "timestamp", b"timestamp", "tool", b"tool"] # noqa: Y015
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["agent", b"agent", "connection_id", b"connection_id", "data", b"data", "event_id", b"event_id", "invocation_id", b"invocation_id", "llm", b"llm", "metadata_json", b"metadata_json", "pair_type", b"pair_type", "parent", b"parent", "parent_run_id", b"parent_run_id", "run_id", b"run_id", "session_id", b"session_id", "source", b"source", "timestamp", b"timestamp", "tool", b"tool"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
_WhichOneofReturnType_data: _TypeAlias = _typing.Literal["llm", "tool"] # noqa: Y015
_WhichOneofArgType_data: _TypeAlias = _typing.Literal["data", b"data"] # noqa: Y015
@@ -398,11 +396,8 @@ class PairedEventBatch(_message.Message):
*,
events: _abc.Iterable[Global___PairedEvent] | None = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["events", b"events"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___PairedEventBatch: _TypeAlias = PairedEventBatch # noqa: Y015
@@ -435,11 +430,8 @@ class McpServer(_message.Message):
transport: _builtins.str = ...,
endpoint: _builtins.str = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["endpoint", b"endpoint", "name", b"name", "transport", b"transport"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___McpServer: _TypeAlias = McpServer # noqa: Y015
@@ -460,11 +452,8 @@ class McpInventory(_message.Message):
*,
servers: _abc.Iterable[Global___McpServer] | None = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["servers", b"servers"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___McpInventory: _TypeAlias = McpInventory # noqa: Y015
@@ -486,11 +475,8 @@ class LLMStack(_message.Message):
provider: _builtins.str = ...,
model: _builtins.str = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["model", b"model", "provider", b"provider"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___LLMStack: _TypeAlias = LLMStack # noqa: Y015
@@ -504,6 +490,7 @@ class SessionLogin(_message.Message):
LLM_STACK_FIELD_NUMBER: _builtins.int
SCHEMA_VERSION_FIELD_NUMBER: _builtins.int
SOURCE_FIELD_NUMBER: _builtins.int
+ CONNECTION_ID_FIELD_NUMBER: _builtins.int
session_id: _builtins.str
"""Session identifier."""
schema_version: _builtins.int
@@ -512,6 +499,13 @@ class SessionLogin(_message.Message):
"""SDK / integration that produced this session (e.g. "claude-code"); the
server branches HITL behavior on it. Empty for legacy SDKs.
"""
+ connection_id: _builtins.str
+ """Optional per-connection routing id, unique per live WebSocket connection.
+ When set, the server routes this connection's verdicts on connection_id
+ instead of session_id (and keys the single-owner guard on it), so multiple
+ concurrent connections sharing one session_id (parallel Claude Code
+ tool-call hooks) no longer evict each other.
+ """
@_builtins.property
def llm_stack(self) -> Global___LLMStack:
"""LLM stack information for this session."""
@@ -523,12 +517,12 @@ class SessionLogin(_message.Message):
llm_stack: Global___LLMStack | None = ...,
schema_version: _builtins.int = ...,
source: _builtins.str = ...,
+ connection_id: _builtins.str = ...,
) -> None: ...
_HasFieldArgType: _TypeAlias = _typing.Literal["llm_stack", b"llm_stack"] # noqa: Y015
def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
- _ClearFieldArgType: _TypeAlias = _typing.Literal["llm_stack", b"llm_stack", "schema_version", b"schema_version", "session_id", b"session_id", "source", b"source"] # noqa: Y015
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["connection_id", b"connection_id", "llm_stack", b"llm_stack", "schema_version", b"schema_version", "session_id", b"session_id", "source", b"source"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___SessionLogin: _TypeAlias = SessionLogin # noqa: Y015
@@ -608,11 +602,8 @@ class PolicySnapshot(_message.Message):
policy_m4: _builtins.bool = ...,
fail_closed_on_classifier_error: _builtins.bool = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["fail_closed_on_classifier_error", b"fail_closed_on_classifier_error", "mode", b"mode", "policy_m0", b"policy_m0", "policy_m2", b"policy_m2", "policy_m3", b"policy_m3", "policy_m4", b"policy_m4"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___PolicySnapshot: _TypeAlias = PolicySnapshot # noqa: Y015
@@ -633,11 +624,8 @@ class HitlResponse(_message.Message):
*,
continue_execution: _builtins.bool = ...,
) -> None: ...
- _HasFieldArgType: _TypeAlias = _Never # noqa: Y015
- def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["continue_execution", b"continue_execution"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___HitlResponse: _TypeAlias = HitlResponse # noqa: Y015
@@ -666,7 +654,6 @@ class LoginAck(_message.Message):
def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["policy", b"policy"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___LoginAck: _TypeAlias = LoginAck # noqa: Y015
@@ -762,6 +749,5 @@ class Verdict(_message.Message):
def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
_ClearFieldArgType: _TypeAlias = _typing.Literal["event_id", b"event_id", "hitl", b"hitl", "mad_code", b"mad_code", "policy", b"policy", "session_id", b"session_id", "status", b"status"] # noqa: Y015
def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
- def WhichOneof(self, oneof_group: _Never) -> None: ...
Global___Verdict: _TypeAlias = Verdict # noqa: Y015
diff --git a/sdk/python/adrian/types.py b/sdk/python/adrian/types.py
index b934a17..37d7b52 100644
--- a/sdk/python/adrian/types.py
+++ b/sdk/python/adrian/types.py
@@ -116,11 +116,13 @@ class LlmEndData(TypedDict):
output: Generated text output.
tool_calls: Tool calls extracted from the response.
usage: Token usage statistics, or ``None`` if unavailable.
+ reasoning: The model's reasoning, where the provider exposes it.
"""
output: str
tool_calls: list[ToolCallRecord]
usage: TokenUsage | None
+ reasoning: str
class ToolStartData(TypedDict):
diff --git a/sdk/python/adrian/ws.py b/sdk/python/adrian/ws.py
index 2462807..4254ccd 100644
--- a/sdk/python/adrian/ws.py
+++ b/sdk/python/adrian/ws.py
@@ -154,6 +154,7 @@ def _paired_event_to_proto(event: PairedEvent) -> pb.PairedEvent:
pb_msg.content = msg["content"]
proto.llm.output = event.data.output
+ proto.llm.reasoning = event.data.reasoning
for tc in event.data.tool_calls:
pb_tc = proto.llm.tool_calls.add()
diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml
index 632d4aa..2081290 100644
--- a/sdk/python/pyproject.toml
+++ b/sdk/python/pyproject.toml
@@ -51,6 +51,7 @@ dev = [
"ruff==0.15.12",
"basedpyright==1.39.3",
"pre-commit>=4.6.0",
+ "mypy-protobuf==5.0.0",
"langgraph==1.1.2",
"langgraph-prebuilt==1.0.8",
"langchain-mcp-adapters>=0.2.2",
diff --git a/sdk/python/tests/test_anthropic_handler.py b/sdk/python/tests/test_anthropic_handler.py
index ff2848f..7e8fa56 100644
--- a/sdk/python/tests/test_anthropic_handler.py
+++ b/sdk/python/tests/test_anthropic_handler.py
@@ -23,12 +23,14 @@
_emit_pair,
_extract_anthropic_tool_calls,
_extract_anthropic_usage,
+ _extract_reasoning,
_extract_response_text,
_flatten_anthropic_messages,
_flatten_content,
_gate_response,
_GatedAsyncMessageStreamManager,
_GatedMessageStreamManager,
+ _request_summarised_thinking,
_resolve_invocation_id,
_rewrite_blocked_response,
_safe_snapshot,
@@ -92,6 +94,23 @@ def _make_text_response(
return response
+def _make_thinking_response(
+ *,
+ model: str = "claude-opus-5",
+ thinking: str = "Let me work through this.",
+ text: str = "Hello!",
+) -> MagicMock:
+ """Mock response whose content leads with a thinking block."""
+ thinking_block = MagicMock()
+ thinking_block.type = "thinking"
+ thinking_block.thinking = thinking
+
+ response = _make_text_response(model=model, text=text)
+ response.content = [thinking_block, *response.content]
+
+ return response
+
+
def _wired_hooks(config: AdrianConfig) -> tuple[HookRegistry, _Collector]:
"""Return a HookRegistry + Collector pair and wire them into the handler."""
collector = _Collector()
@@ -553,6 +572,121 @@ def test_parent_is_none(self) -> None:
)
assert pair.parent is None
+ def test_thinking_becomes_reasoning(self) -> None:
+ pair = build_anthropic_llm_pair(
+ flat_messages=[ChatMessage(role="user", content="hi")],
+ response=_make_thinking_response(
+ thinking="Weighing the options.", text="Done."
+ ),
+ model="m",
+ session_id="s",
+ invocation_id="i",
+ run_id="r",
+ )
+ assert isinstance(pair.data, LlmPairData)
+ assert pair.data.reasoning == "Weighing the options."
+ # Reasoning is additive: the response text is untouched.
+ assert pair.data.output == "Done."
+
+ def test_no_thinking_leaves_reasoning_empty(self) -> None:
+ pair = build_anthropic_llm_pair(
+ flat_messages=[ChatMessage(role="user", content="hi")],
+ response=_make_text_response(text="Done."),
+ model="m",
+ session_id="s",
+ invocation_id="i",
+ run_id="r",
+ )
+ assert isinstance(pair.data, LlmPairData)
+ assert pair.data.reasoning == ""
+
+
+# ------------------------------------------------------------------
+# _extract_reasoning
+# ------------------------------------------------------------------
+
+
+class TestExtractReasoning:
+ def test_object_thinking_block(self) -> None:
+ block = MagicMock()
+ block.type = "thinking"
+ block.thinking = "step one"
+ assert _extract_reasoning([block]) == "step one"
+
+ def test_dict_thinking_block(self) -> None:
+ blocks = [{"type": "thinking", "thinking": "step one"}]
+ assert _extract_reasoning(blocks) == "step one"
+
+ def test_multiple_blocks_joined(self) -> None:
+ blocks = [
+ {"type": "thinking", "thinking": "step one"},
+ {"type": "text", "text": "ignored"},
+ {"type": "thinking", "thinking": "step two"},
+ ]
+ assert _extract_reasoning(blocks) == "step one\n\nstep two"
+
+ def test_redacted_thinking_is_not_reasoning(self) -> None:
+ # redacted_thinking carries an encrypted blob, not readable text.
+ blocks = [{"type": "redacted_thinking", "data": "AAAA"}]
+ assert _extract_reasoning(blocks) == ""
+
+ def test_omitted_thinking_yields_nothing(self) -> None:
+ # display="omitted" still emits a block, with empty text.
+ blocks = [{"type": "thinking", "thinking": "", "signature": "sig"}]
+ assert _extract_reasoning(blocks) == ""
+
+ def test_text_only_content(self) -> None:
+ assert _extract_reasoning([{"type": "text", "text": "hi"}]) == ""
+
+
+# ------------------------------------------------------------------
+# _request_summarised_thinking
+# ------------------------------------------------------------------
+
+
+class TestRequestSummarisedThinking:
+ def test_adaptive_gets_summarized_display(self) -> None:
+ kwargs: dict[str, Any] = {"thinking": {"type": "adaptive"}}
+ _request_summarised_thinking(kwargs)
+ assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
+
+ def test_enabled_gets_summarized_display(self) -> None:
+ kwargs: dict[str, Any] = {
+ "thinking": {"type": "enabled", "budget_tokens": 2048}
+ }
+ _request_summarised_thinking(kwargs)
+ assert kwargs["thinking"]["display"] == "summarized"
+ assert kwargs["thinking"]["budget_tokens"] == 2048
+
+ def test_explicit_display_is_respected(self) -> None:
+ kwargs: dict[str, Any] = {
+ "thinking": {"type": "adaptive", "display": "omitted"}
+ }
+ _request_summarised_thinking(kwargs)
+ assert kwargs["thinking"]["display"] == "omitted"
+
+ def test_thinking_disabled_untouched(self) -> None:
+ kwargs: dict[str, Any] = {"thinking": {"type": "disabled"}}
+ _request_summarised_thinking(kwargs)
+ assert kwargs["thinking"] == {"type": "disabled"}
+
+ def test_no_thinking_key_untouched(self) -> None:
+ kwargs: dict[str, Any] = {"model": "claude-opus-5"}
+ _request_summarised_thinking(kwargs)
+ assert "thinking" not in kwargs
+
+ def test_non_dict_thinking_untouched(self) -> None:
+ kwargs: dict[str, Any] = {"thinking": "adaptive"}
+ _request_summarised_thinking(kwargs)
+ assert kwargs["thinking"] == "adaptive"
+
+ def test_caller_dict_is_not_mutated_in_place(self) -> None:
+ # The caller may reuse their thinking config across calls.
+ original = {"type": "adaptive"}
+ kwargs: dict[str, Any] = {"thinking": original}
+ _request_summarised_thinking(kwargs)
+ assert original == {"type": "adaptive"}
+
# ------------------------------------------------------------------
# _emit_pair
@@ -1884,7 +2018,9 @@ class TestChatModelScopeSuppression:
"""ChatAnthropic drives the same SDK methods Adrian patches.
Without suppression one LangChain call produces two LLM events: one
- from the callbacks and one from the Anthropic patch.
+ from the callbacks and one from the Anthropic patch. The thinking
+ injection must survive suppression, since it is what gives the
+ LangChain event its reasoning in the first place.
"""
@pytest.fixture(autouse=True) # pyright: ignore[reportUntypedFunctionDecorator]
@@ -1947,6 +2083,24 @@ def test_event_emitted_outside_scope(self) -> None:
assert len(collector.events) == 1
+ def test_thinking_injection_still_applies_inside_scope(self) -> None:
+ from anthropic.resources.messages import Messages
+
+ seen: list[dict[str, Any]] = []
+
+ def _capture(_self: Any, **kw: Any) -> Any: # noqa: ANN401
+ seen.append(kw)
+ return _make_text_response()
+
+ Messages.create = _capture # type: ignore[method-assign, assignment]
+ hooks, _ = _wired_hooks(AdrianConfig(session_id="s"))
+ patch_anthropic(lambda: hooks, lambda: AdrianConfig(session_id="s"))
+
+ with chat_model_scope():
+ Messages.create(MagicMock(), thinking={"type": "adaptive"}, **_KWARGS)
+
+ assert seen[0]["thinking"] == {"type": "adaptive", "display": "summarized"}
+
def test_stream_returns_sdk_manager_inside_scope(self) -> None:
from anthropic.resources.messages import Messages
diff --git a/sdk/python/tests/test_handler.py b/sdk/python/tests/test_handler.py
index eed45f6..2b6f8a2 100644
--- a/sdk/python/tests/test_handler.py
+++ b/sdk/python/tests/test_handler.py
@@ -212,6 +212,182 @@ async def test_tool_calls_extracted(
assert data.tool_calls[0]["name"] == "get_fleet_status"
+class TestReasoningInLlmPair:
+ """Reasoning blocks are captured alongside, not instead of, output.
+
+ The shapes here are the two the providers actually emit: OpenAI's
+ ``reasoning`` block with a summary list, and Anthropic's ``thinking``
+ block, both reaching us through LangChain as content blocks.
+ """
+
+ async def _emit(
+ self,
+ handler: AdrianCallbackHandler,
+ message: AIMessage,
+ ) -> None:
+ run_id = uuid4()
+ await handler.on_chat_model_start(
+ serialized={"kwargs": {"model_name": "gpt-5"}},
+ messages=[[HumanMessage(content="17 * 23?")]],
+ run_id=run_id,
+ )
+ await handler.on_llm_end(
+ response=LLMResult(generations=[[ChatGeneration(message=message)]]),
+ run_id=run_id,
+ )
+
+ async def test_openai_reasoning_summary_captured(
+ self,
+ handler_and_events: tuple[AdrianCallbackHandler, list[PairedEvent]],
+ ) -> None:
+ handler, events = handler_and_events
+ msg = AIMessage(
+ content=[
+ {
+ "type": "reasoning",
+ "summary": [
+ {"type": "summary_text", "text": "First step."},
+ {"type": "summary_text", "text": "Second step."},
+ ],
+ },
+ {"type": "text", "text": "391"},
+ ]
+ )
+ await self._emit(handler, msg)
+
+ data = events[0].data
+ assert isinstance(data, LlmPairData)
+ assert data.reasoning == "First step.\n\nSecond step."
+ assert data.output == "391"
+
+ async def test_anthropic_thinking_captured(
+ self,
+ handler_and_events: tuple[AdrianCallbackHandler, list[PairedEvent]],
+ ) -> None:
+ handler, events = handler_and_events
+ msg = AIMessage(
+ content=[
+ {"type": "thinking", "thinking": "17 * 20 + 17 * 3."},
+ {"type": "text", "text": "391"},
+ ]
+ )
+ await self._emit(handler, msg)
+
+ data = events[0].data
+ assert isinstance(data, LlmPairData)
+ assert data.reasoning == "17 * 20 + 17 * 3."
+ assert data.output == "391"
+
+ async def test_plain_string_content_has_no_reasoning(
+ self,
+ handler_and_events: tuple[AdrianCallbackHandler, list[PairedEvent]],
+ ) -> None:
+ handler, events = handler_and_events
+ await self._emit(handler, AIMessage(content="391"))
+
+ data = events[0].data
+ assert isinstance(data, LlmPairData)
+ assert data.reasoning == ""
+ assert data.output == "391"
+
+ async def test_output_version_v1_reasoning_string_blocks(
+ self,
+ handler_and_events: tuple[AdrianCallbackHandler, list[PairedEvent]],
+ ) -> None:
+ # output_version="v1" drops the summary list and emits one block
+ # per reasoning pass, each carrying a bare `reasoning` string.
+ handler, events = handler_and_events
+ msg = AIMessage(
+ content=[
+ {"type": "reasoning", "id": "rs_1", "reasoning": "First pass."},
+ {"type": "reasoning", "id": "rs_1", "reasoning": "Second pass."},
+ {"type": "text", "text": "391"},
+ ]
+ )
+ await self._emit(handler, msg)
+
+ data = events[0].data
+ assert isinstance(data, LlmPairData)
+ assert data.reasoning == "First pass.\n\nSecond pass."
+ assert data.output == "391"
+
+ async def test_output_version_v0_reasoning_in_additional_kwargs(
+ self,
+ handler_and_events: tuple[AdrianCallbackHandler, list[PairedEvent]],
+ ) -> None:
+ # output_version="v0" keeps content text-only and parks the whole
+ # reasoning block in additional_kwargs.
+ handler, events = handler_and_events
+ msg = AIMessage(
+ content=[{"type": "text", "text": "391"}],
+ additional_kwargs={
+ "reasoning": {
+ "id": "rs_1",
+ "type": "reasoning",
+ "summary": [
+ {"type": "summary_text", "text": "First step."},
+ {"type": "summary_text", "text": "Second step."},
+ ],
+ "encrypted_content": "gAAAA...",
+ }
+ },
+ )
+ await self._emit(handler, msg)
+
+ data = events[0].data
+ assert isinstance(data, LlmPairData)
+ assert data.reasoning == "First step.\n\nSecond step."
+ assert data.output == "391"
+
+ async def test_content_blocks_win_over_additional_kwargs(
+ self,
+ handler_and_events: tuple[AdrianCallbackHandler, list[PairedEvent]],
+ ) -> None:
+ # The fallback must not append to, or duplicate, a block that
+ # content already supplied.
+ handler, events = handler_and_events
+ msg = AIMessage(
+ content=[
+ {
+ "type": "reasoning",
+ "summary": [{"type": "summary_text", "text": "From content."}],
+ },
+ {"type": "text", "text": "391"},
+ ],
+ additional_kwargs={"reasoning": {"reasoning": "From kwargs."}},
+ )
+ await self._emit(handler, msg)
+
+ data = events[0].data
+ assert isinstance(data, LlmPairData)
+ assert data.reasoning == "From content."
+
+ async def test_encrypted_only_reasoning_yields_nothing(
+ self,
+ handler_and_events: tuple[AdrianCallbackHandler, list[PairedEvent]],
+ ) -> None:
+ # o4-mini on an unverified org: block present, summary empty, the
+ # real chain of thought only as an opaque blob.
+ handler, events = handler_and_events
+ msg = AIMessage(
+ content=[
+ {
+ "type": "reasoning",
+ "summary": [],
+ "content": [],
+ "encrypted_content": "gAAAAABqgu5W...",
+ },
+ {"type": "text", "text": "391"},
+ ]
+ )
+ await self._emit(handler, msg)
+
+ data = events[0].data
+ assert isinstance(data, LlmPairData)
+ assert data.reasoning == ""
+ assert data.output == "391"
+
+
class TestExtractModelName:
def test_none_returns_unknown(self) -> None:
assert extract_model_name(None) == "unknown"
diff --git a/sdk/python/tests/test_pii_redactor.py b/sdk/python/tests/test_pii_redactor.py
index fcf7b29..9c99394 100644
--- a/sdk/python/tests/test_pii_redactor.py
+++ b/sdk/python/tests/test_pii_redactor.py
@@ -24,6 +24,7 @@ def _llm_event(
system_prompt: str = "",
user_instruction: str = "",
parent: ParentContext | None = None,
+ reasoning: str = "",
) -> PairedEvent:
return PairedEvent(
event_id="evt-1",
@@ -43,6 +44,7 @@ def _llm_event(
messages=messages or [], # type: ignore[arg-type]
output=output,
tool_calls=tool_calls or [], # type: ignore[arg-type]
+ reasoning=reasoning,
),
)
@@ -99,6 +101,24 @@ def test_redacts_tool_call_args(self) -> None:
args = redacted.data.tool_calls[0]["args"]
assert "user@test.com" not in str(args)
+ def test_redacts_reasoning(self) -> None:
+ # Reasoning quotes the prompt back verbatim, so it leaks the same
+ # PII the other fields do.
+ event = _llm_event(reasoning="The user gave 555-123-4567 as a contact")
+ redactor = PiiRedactor()
+ redacted = redactor.redact_event(event)
+ assert isinstance(redacted.data, LlmPairData)
+ assert "555-123-4567" not in redacted.data.reasoning
+ assert "[PHONE_REDACTED]" in redacted.data.reasoning
+
+ def test_reasoning_pii_triggers_a_copy(self) -> None:
+ # A hit anywhere must not mutate the caller's event in place.
+ event = _llm_event(reasoning="email user@test.com")
+ redacted = PiiRedactor().redact_event(event)
+ assert redacted is not event
+ assert isinstance(event.data, LlmPairData)
+ assert event.data.reasoning == "email user@test.com"
+
def test_redacts_system_prompt(self) -> None:
event = _llm_event(system_prompt="Contact admin@corp.com for help")
redactor = PiiRedactor()
diff --git a/sdk/python/tests/test_ws.py b/sdk/python/tests/test_ws.py
index cbb6da3..2f1f805 100644
--- a/sdk/python/tests/test_ws.py
+++ b/sdk/python/tests/test_ws.py
@@ -102,6 +102,24 @@ def test_llm_pair_with_tool_calls(self) -> None:
assert proto.llm.tool_calls[0].id == "tc-1"
assert proto.llm.tool_calls[0].name == "foo"
+ def test_llm_reasoning_serialised(self) -> None:
+ pe = _llm_pair()
+ assert isinstance(pe.data, LlmPairData)
+ pe.data.reasoning = "thinking it through"
+ proto = _paired_event_to_proto(pe)
+ assert proto.llm.reasoning == "thinking it through"
+ assert proto.llm.output == "sure"
+
+ def test_llm_reasoning_absent_costs_nothing(self) -> None:
+ # An unset proto3 scalar is not serialised, so older clients that
+ # never populate reasoning put no extra bytes on the wire.
+ pe = _llm_pair()
+ proto = _paired_event_to_proto(pe)
+ assert proto.llm.reasoning == ""
+ without = len(proto.SerializeToString())
+ proto.llm.reasoning = "x"
+ assert len(proto.SerializeToString()) > without
+
def test_parent_context(self) -> None:
pe = _tool_pair()
pe.parent = ParentContext(
diff --git a/sdk/python/uv.lock b/sdk/python/uv.lock
index 35c6157..1a6da39 100644
--- a/sdk/python/uv.lock
+++ b/sdk/python/uv.lock
@@ -22,6 +22,7 @@ dev = [
{ name = "langchain-mcp-adapters" },
{ name = "langgraph" },
{ name = "langgraph-prebuilt" },
+ { name = "mypy-protobuf" },
{ name = "pre-commit" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -38,6 +39,7 @@ requires-dist = [
{ name = "langchain-mcp-adapters", marker = "extra == 'dev'", specifier = ">=0.2.2" },
{ name = "langgraph", marker = "extra == 'dev'", specifier = "==1.1.2" },
{ name = "langgraph-prebuilt", marker = "extra == 'dev'", specifier = "==1.0.8" },
+ { name = "mypy-protobuf", marker = "extra == 'dev'", specifier = "==5.0.0" },
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.6.0" },
{ name = "protobuf", specifier = ">=5.29.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
@@ -775,6 +777,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" },
]
+[[package]]
+name = "mypy-protobuf"
+version = "5.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+ { name = "types-protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/48/658827446368bca30a94e545598065587ece9cd09b678d7d2895c37a59d2/mypy_protobuf-5.0.0.tar.gz", hash = "sha256:6fdd1cfdbb4419c713291d800a332d4bba6510dbd1341ed95e0bcc82fcadb6b5", size = 37309, upload-time = "2026-01-13T17:10:12.616Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/b1/ab1e7a49930a8c1d1f7a570bbd4ec7d552ef035acc7aa4b97906e17a34a9/mypy_protobuf-5.0.0-py3-none-any.whl", hash = "sha256:3a7dd753ef3e3b8783a824eb51f07983f62812f9ec066e4fbb1b22d6c5dc36d0", size = 26008, upload-time = "2026-01-13T17:10:11.053Z" },
+]
+
[[package]]
name = "nodeenv"
version = "1.10.0"
@@ -1413,6 +1428,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
]
+[[package]]
+name = "types-protobuf"
+version = "7.34.1.20260518"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/29/59/e2b13b499d15e6720150c4b1a8d91e31fcacf716b432397475b3151ff7e4/types_protobuf-7.34.1.20260518.tar.gz", hash = "sha256:28cfaded25889cb83ebfb63cfb0a43628f0b6f3785767bec17287dc6468795f2", size = 68936, upload-time = "2026-05-18T06:01:47.332Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/1f/ec5caf72c2e3b688ca3927e0979a04ddad19e1afc4bf1c199bd743e0f419/types_protobuf-7.34.1.20260518-py3-none-any.whl", hash = "sha256:a0a5337413347166439c0e07cbc26c6164d091401c6f01b1dfd8cdb966c4dd8f", size = 85992, upload-time = "2026-05-18T06:01:45.696Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"
diff --git a/sdk/typescript/packages/core/src/proto/schema.ts b/sdk/typescript/packages/core/src/proto/schema.ts
index 818a722..9fb48b0 100644
--- a/sdk/typescript/packages/core/src/proto/schema.ts
+++ b/sdk/typescript/packages/core/src/proto/schema.ts
@@ -61,7 +61,7 @@ message ChatMessage { string role = 1; string content = 2; }
message ToolCall { string name = 1; string args = 2; string id = 3; }
message TokenUsage { int32 prompt_tokens = 1; int32 completion_tokens = 2; int32 total_tokens = 3; }
message AgentContext { string agent_id = 1; string system_prompt = 2; string user_instruction = 3; }
-message LlmPairData { string model = 1; repeated ChatMessage messages = 2; string output = 3; repeated ToolCall tool_calls = 4; TokenUsage usage = 5; }
+message LlmPairData { string model = 1; repeated ChatMessage messages = 2; string output = 3; repeated ToolCall tool_calls = 4; TokenUsage usage = 5; string reasoning = 6; }
message ToolPairData { string tool_name = 1; string tool_call_id = 2; string input = 3; string output = 4; }
message PairedEvent { string event_id = 1; string invocation_id = 2; string session_id = 3; string run_id = 4; string parent_run_id = 5; string timestamp = 6; PairType pair_type = 7; AgentContext agent = 8; AgentContext parent = 9; oneof data { LlmPairData llm = 10; ToolPairData tool = 11; } bytes metadata_json = 20; }
message PairedEventBatch { repeated PairedEvent events = 1; }