deepagents-code 0.2.1__py3-none-any.whl → 0.2.2__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.
- deepagents_code/_build_info.py +1 -1
- deepagents_code/_checkpointer_adapter.py +86 -0
- deepagents_code/_env_vars.py +29 -3
- deepagents_code/_git.py +8 -2
- deepagents_code/_js_cost.py +15 -0
- deepagents_code/_langsmith_runtime.py +91 -0
- deepagents_code/_run_queue_wakeup.py +156 -0
- deepagents_code/_textual_patches.py +5 -0
- deepagents_code/_trace_redaction.py +145 -0
- deepagents_code/_version.py +1 -1
- deepagents_code/app.py +778 -175
- deepagents_code/app.tcss +1 -1
- deepagents_code/auto_mode.py +180 -9
- deepagents_code/btw.py +176 -37
- deepagents_code/btw_api.py +80 -1
- deepagents_code/built_in_skills/deepagents-plugin-discovery/SKILL.md +3 -1
- deepagents_code/client/launch/_server_bootstrap.py +5 -0
- deepagents_code/client/launch/server.py +55 -13
- deepagents_code/client/launch/server_manager.py +97 -4
- deepagents_code/client/non_interactive.py +39 -30
- deepagents_code/client/remote_client.py +44 -0
- deepagents_code/config.py +136 -10
- deepagents_code/config_manifest.py +13 -1
- deepagents_code/goal_tools.py +25 -9
- deepagents_code/hooks/models/domain.py +6 -1
- deepagents_code/hooks/transcript.py +24 -5
- deepagents_code/integrations/sandbox_factory.py +69 -2
- deepagents_code/local_context.py +85 -20
- deepagents_code/main.py +83 -3
- deepagents_code/memory_guard.py +4 -0
- deepagents_code/model_config.py +88 -14
- deepagents_code/offload_api.py +84 -6
- deepagents_code/plugins/__init__.py +4 -0
- deepagents_code/plugins/adapters/mcp.py +2 -4
- deepagents_code/plugins/adapters/skills.py +13 -9
- deepagents_code/plugins/commands_cli.py +127 -4
- deepagents_code/plugins/discovery.py +431 -141
- deepagents_code/plugins/models.py +55 -1
- deepagents_code/plugins/store.py +40 -1
- deepagents_code/server_graph.py +2 -0
- deepagents_code/sessions.py +8 -2
- deepagents_code/thread_ownership.py +59 -15
- deepagents_code/tui/modals/plugin_manager/__init__.py +57 -0
- deepagents_code/tui/modals/plugin_manager/content.py +41 -0
- deepagents_code/tui/modals/plugin_manager/models.py +8 -1
- deepagents_code/tui/modals/plugin_manager/state.py +25 -2
- deepagents_code/tui/widgets/_links.py +43 -2
- deepagents_code/tui/widgets/_static_update.py +35 -0
- deepagents_code/tui/widgets/autocomplete.py +185 -57
- deepagents_code/tui/widgets/chat_input.py +43 -1
- deepagents_code/tui/widgets/final_markdown/__init__.py +18 -0
- deepagents_code/tui/widgets/final_markdown/_layout.py +287 -0
- deepagents_code/tui/widgets/final_markdown/_parse.py +158 -0
- deepagents_code/tui/widgets/final_markdown/_rows.py +126 -0
- deepagents_code/tui/widgets/final_markdown/_table.py +136 -0
- deepagents_code/tui/widgets/final_markdown/_widget.py +329 -0
- deepagents_code/tui/widgets/loading.py +5 -3
- deepagents_code/tui/widgets/message_store.py +93 -9
- deepagents_code/tui/widgets/messages.py +179 -67
- deepagents_code/tui/widgets/selection_action.py +131 -0
- deepagents_code/tui/widgets/thread_selector.py +218 -49
- deepagents_code/ui.py +3 -0
- deepagents_code/workspace_diagnostics.py +7 -2
- {deepagents_code-0.2.1.dist-info → deepagents_code-0.2.2.dist-info}/METADATA +22 -22
- {deepagents_code-0.2.1.dist-info → deepagents_code-0.2.2.dist-info}/RECORD +69 -57
- {deepagents_code-0.2.1.data → deepagents_code-0.2.2.data}/data/deepagents_code/default_agent_prompt.md +0 -0
- {deepagents_code-0.2.1.dist-info → deepagents_code-0.2.2.dist-info}/WHEEL +0 -0
- {deepagents_code-0.2.1.dist-info → deepagents_code-0.2.2.dist-info}/entry_points.txt +0 -0
- {deepagents_code-0.2.1.dist-info → deepagents_code-0.2.2.dist-info}/licenses/LICENSE +0 -0
deepagents_code/_build_info.py
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Forward delta-channel history reads through langgraph-api's saver adapter.
|
|
2
|
+
|
|
3
|
+
The LangGraph dev server wraps a custom checkpointer in
|
|
4
|
+
`langgraph_api._checkpointer._adapter._CustomCheckpointerAdapter`. The adapter
|
|
5
|
+
forwards unknown attributes to the inner saver through `__getattr__`, but
|
|
6
|
+
`BaseCheckpointSaver` already defines `aget_delta_channel_history`, so the
|
|
7
|
+
forwarding never fires. The adapter then runs the base ancestor walk: one
|
|
8
|
+
`aget_tuple` and one full checkpoint decode per ancestor. The SQLite saver
|
|
9
|
+
overrides the method with two indexed queries, and long threads pay the
|
|
10
|
+
difference on every state read and every run start.
|
|
11
|
+
|
|
12
|
+
This module is a best-effort patch over a private langgraph-api class. It only
|
|
13
|
+
patches when the adapter still inherits the base method, and it falls back to
|
|
14
|
+
the base walk when the inner saver has no override of its own. Remove it when
|
|
15
|
+
the adapter forwards these methods itself.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Mapping, Sequence
|
|
27
|
+
|
|
28
|
+
from langchain_core.runnables import RunnableConfig
|
|
29
|
+
from langgraph.checkpoint.base import DeltaChannelHistory
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_BASE_ASYNC = BaseCheckpointSaver.aget_delta_channel_history
|
|
34
|
+
_BASE_SYNC = BaseCheckpointSaver.get_delta_channel_history
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _overriding_inner(
|
|
38
|
+
adapter: BaseCheckpointSaver, name: str
|
|
39
|
+
) -> BaseCheckpointSaver | None:
|
|
40
|
+
"""Return the adapter's inner saver if it overrides the base method `name`."""
|
|
41
|
+
inner = adapter.__dict__.get("_inner")
|
|
42
|
+
if not isinstance(inner, BaseCheckpointSaver):
|
|
43
|
+
return None
|
|
44
|
+
if getattr(type(inner), name) is getattr(BaseCheckpointSaver, name):
|
|
45
|
+
return None
|
|
46
|
+
return inner
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _aget_delta_channel_history(
|
|
50
|
+
self: BaseCheckpointSaver, *, config: RunnableConfig, channels: Sequence[str]
|
|
51
|
+
) -> Mapping[str, DeltaChannelHistory]:
|
|
52
|
+
inner = _overriding_inner(self, "aget_delta_channel_history")
|
|
53
|
+
if inner is None:
|
|
54
|
+
return await _BASE_ASYNC(self, config=config, channels=channels)
|
|
55
|
+
return await inner.aget_delta_channel_history(config=config, channels=channels)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_delta_channel_history(
|
|
59
|
+
self: BaseCheckpointSaver, *, config: RunnableConfig, channels: Sequence[str]
|
|
60
|
+
) -> Mapping[str, DeltaChannelHistory]:
|
|
61
|
+
inner = _overriding_inner(self, "get_delta_channel_history")
|
|
62
|
+
if inner is None:
|
|
63
|
+
return _BASE_SYNC(self, config=config, channels=channels)
|
|
64
|
+
return inner.get_delta_channel_history(config=config, channels=channels)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def install_delta_history_forwarding() -> None:
|
|
68
|
+
"""Make langgraph-api's checkpointer adapter use the inner saver's fast path.
|
|
69
|
+
|
|
70
|
+
Call this in the server process before the adapter serves requests. It is
|
|
71
|
+
idempotent and a no-op when the adapter is missing or already overrides
|
|
72
|
+
the methods.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
# The adapter has no public seam; this patch exists to reach it.
|
|
76
|
+
from langgraph_api._checkpointer._adapter import ( # noqa: PLC2701
|
|
77
|
+
_CustomCheckpointerAdapter,
|
|
78
|
+
)
|
|
79
|
+
except ImportError:
|
|
80
|
+
logger.warning("langgraph-api checkpointer adapter not found; skipping patch")
|
|
81
|
+
return
|
|
82
|
+
adapter = _CustomCheckpointerAdapter
|
|
83
|
+
if adapter.aget_delta_channel_history is _BASE_ASYNC:
|
|
84
|
+
adapter.aget_delta_channel_history = _aget_delta_channel_history
|
|
85
|
+
if adapter.get_delta_channel_history is _BASE_SYNC:
|
|
86
|
+
adapter.get_delta_channel_history = _get_delta_channel_history
|
deepagents_code/_env_vars.py
CHANGED
|
@@ -77,6 +77,15 @@ Values outside 1-900 seconds are ignored in favor of the next config source.
|
|
|
77
77
|
Also settable via `[mcp].tool_timeout` in config.toml.
|
|
78
78
|
"""
|
|
79
79
|
|
|
80
|
+
ASK_ABOUT_SELECTION = "DEEPAGENTS_CODE_ASK_ABOUT_SELECTION"
|
|
81
|
+
"""Show "Ask about this" when conversation text is selected.
|
|
82
|
+
|
|
83
|
+
Enabled by default; set to a falsy value (`0`, `false`, `no`, `off`) to hide
|
|
84
|
+
it. Parsed by `classify_env_bool` (an unrecognized or empty value falls through
|
|
85
|
+
to the config value rather than forcing the default). Also settable via
|
|
86
|
+
`[ui].ask_about_selection` in config.toml. `/reload` applies a change.
|
|
87
|
+
"""
|
|
88
|
+
|
|
80
89
|
AUTO_UPDATE = "DEEPAGENTS_CODE_AUTO_UPDATE"
|
|
81
90
|
"""Toggle automatic app updates. Enabled by default; set to a falsy value
|
|
82
91
|
('0', 'false', 'no', 'off', or empty) to opt out."""
|
|
@@ -277,13 +286,20 @@ EXPERIMENTAL = "DEEPAGENTS_CODE_EXPERIMENTAL"
|
|
|
277
286
|
Off by default; parsed by `is_env_truthy` (see there for the accepted truthy
|
|
278
287
|
values). Marks experimental runs in UI/trace metadata. Behavior behind this
|
|
279
288
|
flag may change or be removed without notice.
|
|
289
|
+
|
|
290
|
+
Set only through the launching shell or global `~/.deepagents/.env`; a project
|
|
291
|
+
`.env` cannot set it.
|
|
280
292
|
"""
|
|
281
293
|
|
|
282
294
|
EXTENSIONS = "DEEPAGENTS_CODE_EXTENSIONS"
|
|
283
295
|
"""Enable loading installed-plugin and trusted-project Python extensions."""
|
|
284
296
|
|
|
285
297
|
EXTENSIONS_TRUST = "DEEPAGENTS_CODE_EXTENSIONS_TRUST"
|
|
286
|
-
"""Default project extension trust policy: `ask`, `always`, or `never`.
|
|
298
|
+
"""Default project extension trust policy: `ask`, `always`, or `never`.
|
|
299
|
+
|
|
300
|
+
Set only through the launching shell or global `~/.deepagents/.env`; a project
|
|
301
|
+
`.env` cannot set it.
|
|
302
|
+
"""
|
|
287
303
|
|
|
288
304
|
EXTERNAL_EVENT_SOCKET = "DEEPAGENTS_CODE_EXTERNAL_EVENT_SOCKET"
|
|
289
305
|
"""Enable the local Unix-socket external event listener.
|
|
@@ -296,7 +312,11 @@ EXTERNAL_EVENT_SOCKET_PATH = "DEEPAGENTS_CODE_EXTERNAL_EVENT_SOCKET_PATH"
|
|
|
296
312
|
"""Override the default Unix-socket path for the external event listener."""
|
|
297
313
|
|
|
298
314
|
EXTRA_SKILLS_DIRS = "DEEPAGENTS_CODE_EXTRA_SKILLS_DIRS"
|
|
299
|
-
"""Paths added to the skill containment allowlist, split with `os.pathsep`.
|
|
315
|
+
"""Paths added to the skill containment allowlist, split with `os.pathsep`.
|
|
316
|
+
|
|
317
|
+
Set only through the launching shell or global `~/.deepagents/.env`; a project
|
|
318
|
+
`.env` cannot set it.
|
|
319
|
+
"""
|
|
300
320
|
|
|
301
321
|
GOAL_AUTO_ACCEPT_CRITERIA = "DEEPAGENTS_CODE_GOAL_AUTO_ACCEPT_CRITERIA"
|
|
302
322
|
"""Apply generated goal criteria automatically in Auto mode.
|
|
@@ -485,6 +505,8 @@ PLUGIN_CACHE_DIR = "DEEPAGENTS_CODE_PLUGIN_CACHE_DIR"
|
|
|
485
505
|
"""Override the plugin install/marketplace cache root.
|
|
486
506
|
|
|
487
507
|
When unset, plugins are stored under `DEFAULT_CONFIG_DIR / "plugins"`.
|
|
508
|
+
Set only through the launching shell or global `~/.deepagents/.env`; a project
|
|
509
|
+
`.env` cannot set it.
|
|
488
510
|
"""
|
|
489
511
|
|
|
490
512
|
PRICES_AUTO_UPDATE = "DEEPAGENTS_CODE_PRICES_AUTO_UPDATE"
|
|
@@ -564,7 +586,11 @@ SERVER_ENV_PREFIX = "DEEPAGENTS_CODE_SERVER_"
|
|
|
564
586
|
"""Environment variable prefix used to pass CLI config to the server subprocess."""
|
|
565
587
|
|
|
566
588
|
SHELL_ALLOW_LIST = "DEEPAGENTS_CODE_SHELL_ALLOW_LIST"
|
|
567
|
-
"""Comma-separated shell commands to allow (or 'recommended'/'all').
|
|
589
|
+
"""Comma-separated shell commands to allow (or 'recommended'/'all').
|
|
590
|
+
|
|
591
|
+
Set only through the launching shell or global `~/.deepagents/.env`; a project
|
|
592
|
+
`.env` cannot set it.
|
|
593
|
+
"""
|
|
568
594
|
|
|
569
595
|
SHOW_HEADER = "DEEPAGENTS_CODE_SHOW_HEADER"
|
|
570
596
|
"""Show Textual's native header bar at the top of the TUI when enabled."""
|
deepagents_code/_git.py
CHANGED
|
@@ -17,6 +17,9 @@ _GIT_DIR_POINTER_PREFIX = "gitdir: "
|
|
|
17
17
|
_GIT_HEAD_REF_PREFIX = "ref: "
|
|
18
18
|
"""Prefix used by `HEAD` when it points at a named ref instead of a commit."""
|
|
19
19
|
|
|
20
|
+
_REFTABLE_HEAD_PLACEHOLDER = "refs/heads/.invalid"
|
|
21
|
+
"""Ref a reftable repository writes to `HEAD`; the real refs live in `reftable/`."""
|
|
22
|
+
|
|
20
23
|
_GIT_REF_PREFIXES = ("refs/heads/", "refs/remotes/", "refs/tags/", "refs/")
|
|
21
24
|
"""Known git ref prefixes stripped when formatting a branch-like display name."""
|
|
22
25
|
|
|
@@ -411,7 +414,8 @@ def read_git_branch_from_filesystem(path: str | Path) -> str | None:
|
|
|
411
414
|
Returns:
|
|
412
415
|
The abbreviated branch name, `HEAD` for detached HEAD, an empty string
|
|
413
416
|
when `path` is not inside a git repository, or `None` when metadata
|
|
414
|
-
exists but cannot be parsed confidently
|
|
417
|
+
exists but cannot be parsed confidently (including reftable
|
|
418
|
+
repositories, whose `HEAD` file only holds a placeholder).
|
|
415
419
|
"""
|
|
416
420
|
git_dir = find_git_dir(path)
|
|
417
421
|
if git_dir is None:
|
|
@@ -431,7 +435,9 @@ def read_git_branch_from_filesystem(path: str | Path) -> str | None:
|
|
|
431
435
|
return ""
|
|
432
436
|
if head.startswith(_GIT_HEAD_REF_PREFIX):
|
|
433
437
|
ref = head.removeprefix(_GIT_HEAD_REF_PREFIX).strip()
|
|
434
|
-
|
|
438
|
+
if not ref or ref == _REFTABLE_HEAD_PLACEHOLDER:
|
|
439
|
+
return None
|
|
440
|
+
return _abbreviate_git_ref(ref)
|
|
435
441
|
return "HEAD"
|
|
436
442
|
|
|
437
443
|
|
deepagents_code/_js_cost.py
CHANGED
|
@@ -7,6 +7,7 @@ import dataclasses
|
|
|
7
7
|
import hashlib
|
|
8
8
|
import itertools
|
|
9
9
|
import json
|
|
10
|
+
import logging
|
|
10
11
|
import math
|
|
11
12
|
from collections.abc import Mapping
|
|
12
13
|
from typing import TYPE_CHECKING, override
|
|
@@ -19,6 +20,7 @@ from langgraph.checkpoint.base import (
|
|
|
19
20
|
empty_checkpoint,
|
|
20
21
|
)
|
|
21
22
|
from langgraph.config import get_config
|
|
23
|
+
from langgraph.errors import GraphBubbleUp, NodeCancelledError
|
|
22
24
|
from langgraph.types import Command
|
|
23
25
|
|
|
24
26
|
from deepagents_code.cost_tracking import (
|
|
@@ -37,6 +39,8 @@ if TYPE_CHECKING:
|
|
|
37
39
|
|
|
38
40
|
from deepagents_code.cost_tracking import CostBreakdown
|
|
39
41
|
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
40
44
|
_ACCOUNTING_KEY = "__deepagents_js_cost_owner"
|
|
41
45
|
_RESPONSE_FORMAT_KEY = "__deepagents_subagent_response_format"
|
|
42
46
|
|
|
@@ -172,6 +176,17 @@ def _cost_task(
|
|
|
172
176
|
config=config,
|
|
173
177
|
tool_call_id=runtime.tool_call_id,
|
|
174
178
|
)
|
|
179
|
+
except asyncio.CancelledError, GraphBubbleUp, NodeCancelledError:
|
|
180
|
+
raise
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
logger.debug("Subagent %r failed", subagent_type, exc_info=exc)
|
|
183
|
+
return {
|
|
184
|
+
"error": (
|
|
185
|
+
f"Subagent {subagent_type!r} failed: "
|
|
186
|
+
f"{type(exc).__name__}: {str(exc)[:300]}. You may retry this task."
|
|
187
|
+
),
|
|
188
|
+
"failed": True,
|
|
189
|
+
}
|
|
175
190
|
finally:
|
|
176
191
|
if invocation is not None:
|
|
177
192
|
active.discard(invocation)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Run untraced `@traceable` bookkeeping inline on the server loop.
|
|
2
|
+
|
|
3
|
+
LangChain wraps every middleware `awrap_tool_call` and `awrap_model_call` hook
|
|
4
|
+
in `langsmith.traceable`. Its async wrapper sends two calls to a worker thread
|
|
5
|
+
per hook: one to set up the run and one to end it. It does this even when
|
|
6
|
+
tracing is off and both calls only build an empty container. About 20 traced
|
|
7
|
+
hooks run per tool round trip, so each round trip pays for about 40 thread
|
|
8
|
+
hops that do no work.
|
|
9
|
+
|
|
10
|
+
`install_untraced_inline_override` installs a LangSmith runtime override. The
|
|
11
|
+
override runs those two calls inline when they cannot trace, and gives every
|
|
12
|
+
other call to LangSmith's default thread hop.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import logging
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
import contextvars
|
|
23
|
+
from collections.abc import Awaitable, Callable
|
|
24
|
+
|
|
25
|
+
DefaultAioToThread = Callable[..., Awaitable[Any]]
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _target(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
31
|
+
"""Return the function a `functools.partial` wraps, else `func` itself.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The underlying callable.
|
|
35
|
+
"""
|
|
36
|
+
return func.func if isinstance(func, functools.partial) else func
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _runs_inline(
|
|
40
|
+
ctx: contextvars.Context,
|
|
41
|
+
func: Callable[..., Any],
|
|
42
|
+
args: tuple[Any, ...],
|
|
43
|
+
) -> bool:
|
|
44
|
+
"""Whether a `traceable` bookkeeping call cannot trace, so it is cheap.
|
|
45
|
+
|
|
46
|
+
Only run setup and run end qualify. Any other call (for example an async
|
|
47
|
+
`Client` API method) can do network I/O, so it keeps the thread hop.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
`True` when the call is safe to run on the event loop.
|
|
51
|
+
"""
|
|
52
|
+
from langsmith import run_helpers
|
|
53
|
+
from langsmith.utils import tracing_is_enabled
|
|
54
|
+
|
|
55
|
+
target = _target(func)
|
|
56
|
+
if target is getattr(run_helpers, "_setup_run", None):
|
|
57
|
+
return not ctx.run(tracing_is_enabled)
|
|
58
|
+
if target is getattr(run_helpers, "_handle_container_end", None):
|
|
59
|
+
container = args[0] if args else None
|
|
60
|
+
return isinstance(container, dict) and container.get("new_run") is None
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def _untraced_inline_aio_to_thread(
|
|
65
|
+
default: DefaultAioToThread,
|
|
66
|
+
ctx: contextvars.Context,
|
|
67
|
+
func: Callable[..., Any],
|
|
68
|
+
/,
|
|
69
|
+
*args: Any, # forwards arbitrary LangSmith call arguments
|
|
70
|
+
**kwargs: Any, # forwards arbitrary LangSmith call arguments
|
|
71
|
+
) -> Any: # noqa: ANN401 # returns whatever `func` returns
|
|
72
|
+
"""Run untraced bookkeeping inline; hop to a thread for everything else.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The result of `func`.
|
|
76
|
+
"""
|
|
77
|
+
if _runs_inline(ctx, func, args):
|
|
78
|
+
return ctx.run(func, *args, **kwargs)
|
|
79
|
+
return await default(ctx, func, *args, **kwargs)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def install_untraced_inline_override() -> None:
|
|
83
|
+
"""Install the override for this process.
|
|
84
|
+
|
|
85
|
+
The override is process-global. Call it only in the server process, which
|
|
86
|
+
owns its event loop and pins one tracing configuration for its lifetime.
|
|
87
|
+
"""
|
|
88
|
+
import langsmith
|
|
89
|
+
|
|
90
|
+
langsmith.set_runtime_overrides(aio_to_thread=_untraced_inline_aio_to_thread)
|
|
91
|
+
logger.debug("Installed inline LangSmith runtime override for untraced runs")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Wake the in-memory run queue when a run is created.
|
|
2
|
+
|
|
3
|
+
The server runs on `langgraph_runtime_inmem`. Its background scheduler polls
|
|
4
|
+
for pending runs: `Runs.next(wait=True)` sleeps 0.5 s, then claims only runs
|
|
5
|
+
created before that sleep began. Every user turn and every interrupt resume is
|
|
6
|
+
a new run, so each one waits 0.5-1.0 s before a worker starts it.
|
|
7
|
+
|
|
8
|
+
This patch makes `Runs.put` set an event and makes a waiting `Runs.next` wait
|
|
9
|
+
on that event instead of the fixed sleep. The stock `Runs.next` still claims
|
|
10
|
+
the runs, so queue order and per-thread locking do not change. The 0.5 s
|
|
11
|
+
timeout stays as a fallback, so a missed wakeup costs at most the stock delay.
|
|
12
|
+
|
|
13
|
+
`Runs.next` and `Runs.put` are private APIs. If their shape is not what this
|
|
14
|
+
module expects, the patch logs at debug level and does nothing. Remove this
|
|
15
|
+
module when `langgraph_runtime_inmem` wakes the scheduler on put.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import contextlib
|
|
22
|
+
import functools
|
|
23
|
+
import inspect
|
|
24
|
+
import logging
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_POLL_INTERVAL = 0.5
|
|
33
|
+
"""Fallback wait in seconds; matches the stock `Runs.next` sleep."""
|
|
34
|
+
|
|
35
|
+
_PATCHED_MARKER = "_dcode_run_queue_wakeup"
|
|
36
|
+
"""Attribute set on each wrapper so a second install is a no-op."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _Wakeup:
|
|
40
|
+
"""Edge-triggered signal from `Runs.put` to a waiting `Runs.next`.
|
|
41
|
+
|
|
42
|
+
The event binds to the scheduler loop on the first wait. A put before
|
|
43
|
+
that first wait is not lost: the scheduler then waits the fallback
|
|
44
|
+
interval, as stock does.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self._event: asyncio.Event | None = None
|
|
49
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
50
|
+
|
|
51
|
+
async def wait(self) -> None:
|
|
52
|
+
"""Wait until a run is put, or until the fallback interval passes."""
|
|
53
|
+
if self._event is None:
|
|
54
|
+
self._event = asyncio.Event()
|
|
55
|
+
self._loop = asyncio.get_running_loop()
|
|
56
|
+
with contextlib.suppress(TimeoutError):
|
|
57
|
+
async with asyncio.timeout(_POLL_INTERVAL):
|
|
58
|
+
await self._event.wait()
|
|
59
|
+
self._event.clear()
|
|
60
|
+
|
|
61
|
+
def notify(self) -> None:
|
|
62
|
+
"""Wake the scheduler; safe from any thread and after loop close."""
|
|
63
|
+
event, loop = self._event, self._loop
|
|
64
|
+
if event is None or loop is None:
|
|
65
|
+
return
|
|
66
|
+
# The loop can close during shutdown; the wakeup is then moot.
|
|
67
|
+
with contextlib.suppress(RuntimeError):
|
|
68
|
+
loop.call_soon_threadsafe(event.set)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_WAKEUP = _Wakeup()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _wrap_next(
|
|
75
|
+
original: Callable[[bool, int], AsyncIterator[object]],
|
|
76
|
+
) -> Callable[[bool, int], AsyncIterator[object]]:
|
|
77
|
+
"""Replace the fixed poll sleep with a wait on `_WAKEUP`.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A drop-in replacement for the upstream `Runs.next`.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
@functools.wraps(original)
|
|
84
|
+
async def next_runs(wait: bool, limit: int = 1) -> AsyncIterator[object]:
|
|
85
|
+
if wait:
|
|
86
|
+
await _WAKEUP.wait()
|
|
87
|
+
# `wait=False` skips the sleep and claims runs created before now,
|
|
88
|
+
# which includes the run that woke us.
|
|
89
|
+
async for item in original(False, limit):
|
|
90
|
+
yield item
|
|
91
|
+
|
|
92
|
+
setattr(next_runs, _PATCHED_MARKER, True)
|
|
93
|
+
return next_runs
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _wrap_put(
|
|
97
|
+
original: Callable[..., Awaitable[object]],
|
|
98
|
+
) -> Callable[..., Awaitable[object]]:
|
|
99
|
+
"""Notify `_WAKEUP` after each run insert.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
A drop-in replacement for the upstream `Runs.put`.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
@functools.wraps(original)
|
|
106
|
+
async def put(*args: object, **kwargs: object) -> object:
|
|
107
|
+
result = await original(*args, **kwargs)
|
|
108
|
+
_WAKEUP.notify()
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
setattr(put, _PATCHED_MARKER, True)
|
|
112
|
+
return put
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _has_expected_shape(runs: type) -> bool:
|
|
116
|
+
"""Check that `Runs.next` and `Runs.put` look like the pinned version.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
`True` when both are static methods with the expected signatures.
|
|
120
|
+
"""
|
|
121
|
+
next_attr = runs.__dict__.get("next")
|
|
122
|
+
put_attr = runs.__dict__.get("put")
|
|
123
|
+
if not isinstance(next_attr, staticmethod) or not isinstance(
|
|
124
|
+
put_attr, staticmethod
|
|
125
|
+
):
|
|
126
|
+
return False
|
|
127
|
+
next_fn, put_fn = next_attr.__func__, put_attr.__func__
|
|
128
|
+
params = list(inspect.signature(next_fn).parameters)
|
|
129
|
+
return (
|
|
130
|
+
inspect.isasyncgenfunction(next_fn)
|
|
131
|
+
and params[:2] == ["wait", "limit"]
|
|
132
|
+
and inspect.iscoroutinefunction(put_fn)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def install_run_queue_wakeup() -> bool:
|
|
137
|
+
"""Patch the in-memory run queue to start new runs without the poll delay.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
`True` when the patch is active after the call, else `False`.
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
from langgraph_runtime_inmem.ops import Runs
|
|
144
|
+
except ImportError:
|
|
145
|
+
logger.debug("Run queue wakeup skipped: in-memory runtime not installed")
|
|
146
|
+
return False
|
|
147
|
+
if getattr(Runs.next, _PATCHED_MARKER, False):
|
|
148
|
+
return True
|
|
149
|
+
if not _has_expected_shape(Runs):
|
|
150
|
+
logger.debug("Run queue wakeup skipped: Runs.next/put changed upstream")
|
|
151
|
+
return False
|
|
152
|
+
wrappers = {"next": _wrap_next, "put": _wrap_put}
|
|
153
|
+
for name, wrap in wrappers.items():
|
|
154
|
+
setattr(Runs, name, staticmethod(wrap(getattr(Runs, name))))
|
|
155
|
+
logger.debug("Run queue wakeup installed")
|
|
156
|
+
return True
|
|
@@ -293,6 +293,11 @@ else:
|
|
|
293
293
|
|
|
294
294
|
|
|
295
295
|
def _rendered_text(widget: Widget) -> str | None:
|
|
296
|
+
# A widget that renders a custom visual (such as `FinalMarkdown`) can
|
|
297
|
+
# publish the text its selection offsets index into.
|
|
298
|
+
selectable = getattr(widget, "selectable_text", None)
|
|
299
|
+
if isinstance(selectable, str):
|
|
300
|
+
return selectable
|
|
296
301
|
visual = widget._render() # match Textual's get_selection path
|
|
297
302
|
if isinstance(visual, (Content, Text)):
|
|
298
303
|
return str(visual)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""LangSmith secret redaction that reuses results across traced runs.
|
|
2
|
+
|
|
3
|
+
Each traced run carries the full message history, so the stock secret
|
|
4
|
+
anonymizer re-runs every rule over strings it already scanned on the previous
|
|
5
|
+
model call. Redaction is a pure function of each string, so this module caches
|
|
6
|
+
the masked result per input string and skips the regex passes on a repeat.
|
|
7
|
+
|
|
8
|
+
Imported lazily: it pulls in `langsmith.anonymizer`, which the tracing-off
|
|
9
|
+
startup path does not need.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import threading
|
|
15
|
+
from collections import OrderedDict
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from langsmith.anonymizer import (
|
|
19
|
+
DEFAULT_SECRET_RULES,
|
|
20
|
+
RuleNodeProcessor,
|
|
21
|
+
StringNode,
|
|
22
|
+
StringNodeProcessor,
|
|
23
|
+
create_anonymizer,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
|
|
29
|
+
from langsmith.anonymizer import StringNodeRule
|
|
30
|
+
|
|
31
|
+
_MAX_DEPTH = 24
|
|
32
|
+
"""Traversal depth; matches the default of `create_secret_anonymizer`."""
|
|
33
|
+
|
|
34
|
+
_MAX_ENTRIES = 50_000
|
|
35
|
+
"""Most distinct strings the cache keeps before it evicts the oldest."""
|
|
36
|
+
|
|
37
|
+
_MAX_CHARS = 16 * 1024 * 1024
|
|
38
|
+
"""Most characters (keys plus changed values) the cache keeps alive."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CachingRuleProcessor(StringNodeProcessor):
|
|
42
|
+
"""Apply secret rules per string and remember the result in a bounded LRU.
|
|
43
|
+
|
|
44
|
+
The output for a string is the same whether it comes from the cache or
|
|
45
|
+
from the rules, so the cache cannot let a secret through. Strings too large
|
|
46
|
+
for the character budget are masked each time and not cached.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
rules: list[StringNodeRule],
|
|
52
|
+
*,
|
|
53
|
+
max_entries: int = _MAX_ENTRIES,
|
|
54
|
+
max_chars: int = _MAX_CHARS,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Build the processor.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
rules: Redaction rules, applied in order to every string.
|
|
60
|
+
max_entries: Upper bound on cached strings.
|
|
61
|
+
max_chars: Upper bound on characters the cache holds.
|
|
62
|
+
"""
|
|
63
|
+
self._rules = RuleNodeProcessor(rules)
|
|
64
|
+
self._max_entries = max_entries
|
|
65
|
+
self._max_chars = max_chars
|
|
66
|
+
self._cache: OrderedDict[str, str] = OrderedDict()
|
|
67
|
+
self._chars = 0
|
|
68
|
+
# The tracer can call the anonymizer from its background thread as
|
|
69
|
+
# well as from the event loop.
|
|
70
|
+
self._lock = threading.Lock()
|
|
71
|
+
|
|
72
|
+
def mask_nodes(self, nodes: list[StringNode]) -> list[StringNode]:
|
|
73
|
+
"""Return the nodes whose value changes after masking.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
nodes: String nodes extracted from a traced payload.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Replacement nodes, one for each string that the rules changed.
|
|
80
|
+
"""
|
|
81
|
+
# One lock round-trip per payload, not per string: a long history has
|
|
82
|
+
# tens of thousands of string nodes.
|
|
83
|
+
with self._lock:
|
|
84
|
+
hits = [self._lookup(node["value"]) for node in nodes]
|
|
85
|
+
result: list[StringNode] = []
|
|
86
|
+
for node, hit in zip(nodes, hits, strict=True):
|
|
87
|
+
value = node["value"]
|
|
88
|
+
masked = self._apply_rules(value) if hit is None else hit
|
|
89
|
+
if masked != value:
|
|
90
|
+
result.append(StringNode(value=masked, path=node["path"]))
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def _lookup(self, value: str) -> str | None:
|
|
94
|
+
"""Return the cached mask for `value` and mark it recent; caller locks."""
|
|
95
|
+
masked = self._cache.get(value)
|
|
96
|
+
if masked is not None:
|
|
97
|
+
self._cache.move_to_end(value)
|
|
98
|
+
return masked
|
|
99
|
+
|
|
100
|
+
def _apply_rules(self, value: str) -> str:
|
|
101
|
+
"""Run every rule over `value` and cache the result.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The masked string, or `value` itself when no rule matched.
|
|
105
|
+
"""
|
|
106
|
+
changed = self._rules.mask_nodes([StringNode(value=value, path=[])])
|
|
107
|
+
masked = changed[0]["value"] if changed else value
|
|
108
|
+
self._store(value, masked)
|
|
109
|
+
return masked
|
|
110
|
+
|
|
111
|
+
def _store(self, value: str, masked: str) -> None:
|
|
112
|
+
"""Cache `masked` for `value` and evict the oldest entries past bounds."""
|
|
113
|
+
cost = _cost(value, masked)
|
|
114
|
+
if cost > self._max_chars:
|
|
115
|
+
return
|
|
116
|
+
with self._lock:
|
|
117
|
+
if value in self._cache:
|
|
118
|
+
return
|
|
119
|
+
self._cache[value] = masked
|
|
120
|
+
self._chars += cost
|
|
121
|
+
while len(self._cache) > self._max_entries or self._chars > self._max_chars:
|
|
122
|
+
old_value, old_masked = self._cache.popitem(last=False)
|
|
123
|
+
self._chars -= _cost(old_value, old_masked)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _cost(value: str, masked: str) -> int:
|
|
127
|
+
"""Return the characters a cache entry keeps alive.
|
|
128
|
+
|
|
129
|
+
An unchanged string stores the key object as its own value, so it counts
|
|
130
|
+
once.
|
|
131
|
+
"""
|
|
132
|
+
return len(value) + (0 if masked is value else len(masked))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def create_cached_secret_anonymizer() -> Callable[[Any], Any]:
|
|
136
|
+
"""Build a secret anonymizer that matches `create_secret_anonymizer`.
|
|
137
|
+
|
|
138
|
+
It uses the same default rules and depth, and adds a per-string cache so
|
|
139
|
+
unchanged history strings are not scanned again on each traced run.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
An anonymizer to pass to `langsmith.Client(anonymizer=...)`.
|
|
143
|
+
"""
|
|
144
|
+
processor = CachingRuleProcessor(list(DEFAULT_SECRET_RULES))
|
|
145
|
+
return create_anonymizer(processor, max_depth=_MAX_DEPTH)
|
deepagents_code/_version.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Keep the `x-release-please-version` annotation — release-please uses it to
|
|
4
4
|
# bump `__version__` in sync with `pyproject.toml` on every release PR.
|
|
5
|
-
__version__ = "0.2.
|
|
5
|
+
__version__ = "0.2.2" # x-release-please-version
|
|
6
6
|
|
|
7
7
|
DOCS_URL = "https://docs.langchain.com/oss/python/deepagents/code"
|
|
8
8
|
"""URL for `deepagents-code` documentation."""
|