kagent-claude 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kagent/claude/__init__.py +23 -0
- kagent/claude/_a2a.py +132 -0
- kagent/claude/_converters.py +217 -0
- kagent/claude/_error_mappings.py +186 -0
- kagent/claude/_executor.py +974 -0
- kagent/claude/_hitl.py +363 -0
- kagent/claude/_metadata_utils.py +94 -0
- kagent/claude/_session_store.py +83 -0
- kagent/claude/_tracing.py +83 -0
- kagent/claude/py.typed +0 -0
- kagent/claude/server.py +324 -0
- kagent_claude-0.2.0.dist-info/METADATA +87 -0
- kagent_claude-0.2.0.dist-info/RECORD +15 -0
- kagent_claude-0.2.0.dist-info/WHEEL +4 -0
- kagent_claude-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from ._a2a import KAgentApp
|
|
4
|
+
from ._executor import ClaudeAgentExecutor, ClaudeAgentExecutorConfig
|
|
5
|
+
from ._hitl import ApprovalDecision, HitlBridge
|
|
6
|
+
from ._session_store import ClaudeSessionStore, SessionStore
|
|
7
|
+
from ._tracing import trace_query
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"KAgentApp",
|
|
11
|
+
"ClaudeAgentExecutor",
|
|
12
|
+
"ClaudeAgentExecutorConfig",
|
|
13
|
+
"ClaudeSessionStore",
|
|
14
|
+
"SessionStore",
|
|
15
|
+
"HitlBridge",
|
|
16
|
+
"ApprovalDecision",
|
|
17
|
+
"trace_query",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
__version__ = version("kagent-claude")
|
|
22
|
+
except PackageNotFoundError:
|
|
23
|
+
__version__ = "0.0.0-dev"
|
kagent/claude/_a2a.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""KAgent Claude Agent SDK A2A Server Integration."""
|
|
2
|
+
|
|
3
|
+
import faulthandler
|
|
4
|
+
import logging
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from a2a.server.apps import A2AStarletteApplication
|
|
9
|
+
from a2a.server.request_handlers import DefaultRequestHandler
|
|
10
|
+
from a2a.types import AgentCard
|
|
11
|
+
from claude_agent_sdk import ClaudeAgentOptions
|
|
12
|
+
from fastapi import FastAPI, Request
|
|
13
|
+
from fastapi.responses import PlainTextResponse
|
|
14
|
+
|
|
15
|
+
from kagent.core import KAgentConfig, configure_logging, configure_tracing
|
|
16
|
+
from kagent.core.a2a import (
|
|
17
|
+
KAgentRequestContextBuilder,
|
|
18
|
+
KAgentTaskStore,
|
|
19
|
+
get_a2a_max_content_length,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ._executor import ClaudeAgentExecutor, ClaudeAgentExecutorConfig
|
|
23
|
+
from ._session_store import ClaudeSessionStore
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _health_check(request: Request) -> PlainTextResponse:
|
|
29
|
+
return PlainTextResponse("OK")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _thread_dump(request: Request) -> PlainTextResponse:
|
|
33
|
+
import tempfile
|
|
34
|
+
|
|
35
|
+
with tempfile.TemporaryFile(mode="w+") as tmp:
|
|
36
|
+
faulthandler.dump_traceback(file=tmp, all_threads=True)
|
|
37
|
+
tmp.seek(0)
|
|
38
|
+
return PlainTextResponse(tmp.read())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class KAgentApp:
|
|
42
|
+
"""
|
|
43
|
+
Builds an A2A-compliant HTTP server wrapping the Claude Agent SDK.
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
app = KAgentApp(
|
|
47
|
+
options=ClaudeAgentOptions(allowed_tools=["Bash", "Read"]),
|
|
48
|
+
agent_card=AgentCard(...),
|
|
49
|
+
config=KAgentConfig(), # reads from KAGENT_URL, KAGENT_NAME, KAGENT_NAMESPACE env vars
|
|
50
|
+
)
|
|
51
|
+
app.run()
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
options: ClaudeAgentOptions,
|
|
58
|
+
agent_card: AgentCard,
|
|
59
|
+
config: KAgentConfig = None,
|
|
60
|
+
executor_config: ClaudeAgentExecutorConfig | None = None,
|
|
61
|
+
tracing: bool = True,
|
|
62
|
+
):
|
|
63
|
+
self._options = options
|
|
64
|
+
self.agent_card = AgentCard.model_validate(agent_card)
|
|
65
|
+
self.config = config or KAgentConfig()
|
|
66
|
+
self._enable_tracing = tracing
|
|
67
|
+
self._session_store = ClaudeSessionStore()
|
|
68
|
+
self._executor_config = executor_config or ClaudeAgentExecutorConfig()
|
|
69
|
+
|
|
70
|
+
def build(self) -> FastAPI:
|
|
71
|
+
"""Construct and return the FastAPI ASGI application."""
|
|
72
|
+
http_client = httpx.AsyncClient(base_url=self.config.url)
|
|
73
|
+
|
|
74
|
+
agent_executor = ClaudeAgentExecutor(
|
|
75
|
+
options=self._options,
|
|
76
|
+
session_store=self._session_store,
|
|
77
|
+
app_name=self.config.app_name,
|
|
78
|
+
config=self._executor_config,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
task_store = KAgentTaskStore(http_client)
|
|
82
|
+
request_context_builder = KAgentRequestContextBuilder(task_store=task_store)
|
|
83
|
+
request_handler = DefaultRequestHandler(
|
|
84
|
+
agent_executor=agent_executor,
|
|
85
|
+
task_store=task_store,
|
|
86
|
+
request_context_builder=request_context_builder,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
max_content_length = get_a2a_max_content_length()
|
|
90
|
+
a2a_app = A2AStarletteApplication(
|
|
91
|
+
agent_card=self.agent_card,
|
|
92
|
+
http_handler=request_handler,
|
|
93
|
+
max_content_length=max_content_length,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Lifespan for graceful shutdown
|
|
97
|
+
@asynccontextmanager
|
|
98
|
+
async def lifespan(app: FastAPI):
|
|
99
|
+
logger.info(f"KAgent Claude starting: {self.config.app_name}")
|
|
100
|
+
yield
|
|
101
|
+
# Shutdown: cancel running queries
|
|
102
|
+
await agent_executor.shutdown()
|
|
103
|
+
await http_client.aclose()
|
|
104
|
+
logger.info(f"KAgent Claude stopped: {self.config.app_name}")
|
|
105
|
+
|
|
106
|
+
faulthandler.enable()
|
|
107
|
+
app = FastAPI(
|
|
108
|
+
title=f"KAgent Claude: {self.config.app_name}",
|
|
109
|
+
description=f"Claude Agent SDK with KAgent integration: {self.agent_card.description}",
|
|
110
|
+
version=self.agent_card.version,
|
|
111
|
+
lifespan=lifespan,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
configure_logging()
|
|
115
|
+
|
|
116
|
+
if self._enable_tracing:
|
|
117
|
+
try:
|
|
118
|
+
configure_tracing(self.config.name, self.config.namespace, app)
|
|
119
|
+
except Exception:
|
|
120
|
+
logger.exception("Failed to configure tracing")
|
|
121
|
+
|
|
122
|
+
app.add_route("/health", methods=["GET"], route=_health_check)
|
|
123
|
+
app.add_route("/thread_dump", methods=["GET"], route=_thread_dump)
|
|
124
|
+
a2a_app.add_routes_to_app(app)
|
|
125
|
+
|
|
126
|
+
return app
|
|
127
|
+
|
|
128
|
+
def run(self, host: str = "0.0.0.0", port: int = 8080) -> None:
|
|
129
|
+
"""Start the uvicorn server. Blocks until shutdown."""
|
|
130
|
+
import uvicorn
|
|
131
|
+
|
|
132
|
+
uvicorn.run(self.build(), host=host, port=port)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convert Claude Agent SDK messages to A2A DataParts for streaming intermediate events.
|
|
3
|
+
|
|
4
|
+
The Claude Agent SDK yields several message types during execution:
|
|
5
|
+
- SystemMessage (subtype="init") — session initialization
|
|
6
|
+
- AssistantMessage — Claude's thinking/response with content blocks
|
|
7
|
+
- ToolUseBlock (in AssistantMessage.content) — tool invocation
|
|
8
|
+
- ToolResultBlock (in UserMessage/ResultMessage) — tool result
|
|
9
|
+
- ResultMessage — final aggregated result
|
|
10
|
+
|
|
11
|
+
This module converts these into structured A2A DataParts so the kagent
|
|
12
|
+
dashboard can display real-time agent activity.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from a2a.types import DataPart, Message, Part, Role, TaskState, TaskStatus, TaskStatusUpdateEvent, TextPart
|
|
20
|
+
|
|
21
|
+
from kagent.core.a2a import (
|
|
22
|
+
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
|
|
23
|
+
A2A_DATA_PART_METADATA_TYPE_KEY,
|
|
24
|
+
get_kagent_metadata_key,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Metadata type for tool results (matching langgraph's convention)
|
|
28
|
+
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE = "function_response"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def convert_assistant_message(message) -> list[Part] | None:
|
|
32
|
+
"""
|
|
33
|
+
Convert an AssistantMessage to A2A Parts.
|
|
34
|
+
|
|
35
|
+
AssistantMessage has a .content list with TextBlock and ToolUseBlock items.
|
|
36
|
+
Returns Parts for streaming to the dashboard, or None if nothing useful.
|
|
37
|
+
"""
|
|
38
|
+
content = getattr(message, "content", None)
|
|
39
|
+
if not content:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
parts: list[Part] = []
|
|
43
|
+
|
|
44
|
+
for block in content:
|
|
45
|
+
block_type = getattr(block, "type", None)
|
|
46
|
+
|
|
47
|
+
if block_type == "tool_use":
|
|
48
|
+
# Tool invocation — emit as a DataPart with function_call metadata
|
|
49
|
+
tool_name = getattr(block, "name", "unknown_tool")
|
|
50
|
+
tool_input = getattr(block, "input", {})
|
|
51
|
+
tool_id = getattr(block, "id", "")
|
|
52
|
+
|
|
53
|
+
data_part = DataPart(
|
|
54
|
+
data={
|
|
55
|
+
"id": tool_id,
|
|
56
|
+
"name": tool_name,
|
|
57
|
+
"args": tool_input,
|
|
58
|
+
},
|
|
59
|
+
metadata={
|
|
60
|
+
get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
parts.append(Part(data_part))
|
|
64
|
+
|
|
65
|
+
elif block_type == "text" or (hasattr(block, "text") and isinstance(getattr(block, "text", None), str)):
|
|
66
|
+
text = getattr(block, "text", "")
|
|
67
|
+
if text:
|
|
68
|
+
parts.append(Part(TextPart(text=text)))
|
|
69
|
+
|
|
70
|
+
return parts if parts else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def convert_tool_result_message(message) -> list[Part] | None:
|
|
74
|
+
"""
|
|
75
|
+
Convert a tool result message to A2A Parts.
|
|
76
|
+
|
|
77
|
+
In the Claude Agent SDK, tool results appear as content blocks with
|
|
78
|
+
type="tool_result" containing the output.
|
|
79
|
+
"""
|
|
80
|
+
content = getattr(message, "content", None)
|
|
81
|
+
if not content:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
parts: list[Part] = []
|
|
85
|
+
|
|
86
|
+
for block in content:
|
|
87
|
+
block_type = getattr(block, "type", None)
|
|
88
|
+
|
|
89
|
+
if block_type == "tool_result":
|
|
90
|
+
tool_use_id = getattr(block, "tool_use_id", "")
|
|
91
|
+
tool_name = getattr(block, "name", None) or "tool_result"
|
|
92
|
+
result_content = getattr(block, "content", "")
|
|
93
|
+
|
|
94
|
+
# Result content can be a string or a list of content blocks
|
|
95
|
+
if isinstance(result_content, list):
|
|
96
|
+
text_parts = []
|
|
97
|
+
for sub in result_content:
|
|
98
|
+
if hasattr(sub, "text"):
|
|
99
|
+
text_parts.append(sub.text)
|
|
100
|
+
result_text = "\n".join(text_parts)
|
|
101
|
+
else:
|
|
102
|
+
result_text = str(result_content) if result_content else ""
|
|
103
|
+
|
|
104
|
+
# Truncate very long results for the dashboard
|
|
105
|
+
display_text = result_text[:2000] + "..." if len(result_text) > 2000 else result_text
|
|
106
|
+
|
|
107
|
+
data_part = DataPart(
|
|
108
|
+
data={
|
|
109
|
+
"id": tool_use_id,
|
|
110
|
+
"name": tool_name,
|
|
111
|
+
"response": {"result": display_text},
|
|
112
|
+
},
|
|
113
|
+
metadata={
|
|
114
|
+
get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY): A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
parts.append(Part(data_part))
|
|
118
|
+
|
|
119
|
+
return parts if parts else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def classify_sdk_message(message) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Classify a Claude Agent SDK message by type.
|
|
125
|
+
|
|
126
|
+
Returns one of: "system", "assistant", "user", "result", "unknown"
|
|
127
|
+
"""
|
|
128
|
+
type_name = type(message).__name__
|
|
129
|
+
|
|
130
|
+
if type_name == "SystemMessage":
|
|
131
|
+
return "system"
|
|
132
|
+
elif type_name == "AssistantMessage":
|
|
133
|
+
return "assistant"
|
|
134
|
+
elif type_name == "UserMessage":
|
|
135
|
+
return "user"
|
|
136
|
+
elif type_name == "ResultMessage":
|
|
137
|
+
return "result"
|
|
138
|
+
else:
|
|
139
|
+
return "unknown"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def convert_message_to_parts(message) -> list[Part] | None:
|
|
143
|
+
"""
|
|
144
|
+
Convert any Claude Agent SDK message to A2A Parts for streaming.
|
|
145
|
+
|
|
146
|
+
Returns None if the message shouldn't be streamed (e.g., system init,
|
|
147
|
+
final result which is handled separately).
|
|
148
|
+
"""
|
|
149
|
+
msg_type = classify_sdk_message(message)
|
|
150
|
+
|
|
151
|
+
if msg_type == "assistant":
|
|
152
|
+
return convert_assistant_message(message)
|
|
153
|
+
elif msg_type == "user":
|
|
154
|
+
# User messages in the SDK are typically tool results
|
|
155
|
+
return convert_tool_result_message(message)
|
|
156
|
+
elif msg_type == "system":
|
|
157
|
+
# System messages (init, etc.) — don't stream
|
|
158
|
+
return None
|
|
159
|
+
elif msg_type == "result":
|
|
160
|
+
# Final result — handled by the main executor flow
|
|
161
|
+
return None
|
|
162
|
+
else:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def make_message_id(message, index: int) -> str:
|
|
167
|
+
"""Generate a deterministic message ID for deduplication."""
|
|
168
|
+
# Use message type + index for a stable ID
|
|
169
|
+
type_name = type(message).__name__
|
|
170
|
+
content_hash = hashlib.md5(
|
|
171
|
+
f"{type_name}:{index}".encode(), usedforsecurity=False
|
|
172
|
+
).hexdigest()[:12]
|
|
173
|
+
return f"msg-{content_hash}"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class StreamingEventEmitter:
|
|
177
|
+
"""
|
|
178
|
+
Manages streaming of intermediate events to the A2A event queue.
|
|
179
|
+
|
|
180
|
+
Handles deduplication (won't re-emit the same message) and provides
|
|
181
|
+
a clean interface for the executor to stream events.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(self, task_id: str, context_id: str):
|
|
185
|
+
self.task_id = task_id
|
|
186
|
+
self.context_id = context_id
|
|
187
|
+
self._sent_ids: set[str] = set()
|
|
188
|
+
|
|
189
|
+
def should_emit(self, message_id: str) -> bool:
|
|
190
|
+
"""Check if this message has already been emitted."""
|
|
191
|
+
if message_id in self._sent_ids:
|
|
192
|
+
return False
|
|
193
|
+
self._sent_ids.add(message_id)
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
def build_streaming_event(
|
|
197
|
+
self,
|
|
198
|
+
parts: list[Part],
|
|
199
|
+
message_id: str,
|
|
200
|
+
metadata: dict[str, Any] | None = None,
|
|
201
|
+
) -> TaskStatusUpdateEvent:
|
|
202
|
+
"""Build a TaskStatusUpdateEvent for streaming intermediate progress."""
|
|
203
|
+
return TaskStatusUpdateEvent(
|
|
204
|
+
task_id=self.task_id,
|
|
205
|
+
status=TaskStatus(
|
|
206
|
+
state=TaskState.working,
|
|
207
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
208
|
+
message=Message(
|
|
209
|
+
message_id=message_id,
|
|
210
|
+
role=Role.agent,
|
|
211
|
+
parts=parts,
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
context_id=self.context_id,
|
|
215
|
+
final=False,
|
|
216
|
+
metadata=metadata,
|
|
217
|
+
)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Error classification and user-friendly error messages for Claude Agent SDK errors."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
# Claude Agent SDK specific error patterns
|
|
10
|
+
_ANTHROPIC_RATE_LIMIT_PATTERNS = (
|
|
11
|
+
"rate_limit",
|
|
12
|
+
"429",
|
|
13
|
+
"too many requests",
|
|
14
|
+
"overloaded",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_ANTHROPIC_AUTH_PATTERNS = (
|
|
18
|
+
"authentication",
|
|
19
|
+
"unauthorized",
|
|
20
|
+
"invalid api key",
|
|
21
|
+
"401",
|
|
22
|
+
"invalid x-api-key",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_ANTHROPIC_CONTEXT_PATTERNS = (
|
|
26
|
+
"context window",
|
|
27
|
+
"too many tokens",
|
|
28
|
+
"maximum context length",
|
|
29
|
+
"prompt is too long",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_NETWORK_PATTERNS = (
|
|
33
|
+
"connection",
|
|
34
|
+
"timeout",
|
|
35
|
+
"dns",
|
|
36
|
+
"ssl",
|
|
37
|
+
"certificate",
|
|
38
|
+
"network",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_PERMISSION_PATTERNS = (
|
|
42
|
+
"permission denied",
|
|
43
|
+
"not allowed",
|
|
44
|
+
"forbidden",
|
|
45
|
+
"403",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
_CLI_NOT_FOUND_PATTERNS = (
|
|
49
|
+
"cli not found",
|
|
50
|
+
"command not found",
|
|
51
|
+
"no such file or directory",
|
|
52
|
+
"claude: not found",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ClassifiedError:
|
|
58
|
+
"""A classified error with user-friendly message and structured metadata."""
|
|
59
|
+
|
|
60
|
+
error_type: str
|
|
61
|
+
user_message: str
|
|
62
|
+
detail: str
|
|
63
|
+
is_transient: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def classify_error(exception: Exception) -> ClassifiedError:
|
|
67
|
+
"""
|
|
68
|
+
Classify an exception into a user-friendly error with structured metadata.
|
|
69
|
+
|
|
70
|
+
Returns a ClassifiedError with:
|
|
71
|
+
- error_type: machine-readable category (e.g., "rate_limit", "auth", "timeout")
|
|
72
|
+
- user_message: what to show the user
|
|
73
|
+
- detail: raw exception info for debugging
|
|
74
|
+
- is_transient: whether this error might succeed on retry
|
|
75
|
+
"""
|
|
76
|
+
error_str = str(exception).lower()
|
|
77
|
+
error_class = type(exception).__name__
|
|
78
|
+
raw_detail = f"{error_class}: {exception}"
|
|
79
|
+
|
|
80
|
+
# Rate limiting
|
|
81
|
+
if any(p in error_str for p in _ANTHROPIC_RATE_LIMIT_PATTERNS):
|
|
82
|
+
return ClassifiedError(
|
|
83
|
+
error_type="rate_limit",
|
|
84
|
+
user_message="Claude is currently rate-limited. The request will be retried automatically.",
|
|
85
|
+
detail=raw_detail,
|
|
86
|
+
is_transient=True,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Authentication errors
|
|
90
|
+
if any(p in error_str for p in _ANTHROPIC_AUTH_PATTERNS):
|
|
91
|
+
return ClassifiedError(
|
|
92
|
+
error_type="auth",
|
|
93
|
+
user_message="Authentication failed. Please verify the Anthropic API key is valid.",
|
|
94
|
+
detail=raw_detail,
|
|
95
|
+
is_transient=False,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Context window exceeded
|
|
99
|
+
if any(p in error_str for p in _ANTHROPIC_CONTEXT_PATTERNS):
|
|
100
|
+
return ClassifiedError(
|
|
101
|
+
error_type="context_overflow",
|
|
102
|
+
user_message="The conversation exceeded Claude's context window. Please start a new conversation.",
|
|
103
|
+
detail=raw_detail,
|
|
104
|
+
is_transient=False,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Network / connectivity
|
|
108
|
+
if any(p in error_str for p in _NETWORK_PATTERNS):
|
|
109
|
+
return ClassifiedError(
|
|
110
|
+
error_type="network",
|
|
111
|
+
user_message="A network error occurred while communicating with Claude. This may be transient.",
|
|
112
|
+
detail=raw_detail,
|
|
113
|
+
is_transient=True,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Permission denied (tool execution)
|
|
117
|
+
if any(p in error_str for p in _PERMISSION_PATTERNS):
|
|
118
|
+
return ClassifiedError(
|
|
119
|
+
error_type="permission",
|
|
120
|
+
user_message="A tool execution was denied due to insufficient permissions.",
|
|
121
|
+
detail=raw_detail,
|
|
122
|
+
is_transient=False,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# CLI not found
|
|
126
|
+
if any(p in error_str for p in _CLI_NOT_FOUND_PATTERNS):
|
|
127
|
+
return ClassifiedError(
|
|
128
|
+
error_type="cli_not_found",
|
|
129
|
+
user_message="The Claude Agent SDK CLI binary was not found. Ensure claude-agent-sdk is installed correctly.",
|
|
130
|
+
detail=raw_detail,
|
|
131
|
+
is_transient=False,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Timeout (asyncio)
|
|
135
|
+
if isinstance(exception, (TimeoutError, asyncio.TimeoutError)):
|
|
136
|
+
return ClassifiedError(
|
|
137
|
+
error_type="timeout",
|
|
138
|
+
user_message="The Claude query timed out. The task may be too complex or Claude may be slow to respond.",
|
|
139
|
+
detail=raw_detail,
|
|
140
|
+
is_transient=True,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Cancellation
|
|
144
|
+
if isinstance(exception, asyncio.CancelledError):
|
|
145
|
+
return ClassifiedError(
|
|
146
|
+
error_type="cancelled",
|
|
147
|
+
user_message="The task was cancelled.",
|
|
148
|
+
detail=raw_detail,
|
|
149
|
+
is_transient=False,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Process error (CLI exited non-zero)
|
|
153
|
+
if "exit code" in error_str or "process" in error_str:
|
|
154
|
+
return ClassifiedError(
|
|
155
|
+
error_type="process_error",
|
|
156
|
+
user_message="The Claude process exited unexpectedly. This may indicate a configuration issue.",
|
|
157
|
+
detail=raw_detail,
|
|
158
|
+
is_transient=True,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# JSON decode error (malformed response from CLI)
|
|
162
|
+
if "json" in error_str and ("decode" in error_str or "parse" in error_str):
|
|
163
|
+
return ClassifiedError(
|
|
164
|
+
error_type="parse_error",
|
|
165
|
+
user_message="Failed to parse Claude's response. This is usually a transient issue.",
|
|
166
|
+
detail=raw_detail,
|
|
167
|
+
is_transient=True,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Unknown / generic
|
|
171
|
+
return ClassifiedError(
|
|
172
|
+
error_type="unknown",
|
|
173
|
+
user_message=f"An unexpected error occurred: {error_class}",
|
|
174
|
+
detail=raw_detail,
|
|
175
|
+
is_transient=False,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_error_metadata(classified: ClassifiedError) -> dict:
|
|
180
|
+
"""Build structured error metadata for A2A event metadata."""
|
|
181
|
+
return {
|
|
182
|
+
"kagent.claude.error_type": classified.error_type,
|
|
183
|
+
"kagent.claude.error_detail": classified.detail,
|
|
184
|
+
"kagent.claude.error_transient": classified.is_transient,
|
|
185
|
+
}
|
|
186
|
+
|