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,235 @@
1
+ """Lightweight session statistics, token formatting, and usage-table rendering.
2
+
3
+ Holds `SessionStats`/`ModelStats`, the `format_token_count` formatter, and
4
+ `print_usage_table` (which imports `rich.table` lazily). The module is
5
+ intentionally kept free of heavy top-level dependencies (no pydantic, no
6
+ config, no widget imports) so that `app.py` can import `SessionStats` and
7
+ `format_token_count` at module level without pulling in the full
8
+ `textual_adapter` dependency tree.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from typing import TYPE_CHECKING, Literal
15
+
16
+ from deepagents_code.formatting import format_duration
17
+
18
+ if TYPE_CHECKING:
19
+ from rich.console import Console
20
+
21
+ SpinnerStatus = (
22
+ Literal[
23
+ "Thinking",
24
+ "Offloading",
25
+ "Loading thread",
26
+ "Drafting acceptance criteria",
27
+ ]
28
+ | None
29
+ )
30
+ """Valid spinner display states, or `None` to hide."""
31
+
32
+
33
+ @dataclass
34
+ class ModelStats:
35
+ """Token stats for a single model within a session."""
36
+
37
+ request_count: int = 0
38
+ """Number of LLM API requests made to this model."""
39
+
40
+ input_tokens: int = 0
41
+ """Cumulative input tokens sent to this model."""
42
+
43
+ output_tokens: int = 0
44
+ """Cumulative output tokens received from this model."""
45
+
46
+ provider: str = ""
47
+ """Provider that served this model (e.g. `openai`), or `""` when unknown."""
48
+
49
+ model_name: str = ""
50
+ """Model name displayed in usage output."""
51
+
52
+
53
+ ModelStatsKey = tuple[str, str]
54
+ """Per-model dict key: the `(provider, model_name)` pair.
55
+
56
+ Pairing the provider with the model name keeps the same model served by
57
+ different providers (e.g. `gpt-5.5` via `openai` vs `azure`) in separate rows
58
+ instead of collapsing them. The key is always built from the same values stored
59
+ on the corresponding `ModelStats`, so key and fields never diverge.
60
+ """
61
+
62
+
63
+ @dataclass
64
+ class SessionStats:
65
+ """Stats accumulated over a single agent turn (or full session)."""
66
+
67
+ request_count: int = 0
68
+ """Total LLM API requests made.
69
+
70
+ Each chunk with `usage_metadata` counts as one completed request.
71
+ """
72
+
73
+ input_tokens: int = 0
74
+ """Cumulative input tokens across all LLM requests."""
75
+
76
+ output_tokens: int = 0
77
+ """Cumulative output tokens across all LLM requests."""
78
+
79
+ wall_time_seconds: float = 0.0
80
+ """Wall-clock duration from stream start to end."""
81
+
82
+ per_model: dict[ModelStatsKey, ModelStats] = field(default_factory=dict)
83
+ """Per-model breakdown keyed by `(provider, model_name)`.
84
+
85
+ Populated only when `record_request` receives a non-empty `model_name`. Empty
86
+ dict means no named-model requests were recorded; `print_usage_table` omits
87
+ the model table in that case and shows only the wall-time line (if applicable).
88
+ """
89
+
90
+ def record_request(
91
+ self,
92
+ model_name: str,
93
+ input_toks: int,
94
+ output_toks: int,
95
+ provider: str = "",
96
+ ) -> None:
97
+ """Accumulate token counts for one completed LLM request.
98
+
99
+ Updates both the session totals and the per-model breakdown.
100
+
101
+ Args:
102
+ model_name: The model that served this request. Combined with
103
+ `provider` to form the per-model key. Pass an empty string to
104
+ skip the per-model breakdown for this request.
105
+ input_toks: Input tokens for this request.
106
+ output_toks: Output tokens for this request.
107
+ provider: Provider that served the model (e.g. `openai`). Combined
108
+ with `model_name` to form the per-model key, so the same model
109
+ served by different providers is tracked separately.
110
+ """
111
+ self.request_count += 1
112
+ self.input_tokens += input_toks
113
+ self.output_tokens += output_toks
114
+ if model_name:
115
+ key = (provider, model_name)
116
+ entry = self.per_model.setdefault(
117
+ key,
118
+ ModelStats(provider=provider, model_name=model_name),
119
+ )
120
+ entry.request_count += 1
121
+ entry.input_tokens += input_toks
122
+ entry.output_tokens += output_toks
123
+
124
+ def merge(self, other: SessionStats) -> None:
125
+ """Merge another `SessionStats` into this one (mutates *self*).
126
+
127
+ Used to accumulate per-turn stats into a session-level total.
128
+
129
+ Args:
130
+ other: The stats to fold in.
131
+ """
132
+ self.request_count += other.request_count
133
+ self.input_tokens += other.input_tokens
134
+ self.output_tokens += other.output_tokens
135
+ self.wall_time_seconds += other.wall_time_seconds
136
+ for key, ms in other.per_model.items():
137
+ entry = self.per_model.setdefault(
138
+ key,
139
+ ModelStats(provider=ms.provider, model_name=ms.model_name),
140
+ )
141
+ entry.request_count += ms.request_count
142
+ entry.input_tokens += ms.input_tokens
143
+ entry.output_tokens += ms.output_tokens
144
+
145
+
146
+ def format_token_count(count: int) -> str:
147
+ """Format a token count into a human-readable short string.
148
+
149
+ Args:
150
+ count: Number of tokens.
151
+
152
+ Returns:
153
+ Formatted string like `'12.5K'`, `'1.2M'`, or `'500'`.
154
+ """
155
+ if count >= 1_000_000: # noqa: PLR2004
156
+ return f"{count / 1_000_000:.1f}M"
157
+ if count >= 1000: # noqa: PLR2004
158
+ return f"{count / 1000:.1f}K"
159
+ return str(count)
160
+
161
+
162
+ def print_usage_table(
163
+ stats: SessionStats,
164
+ wall_time: float,
165
+ console: Console,
166
+ ) -> None:
167
+ """Print a model-usage stats table to a Rich console.
168
+
169
+ Each row shows the serving provider alongside the model name. When the
170
+ session spans multiple models each gets its own row with a totals row
171
+ appended; single-model sessions show one row.
172
+
173
+ Args:
174
+ stats: Cumulative session stats.
175
+ wall_time: Total wall-clock time in seconds.
176
+ console: Rich console for output.
177
+ """
178
+ from rich.table import Table
179
+
180
+ has_time = wall_time >= 0.1 # noqa: PLR2004
181
+ if not (stats.request_count or stats.input_tokens or has_time):
182
+ return
183
+
184
+ if stats.per_model:
185
+ multi_model = len(stats.per_model) > 1
186
+
187
+ table = Table(
188
+ show_header=True,
189
+ header_style="bold",
190
+ box=None,
191
+ padding=(0, 2, 0, 0),
192
+ show_edge=False,
193
+ )
194
+ table.add_column("Provider", style="dim")
195
+ table.add_column("Model", style="dim")
196
+ table.add_column("Reqs", justify="right", style="dim")
197
+ table.add_column("InputTok", justify="right", style="dim")
198
+ table.add_column("OutputTok", justify="right", style="dim")
199
+
200
+ if multi_model:
201
+ for ms in stats.per_model.values():
202
+ table.add_row(
203
+ ms.provider,
204
+ ms.model_name,
205
+ str(ms.request_count),
206
+ format_token_count(ms.input_tokens),
207
+ format_token_count(ms.output_tokens),
208
+ )
209
+ table.add_row(
210
+ "",
211
+ "Total",
212
+ str(stats.request_count),
213
+ format_token_count(stats.input_tokens),
214
+ format_token_count(stats.output_tokens),
215
+ )
216
+ else:
217
+ ms = next(iter(stats.per_model.values()))
218
+ table.add_row(
219
+ ms.provider,
220
+ ms.model_name,
221
+ str(stats.request_count),
222
+ format_token_count(stats.input_tokens),
223
+ format_token_count(stats.output_tokens),
224
+ )
225
+
226
+ console.print()
227
+ console.print("[bold]Usage Stats[/bold]")
228
+ console.print(table)
229
+ if has_time:
230
+ console.print()
231
+ console.print(
232
+ f"Agent active {format_duration(wall_time)}",
233
+ style="dim",
234
+ highlight=False,
235
+ )
@@ -0,0 +1,45 @@
1
+ """Stderr marker emission used by the langgraph server graph entry point.
2
+
3
+ Lives in its own module so unit tests can exercise the marker contract
4
+ without triggering `server_graph.make_graph()` at import time.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import sys
11
+ import traceback
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ STARTUP_ERROR_MARKER = "DEEPAGENTS_STARTUP_ERROR:"
16
+ """Stderr marker the parent app scans for in `server._extract_startup_error_marker`
17
+ to upgrade an opaque "Server process exited with code N" into a one-line summary.
18
+ Format is `{STARTUP_ERROR_MARKER}{single-line message}`."""
19
+
20
+
21
+ def emit_startup_failure(exc: BaseException) -> None:
22
+ """Report a server graph startup failure to the parent app process.
23
+
24
+ Emits two stderr outputs: the full traceback for logs/debugging, then a
25
+ single-line `{STARTUP_ERROR_MARKER}{type}: {summary}` line that
26
+ `server._extract_startup_error_marker` parses to upgrade an opaque
27
+ "Server process exited with code N" into an actionable summary.
28
+
29
+ Args:
30
+ exc: The exception raised during graph initialization.
31
+ """
32
+ logger.critical("Failed to initialize server graph", exc_info=exc)
33
+ print( # noqa: T201 # stderr fallback — logger may not reach parent process
34
+ f"Failed to initialize server graph: {exc}\n{traceback.format_exc()}",
35
+ file=sys.stderr,
36
+ )
37
+ # Marker contract is single-line; guard against multi-line/empty `str(exc)`
38
+ # and include the type so e.g. `ValueError` and `RuntimeError` are
39
+ # distinguishable in the parent's truncated summary.
40
+ exc_lines = str(exc).splitlines()
41
+ summary = exc_lines[0] if exc_lines else "<no message>"
42
+ print( # noqa: T201
43
+ f"{STARTUP_ERROR_MARKER}{type(exc).__name__}: {summary}",
44
+ file=sys.stderr,
45
+ )
@@ -0,0 +1,305 @@
1
+ """Internal fake chat models for local integration tests.
2
+
3
+ The tool-binding base these build on (`_fake_models._ToolBindingFakeModel`) is
4
+ factored out into a use-neutral module so the `dcode tools list` enumeration
5
+ path can reuse it without importing this test-named module.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from langchain_core.messages import AIMessage, BaseMessage
13
+ from langchain_core.outputs import ChatGeneration, ChatResult
14
+
15
+ from deepagents_code._fake_models import _ToolBindingFakeModel
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+
20
+ from langchain_core.callbacks import CallbackManagerForLLMRun
21
+
22
+
23
+ # Prompt markers that drive `ToolCallingIntegrationChatModel`. Each marker is the
24
+ # full token (including the trailing `=`); the file path follows on the same line,
25
+ # e.g. `DCA_TEST_WRITE_FILE=/tmp/out.txt`. These are the single source of truth
26
+ # shared with the integration tests, so the model and tests cannot drift apart.
27
+ DCA_TEST_WRITE_FILE_MARKER = "DCA_TEST_WRITE_FILE="
28
+ DCA_TEST_DELEGATE_WRITE_MARKER = "DCA_TEST_DELEGATE_WRITE="
29
+ DCA_SUBAGENT_WRITE_FILE_MARKER = "DCA_SUBAGENT_WRITE_FILE="
30
+
31
+ # Distinct file contents per write path, so a test asserting on file content can
32
+ # prove which branch executed — in particular that subagent mode delegated through
33
+ # the `task` tool rather than writing directly.
34
+ TOP_LEVEL_WRITE_CONTENT = "auto-approved"
35
+ SUBAGENT_WRITE_CONTENT = "auto-approved-subagent"
36
+
37
+
38
+ class DeterministicIntegrationChatModel(_ToolBindingFakeModel):
39
+ """Deterministic chat model for integration tests.
40
+
41
+ This subclasses `_ToolBindingFakeModel` (itself a `GenericFakeChatModel`) so
42
+ the implementation stays aligned with the core fake-chat-model test surface,
43
+ while overriding generation to remain prompt-driven and restart-safe for real
44
+ CLI server integration tests.
45
+
46
+ Why the existing `langchain_core` fakes cannot be reused here:
47
+
48
+ 1. Every core fake (`GenericFakeChatModel`, `FakeListChatModel`,
49
+ `FakeMessagesListChatModel`) pops from an iterator or cycles an index —
50
+ the actual prompt is ignored. App integration tests start and stop the
51
+ server process, which resets in-memory state. An iterator-based model
52
+ either raises `StopIteration` or replays from the beginning after a
53
+ restart, producing wrong or missing responses. This model derives output
54
+ solely from the prompt text, so identical input always produces
55
+ identical output regardless of process lifecycle.
56
+
57
+ 2. The agent runtime calls `model.bind_tools(schemas)` during
58
+ initialization. A bare `GenericFakeChatModel` inherits
59
+ `BaseChatModel.bind_tools`, which raises `NotImplementedError` in any
60
+ agent-loop context. The inherited `_ToolBindingFakeModel` supplies a
61
+ no-op passthrough.
62
+
63
+ 3. The app server reads `model.profile` for capability negotiation (e.g.
64
+ `tool_calling`, `max_input_tokens`). A bare fake's `profile` is `None`,
65
+ causing silent misconfiguration at runtime. The inherited
66
+ `_ToolBindingFakeModel` supplies a minimal profile.
67
+
68
+ Additionally, the compact middleware issues summarization prompts mid-
69
+ conversation. A list-based model cannot distinguish these from normal user
70
+ turns without pre-knowledge of exact call ordering, whereas this model
71
+ detects summary requests by inspecting the prompt content.
72
+ """
73
+
74
+ model: str = "fake"
75
+ # `messages`, `profile`, and the `bind_tools` passthrough are inherited from
76
+ # `_ToolBindingFakeModel`; this model adds only prompt-driven generation.
77
+
78
+ def _generate(
79
+ self,
80
+ messages: list[BaseMessage],
81
+ stop: list[str] | None = None, # noqa: ARG002
82
+ run_manager: CallbackManagerForLLMRun | None = None, # noqa: ARG002
83
+ **kwargs: Any, # noqa: ARG002
84
+ ) -> ChatResult:
85
+ """Produce a deterministic reply derived from the prompt text.
86
+
87
+ Returns:
88
+ A single-message `ChatResult` with deterministic content.
89
+ """
90
+ prompt = "\n".join(
91
+ text
92
+ for message in messages
93
+ if (text := self._stringify_message(message)).strip()
94
+ )
95
+ if self._looks_like_summary_request(prompt):
96
+ content = "integration summary"
97
+ else:
98
+ excerpt = " ".join(prompt.split()[-18:])
99
+ if excerpt:
100
+ content = f"integration reply: {excerpt}"
101
+ else:
102
+ content = "integration reply"
103
+
104
+ return ChatResult(
105
+ generations=[ChatGeneration(message=AIMessage(content=content))]
106
+ )
107
+
108
+ @property
109
+ def _llm_type(self) -> str:
110
+ """LangChain model type identifier."""
111
+ return "deterministic-integration"
112
+
113
+ @staticmethod
114
+ def _stringify_message(message: BaseMessage) -> str:
115
+ """Flatten message content into plain text for deterministic responses.
116
+
117
+ Returns:
118
+ Plain-text content extracted from the message.
119
+ """
120
+ content = message.content
121
+ if isinstance(content, str):
122
+ return content
123
+ if isinstance(content, list):
124
+ parts: list[str] = []
125
+ for block in content:
126
+ if isinstance(block, str):
127
+ parts.append(block)
128
+ elif isinstance(block, dict) and block.get("type") == "text":
129
+ text = block.get("text")
130
+ if isinstance(text, str):
131
+ parts.append(text)
132
+ return " ".join(parts)
133
+ return str(content)
134
+
135
+ @staticmethod
136
+ def _looks_like_summary_request(prompt: str) -> bool:
137
+ """Detect the middleware's summary-generation prompt.
138
+
139
+ Returns:
140
+ `True` when the prompt appears to be a summarization request.
141
+ """
142
+ lowered = prompt.lower()
143
+ return (
144
+ "messages to summarize" in lowered
145
+ or "condense the following conversation" in lowered
146
+ or "<summary>" in lowered
147
+ )
148
+
149
+
150
+ def _extract_marker_path(prompt: str, marker: str) -> str:
151
+ """Extract the file path that follows a prompt marker on the same line.
152
+
153
+ Args:
154
+ prompt: The flattened prompt text.
155
+ marker: The marker token (including its trailing `=`) to locate.
156
+
157
+ Returns:
158
+ The stripped file path immediately following the marker.
159
+
160
+ Raises:
161
+ ValueError: If the marker is present but not followed by a path, so a
162
+ malformed test prompt fails loudly here instead of silently
163
+ degrading to a `"done"` reply or raising an opaque `IndexError`.
164
+ """
165
+ _, _, tail = prompt.partition(marker)
166
+ lines = tail.splitlines()
167
+ file_path = lines[0].strip() if lines else ""
168
+ if not file_path:
169
+ msg = (
170
+ f"Test model saw marker {marker!r} but found no file path after it; "
171
+ f"check the integration-test prompt construction."
172
+ )
173
+ raise ValueError(msg)
174
+ return file_path
175
+
176
+
177
+ def _tool_call_result(name: str, args: dict[str, Any], call_id: str) -> ChatResult:
178
+ """Build a single-tool-call `ChatResult`.
179
+
180
+ Returns:
181
+ A `ChatResult` wrapping an `AIMessage` with exactly one tool call.
182
+ """
183
+ return ChatResult(
184
+ generations=[
185
+ ChatGeneration(
186
+ message=AIMessage(
187
+ content="",
188
+ tool_calls=[
189
+ {
190
+ "name": name,
191
+ "args": args,
192
+ "id": call_id,
193
+ "type": "tool_call",
194
+ }
195
+ ],
196
+ )
197
+ )
198
+ ]
199
+ )
200
+
201
+
202
+ class ToolCallingIntegrationChatModel(DeterministicIntegrationChatModel):
203
+ """Deterministic tool-calling model for auto-approve integration tests.
204
+
205
+ Generation is driven entirely by prompt markers (the module-level `DCA_*`
206
+ constants), so output is restart-safe and independent of call ordering — the
207
+ same rationale as the parent `DeterministicIntegrationChatModel`:
208
+
209
+ - `DCA_TEST_WRITE_FILE=<path>` emits a top-level `write_file` call.
210
+ - `DCA_TEST_DELEGATE_WRITE=<path>` emits a `task` call delegating to the
211
+ `general-purpose` subagent, whose prompt then carries
212
+ `DCA_SUBAGENT_WRITE_FILE=<path>` to trigger the subagent's `write_file`.
213
+ - `DCA_SUBAGENT_WRITE_FILE=<path>` emits the subagent's `write_file` call.
214
+
215
+ Each marker fires only on the agent's first turn (no prior `ToolMessage`),
216
+ so once the tool result returns the model replies with a plain `"done"` and
217
+ the agent loop terminates instead of re-issuing the tool call.
218
+ """
219
+
220
+ # Only `_generate` is overridden; the inherited `_stream` would bypass this
221
+ # marker dispatch entirely, so streaming must be disabled.
222
+ disable_streaming: bool = True
223
+
224
+ def _generate(
225
+ self,
226
+ messages: list[BaseMessage],
227
+ stop: list[str] | None = None, # noqa: ARG002
228
+ run_manager: CallbackManagerForLLMRun | None = None, # noqa: ARG002
229
+ **kwargs: Any, # noqa: ARG002
230
+ ) -> ChatResult:
231
+ """Emit a deterministic tool call (or terminal reply) from prompt markers.
232
+
233
+ The `has_tool_result` guard ensures each marker fires only on the
234
+ agent's first turn; once a `ToolMessage` is present the model returns
235
+ `"done"` so the agent loop terminates.
236
+
237
+ A recognized marker with no file path raises `ValueError` (via
238
+ `_extract_marker_path`) rather than degrading to `"done"`.
239
+
240
+ Returns:
241
+ A single-message `ChatResult`: an `AIMessage` carrying a `task` or
242
+ `write_file` tool call when a marker matches and no tool result is
243
+ present yet, otherwise a plain `"done"` reply.
244
+ """
245
+ prompt = "\n".join(
246
+ text
247
+ for message in messages
248
+ if (text := self._stringify_message(message)).strip()
249
+ )
250
+ has_tool_result = any(message.type == "tool" for message in messages)
251
+ if not has_tool_result:
252
+ for marker, build_tool_call in self._marker_dispatch():
253
+ if marker in prompt:
254
+ name, args, call_id = build_tool_call(
255
+ _extract_marker_path(prompt, marker)
256
+ )
257
+ return _tool_call_result(name, args, call_id)
258
+
259
+ return ChatResult(
260
+ generations=[ChatGeneration(message=AIMessage(content="done"))]
261
+ )
262
+
263
+ @staticmethod
264
+ def _marker_dispatch() -> tuple[
265
+ tuple[str, Callable[[str], tuple[str, dict[str, Any], str]]], ...
266
+ ]:
267
+ """Return ordered `(marker, tool-call builder)` pairs.
268
+
269
+ The delegate marker is checked before the plain write markers because
270
+ its emitted `task` description embeds `DCA_SUBAGENT_WRITE_FILE=`; the
271
+ first marker found in the prompt wins.
272
+
273
+ Returns:
274
+ Marker-to-builder pairs in precedence order. Each builder maps an
275
+ extracted file path to a `(tool_name, args, call_id)` triple.
276
+ """
277
+ return (
278
+ (
279
+ DCA_TEST_DELEGATE_WRITE_MARKER,
280
+ lambda path: (
281
+ "task",
282
+ {
283
+ "description": f"{DCA_SUBAGENT_WRITE_FILE_MARKER}{path}",
284
+ "subagent_type": "general-purpose",
285
+ },
286
+ "call_task",
287
+ ),
288
+ ),
289
+ (
290
+ DCA_SUBAGENT_WRITE_FILE_MARKER,
291
+ lambda path: (
292
+ "write_file",
293
+ {"file_path": path, "content": SUBAGENT_WRITE_CONTENT},
294
+ "call_write_file",
295
+ ),
296
+ ),
297
+ (
298
+ DCA_TEST_WRITE_FILE_MARKER,
299
+ lambda path: (
300
+ "write_file",
301
+ {"file_path": path, "content": TOP_LEVEL_WRITE_CONTENT},
302
+ "call_write_file",
303
+ ),
304
+ ),
305
+ )