haijun-agent-sdk 0.1.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.
- haijun_agent_sdk/__init__.py +770 -0
- haijun_agent_sdk/_bundled/.gitignore +3 -0
- haijun_agent_sdk/_cli_version.py +3 -0
- haijun_agent_sdk/_errors.py +131 -0
- haijun_agent_sdk/_internal/__init__.py +1 -0
- haijun_agent_sdk/_internal/_mcp_compat.py +151 -0
- haijun_agent_sdk/_internal/_task_compat.py +187 -0
- haijun_agent_sdk/_internal/client.py +193 -0
- haijun_agent_sdk/_internal/message_parser.py +389 -0
- haijun_agent_sdk/_internal/query.py +983 -0
- haijun_agent_sdk/_internal/sdk_mcp_bridge.py +382 -0
- haijun_agent_sdk/_internal/session_import.py +162 -0
- haijun_agent_sdk/_internal/session_mutations.py +957 -0
- haijun_agent_sdk/_internal/session_resume.py +612 -0
- haijun_agent_sdk/_internal/session_store.py +193 -0
- haijun_agent_sdk/_internal/session_store_validation.py +45 -0
- haijun_agent_sdk/_internal/session_summary.py +229 -0
- haijun_agent_sdk/_internal/sessions.py +2007 -0
- haijun_agent_sdk/_internal/transcript_mirror_batcher.py +218 -0
- haijun_agent_sdk/_internal/transport/__init__.py +87 -0
- haijun_agent_sdk/_internal/transport/subprocess_cli.py +1183 -0
- haijun_agent_sdk/_version.py +3 -0
- haijun_agent_sdk/client.py +597 -0
- haijun_agent_sdk/py.typed +0 -0
- haijun_agent_sdk/query.py +127 -0
- haijun_agent_sdk/telemetry.py +181 -0
- haijun_agent_sdk/testing/__init__.py +9 -0
- haijun_agent_sdk/testing/session_store_conformance.py +327 -0
- haijun_agent_sdk/types.py +2456 -0
- haijun_agent_sdk-0.1.0.dist-info/METADATA +405 -0
- haijun_agent_sdk-0.1.0.dist-info/RECORD +33 -0
- haijun_agent_sdk-0.1.0.dist-info/WHEEL +4 -0
- haijun_agent_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
"""Haijun SDK for Python."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import types as builtin_types
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import (
|
|
9
|
+
TYPE_CHECKING,
|
|
10
|
+
Annotated,
|
|
11
|
+
Any,
|
|
12
|
+
Generic,
|
|
13
|
+
TypeVar,
|
|
14
|
+
Union,
|
|
15
|
+
get_args,
|
|
16
|
+
get_origin,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if sys.version_info >= (3, 11):
|
|
20
|
+
from typing import get_type_hints as _get_type_hints
|
|
21
|
+
from typing import is_typeddict
|
|
22
|
+
else:
|
|
23
|
+
# On 3.10, stdlib is_typeddict doesn't recognize typing_extensions.TypedDict
|
|
24
|
+
# subclasses, and stdlib get_type_hints doesn't strip NotRequired markers.
|
|
25
|
+
from typing_extensions import get_type_hints as _get_type_hints
|
|
26
|
+
from typing_extensions import is_typeddict
|
|
27
|
+
|
|
28
|
+
import jsonschema
|
|
29
|
+
from mcp.types import CallToolResult, Tool
|
|
30
|
+
from mcp.types import ToolAnnotations as _McpToolAnnotations
|
|
31
|
+
|
|
32
|
+
from ._errors import (
|
|
33
|
+
CLIConnectionError,
|
|
34
|
+
CLIJSONDecodeError,
|
|
35
|
+
CLINotFoundError,
|
|
36
|
+
HaijunSDKError,
|
|
37
|
+
ProcessError,
|
|
38
|
+
ResultError,
|
|
39
|
+
)
|
|
40
|
+
from ._internal._mcp_compat import build_tool_server
|
|
41
|
+
from ._internal.session_import import import_session_to_store
|
|
42
|
+
from ._internal.session_mutations import (
|
|
43
|
+
ForkSessionResult,
|
|
44
|
+
delete_session,
|
|
45
|
+
delete_session_via_store,
|
|
46
|
+
fork_session,
|
|
47
|
+
fork_session_via_store,
|
|
48
|
+
rename_session,
|
|
49
|
+
rename_session_via_store,
|
|
50
|
+
tag_session,
|
|
51
|
+
tag_session_via_store,
|
|
52
|
+
)
|
|
53
|
+
from ._internal.session_store import InMemorySessionStore, project_key_for_directory
|
|
54
|
+
from ._internal.session_summary import fold_session_summary
|
|
55
|
+
from ._internal.sessions import (
|
|
56
|
+
get_session_info,
|
|
57
|
+
get_session_info_from_store,
|
|
58
|
+
get_session_messages,
|
|
59
|
+
get_session_messages_from_store,
|
|
60
|
+
get_subagent_messages,
|
|
61
|
+
get_subagent_messages_from_store,
|
|
62
|
+
list_sessions,
|
|
63
|
+
list_sessions_from_store,
|
|
64
|
+
list_subagents,
|
|
65
|
+
list_subagents_from_store,
|
|
66
|
+
)
|
|
67
|
+
from ._internal.transport import Transport
|
|
68
|
+
from ._version import __version__
|
|
69
|
+
from .client import HaijunSDKClient
|
|
70
|
+
from .query import query
|
|
71
|
+
from .types import (
|
|
72
|
+
TERMINAL_TASK_STATUSES,
|
|
73
|
+
AgentDefinition,
|
|
74
|
+
AssistantMessage,
|
|
75
|
+
BaseHookInput,
|
|
76
|
+
CanUseTool,
|
|
77
|
+
CanUseToolShadowedWarning,
|
|
78
|
+
ContentBlock,
|
|
79
|
+
ContextUsageCategory,
|
|
80
|
+
ContextUsageResponse,
|
|
81
|
+
ConversationResetMessage,
|
|
82
|
+
DeferredToolUse,
|
|
83
|
+
EffortLevel,
|
|
84
|
+
HaijunAgentOptions,
|
|
85
|
+
HookCallback,
|
|
86
|
+
HookContext,
|
|
87
|
+
HookEventMessage,
|
|
88
|
+
HookInput,
|
|
89
|
+
HookJSONOutput,
|
|
90
|
+
HookMatcher,
|
|
91
|
+
McpSdkServerConfig,
|
|
92
|
+
McpServerConfig,
|
|
93
|
+
McpServerConnectionStatus,
|
|
94
|
+
McpServerInfo,
|
|
95
|
+
McpServerStatus,
|
|
96
|
+
McpServerStatusConfig,
|
|
97
|
+
McpStatusResponse,
|
|
98
|
+
McpToolAnnotations,
|
|
99
|
+
McpToolInfo,
|
|
100
|
+
Message,
|
|
101
|
+
MessageOrigin,
|
|
102
|
+
MessageOriginKind,
|
|
103
|
+
MirrorErrorMessage,
|
|
104
|
+
ModelUsage,
|
|
105
|
+
NotificationHookInput,
|
|
106
|
+
NotificationHookSpecificOutput,
|
|
107
|
+
PermissionMode,
|
|
108
|
+
PermissionRequestHookInput,
|
|
109
|
+
PermissionRequestHookSpecificOutput,
|
|
110
|
+
PermissionResult,
|
|
111
|
+
PermissionResultAllow,
|
|
112
|
+
PermissionResultDeny,
|
|
113
|
+
PermissionUpdate,
|
|
114
|
+
PostToolUseFailureHookInput,
|
|
115
|
+
PostToolUseFailureHookSpecificOutput,
|
|
116
|
+
PostToolUseHookInput,
|
|
117
|
+
PreCompactHookInput,
|
|
118
|
+
PreToolUseHookInput,
|
|
119
|
+
RateLimitEvent,
|
|
120
|
+
RateLimitInfo,
|
|
121
|
+
RateLimitStatus,
|
|
122
|
+
RateLimitType,
|
|
123
|
+
ResultMessage,
|
|
124
|
+
SandboxIgnoreViolations,
|
|
125
|
+
SandboxNetworkConfig,
|
|
126
|
+
SandboxSettings,
|
|
127
|
+
SdkBeta,
|
|
128
|
+
SdkPluginConfig,
|
|
129
|
+
SDKSessionInfo,
|
|
130
|
+
ServerToolName,
|
|
131
|
+
ServerToolResultBlock,
|
|
132
|
+
ServerToolUseBlock,
|
|
133
|
+
SessionKey,
|
|
134
|
+
SessionListSubkeysKey,
|
|
135
|
+
SessionMessage,
|
|
136
|
+
SessionStore,
|
|
137
|
+
SessionStoreEntry,
|
|
138
|
+
SessionStoreFlushMode,
|
|
139
|
+
SessionStoreListEntry,
|
|
140
|
+
SessionSummaryEntry,
|
|
141
|
+
SettingSource,
|
|
142
|
+
StopHookInput,
|
|
143
|
+
StreamEvent,
|
|
144
|
+
SubagentStartHookInput,
|
|
145
|
+
SubagentStartHookSpecificOutput,
|
|
146
|
+
SubagentStopHookInput,
|
|
147
|
+
SystemMessage,
|
|
148
|
+
TaskBudget,
|
|
149
|
+
TaskNotificationMessage,
|
|
150
|
+
TaskNotificationOriginSubkind,
|
|
151
|
+
TaskNotificationStatus,
|
|
152
|
+
TaskProgressMessage,
|
|
153
|
+
TaskStartedMessage,
|
|
154
|
+
TaskUpdatedMessage,
|
|
155
|
+
TaskUpdatedStatus,
|
|
156
|
+
TaskUsage,
|
|
157
|
+
TextBlock,
|
|
158
|
+
ThinkingBlock,
|
|
159
|
+
ThinkingConfig,
|
|
160
|
+
ThinkingConfigAdaptive,
|
|
161
|
+
ThinkingConfigDisabled,
|
|
162
|
+
ThinkingConfigEnabled,
|
|
163
|
+
ToolPermissionContext,
|
|
164
|
+
ToolResultBlock,
|
|
165
|
+
ToolUseBlock,
|
|
166
|
+
UserMessage,
|
|
167
|
+
UserPromptSubmitHookInput,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# MCP Server Support
|
|
171
|
+
|
|
172
|
+
logger = logging.getLogger(__name__)
|
|
173
|
+
|
|
174
|
+
T = TypeVar("T")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# The snake_case spelling of each hint, mapped to its wire (camelCase) name.
|
|
178
|
+
_HINT_WIRE_NAMES = {
|
|
179
|
+
"read_only_hint": "readOnlyHint",
|
|
180
|
+
"destructive_hint": "destructiveHint",
|
|
181
|
+
"idempotent_hint": "idempotentHint",
|
|
182
|
+
"open_world_hint": "openWorldHint",
|
|
183
|
+
"max_result_size_chars": "maxResultSizeChars",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ToolAnnotations(_McpToolAnnotations, extra="allow"):
|
|
188
|
+
"""Hints about a tool's behavior, plus ``maxResultSizeChars``.
|
|
189
|
+
|
|
190
|
+
A ``mcp.types.ToolAnnotations`` that takes every hint in either spelling
|
|
191
|
+
on every supported mcp version: ``ToolAnnotations(readOnlyHint=True)``
|
|
192
|
+
and ``ToolAnnotations(read_only_hint=True)`` are the same thing, and both
|
|
193
|
+
type-check. (mcp's own class differs between majors here: 1.x only knows
|
|
194
|
+
the camelCase names and 2.x prefers snake_case.) Attribute access follows
|
|
195
|
+
the installed mcp: ``.readOnlyHint`` on 1.x, ``.read_only_hint`` on 2.x.
|
|
196
|
+
|
|
197
|
+
``maxResultSizeChars`` (``max_result_size_chars``) is not an MCP hint but
|
|
198
|
+
a Haijun Code one: the size, in characters, up to which Haijun Code keeps
|
|
199
|
+
this tool's result inline instead of persisting it to a file and showing
|
|
200
|
+
a preview. It travels to the CLI in the tool's ``_meta``.
|
|
201
|
+
|
|
202
|
+
Either this class or a plain ``mcp.types.ToolAnnotations`` is accepted
|
|
203
|
+
wherever the SDK takes annotations.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
maxResultSizeChars: int | None = None # noqa: N815 - the wire spelling
|
|
207
|
+
|
|
208
|
+
if TYPE_CHECKING:
|
|
209
|
+
|
|
210
|
+
def __init__( # both spellings, deliberately
|
|
211
|
+
self,
|
|
212
|
+
*,
|
|
213
|
+
title: str | None = None,
|
|
214
|
+
readOnlyHint: bool | None = None, # noqa: N803
|
|
215
|
+
read_only_hint: bool | None = None,
|
|
216
|
+
destructiveHint: bool | None = None, # noqa: N803
|
|
217
|
+
destructive_hint: bool | None = None,
|
|
218
|
+
idempotentHint: bool | None = None, # noqa: N803
|
|
219
|
+
idempotent_hint: bool | None = None,
|
|
220
|
+
openWorldHint: bool | None = None, # noqa: N803
|
|
221
|
+
open_world_hint: bool | None = None,
|
|
222
|
+
maxResultSizeChars: int | None = None, # noqa: N803
|
|
223
|
+
max_result_size_chars: int | None = None,
|
|
224
|
+
**extra: Any,
|
|
225
|
+
) -> None: ...
|
|
226
|
+
|
|
227
|
+
else:
|
|
228
|
+
|
|
229
|
+
def __init__(self, **data: Any) -> None:
|
|
230
|
+
# Fold the snake_case spellings into the wire names, which every
|
|
231
|
+
# mcp version accepts (as field names on 1.x, as aliases on 2.x).
|
|
232
|
+
# Given both, the wire name wins.
|
|
233
|
+
for snake, wire in _HINT_WIRE_NAMES.items():
|
|
234
|
+
if snake in data:
|
|
235
|
+
value = data.pop(snake)
|
|
236
|
+
data.setdefault(wire, value)
|
|
237
|
+
super().__init__(**data)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass
|
|
241
|
+
class SdkMcpTool(Generic[T]):
|
|
242
|
+
"""Definition for an SDK MCP tool."""
|
|
243
|
+
|
|
244
|
+
name: str
|
|
245
|
+
description: str
|
|
246
|
+
input_schema: type[T] | dict[str, Any]
|
|
247
|
+
handler: Callable[[T], Awaitable[dict[str, Any]]]
|
|
248
|
+
annotations: _McpToolAnnotations | None = None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def tool(
|
|
252
|
+
name: str,
|
|
253
|
+
description: str,
|
|
254
|
+
input_schema: type | dict[str, Any],
|
|
255
|
+
annotations: _McpToolAnnotations | None = None,
|
|
256
|
+
) -> Callable[[Callable[[Any], Awaitable[dict[str, Any]]]], SdkMcpTool[Any]]:
|
|
257
|
+
"""Decorator for defining MCP tools with type safety.
|
|
258
|
+
|
|
259
|
+
Creates a tool that can be used with SDK MCP servers. The tool runs
|
|
260
|
+
in-process within your Python application, providing better performance
|
|
261
|
+
than external MCP servers.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name: Unique identifier for the tool. This is what Haijun will use
|
|
265
|
+
to reference the tool in function calls.
|
|
266
|
+
description: Human-readable description of what the tool does.
|
|
267
|
+
This helps Haijun understand when to use the tool.
|
|
268
|
+
input_schema: Schema defining the tool's input parameters.
|
|
269
|
+
Can be either:
|
|
270
|
+
- A dictionary mapping parameter names to types (e.g., {"text": str})
|
|
271
|
+
- A TypedDict class for more complex schemas
|
|
272
|
+
- A JSON Schema dictionary for full validation
|
|
273
|
+
Use ``Annotated[type, "description"]`` to add a description to a
|
|
274
|
+
parameter in either dict-style or TypedDict schemas.
|
|
275
|
+
annotations: Optional MCP tool annotations (hints such as
|
|
276
|
+
``readOnlyHint`` or ``destructiveHint``) advertised to Haijun.
|
|
277
|
+
``ToolAnnotations(maxResultSizeChars=N)`` additionally raises the
|
|
278
|
+
size up to which Haijun Code keeps this tool's result inline
|
|
279
|
+
instead of persisting it to a file and showing a preview.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
A decorator function that wraps the tool implementation and returns
|
|
283
|
+
an SdkMcpTool instance ready for use with create_sdk_mcp_server().
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
Basic tool with simple schema:
|
|
287
|
+
>>> @tool("greet", "Greet a user", {"name": str})
|
|
288
|
+
... async def greet(args):
|
|
289
|
+
... return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}
|
|
290
|
+
|
|
291
|
+
Tool with multiple parameters:
|
|
292
|
+
>>> @tool("add", "Add two numbers", {"a": float, "b": float})
|
|
293
|
+
... async def add_numbers(args):
|
|
294
|
+
... result = args["a"] + args["b"]
|
|
295
|
+
... return {"content": [{"type": "text", "text": f"Result: {result}"}]}
|
|
296
|
+
|
|
297
|
+
Tool with error handling:
|
|
298
|
+
>>> @tool("divide", "Divide two numbers", {"a": float, "b": float})
|
|
299
|
+
... async def divide(args):
|
|
300
|
+
... if args["b"] == 0:
|
|
301
|
+
... return {"content": [{"type": "text", "text": "Error: Division by zero"}], "is_error": True}
|
|
302
|
+
... return {"content": [{"type": "text", "text": f"Result: {args['a'] / args['b']}"}]}
|
|
303
|
+
|
|
304
|
+
Read-only tool that may return a large result:
|
|
305
|
+
>>> @tool(
|
|
306
|
+
... "get_schema",
|
|
307
|
+
... "Return the database schema",
|
|
308
|
+
... {},
|
|
309
|
+
... annotations=ToolAnnotations(readOnlyHint=True, maxResultSizeChars=500_000),
|
|
310
|
+
... )
|
|
311
|
+
... async def get_schema(args):
|
|
312
|
+
... return {"content": [{"type": "text", "text": load_schema()}]}
|
|
313
|
+
|
|
314
|
+
Notes:
|
|
315
|
+
- The tool function must be async (defined with async def)
|
|
316
|
+
- The function receives a single dict argument with the input parameters
|
|
317
|
+
- Arguments are validated against the input schema before the function
|
|
318
|
+
is called; invalid input is reported back to Haijun as an error result
|
|
319
|
+
- The function should return a dict with a "content" key containing the response
|
|
320
|
+
- Errors can be indicated by including "is_error": True in the response;
|
|
321
|
+
an exception raised by the function is reported the same way
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def decorator(
|
|
325
|
+
handler: Callable[[Any], Awaitable[dict[str, Any]]],
|
|
326
|
+
) -> SdkMcpTool[Any]:
|
|
327
|
+
return SdkMcpTool(
|
|
328
|
+
name=name,
|
|
329
|
+
description=description,
|
|
330
|
+
input_schema=input_schema,
|
|
331
|
+
handler=handler,
|
|
332
|
+
annotations=annotations,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return decorator
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _python_type_to_json_schema(py_type: Any) -> dict[str, Any]:
|
|
339
|
+
"""Convert a Python type annotation to a JSON Schema dict."""
|
|
340
|
+
origin = get_origin(py_type)
|
|
341
|
+
|
|
342
|
+
# NotRequired/Required/ReadOnly survive include_extras=True; unwrap them
|
|
343
|
+
if getattr(origin, "_name", None) in ("NotRequired", "Required", "ReadOnly"):
|
|
344
|
+
return _python_type_to_json_schema(get_args(py_type)[0])
|
|
345
|
+
|
|
346
|
+
if origin is Annotated:
|
|
347
|
+
args = get_args(py_type)
|
|
348
|
+
schema = _python_type_to_json_schema(args[0])
|
|
349
|
+
for meta in args[1:]:
|
|
350
|
+
if isinstance(meta, str):
|
|
351
|
+
schema["description"] = meta
|
|
352
|
+
break
|
|
353
|
+
return schema
|
|
354
|
+
|
|
355
|
+
if py_type is str:
|
|
356
|
+
return {"type": "string"}
|
|
357
|
+
if py_type is int:
|
|
358
|
+
return {"type": "integer"}
|
|
359
|
+
if py_type is float:
|
|
360
|
+
return {"type": "number"}
|
|
361
|
+
if py_type is bool:
|
|
362
|
+
return {"type": "boolean"}
|
|
363
|
+
|
|
364
|
+
origin = getattr(py_type, "__origin__", None)
|
|
365
|
+
|
|
366
|
+
if origin is Union or isinstance(py_type, builtin_types.UnionType):
|
|
367
|
+
args = py_type.__args__
|
|
368
|
+
non_none = [a for a in args if a is not builtin_types.NoneType]
|
|
369
|
+
if len(non_none) == 1:
|
|
370
|
+
return _python_type_to_json_schema(non_none[0])
|
|
371
|
+
return {"anyOf": [_python_type_to_json_schema(a) for a in non_none]}
|
|
372
|
+
|
|
373
|
+
if origin is list:
|
|
374
|
+
item_args = getattr(py_type, "__args__", None)
|
|
375
|
+
if item_args:
|
|
376
|
+
return {"type": "array", "items": _python_type_to_json_schema(item_args[0])}
|
|
377
|
+
return {"type": "array"}
|
|
378
|
+
if origin is dict:
|
|
379
|
+
return {"type": "object"}
|
|
380
|
+
|
|
381
|
+
if py_type is list:
|
|
382
|
+
return {"type": "array"}
|
|
383
|
+
if py_type is dict:
|
|
384
|
+
return {"type": "object"}
|
|
385
|
+
|
|
386
|
+
if is_typeddict(py_type):
|
|
387
|
+
return _typeddict_to_json_schema(py_type)
|
|
388
|
+
|
|
389
|
+
return {"type": "string"}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _typeddict_to_json_schema(td_class: type) -> dict[str, Any]:
|
|
393
|
+
"""Convert a TypedDict class to a JSON Schema dict."""
|
|
394
|
+
hints = _get_type_hints(td_class, include_extras=True)
|
|
395
|
+
|
|
396
|
+
properties: dict[str, Any] = {}
|
|
397
|
+
for field_name, field_type in hints.items():
|
|
398
|
+
properties[field_name] = _python_type_to_json_schema(field_type)
|
|
399
|
+
|
|
400
|
+
required_keys = getattr(td_class, "__required_keys__", set(properties.keys()))
|
|
401
|
+
schema: dict[str, Any] = {
|
|
402
|
+
"type": "object",
|
|
403
|
+
"properties": properties,
|
|
404
|
+
}
|
|
405
|
+
if required_keys:
|
|
406
|
+
schema["required"] = sorted(required_keys)
|
|
407
|
+
return schema
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _build_input_schema(tool_def: SdkMcpTool[Any]) -> dict[str, Any]:
|
|
411
|
+
"""Turn a tool's declared input_schema into the JSON Schema sent on the wire."""
|
|
412
|
+
if isinstance(tool_def.input_schema, dict):
|
|
413
|
+
if (
|
|
414
|
+
"type" in tool_def.input_schema
|
|
415
|
+
and "properties" in tool_def.input_schema
|
|
416
|
+
and isinstance(tool_def.input_schema["type"], str)
|
|
417
|
+
):
|
|
418
|
+
return tool_def.input_schema
|
|
419
|
+
properties = {
|
|
420
|
+
param_name: _python_type_to_json_schema(param_type)
|
|
421
|
+
for param_name, param_type in tool_def.input_schema.items()
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
"type": "object",
|
|
425
|
+
"properties": properties,
|
|
426
|
+
"required": list(properties.keys()),
|
|
427
|
+
}
|
|
428
|
+
if is_typeddict(tool_def.input_schema):
|
|
429
|
+
return _typeddict_to_json_schema(tool_def.input_schema)
|
|
430
|
+
return {"type": "object", "properties": {}}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _build_meta(tool_def: SdkMcpTool[Any]) -> dict[str, Any] | None:
|
|
434
|
+
# Client-specific hints travel in _meta under namespaced keys because MCP
|
|
435
|
+
# clients drop annotation fields they do not know. maxResultSizeChars is
|
|
436
|
+
# the size up to which Haijun Code keeps a tool result inline rather than
|
|
437
|
+
# persisting it and showing a preview; it rides on the annotations object
|
|
438
|
+
# as an extra (or subclass-declared) field.
|
|
439
|
+
max_size = getattr(tool_def.annotations, "maxResultSizeChars", None)
|
|
440
|
+
if max_size is None:
|
|
441
|
+
return None
|
|
442
|
+
return {"haijun/maxResultSizeChars": max_size}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _tool_error_result(message: str) -> CallToolResult:
|
|
446
|
+
return CallToolResult.model_validate(
|
|
447
|
+
{"content": [{"type": "text", "text": message}], "isError": True}
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _convert_tool_content(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
452
|
+
"""Map the content of a tool handler's result dict to MCP content blocks.
|
|
453
|
+
|
|
454
|
+
Text and image blocks pass through. Resource links and text resources are
|
|
455
|
+
flattened to text because that is what the CLI renders; binary resources
|
|
456
|
+
and unknown block types are dropped with a warning.
|
|
457
|
+
"""
|
|
458
|
+
content: list[dict[str, Any]] = []
|
|
459
|
+
for item in items:
|
|
460
|
+
item_type = item.get("type")
|
|
461
|
+
if item_type == "text":
|
|
462
|
+
content.append({"type": "text", "text": item["text"]})
|
|
463
|
+
elif item_type == "image":
|
|
464
|
+
content.append(
|
|
465
|
+
{"type": "image", "data": item["data"], "mimeType": item["mimeType"]}
|
|
466
|
+
)
|
|
467
|
+
elif item_type == "resource_link":
|
|
468
|
+
parts = [
|
|
469
|
+
str(part)
|
|
470
|
+
for part in (item.get("name"), item.get("uri"), item.get("description"))
|
|
471
|
+
if part
|
|
472
|
+
]
|
|
473
|
+
content.append(
|
|
474
|
+
{"type": "text", "text": "\n".join(parts) if parts else "Resource link"}
|
|
475
|
+
)
|
|
476
|
+
elif item_type == "resource":
|
|
477
|
+
resource = item.get("resource") or {}
|
|
478
|
+
if "text" in resource:
|
|
479
|
+
content.append({"type": "text", "text": resource["text"]})
|
|
480
|
+
else:
|
|
481
|
+
logger.warning(
|
|
482
|
+
"Binary embedded resource cannot be converted to text, skipping"
|
|
483
|
+
)
|
|
484
|
+
else:
|
|
485
|
+
logger.warning(
|
|
486
|
+
"Unsupported content type %r in tool result, skipping", item_type )
|
|
487
|
+
return content
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def create_sdk_mcp_server(
|
|
491
|
+
name: str, version: str = "1.0.0", tools: list[SdkMcpTool[Any]] | None = None ) -> McpSdkServerConfig:
|
|
492
|
+
"""Create an in-process MCP server that runs within your Python application.
|
|
493
|
+
|
|
494
|
+
Unlike external MCP servers that run as separate processes, SDK MCP servers
|
|
495
|
+
run directly in your application's process. This provides:
|
|
496
|
+
- Better performance (no IPC overhead)
|
|
497
|
+
- Simpler deployment (single process)
|
|
498
|
+
- Easier debugging (same process)
|
|
499
|
+
- Direct access to your application's state
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
name: Unique identifier for the server. This name is used to reference
|
|
503
|
+
the server in the mcp_servers configuration.
|
|
504
|
+
version: Server version string. Defaults to "1.0.0". This is for
|
|
505
|
+
informational purposes and doesn't affect functionality.
|
|
506
|
+
tools: List of SdkMcpTool instances created with the @tool decorator.
|
|
507
|
+
These are the functions that Haijun can call through this server.
|
|
508
|
+
If None or empty, the server will have no tools (rarely useful).
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
McpSdkServerConfig: A configuration object that can be passed to
|
|
512
|
+
HaijunAgentOptions.mcp_servers. Its ``instance`` is a regular
|
|
513
|
+
``mcp.server.Server`` that the SDK connects to Haijun Code over an
|
|
514
|
+
in-memory MCP transport.
|
|
515
|
+
|
|
516
|
+
Example:
|
|
517
|
+
Simple calculator server:
|
|
518
|
+
>>> @tool("add", "Add numbers", {"a": float, "b": float})
|
|
519
|
+
... async def add(args):
|
|
520
|
+
... return {"content": [{"type": "text", "text": f"Sum: {args['a'] + args['b']}"}]}
|
|
521
|
+
>>>
|
|
522
|
+
>>> @tool("multiply", "Multiply numbers", {"a": float, "b": float})
|
|
523
|
+
... async def multiply(args):
|
|
524
|
+
... return {"content": [{"type": "text", "text": f"Product: {args['a'] * args['b']}"}]}
|
|
525
|
+
>>>
|
|
526
|
+
>>> calculator = create_sdk_mcp_server(
|
|
527
|
+
... name="calculator",
|
|
528
|
+
... version="2.0.0",
|
|
529
|
+
... tools=[add, multiply]
|
|
530
|
+
... )
|
|
531
|
+
>>>
|
|
532
|
+
>>> # Use with Haijun
|
|
533
|
+
>>> options = HaijunAgentOptions(
|
|
534
|
+
... mcp_servers={"calc": calculator},
|
|
535
|
+
... allowed_tools=["add", "multiply"]
|
|
536
|
+
... )
|
|
537
|
+
|
|
538
|
+
Server with application state access:
|
|
539
|
+
>>> class DataStore:
|
|
540
|
+
... def __init__(self):
|
|
541
|
+
... self.items = []
|
|
542
|
+
...
|
|
543
|
+
>>> store = DataStore()
|
|
544
|
+
>>>
|
|
545
|
+
>>> @tool("add_item", "Add item to store", {"item": str})
|
|
546
|
+
... async def add_item(args):
|
|
547
|
+
... store.items.append(args["item"])
|
|
548
|
+
... return {"content": [{"type": "text", "text": f"Added: {args['item']}"}]}
|
|
549
|
+
>>>
|
|
550
|
+
>>> server = create_sdk_mcp_server("store", tools=[add_item])
|
|
551
|
+
|
|
552
|
+
Bringing your own server: any ``mcp.server.Server`` built with the
|
|
553
|
+
installed ``mcp`` package can be used in place of this helper, and
|
|
554
|
+
the requests it handles (tools, resources, prompts, ...) are served.
|
|
555
|
+
Requests and notifications the server sends to the client (sampling,
|
|
556
|
+
elicitation, roots, logging, progress) are not forwarded yet:
|
|
557
|
+
>>> from mcp.server import Server
|
|
558
|
+
>>> my_server = Server("mine") # register handlers with the mcp API
|
|
559
|
+
>>> config = McpSdkServerConfig(type="sdk", name="mine", instance=my_server)
|
|
560
|
+
|
|
561
|
+
Notes:
|
|
562
|
+
- The server runs in the same process as your Python application
|
|
563
|
+
- Tools have direct access to your application's variables and state
|
|
564
|
+
- No subprocess or IPC overhead for tool calls
|
|
565
|
+
- Server lifecycle is managed automatically by the SDK
|
|
566
|
+
- Tool arguments are validated against the tool's input schema, and
|
|
567
|
+
unknown tools, invalid arguments and exceptions raised by a handler
|
|
568
|
+
are all reported to Haijun as error results rather than failing the
|
|
569
|
+
request
|
|
570
|
+
- Works with both mcp 1.x and mcp 2.x
|
|
571
|
+
|
|
572
|
+
See Also:
|
|
573
|
+
- tool(): Decorator for creating tool functions
|
|
574
|
+
- HaijunAgentOptions: Configuration for using servers with query()
|
|
575
|
+
"""
|
|
576
|
+
tools = tools or []
|
|
577
|
+
tools_by_name = {tool_def.name: tool_def for tool_def in tools}
|
|
578
|
+
schemas = {tool_def.name: _build_input_schema(tool_def) for tool_def in tools}
|
|
579
|
+
wire_tools = [
|
|
580
|
+
Tool.model_validate(
|
|
581
|
+
{
|
|
582
|
+
"name": tool_def.name,
|
|
583
|
+
"description": tool_def.description,
|
|
584
|
+
"inputSchema": schemas[tool_def.name],
|
|
585
|
+
"annotations": tool_def.annotations,
|
|
586
|
+
"_meta": _build_meta(tool_def),
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
for tool_def in tools
|
|
590
|
+
]
|
|
591
|
+
|
|
592
|
+
async def run_tool(tool_name: str, arguments: dict[str, Any]) -> CallToolResult:
|
|
593
|
+
# The SDK owns these semantics so a tool behaves the same on every
|
|
594
|
+
# supported mcp version: unknown tools, invalid arguments and handler
|
|
595
|
+
# failures all come back as isError results the model can read, never
|
|
596
|
+
# as protocol errors.
|
|
597
|
+
tool_def = tools_by_name.get(tool_name)
|
|
598
|
+
if tool_def is None:
|
|
599
|
+
return _tool_error_result(f"Tool '{tool_name}' not found")
|
|
600
|
+
try:
|
|
601
|
+
try:
|
|
602
|
+
jsonschema.validate(instance=arguments, schema=schemas[tool_name])
|
|
603
|
+
except jsonschema.ValidationError as e:
|
|
604
|
+
return _tool_error_result(f"Input validation error: {e.message}")
|
|
605
|
+
result = await tool_def.handler(arguments)
|
|
606
|
+
return CallToolResult.model_validate(
|
|
607
|
+
{
|
|
608
|
+
"content": _convert_tool_content(result.get("content", [])),
|
|
609
|
+
"isError": result.get("is_error", False),
|
|
610
|
+
}
|
|
611
|
+
)
|
|
612
|
+
except Exception as e:
|
|
613
|
+
return _tool_error_result(str(e))
|
|
614
|
+
|
|
615
|
+
server = build_tool_server(name, version, wire_tools, run_tool)
|
|
616
|
+
|
|
617
|
+
return McpSdkServerConfig(type="sdk", name=name, instance=server)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
__all__ = [
|
|
621
|
+
# Main exports
|
|
622
|
+
"query",
|
|
623
|
+
"__version__",
|
|
624
|
+
# Transport
|
|
625
|
+
"Transport",
|
|
626
|
+
"HaijunSDKClient",
|
|
627
|
+
# Types
|
|
628
|
+
"PermissionMode",
|
|
629
|
+
"EffortLevel",
|
|
630
|
+
"McpServerConfig",
|
|
631
|
+
"McpSdkServerConfig",
|
|
632
|
+
"McpServerStatus",
|
|
633
|
+
"McpServerStatusConfig",
|
|
634
|
+
"McpServerConnectionStatus",
|
|
635
|
+
"McpServerInfo",
|
|
636
|
+
"McpStatusResponse",
|
|
637
|
+
"McpToolAnnotations",
|
|
638
|
+
"McpToolInfo",
|
|
639
|
+
"UserMessage",
|
|
640
|
+
"AssistantMessage",
|
|
641
|
+
"SystemMessage",
|
|
642
|
+
"TaskStartedMessage",
|
|
643
|
+
"TaskProgressMessage",
|
|
644
|
+
"TaskUpdatedMessage",
|
|
645
|
+
"TaskNotificationMessage",
|
|
646
|
+
"TaskNotificationStatus",
|
|
647
|
+
"TaskUpdatedStatus",
|
|
648
|
+
"TERMINAL_TASK_STATUSES",
|
|
649
|
+
"TaskUsage",
|
|
650
|
+
"ResultMessage",
|
|
651
|
+
"ModelUsage",
|
|
652
|
+
"DeferredToolUse",
|
|
653
|
+
"RateLimitEvent",
|
|
654
|
+
"RateLimitInfo",
|
|
655
|
+
"RateLimitStatus",
|
|
656
|
+
"RateLimitType",
|
|
657
|
+
"StreamEvent",
|
|
658
|
+
"ConversationResetMessage",
|
|
659
|
+
"Message",
|
|
660
|
+
"MessageOrigin",
|
|
661
|
+
"MessageOriginKind",
|
|
662
|
+
"TaskNotificationOriginSubkind",
|
|
663
|
+
"HaijunAgentOptions",
|
|
664
|
+
"TaskBudget",
|
|
665
|
+
"TextBlock",
|
|
666
|
+
"ThinkingBlock",
|
|
667
|
+
"ThinkingConfig",
|
|
668
|
+
"ThinkingConfigAdaptive",
|
|
669
|
+
"ThinkingConfigEnabled",
|
|
670
|
+
"ThinkingConfigDisabled",
|
|
671
|
+
"ToolUseBlock",
|
|
672
|
+
"ToolResultBlock",
|
|
673
|
+
"ServerToolName",
|
|
674
|
+
"ServerToolUseBlock",
|
|
675
|
+
"ServerToolResultBlock",
|
|
676
|
+
"ContentBlock",
|
|
677
|
+
"ContextUsageCategory",
|
|
678
|
+
"ContextUsageResponse",
|
|
679
|
+
# Tool callbacks
|
|
680
|
+
"CanUseTool",
|
|
681
|
+
"CanUseToolShadowedWarning",
|
|
682
|
+
"ToolPermissionContext",
|
|
683
|
+
"PermissionResult",
|
|
684
|
+
"PermissionResultAllow",
|
|
685
|
+
"PermissionResultDeny",
|
|
686
|
+
"PermissionUpdate",
|
|
687
|
+
# Hook support
|
|
688
|
+
"HookCallback",
|
|
689
|
+
"HookContext",
|
|
690
|
+
"HookInput",
|
|
691
|
+
"HookEventMessage",
|
|
692
|
+
"BaseHookInput",
|
|
693
|
+
"PreToolUseHookInput",
|
|
694
|
+
"PostToolUseHookInput",
|
|
695
|
+
"PostToolUseFailureHookInput",
|
|
696
|
+
"PostToolUseFailureHookSpecificOutput",
|
|
697
|
+
"UserPromptSubmitHookInput",
|
|
698
|
+
"StopHookInput",
|
|
699
|
+
"SubagentStopHookInput",
|
|
700
|
+
"PreCompactHookInput",
|
|
701
|
+
"NotificationHookInput",
|
|
702
|
+
"SubagentStartHookInput",
|
|
703
|
+
"PermissionRequestHookInput",
|
|
704
|
+
"NotificationHookSpecificOutput",
|
|
705
|
+
"SubagentStartHookSpecificOutput",
|
|
706
|
+
"PermissionRequestHookSpecificOutput",
|
|
707
|
+
"HookJSONOutput",
|
|
708
|
+
"HookMatcher",
|
|
709
|
+
# Agent support
|
|
710
|
+
"AgentDefinition",
|
|
711
|
+
"SettingSource",
|
|
712
|
+
# Plugin support
|
|
713
|
+
"SdkPluginConfig",
|
|
714
|
+
# Session listing
|
|
715
|
+
"list_sessions",
|
|
716
|
+
"get_session_info",
|
|
717
|
+
"get_session_messages",
|
|
718
|
+
"list_subagents",
|
|
719
|
+
"get_subagent_messages",
|
|
720
|
+
"SDKSessionInfo",
|
|
721
|
+
"SessionMessage",
|
|
722
|
+
# Session store
|
|
723
|
+
"SessionKey",
|
|
724
|
+
"SessionStore",
|
|
725
|
+
"SessionStoreEntry",
|
|
726
|
+
"SessionStoreFlushMode",
|
|
727
|
+
"SessionStoreListEntry",
|
|
728
|
+
"SessionSummaryEntry",
|
|
729
|
+
"SessionListSubkeysKey",
|
|
730
|
+
"InMemorySessionStore",
|
|
731
|
+
"fold_session_summary",
|
|
732
|
+
"MirrorErrorMessage",
|
|
733
|
+
"project_key_for_directory",
|
|
734
|
+
"import_session_to_store",
|
|
735
|
+
# Session listing (SessionStore-backed async variants)
|
|
736
|
+
"list_sessions_from_store",
|
|
737
|
+
"get_session_info_from_store",
|
|
738
|
+
"get_session_messages_from_store",
|
|
739
|
+
"list_subagents_from_store",
|
|
740
|
+
"get_subagent_messages_from_store",
|
|
741
|
+
# Session mutations
|
|
742
|
+
"rename_session",
|
|
743
|
+
"tag_session",
|
|
744
|
+
"delete_session",
|
|
745
|
+
"fork_session",
|
|
746
|
+
"ForkSessionResult",
|
|
747
|
+
# Session mutations (SessionStore-backed async variants)
|
|
748
|
+
"rename_session_via_store",
|
|
749
|
+
"tag_session_via_store",
|
|
750
|
+
"delete_session_via_store",
|
|
751
|
+
"fork_session_via_store",
|
|
752
|
+
# Beta support
|
|
753
|
+
"SdkBeta",
|
|
754
|
+
# Sandbox support
|
|
755
|
+
"SandboxSettings",
|
|
756
|
+
"SandboxNetworkConfig",
|
|
757
|
+
"SandboxIgnoreViolations",
|
|
758
|
+
# MCP Server Support
|
|
759
|
+
"create_sdk_mcp_server",
|
|
760
|
+
"tool",
|
|
761
|
+
"SdkMcpTool",
|
|
762
|
+
"ToolAnnotations",
|
|
763
|
+
# Errors
|
|
764
|
+
"HaijunSDKError",
|
|
765
|
+
"CLIConnectionError",
|
|
766
|
+
"CLINotFoundError",
|
|
767
|
+
"ProcessError",
|
|
768
|
+
"ResultError",
|
|
769
|
+
"CLIJSONDecodeError",
|
|
770
|
+
]
|