zjcode 0.0.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.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Rubric middleware retries for transient grader transport failures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from deepagents.middleware.rubric import GraderResponse, RubricMiddleware, RubricState
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _exception_chain(exc: BaseException) -> Iterator[BaseException]:
|
|
18
|
+
"""Yield an exception, its explicit/implicit causes, and group members once.
|
|
19
|
+
|
|
20
|
+
Descends into `BaseExceptionGroup` members as well as `__cause__` and
|
|
21
|
+
`__context__`, so a transient transport error wrapped in an async task group
|
|
22
|
+
is still discovered. Each exception is yielded at most once.
|
|
23
|
+
"""
|
|
24
|
+
pending = [exc]
|
|
25
|
+
seen: set[int] = set()
|
|
26
|
+
while pending:
|
|
27
|
+
current = pending.pop()
|
|
28
|
+
if id(current) in seen:
|
|
29
|
+
continue
|
|
30
|
+
seen.add(id(current))
|
|
31
|
+
yield current
|
|
32
|
+
if isinstance(current, BaseExceptionGroup):
|
|
33
|
+
pending.extend(current.exceptions)
|
|
34
|
+
if current.__cause__ is not None:
|
|
35
|
+
pending.append(current.__cause__)
|
|
36
|
+
elif current.__context__ is not None:
|
|
37
|
+
pending.append(current.__context__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_transient_grader_transport_error(exc: BaseException) -> bool:
|
|
41
|
+
"""Return whether a grader failure is a retryable transport/read error.
|
|
42
|
+
|
|
43
|
+
Matches response-read faults (`httpx`/`httpcore` `ReadError`) and
|
|
44
|
+
response-framing faults (`RemoteProtocolError`, aiohttp
|
|
45
|
+
`TransferEncodingError`). Connect/timeout errors are intentionally excluded
|
|
46
|
+
so only mid-response transport failures trigger the retry.
|
|
47
|
+
"""
|
|
48
|
+
for current in _exception_chain(exc):
|
|
49
|
+
if isinstance(current, (httpx.ReadError, httpx.RemoteProtocolError)):
|
|
50
|
+
return True
|
|
51
|
+
error_type = type(current)
|
|
52
|
+
if error_type.__module__.startswith("httpcore") and error_type.__name__ in {
|
|
53
|
+
"ReadError",
|
|
54
|
+
"RemoteProtocolError",
|
|
55
|
+
}:
|
|
56
|
+
return True
|
|
57
|
+
if (
|
|
58
|
+
error_type.__module__ == "aiohttp.http_exceptions"
|
|
59
|
+
and error_type.__name__ == "TransferEncodingError"
|
|
60
|
+
and "Not enough data to satisfy transfer length header" in str(current)
|
|
61
|
+
):
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ReliableRubricMiddleware(RubricMiddleware):
|
|
67
|
+
"""Retry one transient grader transport failure without rerunning agent work.
|
|
68
|
+
|
|
69
|
+
The retry re-invokes only the grader sub-agent, never the task agent, so it
|
|
70
|
+
relies on the grader's own tools being read-only/idempotent. A second
|
|
71
|
+
failure — transient or not — propagates to the base middleware, which
|
|
72
|
+
surfaces it as a `grader_error` result rather than a silent success.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def _grade(self, state: RubricState, iteration: int) -> GraderResponse:
|
|
76
|
+
try:
|
|
77
|
+
return super()._grade(state, iteration)
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
if not _is_transient_grader_transport_error(exc):
|
|
80
|
+
raise
|
|
81
|
+
logger.warning(
|
|
82
|
+
"Rubric grader transport failed; retrying grading once",
|
|
83
|
+
exc_info=True,
|
|
84
|
+
)
|
|
85
|
+
return super()._grade(state, iteration)
|
|
86
|
+
|
|
87
|
+
async def _agrade(self, state: RubricState, iteration: int) -> GraderResponse:
|
|
88
|
+
try:
|
|
89
|
+
return await super()._agrade(state, iteration)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
if not _is_transient_grader_transport_error(exc):
|
|
92
|
+
raise
|
|
93
|
+
logger.warning(
|
|
94
|
+
"Rubric grader transport failed; retrying grading once",
|
|
95
|
+
exc_info=True,
|
|
96
|
+
)
|
|
97
|
+
return await super()._agrade(state, iteration)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Schema and middleware for per-checkpoint state restored when resuming.
|
|
2
|
+
|
|
3
|
+
`ResumeState` declares several checkpointed, schema-private channels. They fall
|
|
4
|
+
into two groups with *different* write paths:
|
|
5
|
+
|
|
6
|
+
Written from inside the graph on successful model turns:
|
|
7
|
+
|
|
8
|
+
- `_context_tokens` — total context tokens from the latest
|
|
9
|
+
`AIMessage.usage_metadata`, written by `ResumeStateMiddleware.after_model`.
|
|
10
|
+
Powers `/tokens` and the status bar.
|
|
11
|
+
- `_model_spec` / `_model_params` — the model and invocation params effectively
|
|
12
|
+
in use for the turn, written by `ConfigurableModelMiddleware` after a
|
|
13
|
+
successful model call. Lets `dcode -r` restore the model the resumed thread
|
|
14
|
+
was actually using instead of falling back to the user's global default.
|
|
15
|
+
|
|
16
|
+
Written primarily by the TUI client, via `aupdate_state` (see
|
|
17
|
+
`DeepAgentsApp._persist_goal_rubric_state`) — these are user/agent-owned. Most
|
|
18
|
+
have no model-node write site; the two exceptions are called out below:
|
|
19
|
+
|
|
20
|
+
- `_goal_objective` / `_goal_status` / `_goal_rubric` / `_goal_status_note` —
|
|
21
|
+
the accepted goal and its lifecycle status. `_goal_objective`/`_goal_rubric`
|
|
22
|
+
are client-only, but `_goal_status`/`_goal_status_note` are *also* written
|
|
23
|
+
from inside the graph by the agent's `update_goal` tool.
|
|
24
|
+
- `_pending_goal_completion_note` — an agent-requested completion awaiting the
|
|
25
|
+
post-turn rubric result and, when needed, user approval.
|
|
26
|
+
- `_sticky_rubric` — the TUI-owned persistent rubric. This is separate from
|
|
27
|
+
the public `rubric` graph input so one-shot rubric turns can be checkpointed
|
|
28
|
+
without being restored as sticky state.
|
|
29
|
+
- `_pending_goal_objective` / `_pending_goal_rubric` / `_pending_goal_kind` — a
|
|
30
|
+
proposed goal or amendment awaiting user acceptance of its criteria.
|
|
31
|
+
|
|
32
|
+
All of these are facts the CLI reads back from `state_values` on thread resume
|
|
33
|
+
so it can rehydrate the session without replaying or re-tokenizing history.
|
|
34
|
+
|
|
35
|
+
The model-turn channels are persisted from inside the graph (rather than via a
|
|
36
|
+
separate client-side `aupdate_state` call) so the write rides the same checkpoint
|
|
37
|
+
as the model response and avoids creating a standalone `UpdateState` run in
|
|
38
|
+
LangSmith. Because they are versioned channel state, resuming a specific
|
|
39
|
+
checkpoint yields the values as of *that* checkpoint — not a thread-level
|
|
40
|
+
aggregate. The goal/rubric channels are client-written because the user sets
|
|
41
|
+
them outside any model turn (except `_goal_status`/`_goal_status_note`, which the
|
|
42
|
+
`update_goal` tool also writes from inside the graph). Both paths work
|
|
43
|
+
identically against local and remote (HTTP) graphs.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from typing import (
|
|
49
|
+
TYPE_CHECKING,
|
|
50
|
+
Annotated,
|
|
51
|
+
Any,
|
|
52
|
+
Literal,
|
|
53
|
+
NotRequired,
|
|
54
|
+
cast,
|
|
55
|
+
get_args,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
from langchain.agents.middleware.types import (
|
|
59
|
+
AgentMiddleware,
|
|
60
|
+
AgentState,
|
|
61
|
+
ContextT,
|
|
62
|
+
PrivateStateAttr,
|
|
63
|
+
)
|
|
64
|
+
from langchain_core.messages import AIMessage
|
|
65
|
+
|
|
66
|
+
if TYPE_CHECKING:
|
|
67
|
+
from langgraph.runtime import Runtime
|
|
68
|
+
|
|
69
|
+
GoalStatus = Literal["active", "paused", "blocked", "complete"]
|
|
70
|
+
"""Lifecycle status of a TUI-owned goal.
|
|
71
|
+
|
|
72
|
+
`active` and `blocked` are unfinished working states, `paused` preserves the goal
|
|
73
|
+
without driving work, and `complete` is terminal. A blocked goal is still
|
|
74
|
+
considered actionable (`active=True`) by `get_goal`, whereas a paused goal is
|
|
75
|
+
unfinished but reports `active=False`.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
GoalProposalKind = Literal["create", "amend"]
|
|
79
|
+
"""Whether a pending review creates a goal or amends the current one."""
|
|
80
|
+
|
|
81
|
+
_GOAL_STATUS_VALUES: frozenset[str] = frozenset(get_args(GoalStatus))
|
|
82
|
+
_GOAL_PROPOSAL_KIND_VALUES: frozenset[str] = frozenset(get_args(GoalProposalKind))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def coerce_goal_proposal_kind(value: object) -> GoalProposalKind | None:
|
|
86
|
+
"""Narrow a persisted proposal kind to a known value.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
value: Raw value read from checkpoint state.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The recognized proposal kind, otherwise `None`.
|
|
93
|
+
"""
|
|
94
|
+
if isinstance(value, str) and value in _GOAL_PROPOSAL_KIND_VALUES:
|
|
95
|
+
return cast("GoalProposalKind", value)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def coerce_goal_status(value: object) -> GoalStatus | None:
|
|
100
|
+
"""Narrow a persisted goal-status value to a known `GoalStatus`.
|
|
101
|
+
|
|
102
|
+
A corrupt or forward-version checkpoint can carry an unexpected status
|
|
103
|
+
string (or a non-string). Coercing to `None` rather than passing the raw
|
|
104
|
+
value through keeps the `GoalStatus` `Literal` load-bearing on the read
|
|
105
|
+
path, so an unknown status is treated as "no goal status" instead of a
|
|
106
|
+
silently active goal. Resume/restore callers should log the discard
|
|
107
|
+
separately so it is surfaced rather than dropped; the model-read path
|
|
108
|
+
(`_goal_snapshot`) intentionally treats an unknown status as `active`
|
|
109
|
+
without logging.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
value: Raw value read from checkpoint state.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The value when it is a recognized `GoalStatus`, otherwise `None`.
|
|
116
|
+
"""
|
|
117
|
+
if isinstance(value, str) and value in _GOAL_STATUS_VALUES:
|
|
118
|
+
return cast("GoalStatus", value)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class GoalRubricChannels(AgentState):
|
|
123
|
+
"""Goal/rubric state channels shared by every schema that touches them.
|
|
124
|
+
|
|
125
|
+
Declared once here so each schema that carries these channels —
|
|
126
|
+
`ResumeState` and `goal_tools.GoalToolState` — inherits the *same*
|
|
127
|
+
`PrivateStateAttr`-marked annotations. Middleware state schemas merge with
|
|
128
|
+
later entries winning, so an independent re-declaration that dropped the
|
|
129
|
+
`PrivateStateAttr` marker would override these and leak the field into the
|
|
130
|
+
public graph input/output schema. Inheriting from a single base makes that
|
|
131
|
+
drift unrepresentable.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
_goal_objective: Annotated[NotRequired[str | None], PrivateStateAttr]
|
|
135
|
+
"""Accepted goal objective restored by the TUI on resume."""
|
|
136
|
+
|
|
137
|
+
_goal_status: Annotated[NotRequired[GoalStatus | None], PrivateStateAttr]
|
|
138
|
+
"""Goal lifecycle status (`active`, `paused`, `blocked`, `complete`, or `None`)."""
|
|
139
|
+
|
|
140
|
+
_goal_rubric: Annotated[NotRequired[str | None], PrivateStateAttr]
|
|
141
|
+
"""Accepted rubric associated with `_goal_objective`."""
|
|
142
|
+
|
|
143
|
+
_goal_status_note: Annotated[NotRequired[str | None], PrivateStateAttr]
|
|
144
|
+
"""Evidence or blocker note recorded by `update_goal`."""
|
|
145
|
+
|
|
146
|
+
_pending_goal_completion_note: Annotated[NotRequired[str | None], PrivateStateAttr]
|
|
147
|
+
"""Completion evidence awaiting rubric and user approval."""
|
|
148
|
+
|
|
149
|
+
_sticky_rubric: Annotated[NotRequired[str | None], PrivateStateAttr]
|
|
150
|
+
"""Persistent rubric owned by the TUI, distinct from graph input `rubric`."""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ResumeState(GoalRubricChannels):
|
|
154
|
+
"""Extends agent state with per-checkpoint facts restored on resume.
|
|
155
|
+
|
|
156
|
+
Inherits the shared goal/rubric channels from `GoalRubricChannels` and adds
|
|
157
|
+
the channels unique to resume: the after-model token/spec facts and the
|
|
158
|
+
pending-goal proposal awaiting acceptance.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
_context_tokens: Annotated[NotRequired[int], PrivateStateAttr]
|
|
162
|
+
"""Total context tokens reported by the model's last `usage_metadata`."""
|
|
163
|
+
|
|
164
|
+
_model_spec: Annotated[NotRequired[str], PrivateStateAttr]
|
|
165
|
+
"""`provider:model` spec effectively in use for the latest turn."""
|
|
166
|
+
|
|
167
|
+
_model_params: Annotated[NotRequired[dict[str, Any] | None], PrivateStateAttr]
|
|
168
|
+
"""Invocation params effectively in use for the latest turn."""
|
|
169
|
+
|
|
170
|
+
_pending_goal_objective: Annotated[NotRequired[str | None], PrivateStateAttr]
|
|
171
|
+
"""Goal objective awaiting acceptance of proposed criteria."""
|
|
172
|
+
|
|
173
|
+
_pending_goal_rubric: Annotated[NotRequired[str | None], PrivateStateAttr]
|
|
174
|
+
"""Proposed criteria awaiting user acceptance."""
|
|
175
|
+
|
|
176
|
+
_pending_goal_kind: Annotated[
|
|
177
|
+
NotRequired[GoalProposalKind | None], PrivateStateAttr
|
|
178
|
+
]
|
|
179
|
+
"""Whether the pending review creates or amends a goal."""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _extract_context_tokens(message: AIMessage) -> int | None:
|
|
183
|
+
"""Return the context-token count from an AI message, or `None` if absent.
|
|
184
|
+
|
|
185
|
+
Prefers `input_tokens + output_tokens` when both are reported; falls back
|
|
186
|
+
to `total_tokens` when the model only provides the aggregate.
|
|
187
|
+
"""
|
|
188
|
+
usage = getattr(message, "usage_metadata", None)
|
|
189
|
+
if not usage:
|
|
190
|
+
return None
|
|
191
|
+
input_toks = usage.get("input_tokens", 0) or 0
|
|
192
|
+
output_toks = usage.get("output_tokens", 0) or 0
|
|
193
|
+
if input_toks or output_toks:
|
|
194
|
+
return input_toks + output_toks
|
|
195
|
+
total = usage.get("total_tokens", 0) or 0
|
|
196
|
+
return total or None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ResumeStateMiddleware(AgentMiddleware[ResumeState, ContextT]):
|
|
200
|
+
"""Persists per-checkpoint resume facts after each model call.
|
|
201
|
+
|
|
202
|
+
See the module docstring for why this rides the model node's checkpoint
|
|
203
|
+
instead of a separate `aupdate_state` (avoids a standalone `UpdateState`
|
|
204
|
+
run in LangSmith and works identically against remote graphs).
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
state_schema = ResumeState
|
|
208
|
+
|
|
209
|
+
def after_model( # noqa: PLR6301 # AgentMiddleware hook must be an instance method.
|
|
210
|
+
self,
|
|
211
|
+
state: ResumeState,
|
|
212
|
+
runtime: Runtime[ContextT], # noqa: ARG002
|
|
213
|
+
) -> dict[str, Any] | None:
|
|
214
|
+
"""Write `_context_tokens` for the latest turn.
|
|
215
|
+
|
|
216
|
+
Model metadata is written by `ConfigurableModelMiddleware` from the
|
|
217
|
+
actual request that completed successfully; this hook only records token
|
|
218
|
+
usage from the most recent `AIMessage.usage_metadata`.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
state: Current agent state; only `messages` is inspected.
|
|
222
|
+
runtime: LangGraph runtime required by the middleware interface.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
State update with `_context_tokens`, or `None` when no token count is
|
|
226
|
+
available.
|
|
227
|
+
"""
|
|
228
|
+
update: dict[str, Any] = {}
|
|
229
|
+
|
|
230
|
+
for msg in reversed(state.get("messages") or []):
|
|
231
|
+
if isinstance(msg, AIMessage):
|
|
232
|
+
tokens = _extract_context_tokens(msg)
|
|
233
|
+
if tokens is not None:
|
|
234
|
+
update["_context_tokens"] = tokens
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
return update or None
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Server-side graph entry point for `langgraph dev`.
|
|
2
|
+
|
|
3
|
+
This module is referenced by the generated `langgraph.json` and exposes a graph
|
|
4
|
+
factory that the LangGraph server can load and serve.
|
|
5
|
+
|
|
6
|
+
The graph is created by `make_graph()`, which reads configuration from
|
|
7
|
+
`ServerConfig.from_env()` — the same dataclass the CLI uses to *write* the
|
|
8
|
+
configuration via `ServerConfig.to_env()`. This shared schema ensures the two
|
|
9
|
+
sides stay in sync.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import atexit
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
from deepagents_code._server_config import ServerConfig
|
|
21
|
+
from deepagents_code._startup_error import (
|
|
22
|
+
STARTUP_ERROR_MARKER as _STARTUP_ERROR_MARKER,
|
|
23
|
+
emit_startup_failure,
|
|
24
|
+
)
|
|
25
|
+
from deepagents_code.project_utils import ProjectContext, get_server_project_context
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Awaitable, Callable
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_sandbox_cm: Any = None
|
|
33
|
+
_sandbox_backend: Any = None
|
|
34
|
+
_mcp_session_manager: Any = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _print_startup_error(message: str) -> None:
|
|
38
|
+
"""Print a startup error for both humans and the parent app process.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
message: Concise startup failure to surface in the parent process.
|
|
42
|
+
"""
|
|
43
|
+
print(message, file=sys.stderr) # noqa: T201 # stderr fallback for logs
|
|
44
|
+
print( # noqa: T201 # machine-readable marker consumed by server.py
|
|
45
|
+
f"{_STARTUP_ERROR_MARKER}{message}",
|
|
46
|
+
file=sys.stderr,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_mcp_session_manager() -> Any: # noqa: ANN401
|
|
51
|
+
"""Return the process-wide MCP session manager singleton.
|
|
52
|
+
|
|
53
|
+
Sessions are bound to the langgraph dev server's event loop. Cleanup
|
|
54
|
+
therefore belongs to that loop's normal shutdown path, not `atexit` —
|
|
55
|
+
an atexit handler runs after the loop is already closed and cannot
|
|
56
|
+
await `AsyncExitStack.aclose()` safely. Subprocess handles held by
|
|
57
|
+
stdio transports are released when the Python process exits.
|
|
58
|
+
"""
|
|
59
|
+
global _mcp_session_manager # noqa: PLW0603
|
|
60
|
+
|
|
61
|
+
if _mcp_session_manager is None:
|
|
62
|
+
from deepagents_code.mcp_tools import MCPSessionManager
|
|
63
|
+
|
|
64
|
+
_mcp_session_manager = MCPSessionManager()
|
|
65
|
+
|
|
66
|
+
return _mcp_session_manager
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _build_tools(
|
|
70
|
+
config: ServerConfig,
|
|
71
|
+
project_context: ProjectContext | None,
|
|
72
|
+
) -> tuple[list[Any], list[Any] | None]:
|
|
73
|
+
"""Assemble the tool list based on server config.
|
|
74
|
+
|
|
75
|
+
Loads built-in tools (conditionally including web search when Tavily is
|
|
76
|
+
available) and MCP tools when enabled.
|
|
77
|
+
|
|
78
|
+
MCP discovery is awaited on the server's event loop: LangGraph invokes this
|
|
79
|
+
async factory on its running loop, so discovery must use `await` rather than
|
|
80
|
+
`asyncio.run` (which raises inside a running loop). `stateless=True` ensures
|
|
81
|
+
discovery only uses throwaway sessions, while the shared runtime session
|
|
82
|
+
manager binds real sessions lazily inside the server loop on first tool
|
|
83
|
+
invocation. MCP adapter imports are warmed in a worker thread inside
|
|
84
|
+
`_load_tools_from_config` (only when active servers exist) because first
|
|
85
|
+
import can perform blocking package-resource scans.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
config: Deserialized server configuration.
|
|
89
|
+
project_context: Resolved project context for MCP discovery.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Tuple of `(tools, mcp_server_info)`.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
FileNotFoundError: If the MCP config file is not found.
|
|
96
|
+
RuntimeError: If MCP tool loading fails.
|
|
97
|
+
"""
|
|
98
|
+
from deepagents_code.config import settings
|
|
99
|
+
from deepagents_code.tools import fetch_url, get_current_thread_id, web_search
|
|
100
|
+
|
|
101
|
+
tools: list[Any] = [fetch_url, get_current_thread_id]
|
|
102
|
+
if settings.has_tavily:
|
|
103
|
+
tools.append(web_search)
|
|
104
|
+
|
|
105
|
+
mcp_server_info: list[Any] | None = None
|
|
106
|
+
if not config.no_mcp:
|
|
107
|
+
from deepagents_code.mcp_tools import resolve_and_load_mcp_tools
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
mcp_tools, _, mcp_server_info = await resolve_and_load_mcp_tools(
|
|
111
|
+
explicit_config_path=config.mcp_config_path,
|
|
112
|
+
no_mcp=config.no_mcp,
|
|
113
|
+
trust_project_mcp=config.trust_project_mcp,
|
|
114
|
+
project_context=project_context,
|
|
115
|
+
stateless=True,
|
|
116
|
+
session_manager=_get_mcp_session_manager(),
|
|
117
|
+
)
|
|
118
|
+
except FileNotFoundError:
|
|
119
|
+
logger.exception("MCP config file not found: %s", config.mcp_config_path)
|
|
120
|
+
raise
|
|
121
|
+
except RuntimeError:
|
|
122
|
+
logger.exception(
|
|
123
|
+
"Failed to load MCP tools (config: %s)", config.mcp_config_path
|
|
124
|
+
)
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
tools.extend(mcp_tools)
|
|
128
|
+
if mcp_tools:
|
|
129
|
+
logger.info("Loaded %d MCP tool(s)", len(mcp_tools))
|
|
130
|
+
|
|
131
|
+
return tools, mcp_server_info
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def _make_graph() -> Any: # noqa: ANN401
|
|
135
|
+
"""Create the agent graph from environment-based configuration.
|
|
136
|
+
|
|
137
|
+
Reads `DEEPAGENTS_CODE_SERVER_*` env vars via `ServerConfig.from_env()`
|
|
138
|
+
(the inverse of `ServerConfig.to_env()` used by the app process), resolves a
|
|
139
|
+
model, assembles tools, and compiles the agent graph.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Compiled LangGraph agent graph.
|
|
143
|
+
"""
|
|
144
|
+
config = ServerConfig.from_env()
|
|
145
|
+
project_context = get_server_project_context()
|
|
146
|
+
|
|
147
|
+
from deepagents_code.agent import create_cli_agent, load_async_subagents
|
|
148
|
+
from deepagents_code.config import (
|
|
149
|
+
configure_langsmith_secret_redaction,
|
|
150
|
+
create_model,
|
|
151
|
+
is_memory_auto_save_enabled,
|
|
152
|
+
settings,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if project_context is not None:
|
|
156
|
+
settings.reload_from_environment(start_path=project_context.user_cwd)
|
|
157
|
+
configure_langsmith_secret_redaction()
|
|
158
|
+
|
|
159
|
+
# Offload to a worker thread: `create_model` does blocking disk IO for some
|
|
160
|
+
# providers (e.g. the `openai_codex` token store currently acquires a file
|
|
161
|
+
# lock via `langchain-openai` that calls `os.mkdir`), which `blockbuster`
|
|
162
|
+
# rejects on the server event loop.
|
|
163
|
+
result = await asyncio.to_thread(
|
|
164
|
+
create_model, config.model, extra_kwargs=config.model_params
|
|
165
|
+
)
|
|
166
|
+
result.apply_to_settings()
|
|
167
|
+
|
|
168
|
+
tools, mcp_server_info = await _build_tools(config, project_context)
|
|
169
|
+
|
|
170
|
+
# Create sandbox backend if a sandbox provider is configured.
|
|
171
|
+
# The context manager is created here in the factory, but its reference is
|
|
172
|
+
# stored in a module-level global (and cleaned up via atexit) so the sandbox
|
|
173
|
+
# lives for the entire server process lifetime. `make_graph` caches the built
|
|
174
|
+
# graph, so this runs once per process despite LangGraph's per-run factory
|
|
175
|
+
# invocation.
|
|
176
|
+
global _sandbox_cm, _sandbox_backend # noqa: PLW0603
|
|
177
|
+
sandbox_backend = None
|
|
178
|
+
if config.sandbox_type:
|
|
179
|
+
from deepagents_code.integrations.sandbox_factory import create_sandbox
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
_sandbox_cm = create_sandbox(
|
|
183
|
+
config.sandbox_type,
|
|
184
|
+
sandbox_id=config.sandbox_id,
|
|
185
|
+
snapshot_name=config.sandbox_snapshot_name,
|
|
186
|
+
setup_script_path=config.sandbox_setup,
|
|
187
|
+
)
|
|
188
|
+
_sandbox_backend = _sandbox_cm.__enter__() # noqa: PLC2801 # Context manager kept open for server process lifetime
|
|
189
|
+
sandbox_backend = _sandbox_backend
|
|
190
|
+
|
|
191
|
+
def _cleanup_sandbox() -> None:
|
|
192
|
+
if _sandbox_cm is not None:
|
|
193
|
+
_sandbox_cm.__exit__(None, None, None)
|
|
194
|
+
|
|
195
|
+
atexit.register(_cleanup_sandbox)
|
|
196
|
+
except ImportError:
|
|
197
|
+
logger.exception(
|
|
198
|
+
"Sandbox provider '%s' is not installed", config.sandbox_type
|
|
199
|
+
)
|
|
200
|
+
_print_startup_error(
|
|
201
|
+
f"Sandbox provider '{config.sandbox_type}' is not installed"
|
|
202
|
+
)
|
|
203
|
+
sys.exit(1)
|
|
204
|
+
except NotImplementedError:
|
|
205
|
+
logger.exception("Sandbox type '%s' is not supported", config.sandbox_type)
|
|
206
|
+
_print_startup_error(
|
|
207
|
+
f"Sandbox type '{config.sandbox_type}' is not supported"
|
|
208
|
+
)
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
except ValueError as exc:
|
|
211
|
+
logger.exception(
|
|
212
|
+
"Invalid sandbox configuration for '%s'", config.sandbox_type
|
|
213
|
+
)
|
|
214
|
+
_print_startup_error(f"Invalid sandbox configuration: {exc}")
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
logger.exception("Sandbox creation failed for '%s'", config.sandbox_type)
|
|
218
|
+
_print_startup_error(
|
|
219
|
+
f"Sandbox creation failed for '{config.sandbox_type}': {exc}"
|
|
220
|
+
)
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
|
|
223
|
+
def _create_cli_agent_sync() -> Any: # noqa: ANN401
|
|
224
|
+
async_subagents = load_async_subagents() or None
|
|
225
|
+
|
|
226
|
+
# These process-global settings writes are safe here because `make_graph`
|
|
227
|
+
# is lock-serialized and caches one graph for the server process lifetime.
|
|
228
|
+
if config.interpreter_ptc is not None:
|
|
229
|
+
settings.interpreter_ptc = config.interpreter_ptc
|
|
230
|
+
if config.interpreter_ptc_acknowledge_unsafe:
|
|
231
|
+
settings.interpreter_ptc_acknowledge_unsafe = True
|
|
232
|
+
if config.enable_interpreter:
|
|
233
|
+
settings.enable_interpreter = True
|
|
234
|
+
|
|
235
|
+
agent, _ = create_cli_agent(
|
|
236
|
+
model=result.model,
|
|
237
|
+
assistant_id=config.assistant_id,
|
|
238
|
+
tools=tools,
|
|
239
|
+
sandbox=sandbox_backend,
|
|
240
|
+
sandbox_type=config.sandbox_type,
|
|
241
|
+
system_prompt=config.system_prompt,
|
|
242
|
+
interactive=config.interactive,
|
|
243
|
+
auto_approve=config.auto_approve,
|
|
244
|
+
interrupt_shell_only=config.interrupt_shell_only,
|
|
245
|
+
shell_allow_list=config.shell_allow_list,
|
|
246
|
+
enable_ask_user=config.enable_ask_user,
|
|
247
|
+
enable_memory=config.enable_memory,
|
|
248
|
+
memory_auto_save=is_memory_auto_save_enabled(),
|
|
249
|
+
enable_skills=config.enable_skills,
|
|
250
|
+
enable_shell=config.enable_shell,
|
|
251
|
+
enable_interpreter=config.enable_interpreter,
|
|
252
|
+
rubric_model=config.rubric_model,
|
|
253
|
+
rubric_max_iterations=config.rubric_max_iterations,
|
|
254
|
+
mcp_server_info=mcp_server_info,
|
|
255
|
+
cwd=project_context.user_cwd if project_context is not None else config.cwd,
|
|
256
|
+
project_context=project_context,
|
|
257
|
+
async_subagents=async_subagents,
|
|
258
|
+
)
|
|
259
|
+
return agent
|
|
260
|
+
|
|
261
|
+
return await asyncio.to_thread(_create_cli_agent_sync)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _build_graph_factory() -> Callable[[], Awaitable[Any]]:
|
|
265
|
+
"""Build the cached async graph factory exposed to `langgraph dev`.
|
|
266
|
+
|
|
267
|
+
The returned coroutine function is what `langgraph.json` references. It keeps
|
|
268
|
+
its cache and lock in this closure rather than in module-level globals, so
|
|
269
|
+
importing the module (e.g. for import-only checks) introduces no shared
|
|
270
|
+
mutable state.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
A zero-arg async factory that builds the graph once and returns the
|
|
274
|
+
cached instance on every subsequent call.
|
|
275
|
+
"""
|
|
276
|
+
missing = object()
|
|
277
|
+
graph: Any = missing
|
|
278
|
+
lock = asyncio.Lock()
|
|
279
|
+
|
|
280
|
+
async def make_graph() -> Any: # noqa: ANN401
|
|
281
|
+
"""Create (or return the cached) agent graph for `langgraph dev`.
|
|
282
|
+
|
|
283
|
+
LangGraph loads this async factory from the generated `langgraph.json`
|
|
284
|
+
and invokes it lazily on its event loop — and again on every run. The
|
|
285
|
+
built graph is cached for the process lifetime so MCP discovery, sandbox
|
|
286
|
+
creation, and `atexit` registration each happen exactly once; re-running
|
|
287
|
+
them per request would re-discover MCP servers, leak sandbox sessions,
|
|
288
|
+
and stack duplicate `atexit` handlers. Any construction failure is
|
|
289
|
+
converted into a startup-error marker (scraped by the parent app
|
|
290
|
+
process) before exiting.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Compiled LangGraph agent graph.
|
|
294
|
+
"""
|
|
295
|
+
nonlocal graph
|
|
296
|
+
if graph is not missing:
|
|
297
|
+
return graph
|
|
298
|
+
async with lock:
|
|
299
|
+
if graph is missing:
|
|
300
|
+
try:
|
|
301
|
+
graph = await _make_graph()
|
|
302
|
+
except Exception as exc: # noqa: BLE001 # top-level barrier: any construction failure must surface to the parent as a marker
|
|
303
|
+
emit_startup_failure(exc)
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
return graph
|
|
306
|
+
|
|
307
|
+
return make_graph
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
make_graph = _build_graph_factory()
|