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.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. 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()