langchain-claude-code-mimir 0.1.1__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.
@@ -0,0 +1,22 @@
1
+ from langchain_claude_code.claude_chat_model import ClaudeCodeChatModel
2
+ from langchain_claude_code.claude_code_tools import (
3
+ ClaudeTool,
4
+ normalize_tools,
5
+ DEFAULT_READ_ONLY,
6
+ DEFAULT_WRITE,
7
+ DEFAULT_NETWORK,
8
+ DEFAULT_SHELL,
9
+ )
10
+
11
+ ChatClaudeCode = ClaudeCodeChatModel
12
+
13
+ __all__ = [
14
+ "ClaudeCodeChatModel",
15
+ "ChatClaudeCode",
16
+ "ClaudeTool",
17
+ "normalize_tools",
18
+ "DEFAULT_READ_ONLY",
19
+ "DEFAULT_WRITE",
20
+ "DEFAULT_NETWORK",
21
+ "DEFAULT_SHELL",
22
+ ]
@@ -0,0 +1,921 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextvars
5
+ import inspect
6
+ import logging
7
+ import queue
8
+ import threading
9
+ import time
10
+ from collections.abc import AsyncIterator, Iterator, Sequence
11
+ from pathlib import Path
12
+ from typing import Any, Callable, get_args, get_origin
13
+
14
+ from langchain_core.callbacks import (
15
+ AsyncCallbackManagerForLLMRun,
16
+ CallbackManagerForLLMRun,
17
+ )
18
+ from langchain_core.language_models import BaseChatModel
19
+ from langchain_core.messages import (
20
+ AIMessage,
21
+ AIMessageChunk,
22
+ BaseMessage,
23
+ HumanMessage,
24
+ SystemMessage as LCSystemMessage,
25
+ ToolMessage,
26
+ )
27
+ from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
28
+ from langchain_core.runnables import Runnable, RunnableConfig
29
+ from langchain_core.runnables.config import ensure_config
30
+ from langchain_core.tools import BaseTool
31
+ from pydantic import Field, PrivateAttr
32
+ from pydantic.errors import PydanticInvalidForJsonSchema
33
+
34
+ from claude_agent_sdk import (
35
+ ClaudeAgentOptions,
36
+ ClaudeSDKClient,
37
+ AssistantMessage,
38
+ HookMatcher,
39
+ ResultMessage,
40
+ TextBlock,
41
+ ToolUseBlock,
42
+ ToolResultBlock,
43
+ create_sdk_mcp_server,
44
+ tool as sdk_tool,
45
+ )
46
+ from langchain_claude_code.claude_code_tools import ClaudeTool, normalize_tools
47
+
48
+ log = logging.getLogger(__name__)
49
+
50
+
51
+ def _has_runtime_injected_args(tool: BaseTool) -> bool:
52
+ """Return True if any parameter of the tool's underlying function is
53
+ annotated with a langgraph-runtime injection marker
54
+ (``InjectedToolArg`` or one of its direct-injection subclasses,
55
+ notably ``ToolRuntime``).
56
+
57
+ Such tools require langgraph state (graph runtime, store, channels,
58
+ etc.) to be injected at invocation time by langgraph's ``ToolNode``.
59
+ They can't be bridged through MCP because the MCP transport
60
+ doesn't carry that state — invoking them directly via ``_arun(**args)``
61
+ raises ``TypeError: missing 1 required positional argument: 'runtime'``.
62
+
63
+ Returns ``False`` if the underlying callable can't be introspected
64
+ (defensive — we err on the side of including the tool and letting
65
+ a runtime failure surface, matching pre-fix behavior for non-injected
66
+ tools).
67
+ """
68
+ # Try the InjectedToolArg base class first (covers Annotated[..., InjectedToolArg(...)]).
69
+ try:
70
+ from langchain_core.tools.base import InjectedToolArg
71
+ except ImportError:
72
+ InjectedToolArg = None # type: ignore[assignment]
73
+
74
+ # Direct-injection subclass is what ToolRuntime extends — params
75
+ # carry the raw type (not Annotated[]). Optional import: older
76
+ # langgraph versions don't have it.
77
+ try:
78
+ from langgraph.prebuilt.tool_node import _DirectlyInjectedToolArg
79
+ except ImportError:
80
+ _DirectlyInjectedToolArg = None # type: ignore[assignment]
81
+
82
+ if InjectedToolArg is None and _DirectlyInjectedToolArg is None:
83
+ return False
84
+
85
+ # ``StructuredTool``-style: the wrapped callable is exposed as
86
+ # ``coroutine`` (async) or ``func`` (sync). ``BaseTool``-subclass
87
+ # style: the implementation lives in ``_arun`` / ``_run`` methods
88
+ # on the class. Inspect the most-specific class method to skip
89
+ # the framework's default-stub signature on the base class.
90
+ candidates: list[Any] = []
91
+ for attr in ("coroutine", "func"):
92
+ c = getattr(tool, attr, None)
93
+ if c is not None and callable(c):
94
+ candidates.append(c)
95
+ cls = type(tool)
96
+ for attr in ("_arun", "_run"):
97
+ m = cls.__dict__.get(attr)
98
+ if m is None:
99
+ # Walk MRO for overrides above ``BaseTool`` (which defines
100
+ # both as no-ops). Stop at langchain_core to avoid picking
101
+ # up the framework's signature.
102
+ for base in cls.__mro__[1:]:
103
+ if base.__module__.startswith("langchain_core"):
104
+ break
105
+ if attr in base.__dict__:
106
+ m = base.__dict__[attr]
107
+ break
108
+ if m is not None and callable(m):
109
+ candidates.append(m)
110
+ if not candidates:
111
+ return False
112
+
113
+ for callable_ in candidates:
114
+ try:
115
+ sig = inspect.signature(callable_)
116
+ except (TypeError, ValueError):
117
+ continue
118
+ if _signature_has_injected_param(
119
+ sig, InjectedToolArg, _DirectlyInjectedToolArg,
120
+ ):
121
+ return True
122
+ return False
123
+
124
+
125
+ def _signature_has_injected_param(
126
+ sig: inspect.Signature,
127
+ injected_tool_arg: type | None,
128
+ directly_injected_tool_arg: type | None,
129
+ ) -> bool:
130
+ """Helper for :func:`_has_runtime_injected_args` — scan one signature."""
131
+ for param in sig.parameters.values():
132
+ ann = param.annotation
133
+ if ann is inspect.Parameter.empty:
134
+ continue
135
+ # Direct-injection types (ToolRuntime[X, Y]): the param's annotation
136
+ # is the class itself, possibly subscripted with generics.
137
+ if directly_injected_tool_arg is not None:
138
+ origin = get_origin(ann) or ann
139
+ if isinstance(origin, type) and issubclass(
140
+ origin, directly_injected_tool_arg,
141
+ ):
142
+ return True
143
+ # Annotated[T, InjectedToolArg(...)] form: walk the metadata list.
144
+ if injected_tool_arg is not None:
145
+ for meta in get_args(ann)[1:] if get_origin(ann) is not None else ():
146
+ if isinstance(meta, type) and issubclass(meta, injected_tool_arg):
147
+ return True
148
+ if isinstance(meta, injected_tool_arg):
149
+ return True
150
+ return False
151
+
152
+
153
+ def _generation_info_from_result(msg: "ResultMessage") -> dict[str, Any]:
154
+ """Build a ``generation_info`` dict from an SDK ``ResultMessage``.
155
+
156
+ Mirrors the SDK's per-request metadata onto LangChain's
157
+ ``generation_info`` so downstream consumers can read it off the
158
+ final AIMessage's ``response_metadata``. Used by both ``_generate``
159
+ (non-streaming) and ``_astream`` (streaming) so the two paths
160
+ surface the same field set — previously they diverged: the
161
+ non-streaming path preserved ``num_turns`` / ``is_error`` but
162
+ missed ``finish_reason``, while the streaming path emitted
163
+ ``finish_reason`` but dropped ``num_turns`` / ``is_error``
164
+ entirely. Neither path preserved ``stop_reason``.
165
+
166
+ Field shape (all keys present unless explicitly noted):
167
+
168
+ - ``total_cost_usd``, ``duration_ms``, ``duration_api_ms``,
169
+ ``session_id`` — SDK fields
170
+ - ``num_turns``, ``is_error`` — SDK fields
171
+ - ``finish_reason`` — LangChain
172
+ convention; ``"error"`` when ``msg.is_error`` else ``"stop"``
173
+ - ``stop_reason`` — granular SDK
174
+ reason (``"end_turn"``, ``"max_turns"``, ``"max_tokens"``,
175
+ etc.); only included when present (newer SDK only) AND
176
+ non-``None``. Access is ``getattr``-guarded so the helper
177
+ works on the SDK ``>= 0.1.10`` floor this package declares.
178
+ - ``usage`` — only when
179
+ ``msg.usage`` is non-empty
180
+ """
181
+ info: dict[str, Any] = {
182
+ "total_cost_usd": msg.total_cost_usd,
183
+ "duration_ms": msg.duration_ms,
184
+ "duration_api_ms": msg.duration_api_ms,
185
+ "session_id": msg.session_id,
186
+ "num_turns": msg.num_turns,
187
+ "is_error": msg.is_error,
188
+ "finish_reason": "error" if msg.is_error else "stop",
189
+ }
190
+ # ``stop_reason`` was added to ResultMessage in a later SDK
191
+ # release; use getattr so the helper stays compatible with the
192
+ # >= 0.1.10 floor pinned in pyproject.toml.
193
+ stop_reason = getattr(msg, "stop_reason", None)
194
+ if stop_reason is not None:
195
+ info["stop_reason"] = stop_reason
196
+ if msg.usage:
197
+ info["usage"] = msg.usage
198
+ return info
199
+
200
+
201
+ class ClaudeCodeChatModel(BaseChatModel):
202
+ """LangChain chat model wrapping Claude Code Agent SDK.
203
+
204
+ Uses ClaudeSDKClient for multi-turn conversations with full tool support.
205
+ """
206
+
207
+ model: str = Field(default="opus", description="Model to use (opus, sonnet, haiku)")
208
+ fallback_model: str | None = Field(default=None, description="Fallback model")
209
+ system_prompt: str | None = Field(default=None, description="System prompt")
210
+ permission_mode: str = Field(
211
+ default="default",
212
+ description="Permission mode: default, acceptEdits, plan, bypassPermissions",
213
+ )
214
+ allowed_tools: list[str | ClaudeTool] = Field(default_factory=list, description="Allowed tools")
215
+ disallowed_tools: list[str | ClaudeTool] = Field(default_factory=list, description="Disallowed tools")
216
+ max_turns: int | None = Field(default=None, description="Max conversation turns")
217
+ max_budget_usd: float | None = Field(default=None, description="Max budget in USD")
218
+ effort: str | int | None = Field(
219
+ default=None,
220
+ description=(
221
+ "Reasoning effort forwarded to ClaudeAgentOptions.effort "
222
+ "(e.g. 'low'/'medium'/'high'/'max', or an int token budget). "
223
+ "None leaves Claude's adaptive default. Silently dropped if the "
224
+ "installed claude-agent-sdk predates the effort option."
225
+ ),
226
+ )
227
+ cwd: str | Path | None = Field(default=None, description="Working directory")
228
+ include_partial_messages: bool = Field(
229
+ default=False, description="Enable partial message streaming"
230
+ )
231
+ api_key: str | None = Field(default=None, description="Anthropic API key")
232
+ oauth_token: str | None = Field(default=None, description="OAuth token")
233
+
234
+ _mcp_servers: dict[str, Any] = {}
235
+ _bound_tools: list[BaseTool] = []
236
+ _last_result: ResultMessage | None = None
237
+ _tool_results_var: contextvars.ContextVar | None = PrivateAttr(
238
+ default_factory=lambda: contextvars.ContextVar("claude_code_tool_results")
239
+ )
240
+
241
+ class Config:
242
+ arbitrary_types_allowed = True
243
+
244
+ @property
245
+ def _llm_type(self) -> str:
246
+ return "claude-code-agent"
247
+
248
+ @property
249
+ def _identifying_params(self) -> dict[str, Any]:
250
+ return {
251
+ "model": self.model,
252
+ "permission_mode": self.permission_mode,
253
+ "system_prompt": self.system_prompt,
254
+ "allowed_tools": self.allowed_tools,
255
+ }
256
+
257
+ def invoke(
258
+ self,
259
+ input: Any,
260
+ config: RunnableConfig | None = None,
261
+ *,
262
+ stop: list[str] | None = None,
263
+ **kwargs: Any,
264
+ ) -> AIMessage:
265
+ """Ensure RunnableConfig is available to downstream calls."""
266
+ config = ensure_config(config)
267
+ kwargs.setdefault("_config", config)
268
+ return super().invoke(input, config=config, stop=stop, **kwargs)
269
+
270
+ async def ainvoke(
271
+ self,
272
+ input: Any,
273
+ config: RunnableConfig | None = None,
274
+ *,
275
+ stop: list[str] | None = None,
276
+ **kwargs: Any,
277
+ ) -> AIMessage:
278
+ """Async invoke that propagates RunnableConfig via context."""
279
+ config = ensure_config(config)
280
+ kwargs.setdefault("_config", config)
281
+ return await super().ainvoke(input, config=config, stop=stop, **kwargs)
282
+
283
+ def stream(
284
+ self,
285
+ input: Any,
286
+ config: RunnableConfig | None = None,
287
+ *,
288
+ stop: list[str] | None = None,
289
+ **kwargs: Any,
290
+ ) -> Iterator[AIMessageChunk]:
291
+ """Stream while keeping config in context for session support."""
292
+ config = ensure_config(config)
293
+ kwargs.setdefault("_config", config)
294
+ yield from super().stream(input, config=config, stop=stop, **kwargs)
295
+
296
+ async def astream(
297
+ self,
298
+ input: Any,
299
+ config: RunnableConfig | None = None,
300
+ *,
301
+ stop: list[str] | None = None,
302
+ **kwargs: Any,
303
+ ) -> AsyncIterator[AIMessageChunk]:
304
+ """Async stream while propagating config context."""
305
+ config = ensure_config(config)
306
+ kwargs.setdefault("_config", config)
307
+ async for chunk in super().astream(
308
+ input, config=config, stop=stop, **kwargs
309
+ ):
310
+ yield chunk
311
+
312
+ def _build_options(self, **overrides: Any) -> ClaudeAgentOptions:
313
+ """Build ClaudeAgentOptions from model config."""
314
+ opts = {
315
+ "model": self.model,
316
+ "permission_mode": self.permission_mode,
317
+ "allowed_tools": normalize_tools(list(self.allowed_tools)),
318
+ "disallowed_tools": normalize_tools(list(self.disallowed_tools)),
319
+ }
320
+
321
+ if self.fallback_model:
322
+ opts["fallback_model"] = self.fallback_model
323
+ if self.system_prompt:
324
+ opts["system_prompt"] = self.system_prompt
325
+ if self.max_turns is not None:
326
+ opts["max_turns"] = self.max_turns
327
+ if self.max_budget_usd is not None:
328
+ opts["max_budget_usd"] = self.max_budget_usd
329
+ if self.effort is not None:
330
+ # Kept by the allowed_keys filter below iff the installed
331
+ # claude-agent-sdk exposes ``effort`` (else dropped gracefully).
332
+ opts["effort"] = self.effort
333
+ if self.cwd:
334
+ opts["cwd"] = self.cwd
335
+ if self._mcp_servers:
336
+ opts["mcp_servers"] = self._mcp_servers
337
+
338
+ env: dict[str, str] = {}
339
+ if self.api_key:
340
+ env["ANTHROPIC_API_KEY"] = self.api_key
341
+ if self.oauth_token:
342
+ env["CLAUDE_CODE_OAUTH_TOKEN"] = self.oauth_token
343
+ if env:
344
+ opts["env"] = env
345
+
346
+ if self.include_partial_messages:
347
+ opts["include_partial_messages"] = True
348
+ # When tools are allowed/bound, ensure partial messages so tool results stream back.
349
+ if not opts.get("include_partial_messages") and (
350
+ self._bound_tools or opts.get("allowed_tools")
351
+ ):
352
+ opts["include_partial_messages"] = True
353
+
354
+ allowed_keys = set(ClaudeAgentOptions.__dataclass_fields__.keys())
355
+ for key, value in overrides.items():
356
+ if key.startswith("_"):
357
+ if key == "_mcp_servers":
358
+ opts["mcp_servers"] = value
359
+ continue
360
+ if key in allowed_keys:
361
+ if key in {"allowed_tools", "disallowed_tools"}:
362
+ opts[key] = normalize_tools(list(value or []))
363
+ else:
364
+ opts[key] = value
365
+
366
+ return ClaudeAgentOptions(**opts)
367
+
368
+ def _convert_messages(
369
+ self,
370
+ messages: list[BaseMessage],
371
+ ) -> tuple[str, str | None]:
372
+ """Convert LangChain messages to prompt and system prompt.
373
+
374
+ Returns:
375
+ Tuple of (prompt, system_prompt)
376
+ """
377
+ system_parts: list[str] = []
378
+ conversation_parts: list[str] = []
379
+
380
+ for msg in messages:
381
+ if isinstance(msg, LCSystemMessage):
382
+ system_parts.append(str(msg.content))
383
+ elif isinstance(msg, HumanMessage):
384
+ conversation_parts.append(f"Human: {msg.content}")
385
+ elif isinstance(msg, AIMessage):
386
+ content = str(msg.content) if msg.content else ""
387
+ if getattr(msg, "tool_calls", None):
388
+ tool_info = ", ".join(
389
+ f"{tc['name']}({tc['args']})" for tc in msg.tool_calls
390
+ )
391
+ content = f"{content}\n[Tool calls: {tool_info}]" if content else f"[Tool calls: {tool_info}]"
392
+ conversation_parts.append(f"Assistant: {content}")
393
+ elif isinstance(msg, ToolMessage):
394
+ conversation_parts.append(
395
+ f"Tool ({msg.name}): {msg.content}"
396
+ )
397
+
398
+ system_prompt = "\n\n".join(system_parts) if system_parts else None
399
+ prompt = "\n\n".join(conversation_parts) if conversation_parts else ""
400
+
401
+ return prompt, system_prompt
402
+
403
+ def _parse_assistant_message(
404
+ self,
405
+ message: AssistantMessage,
406
+ ) -> tuple[str, list[dict[str, Any]], list[dict[str, Any]]]:
407
+ """Parse AssistantMessage content blocks.
408
+
409
+ Returns:
410
+ Tuple of (text_content, tool_calls, tool_results)
411
+ """
412
+ text_parts: list[str] = []
413
+ tool_calls: list[dict[str, Any]] = []
414
+ tool_results: list[dict[str, Any]] = []
415
+
416
+ for block in message.content:
417
+ if isinstance(block, TextBlock):
418
+ text_parts.append(block.text)
419
+ elif isinstance(block, ToolResultBlock):
420
+ content_items: list[str] = []
421
+ if isinstance(block.content, str):
422
+ content_items.append(block.content)
423
+ elif isinstance(block.content, list):
424
+ for item in block.content:
425
+ if isinstance(item, dict) and item.get("type") == "text":
426
+ content_items.append(str(item.get("text", "")))
427
+ else:
428
+ content_items.append(str(item))
429
+
430
+ if content_items:
431
+ text_parts.append("\n".join(content_items))
432
+
433
+ tool_results.append(
434
+ {
435
+ "tool_use_id": getattr(block, "tool_use_id", None),
436
+ "content": block.content,
437
+ "is_error": block.is_error,
438
+ }
439
+ )
440
+ elif isinstance(block, ToolUseBlock):
441
+ tool_calls.append({
442
+ "id": block.id,
443
+ "name": block.name,
444
+ "args": block.input,
445
+ })
446
+
447
+ return "\n".join(text_parts), tool_calls, tool_results
448
+
449
+ def _create_ai_message(
450
+ self,
451
+ content: str,
452
+ tool_calls: list[dict[str, Any]] | None = None,
453
+ generation_info: dict[str, Any] | None = None,
454
+ ) -> AIMessage:
455
+ """Create AIMessage with optional tool calls.
456
+
457
+ Note: tool_calls are stored in response_metadata, NOT as AIMessage.tool_calls.
458
+ Claude Code executes tools internally, so exposing tool_calls would cause
459
+ LangGraph to attempt re-execution of already-completed tool calls.
460
+ """
461
+ kwargs: dict[str, Any] = {"content": content}
462
+ if generation_info:
463
+ kwargs["response_metadata"] = generation_info
464
+ if tool_calls:
465
+ kwargs.setdefault("response_metadata", {})
466
+ kwargs["response_metadata"]["internal_tool_calls"] = tool_calls
467
+ return AIMessage(**kwargs)
468
+
469
+ def _install_tool_event_hooks(
470
+ self, options: ClaudeAgentOptions,
471
+ ) -> list[dict[str, Any]]:
472
+ """Register PreToolUse / PostToolUse / PostToolUseFailure hooks
473
+ that capture every tool invocation — built-in (Bash, Read, Edit,
474
+ Write, Glob, ToolSearch), bridged LangChain tools, and MCP
475
+ tools — into the returned events list.
476
+
477
+ Why hooks: built-in tools execute entirely inside the claude
478
+ CLI subprocess. Their ``ToolResultBlock``s arrive in
479
+ ``UserMessage`` content (per the Anthropic API conversation
480
+ convention), which the message loops in ``_aquery`` / ``_astream``
481
+ don't iterate. The SDK fires PreToolUse / PostToolUse hooks for
482
+ EVERY tool invocation regardless of origin (see
483
+ ``claude_agent_sdk._internal.query``'s hook_callback dispatch),
484
+ with ``tool_use_id`` on both pre and post. Capturing via hooks:
485
+
486
+ * Surfaces built-in tool results (the only path that does).
487
+ * Pairs calls and results by ``tool_use_id`` (no name-matching
488
+ ambiguity for bridged tools, where the call carries the
489
+ MCP-prefixed name and the bridged result carries the bare
490
+ ``@tool`` name).
491
+ * Preserves the actual call→result→call→result execution order
492
+ (vs. ``_parse_assistant_message`` which splits content blocks
493
+ into parallel lists, losing order).
494
+
495
+ The returned list is later attached to ``generation_info["tool_events"]``
496
+ in ``_aquery`` and to the final result chunk's ``generation_info``
497
+ in ``_astream``. Items have shape:
498
+
499
+ {"type": "tool_call", "tool_use_id": str, "name": str,
500
+ "input": dict, "ts_mono_ns": int}
501
+ {"type": "tool_result", "tool_use_id": str, "name": str,
502
+ "result": Any, "is_error": False, "ts_mono_ns": int}
503
+ {"type": "tool_result", "tool_use_id": str, "name": str,
504
+ "error": str, "is_error": True, "ts_mono_ns": int}
505
+
506
+ Mutates ``options.hooks``: appends our three callbacks to any
507
+ user-supplied hooks; never replaces them. Our callbacks return
508
+ ``{}`` so they don't influence control flow when chained with
509
+ user hooks (e.g. permission gates).
510
+ """
511
+ events: list[dict[str, Any]] = []
512
+
513
+ async def _pre_hook(
514
+ input_data: dict, tool_use_id: str, signal: Any,
515
+ ) -> dict:
516
+ events.append({
517
+ "type": "tool_call",
518
+ "ts_mono_ns": time.monotonic_ns(),
519
+ "tool_use_id": tool_use_id,
520
+ "name": input_data.get("tool_name", ""),
521
+ "input": input_data.get("tool_input", {}),
522
+ })
523
+ return {}
524
+
525
+ async def _post_hook(
526
+ input_data: dict, tool_use_id: str, signal: Any,
527
+ ) -> dict:
528
+ events.append({
529
+ "type": "tool_result",
530
+ "ts_mono_ns": time.monotonic_ns(),
531
+ "tool_use_id": tool_use_id,
532
+ "name": input_data.get("tool_name", ""),
533
+ "result": input_data.get("tool_response"),
534
+ "is_error": False,
535
+ })
536
+ return {}
537
+
538
+ async def _post_fail_hook(
539
+ input_data: dict, tool_use_id: str, signal: Any,
540
+ ) -> dict:
541
+ events.append({
542
+ "type": "tool_result",
543
+ "ts_mono_ns": time.monotonic_ns(),
544
+ "tool_use_id": tool_use_id,
545
+ "name": input_data.get("tool_name", ""),
546
+ "error": input_data.get("error"),
547
+ "is_error": True,
548
+ })
549
+ return {}
550
+
551
+ our_hooks: dict[str, list[HookMatcher]] = {
552
+ "PreToolUse": [HookMatcher(hooks=[_pre_hook])],
553
+ "PostToolUse": [HookMatcher(hooks=[_post_hook])],
554
+ "PostToolUseFailure": [HookMatcher(hooks=[_post_fail_hook])],
555
+ }
556
+ existing = dict(options.hooks) if options.hooks else {}
557
+ for event, matchers in our_hooks.items():
558
+ existing[event] = list(existing.get(event, [])) + matchers
559
+ options.hooks = existing
560
+ return events
561
+
562
+ async def _aquery(
563
+ self,
564
+ prompt: str,
565
+ config: RunnableConfig | None = None,
566
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
567
+ **kwargs: Any,
568
+ ) -> tuple[str, list[dict[str, Any]], dict[str, Any]]:
569
+ """Execute query and return parsed response.
570
+
571
+ Returns:
572
+ Tuple of (content, tool_calls, generation_info)
573
+ """
574
+ cfg = ensure_config(config) if config is not None else None
575
+ session_id = kwargs.pop("session_id", None) or kwargs.pop("resume", None)
576
+ if cfg:
577
+ session_id = session_id or cfg.get("configurable", {}).get("session_id")
578
+
579
+ options = self._build_options(**kwargs)
580
+ tool_events = self._install_tool_event_hooks(options)
581
+
582
+ if session_id:
583
+ options.resume = session_id
584
+ options.continue_conversation = True
585
+
586
+ all_text: list[str] = []
587
+ all_tool_calls: list[dict[str, Any]] = []
588
+ all_tool_results: list[dict[str, Any]] = []
589
+ generation_info: dict[str, Any] = {}
590
+ tool_results_token = self._tool_results_var.set([])
591
+
592
+ async with ClaudeSDKClient(options=options) as client:
593
+ await client.query(prompt)
594
+
595
+ async for msg in client.receive_response():
596
+ if isinstance(msg, AssistantMessage):
597
+ text, tool_calls, tool_results = self._parse_assistant_message(msg)
598
+ if text:
599
+ all_text.append(text)
600
+ if run_manager:
601
+ await run_manager.on_llm_new_token(text)
602
+ all_tool_calls.extend(tool_calls)
603
+ all_tool_results.extend(tool_results)
604
+
605
+ elif isinstance(msg, ResultMessage):
606
+ self._last_result = msg
607
+ generation_info = _generation_info_from_result(msg)
608
+
609
+ captured = self._tool_results_var.get()
610
+ if captured:
611
+ all_tool_results.extend(captured)
612
+ self._tool_results_var.reset(tool_results_token)
613
+
614
+ if all_tool_results:
615
+ generation_info["tool_results"] = all_tool_results
616
+ if tool_events:
617
+ generation_info["tool_events"] = tool_events
618
+
619
+ return "\n".join(all_text), all_tool_calls, generation_info
620
+
621
+ def _generate(
622
+ self,
623
+ messages: list[BaseMessage],
624
+ stop: list[str] | None = None,
625
+ run_manager: CallbackManagerForLLMRun | None = None,
626
+ **kwargs: Any,
627
+ ) -> ChatResult:
628
+ """Synchronous generation - runs async in event loop."""
629
+ return self._run_sync(self._agenerate(messages, stop=stop, run_manager=None, **kwargs))
630
+
631
+ async def _agenerate(
632
+ self,
633
+ messages: list[BaseMessage],
634
+ stop: list[str] | None = None,
635
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
636
+ **kwargs: Any,
637
+ ) -> ChatResult:
638
+ """Async generation - primary implementation."""
639
+ config = kwargs.pop("_config", None)
640
+ prompt, system_prompt = self._convert_messages(messages)
641
+
642
+ if system_prompt and not self.system_prompt:
643
+ kwargs["system_prompt"] = system_prompt
644
+
645
+ content, tool_calls, generation_info = await self._aquery(
646
+ prompt, config=config, run_manager=run_manager, **kwargs
647
+ )
648
+
649
+ ai_message = self._create_ai_message(content, tool_calls, generation_info)
650
+
651
+ if run_manager and ai_message.id is None:
652
+ ai_message.id = f"run-{run_manager.run_id}"
653
+
654
+ generation = ChatGeneration(
655
+ message=ai_message,
656
+ generation_info=generation_info,
657
+ )
658
+ return ChatResult(generations=[generation])
659
+
660
+ def _stream(
661
+ self,
662
+ messages: list[BaseMessage],
663
+ stop: list[str] | None = None,
664
+ run_manager: CallbackManagerForLLMRun | None = None,
665
+ **kwargs: Any,
666
+ ) -> Iterator[ChatGenerationChunk]:
667
+ """Synchronous streaming using background thread to preserve anyio task affinity."""
668
+ try:
669
+ asyncio.get_running_loop()
670
+ except RuntimeError:
671
+ pass
672
+ else:
673
+ raise RuntimeError(
674
+ "Cannot use synchronous streaming while an event loop is running. Use 'astream' instead."
675
+ )
676
+
677
+ chunk_queue: queue.Queue[ChatGenerationChunk | BaseException | None] = queue.Queue()
678
+
679
+ def run_async_stream() -> None:
680
+ async def produce() -> None:
681
+ try:
682
+ async for chunk in self._astream(
683
+ messages, stop=stop, run_manager=None, **kwargs
684
+ ):
685
+ chunk_queue.put(chunk)
686
+ chunk_queue.put(None)
687
+ except BaseException as exc:
688
+ chunk_queue.put(exc)
689
+
690
+ loop = asyncio.new_event_loop()
691
+ asyncio.set_event_loop(loop)
692
+ try:
693
+ loop.run_until_complete(produce())
694
+ finally:
695
+ asyncio.set_event_loop(None)
696
+ loop.close()
697
+
698
+ thread = threading.Thread(target=run_async_stream, daemon=True)
699
+ thread.start()
700
+
701
+ try:
702
+ while True:
703
+ item = chunk_queue.get()
704
+ if item is None:
705
+ break
706
+ if isinstance(item, BaseException):
707
+ raise item
708
+ yield item
709
+ finally:
710
+ thread.join(timeout=5.0)
711
+
712
+ async def _astream(
713
+ self,
714
+ messages: list[BaseMessage],
715
+ stop: list[str] | None = None,
716
+ run_manager: AsyncCallbackManagerForLLMRun | None = None,
717
+ **kwargs: Any,
718
+ ) -> AsyncIterator[ChatGenerationChunk]:
719
+ """Async streaming - yields chunks as they arrive."""
720
+ config = kwargs.pop("_config", None)
721
+ prompt, system_prompt = self._convert_messages(messages)
722
+
723
+ if system_prompt and not self.system_prompt:
724
+ kwargs["system_prompt"] = system_prompt
725
+
726
+ kwargs["include_partial_messages"] = True
727
+ cfg = ensure_config(config) if config is not None else None
728
+ session_id = kwargs.pop("session_id", None) or kwargs.pop("resume", None)
729
+ if cfg:
730
+ session_id = session_id or cfg.get("configurable", {}).get("session_id")
731
+
732
+ options = self._build_options(**kwargs)
733
+ tool_events = self._install_tool_event_hooks(options)
734
+
735
+ if session_id:
736
+ options.resume = session_id
737
+ options.continue_conversation = True
738
+
739
+ tool_calls_buffer: list[dict[str, Any]] = []
740
+ tool_results_buffer: list[dict[str, Any]] = []
741
+
742
+ async with ClaudeSDKClient(options=options) as client:
743
+ await client.query(prompt)
744
+
745
+ async for msg in client.receive_response():
746
+ if isinstance(msg, AssistantMessage):
747
+ text, tool_calls, tool_results = self._parse_assistant_message(msg)
748
+
749
+ if text:
750
+ chunk = ChatGenerationChunk(
751
+ message=AIMessageChunk(content=text)
752
+ )
753
+ if run_manager:
754
+ await run_manager.on_llm_new_token(text, chunk=chunk)
755
+ yield chunk
756
+
757
+ tool_calls_buffer.extend(tool_calls)
758
+ tool_results_buffer.extend(tool_results)
759
+
760
+ elif isinstance(msg, ResultMessage):
761
+ self._last_result = msg
762
+
763
+ generation_info: dict[str, Any] = (
764
+ _generation_info_from_result(msg)
765
+ )
766
+ if tool_calls_buffer:
767
+ generation_info["internal_tool_calls"] = tool_calls_buffer
768
+ if tool_results_buffer:
769
+ generation_info["internal_tool_results"] = tool_results_buffer
770
+ if tool_events:
771
+ generation_info["tool_events"] = tool_events
772
+
773
+ yield ChatGenerationChunk(
774
+ message=AIMessageChunk(content="", chunk_position="last"),
775
+ generation_info=generation_info,
776
+ )
777
+
778
+ def bind_tools(
779
+ self,
780
+ tools: Sequence[BaseTool],
781
+ *,
782
+ tool_choice: str | dict[str, Any] | None = None,
783
+ **kwargs: Any,
784
+ ) -> Runnable:
785
+ """Bind LangChain tools to the model via MCP server.
786
+
787
+ Tools whose underlying function takes a langgraph-injected
788
+ parameter (notably ``ToolRuntime`` from
789
+ ``langgraph.prebuilt.tool_node``, or any ``InjectedToolArg``
790
+ subclass) are **skipped**: the MCP transport doesn't carry
791
+ langgraph state, so invoking them through the bridge fails
792
+ with ``TypeError: missing 1 required positional argument:
793
+ 'runtime'``. Such tools are typically middleware-injected by
794
+ the agent framework (deepagents' filesystem tools, subagent
795
+ tools, etc.) and are already available to the model through
796
+ the framework's native path — bridging them via MCP is both
797
+ redundant and broken.
798
+ """
799
+ sdk_tools = []
800
+ tool_names = []
801
+ skipped: list[str] = []
802
+
803
+ for lc_tool in tools:
804
+ if _has_runtime_injected_args(lc_tool):
805
+ # Can't bridge through MCP. Log and skip; the framework's
806
+ # native tool path will still serve this tool when the
807
+ # model needs it (it's typically also exposed there).
808
+ skipped.append(lc_tool.name)
809
+ continue
810
+ schema = self._get_tool_schema(lc_tool)
811
+ sdk_func = self._wrap_langchain_tool(lc_tool, schema)
812
+ sdk_tools.append(sdk_func)
813
+ tool_names.append(lc_tool.name)
814
+
815
+ if skipped:
816
+ log.info(
817
+ "skipped %d tool(s) with langgraph-injected args (can't "
818
+ "bridge through MCP): %s",
819
+ len(skipped), ", ".join(skipped),
820
+ )
821
+
822
+ server = create_sdk_mcp_server(
823
+ name="langchain-tools",
824
+ version="1.0.0",
825
+ tools=sdk_tools,
826
+ )
827
+
828
+ allowed = [f"mcp__langchain-tools__{name}" for name in tool_names]
829
+
830
+ return self.model_copy(
831
+ update={
832
+ "_mcp_servers": {"langchain-tools": server},
833
+ "allowed_tools": normalize_tools(list(self.allowed_tools)) + allowed,
834
+ "_bound_tools": list(tools),
835
+ }
836
+ ).bind(**kwargs)
837
+
838
+ def _get_tool_schema(self, tool: BaseTool) -> dict[str, Any]:
839
+ """Extract JSON schema from LangChain tool."""
840
+ if hasattr(tool, "args_schema") and tool.args_schema:
841
+ try:
842
+ return tool.args_schema.model_json_schema()
843
+ except PydanticInvalidForJsonSchema:
844
+ # Some tool arg models include callables that cannot be rendered to JSON Schema.
845
+ return {"type": "object", "properties": {}, "required": []}
846
+ except Exception:
847
+ return {"type": "object", "properties": {}, "required": []}
848
+ return {"type": "object", "properties": {}, "required": []}
849
+
850
+ def _wrap_langchain_tool(
851
+ self,
852
+ tool: BaseTool,
853
+ schema: dict[str, Any],
854
+ ) -> Callable[..., Any]:
855
+ """Wrap LangChain tool as SDK tool function."""
856
+ props = schema.get("properties", {})
857
+ type_map = {
858
+ "string": str,
859
+ "integer": int,
860
+ "number": float,
861
+ "boolean": bool,
862
+ "array": list,
863
+ "object": dict,
864
+ }
865
+
866
+ param_types = {}
867
+ for name, prop in props.items():
868
+ json_type = prop.get("type", "string")
869
+ param_types[name] = type_map.get(json_type, str)
870
+
871
+ @sdk_tool(tool.name, tool.description or "", param_types)
872
+ async def wrapped_tool(args: dict[str, Any]) -> dict[str, Any]:
873
+ try:
874
+ if hasattr(tool, "_arun") and asyncio.iscoroutinefunction(tool._arun):
875
+ result = await tool._arun(**args)
876
+ else:
877
+ result = tool._run(**args)
878
+
879
+ captured = self._tool_results_var.get(None) if self._tool_results_var else None
880
+ if captured is not None:
881
+ captured.append(
882
+ {
883
+ "name": tool.name,
884
+ "args": args,
885
+ "result": result,
886
+ }
887
+ )
888
+
889
+ return {"content": [{"type": "text", "text": str(result)}]}
890
+ except Exception as e:
891
+ return {
892
+ "content": [{"type": "text", "text": f"Error: {e}"}],
893
+ "is_error": True,
894
+ }
895
+
896
+ return wrapped_tool
897
+
898
+ def resume_from_thread(self, thread_id: str) -> Runnable:
899
+ """Return a bound model that resumes the provider session using a LangGraph thread_id."""
900
+ return self.with_config(config={"configurable": {"session_id": thread_id}})
901
+
902
+ def enable_tools(self, tools: list[str | ClaudeTool]) -> Runnable:
903
+ """Return a new model with the given tools added to the allow list."""
904
+ merged = normalize_tools(normalize_tools(list(self.allowed_tools)) + list(tools))
905
+ return self.model_copy(update={"allowed_tools": merged})
906
+
907
+ @property
908
+ def last_result(self) -> ResultMessage | None:
909
+ """Get the last ResultMessage with cost/usage info."""
910
+ return self._last_result
911
+
912
+ def _run_sync(self, coro: asyncio.Future) -> Any:
913
+ """Run coroutine safely from sync context without relying on global loop."""
914
+ try:
915
+ asyncio.get_running_loop()
916
+ except RuntimeError:
917
+ return asyncio.run(coro)
918
+
919
+ raise RuntimeError(
920
+ "Cannot call synchronous ClaudeCodeChatModel methods while an event loop is running. Use 'ainvoke' instead."
921
+ )
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class ClaudeTool(str, Enum):
7
+ ASK_USER_QUESTION = "AskUserQuestion"
8
+ BASH = "Bash"
9
+ BASH_OUTPUT = "BashOutput"
10
+ EDIT = "Edit"
11
+ EXIT_PLAN_MODE = "ExitPlanMode"
12
+ GLOB = "Glob"
13
+ GREP = "Grep"
14
+ KILL_SHELL = "KillShell"
15
+ NOTEBOOK_EDIT = "NotebookEdit"
16
+ READ = "Read"
17
+ SKILL = "Skill"
18
+ SLASH_COMMAND = "SlashCommand"
19
+ TASK = "Task"
20
+ TODO_WRITE = "TodoWrite"
21
+ WEB_FETCH = "WebFetch"
22
+ WEB_SEARCH = "WebSearch"
23
+ WRITE = "Write"
24
+
25
+
26
+ DEFAULT_READ_ONLY = [ClaudeTool.GLOB, ClaudeTool.GREP, ClaudeTool.READ]
27
+ DEFAULT_WRITE = [ClaudeTool.EDIT, ClaudeTool.WRITE]
28
+ DEFAULT_NETWORK = [ClaudeTool.WEB_FETCH, ClaudeTool.WEB_SEARCH]
29
+ DEFAULT_SHELL = [ClaudeTool.BASH, ClaudeTool.BASH_OUTPUT, ClaudeTool.KILL_SHELL]
30
+
31
+
32
+ def normalize_tools(tools: list[str | ClaudeTool]) -> list[str]:
33
+ """Convert enum members/strings to plain strings, preserving order and uniqueness."""
34
+ seen: set[str] = set()
35
+ normalized: list[str] = []
36
+ for tool in tools:
37
+ name = tool.value if isinstance(tool, ClaudeTool) else str(tool)
38
+ if name not in seen:
39
+ seen.add(name)
40
+ normalized.append(name)
41
+ return normalized
File without changes
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-claude-code-mimir
3
+ Version: 0.1.1
4
+ Summary: LangChain chat model wrapper for the Claude Code Agent SDK — Mimir distribution of langchain-claude-code with bundled adapter fixes (upstream PRs #2/#4/#6, unmerged). Import package remains ``langchain_claude_code``.
5
+ Project-URL: Homepage, https://github.com/jasoncarreira/langchain-claude-code
6
+ Project-URL: Repository, https://github.com/jasoncarreira/langchain-claude-code
7
+ Project-URL: Issues, https://github.com/jasoncarreira/langchain-claude-code/issues
8
+ Project-URL: Upstream, https://github.com/agentmish/langchain-claude-code
9
+ Author-email: Tomas Roda <dev@tomasroda.com>
10
+ Maintainer: Jason Carreira
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: agent,anthropic,claude,langchain,llm
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: claude-agent-sdk>=0.1.10
22
+ Requires-Dist: langchain-core>=0.3.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: langgraph>=0.2; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Provides-Extra: examples
28
+ Requires-Dist: ddgs>=9.9.2; extra == 'examples'
29
+ Requires-Dist: deepagents>=0.2.8; extra == 'examples'
30
+ Requires-Dist: langchain-community>=0.4.1; extra == 'examples'
31
+ Requires-Dist: langchain>=1.1.0; extra == 'examples'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # langchain-claude-code
35
+
36
+ [![PyPI version](https://badge.fury.io/py/langchain-claude-code.svg)](https://badge.fury.io/py/langchain-claude-code)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
38
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
39
+
40
+ LangChain chat model wrapper for the Claude Code Agent SDK. Use Claude Code as a drop-in LangChain `BaseChatModel` with full tool support, streaming, and LangGraph compatibility.
41
+
42
+ ## Prerequisites
43
+
44
+ **Claude Code CLI** (required):
45
+
46
+ ```bash
47
+ npm install -g @anthropic-ai/claude-code
48
+ ```
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install langchain-claude-code
54
+ ```
55
+
56
+ With example dependencies:
57
+
58
+ ```bash
59
+ pip install langchain-claude-code[examples]
60
+ ```
61
+
62
+ ## Authentication
63
+
64
+ ### Option 1: API Key (for Anthropic API users)
65
+
66
+ ```python
67
+ from langchain_claude_code import ClaudeCodeChatModel
68
+
69
+ model = ClaudeCodeChatModel(api_key="sk-ant-...")
70
+ ```
71
+
72
+ Or set the environment variable:
73
+
74
+ ```bash
75
+ export ANTHROPIC_API_KEY="sk-ant-..."
76
+ ```
77
+
78
+ ### Option 2: OAuth Token (for Claude Max subscribers)
79
+
80
+ ```python
81
+ from langchain_claude_code import ClaudeCodeChatModel
82
+
83
+ model = ClaudeCodeChatModel(oauth_token="...")
84
+ ```
85
+
86
+ Or set the environment variable:
87
+
88
+ ```bash
89
+ export CLAUDE_CODE_OAUTH_TOKEN="..."
90
+ ```
91
+
92
+ ## Quick Start
93
+
94
+ ### Basic Usage
95
+
96
+ ```python
97
+ import asyncio
98
+ from langchain_claude_code import ClaudeCodeChatModel
99
+ from langchain_core.messages import HumanMessage
100
+
101
+ async def main():
102
+ model = ClaudeCodeChatModel(model="sonnet")
103
+ response = await model.ainvoke([HumanMessage(content="What is 2 + 2?")])
104
+ print(response.content)
105
+
106
+ asyncio.run(main())
107
+ ```
108
+
109
+ ### With Tool Binding
110
+
111
+ ```python
112
+ from langchain_claude_code import ClaudeCodeChatModel
113
+ from langchain_community.tools.ddg_search.tool import DuckDuckGoSearchTool
114
+
115
+ model = ClaudeCodeChatModel(model="haiku")
116
+ model = model.bind_tools([DuckDuckGoSearchTool()])
117
+
118
+ response = await model.ainvoke([
119
+ HumanMessage(content="Search for the latest news about AI")
120
+ ])
121
+ ```
122
+
123
+ ### Using Claude Code's Built-in Tools
124
+
125
+ ```python
126
+ from langchain_claude_code import ClaudeCodeChatModel, ClaudeTool
127
+
128
+ model = ClaudeCodeChatModel(
129
+ model="sonnet",
130
+ allowed_tools=[
131
+ ClaudeTool.WEB_SEARCH,
132
+ ClaudeTool.WEB_FETCH,
133
+ ClaudeTool.BASH,
134
+ ClaudeTool.READ,
135
+ ClaudeTool.WRITE,
136
+ ],
137
+ )
138
+ ```
139
+
140
+ ### Streaming
141
+
142
+ ```python
143
+ async for chunk in model.astream([HumanMessage(content="Write a poem")]):
144
+ print(chunk.content, end="", flush=True)
145
+ ```
146
+
147
+ ## Configuration
148
+
149
+ | Parameter | Type | Default | Description |
150
+ |-----------|------|---------|-------------|
151
+ | `model` | `str` | `"opus"` | Model to use: `opus`, `sonnet`, `haiku` |
152
+ | `permission_mode` | `str` | `"default"` | Permission mode: `default`, `acceptEdits`, `plan`, `bypassPermissions` |
153
+ | `allowed_tools` | `list` | `[]` | List of allowed Claude Code tools |
154
+ | `disallowed_tools` | `list` | `[]` | List of disallowed tools |
155
+ | `system_prompt` | `str` | `None` | Custom system prompt |
156
+ | `max_turns` | `int` | `None` | Maximum conversation turns |
157
+ | `max_budget_usd` | `float` | `None` | Maximum budget in USD |
158
+ | `cwd` | `str` | `None` | Working directory for file operations |
159
+ | `api_key` | `str` | `None` | Anthropic API key |
160
+ | `oauth_token` | `str` | `None` | Claude Code OAuth token |
161
+
162
+ ## Permission Modes
163
+
164
+ | Mode | Description |
165
+ |------|-------------|
166
+ | `default` | Prompts for confirmation on potentially dangerous operations |
167
+ | `acceptEdits` | Automatically accepts file edits without confirmation |
168
+ | `plan` | Planning mode - generates plans without executing |
169
+ | `bypassPermissions` | Bypasses all permission checks (use with caution) |
170
+
171
+ > **Security Note**: When using `acceptEdits` or `bypassPermissions`, Claude Code will modify your filesystem without confirmation. Use these modes only in sandboxed environments or when you trust the input.
172
+
173
+ ## Available Tools
174
+
175
+ ```python
176
+ from langchain_claude_code import ClaudeTool
177
+
178
+ # File operations
179
+ ClaudeTool.READ
180
+ ClaudeTool.WRITE
181
+ ClaudeTool.EDIT
182
+ ClaudeTool.GLOB
183
+ ClaudeTool.GREP
184
+
185
+ # Shell
186
+ ClaudeTool.BASH
187
+ ClaudeTool.BASH_OUTPUT
188
+
189
+ # Web
190
+ ClaudeTool.WEB_SEARCH
191
+ ClaudeTool.WEB_FETCH
192
+
193
+ # Other
194
+ ClaudeTool.TASK
195
+ ClaudeTool.TODO_WRITE
196
+ ```
197
+
198
+ ## Examples
199
+
200
+ See the [examples/](examples/) directory for complete examples:
201
+
202
+ - `bind_tools_example.py` - Using LangChain tools with Claude Code
203
+ - `deepagents_example.py` - Integration with DeepAgents/LangGraph
204
+
205
+ ## License
206
+
207
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -0,0 +1,8 @@
1
+ langchain_claude_code/__init__.py,sha256=zjT4kc6tgZVPpBLpnsnAZ4lXGI2Lj-CYS6R4a1i0Ye0,480
2
+ langchain_claude_code/claude_chat_model.py,sha256=iN1jEyq2sg7eUK-17_fKQy9oiBntJN6D-VjdDXfwhLk,36501
3
+ langchain_claude_code/claude_code_tools.py,sha256=jsTQrommRm3Mi9sFjaDw-Aoc2yvVB0lcesBpns_Q-Dk,1226
4
+ langchain_claude_code/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ langchain_claude_code_mimir-0.1.1.dist-info/METADATA,sha256=J_0RuV1rQzdHTLYCjTDYZthd08plCH17_FbbKUhsI94,6133
6
+ langchain_claude_code_mimir-0.1.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ langchain_claude_code_mimir-0.1.1.dist-info/licenses/LICENSE,sha256=wMiZL1kpZb8Faq58ElWag4X6HDd22Cq-MXXiqtll9q4,1067
8
+ langchain_claude_code_mimir-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tomas Roda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.