devtorch-core 3.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 (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,487 @@
1
+ """
2
+ devtorch_core.wrapper.anthropic
3
+ ================================
4
+ Drop-in replacement for ``anthropic.Anthropic`` that:
5
+
6
+ 1. Injects the RACP system-prompt prefix into every ``messages.create`` call.
7
+ 2. Captures reasoning / RACP blocks from the response to the .GCC/ store.
8
+ 3. Returns clean (block-stripped) text to callers.
9
+ 4. Degrades gracefully if anthropic is not installed or .GCC/ is missing.
10
+
11
+ Usage
12
+ -----
13
+ ::
14
+
15
+ from devtorch_core.wrapper.anthropic import DevTorchAnthropic as Anthropic
16
+
17
+ client = Anthropic() # same constructor signature as anthropic.Anthropic
18
+ response = client.messages.create(
19
+ model="claude-opus-4-5",
20
+ max_tokens=1024,
21
+ messages=[{"role": "user", "content": "Hello"}],
22
+ )
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ import uuid
28
+ from typing import Any, Iterator
29
+
30
+ from .base import CaptureOrchestrator, RACPInjector, _reasoning_text, find_gcc_repo, is_disabled
31
+
32
+ logger = logging.getLogger("devtorch.wrapper.anthropic")
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Optional import — degrade gracefully if anthropic is not installed
36
+ # ---------------------------------------------------------------------------
37
+
38
+ try:
39
+ import anthropic as _anthropic
40
+
41
+ HAS_ANTHROPIC = True
42
+ except ImportError:
43
+ HAS_ANTHROPIC = False
44
+ _anthropic = None # type: ignore[assignment]
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Wrapper
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ class DevTorchAnthropic:
53
+ """
54
+ Drop-in replacement for ``anthropic.Anthropic``.
55
+
56
+ Adds RACP injection and .GCC/ capture without altering the public API
57
+ visible to callers. All DevTorch errors degrade to warnings.
58
+ """
59
+
60
+ def __init__(self, *args: Any, gcc_repo: Any = None, **kwargs: Any) -> None:
61
+ if not HAS_ANTHROPIC:
62
+ raise ImportError(
63
+ "anthropic package not installed. "
64
+ "pip install devtorch[wrapper]"
65
+ )
66
+ self._client = _anthropic.Anthropic(*args, **kwargs) # type: ignore[union-attr]
67
+ self._gcc = gcc_repo or find_gcc_repo()
68
+ self._injector = RACPInjector(self._gcc) if self._gcc else None
69
+ self._capture = CaptureOrchestrator(self._gcc) if self._gcc else None
70
+ self.messages = self._Messages(self)
71
+
72
+ # Proxy attribute access to the underlying client for anything we
73
+ # don't explicitly intercept (e.g. .beta, .models, etc.)
74
+ def __getattr__(self, name: str) -> Any:
75
+ return getattr(self._client, name)
76
+
77
+ # ------------------------------------------------------------------
78
+ # Inner class mirrors anthropic.Anthropic.messages
79
+ # ------------------------------------------------------------------
80
+
81
+ class _Messages:
82
+ def __init__(self, parent: "DevTorchAnthropic") -> None:
83
+ self._p = parent
84
+
85
+ # --------------------------------------------------------------
86
+ # create — main intercept point
87
+ # --------------------------------------------------------------
88
+
89
+ def create(
90
+ self,
91
+ *,
92
+ messages: list,
93
+ system: str = "",
94
+ stream: bool = False,
95
+ **kwargs: Any,
96
+ ) -> Any:
97
+ """
98
+ Inject RACP prefix into *system*, call the real API, capture
99
+ response.
100
+
101
+ If ``stream=True`` returns a streaming wrapper that passes chunks
102
+ through transparently and captures the accumulated response after
103
+ the stream closes.
104
+
105
+ DevTorch errors never propagate to the caller.
106
+ """
107
+ session_id = str(uuid.uuid4())[:8]
108
+ call_id = str(uuid.uuid4())
109
+ prompt_text = _extract_prompt_text(messages)
110
+ model_name = kwargs.get("model", "")
111
+ injected_system = self._inject(system, prompt_text, session_id, call_id, model_name)
112
+
113
+ if stream:
114
+ return self._stream_create(
115
+ messages=messages,
116
+ system=injected_system,
117
+ session_id=session_id,
118
+ call_id=call_id,
119
+ prompt_text=prompt_text,
120
+ model_name=model_name,
121
+ **kwargs,
122
+ )
123
+
124
+ # Non-streaming path
125
+ try:
126
+ response = self._p._client.messages.create(
127
+ messages=messages,
128
+ system=injected_system,
129
+ stream=False,
130
+ **kwargs,
131
+ )
132
+ except Exception:
133
+ # Record failure in the learning layer, then re-raise API errors.
134
+ self._record_llm_outcome(
135
+ prompt_text=prompt_text,
136
+ response_text="",
137
+ thinking_blocks=[],
138
+ session_id=session_id,
139
+ call_id=call_id,
140
+ model_name=model_name,
141
+ outcome="failure",
142
+ )
143
+ raise
144
+
145
+ text, thinking_blocks = self._capture_response(response, session_id, prompt_text=prompt_text)
146
+ model_name = getattr(response, "model", None) or model_name
147
+ self._record_llm_outcome(
148
+ prompt_text=prompt_text,
149
+ response_text=text,
150
+ thinking_blocks=thinking_blocks,
151
+ session_id=session_id,
152
+ call_id=call_id,
153
+ model_name=model_name,
154
+ outcome="success",
155
+ )
156
+
157
+ # Strip RACP blocks from the visible text in each content block
158
+ try:
159
+ response = self._strip_response(response)
160
+ except Exception as exc:
161
+ logger.warning("devtorch: strip_response failed — %s", exc)
162
+
163
+ return response
164
+
165
+ # --------------------------------------------------------------
166
+ # Streaming helper
167
+ # --------------------------------------------------------------
168
+
169
+ def _stream_create(
170
+ self,
171
+ *,
172
+ messages: list,
173
+ system: str,
174
+ session_id: str,
175
+ call_id: str = "",
176
+ prompt_text: str = "",
177
+ model_name: str = "",
178
+ **kwargs: Any,
179
+ ) -> "_StreamingWrapper":
180
+ try:
181
+ raw_stream = self._p._client.messages.create(
182
+ messages=messages,
183
+ system=system,
184
+ stream=True,
185
+ **kwargs,
186
+ )
187
+ except Exception:
188
+ self._record_llm_outcome(
189
+ prompt_text=prompt_text,
190
+ response_text="",
191
+ thinking_blocks=[],
192
+ session_id=session_id,
193
+ call_id=call_id,
194
+ model_name=model_name,
195
+ outcome="failure",
196
+ )
197
+ raise
198
+ return _StreamingWrapper(
199
+ raw_stream, self, session_id,
200
+ call_id=call_id,
201
+ prompt_text=prompt_text,
202
+ model_name=model_name,
203
+ )
204
+
205
+ # --------------------------------------------------------------
206
+ # Injection helpers
207
+ # --------------------------------------------------------------
208
+
209
+ def _inject(
210
+ self,
211
+ system: str,
212
+ prompt_text: str = "",
213
+ session_id: str = "",
214
+ call_id: str = "",
215
+ model_name: str = "",
216
+ ) -> str:
217
+ if self._p._injector and not is_disabled():
218
+ try:
219
+ prefix, _ = self._p._injector.build_system_prefix_with_provenance(
220
+ prompt_text=prompt_text or None,
221
+ session_id=session_id,
222
+ call_id=call_id,
223
+ model_name=model_name,
224
+ )
225
+ if prefix:
226
+ return (prefix + "\n\n" + system).strip()
227
+ except Exception as exc:
228
+ logger.warning("devtorch: RACP injection failed — %s", exc)
229
+ return system
230
+
231
+ # --------------------------------------------------------------
232
+ # Capture helpers
233
+ # --------------------------------------------------------------
234
+
235
+ def _capture_response(
236
+ self,
237
+ response: Any,
238
+ session_id: str,
239
+ *,
240
+ prompt_text: str = "",
241
+ ) -> tuple[str, list]:
242
+ text = ""
243
+ thinking: list = []
244
+ if not self._p._capture or is_disabled():
245
+ return text, thinking
246
+ try:
247
+ # Build a plain dict for the parsers
248
+ if hasattr(response, "model_dump"):
249
+ response_dict = response.model_dump()
250
+ elif isinstance(response, dict):
251
+ response_dict = response
252
+ else:
253
+ response_dict = {}
254
+
255
+ # Extract visible text
256
+ text = _extract_text(response)
257
+
258
+ # Extract thinking blocks
259
+ try:
260
+ from devtorch_core.parser.thinking import extract_thinking_blocks
261
+
262
+ thinking = extract_thinking_blocks(response_dict)
263
+ except Exception:
264
+ pass
265
+
266
+ # Extract token usage from Anthropic response
267
+ prompt_tokens = 0
268
+ completion_tokens = 0
269
+ usage = response_dict.get("usage") if isinstance(response_dict, dict) else None
270
+ if usage is None and hasattr(response, "usage"):
271
+ usage = getattr(response, "usage", None)
272
+ if usage is not None:
273
+ if isinstance(usage, dict):
274
+ prompt_tokens = usage.get("input_tokens", 0)
275
+ completion_tokens = usage.get("output_tokens", 0)
276
+ else:
277
+ prompt_tokens = getattr(usage, "input_tokens", 0)
278
+ completion_tokens = getattr(usage, "output_tokens", 0)
279
+
280
+ self._p._capture.capture(
281
+ text,
282
+ thinking,
283
+ session_id,
284
+ prompt_text=prompt_text,
285
+ prompt_tokens=prompt_tokens,
286
+ completion_tokens=completion_tokens,
287
+ )
288
+ except Exception as exc:
289
+ logger.warning("devtorch: _capture_response failed — %s", exc)
290
+ return text, thinking
291
+
292
+ def _record_llm_outcome(
293
+ self,
294
+ *,
295
+ prompt_text: str,
296
+ response_text: str,
297
+ thinking_blocks: list,
298
+ session_id: str,
299
+ call_id: str,
300
+ model_name: str,
301
+ outcome: str,
302
+ ) -> None:
303
+ if not self._p._capture or is_disabled():
304
+ return
305
+ try:
306
+ reasoning = _reasoning_text(thinking_blocks)
307
+ self._p._capture.record_learning_call(
308
+ call_type="llm",
309
+ reasoning=reasoning,
310
+ session_id=session_id,
311
+ call_id=call_id,
312
+ name=model_name,
313
+ input=prompt_text,
314
+ output=response_text,
315
+ outcome=outcome,
316
+ )
317
+ except Exception as exc:
318
+ logger.warning("devtorch: _record_llm_outcome failed — %s", exc)
319
+
320
+ # --------------------------------------------------------------
321
+ # Strip RACP markup from the response object
322
+ # --------------------------------------------------------------
323
+
324
+ def _strip_response(self, response: Any) -> Any:
325
+ """
326
+ Return response with RACP blocks and inline <thinking> blocks stripped
327
+ from text content blocks. Modifies in-place where possible.
328
+ """
329
+ from devtorch_core.parser.blocks import strip_racp_blocks
330
+ from devtorch_core.reasoning_plus.capture import strip_inline_thinking
331
+
332
+ content = getattr(response, "content", None)
333
+ if content is None:
334
+ return response
335
+
336
+ for block in content:
337
+ if hasattr(block, "text") and isinstance(block.text, str):
338
+ try:
339
+ text = strip_racp_blocks(block.text)
340
+ text = strip_inline_thinking(text)
341
+ block.text = text
342
+ except Exception:
343
+ pass
344
+
345
+ return response
346
+
347
+
348
+ # ---------------------------------------------------------------------------
349
+ # Streaming wrapper
350
+ # ---------------------------------------------------------------------------
351
+
352
+
353
+ class _StreamingWrapper:
354
+ """
355
+ Thin wrapper around the raw Anthropic streaming response.
356
+
357
+ Passes chunks through transparently while accumulating the full text
358
+ in a shadow buffer. Fires capture when the stream closes and records the
359
+ outcome in the Reasoning Plus Learning layer.
360
+ """
361
+
362
+ def __init__(
363
+ self,
364
+ raw_stream: Any,
365
+ messages_obj: "DevTorchAnthropic._Messages",
366
+ session_id: str,
367
+ call_id: str = "",
368
+ prompt_text: str = "",
369
+ model_name: str = "",
370
+ ) -> None:
371
+ self._raw = raw_stream
372
+ self._msgs = messages_obj
373
+ self._session_id = session_id
374
+ self._call_id = call_id
375
+ self._prompt_text = prompt_text
376
+ self._model_name = model_name
377
+ self._accumulated_text = ""
378
+ self._recorded = False
379
+
380
+ def __iter__(self) -> Iterator[Any]:
381
+ try:
382
+ for chunk in self._raw:
383
+ # Accumulate text from delta events
384
+ try:
385
+ delta = getattr(chunk, "delta", None)
386
+ if delta is not None and hasattr(delta, "text"):
387
+ self._accumulated_text += delta.text or ""
388
+ elif isinstance(chunk, dict):
389
+ delta_d = chunk.get("delta", {})
390
+ self._accumulated_text += delta_d.get("text", "")
391
+ except Exception:
392
+ pass
393
+ yield chunk
394
+ except Exception:
395
+ self._on_close(outcome="failure")
396
+ raise
397
+ finally:
398
+ if not self._recorded:
399
+ self._on_close(outcome="success")
400
+
401
+ def __enter__(self) -> "_StreamingWrapper":
402
+ if hasattr(self._raw, "__enter__"):
403
+ self._raw.__enter__()
404
+ return self
405
+
406
+ def __exit__(self, *args: Any) -> None:
407
+ try:
408
+ if hasattr(self._raw, "__exit__"):
409
+ self._raw.__exit__(*args)
410
+ finally:
411
+ if not self._recorded:
412
+ self._on_close(outcome="success")
413
+
414
+ def _on_close(self, outcome: str = "success") -> None:
415
+ if self._recorded:
416
+ return
417
+ self._recorded = True
418
+ if self._accumulated_text:
419
+ try:
420
+ self._msgs._p._capture.capture(
421
+ self._accumulated_text, [], self._session_id,
422
+ prompt_text=self._prompt_text,
423
+ )
424
+ except Exception as exc:
425
+ logger.warning("devtorch: stream capture failed — %s", exc)
426
+ try:
427
+ self._msgs._record_llm_outcome(
428
+ prompt_text=self._prompt_text,
429
+ response_text=self._accumulated_text,
430
+ thinking_blocks=[],
431
+ session_id=self._session_id,
432
+ call_id=self._call_id,
433
+ model_name=self._model_name,
434
+ outcome=outcome,
435
+ )
436
+ except Exception as exc:
437
+ logger.warning("devtorch: stream learning outcome failed — %s", exc)
438
+
439
+ # Proxy everything else to the raw stream
440
+ def __getattr__(self, name: str) -> Any:
441
+ return getattr(self._raw, name)
442
+
443
+
444
+ # ---------------------------------------------------------------------------
445
+ # Internal helpers
446
+ # ---------------------------------------------------------------------------
447
+
448
+
449
+ def _extract_text(response: Any) -> str:
450
+ """Pull plain text from an Anthropic response object or dict."""
451
+ text = ""
452
+ content = getattr(response, "content", None) or (
453
+ response.get("content") if isinstance(response, dict) else None
454
+ )
455
+ if not content:
456
+ return text
457
+ for block in content:
458
+ if isinstance(block, dict):
459
+ if block.get("type") == "text":
460
+ text += block.get("text", "")
461
+ elif hasattr(block, "text"):
462
+ text += block.text or ""
463
+ return text
464
+
465
+
466
+ def _extract_prompt_text(messages: list) -> str:
467
+ """Best-effort concatenation of user-visible message text for smart context."""
468
+ parts: list[str] = []
469
+ for m in messages or []:
470
+ if isinstance(m, dict):
471
+ role = m.get("role", "")
472
+ content = m.get("content", "")
473
+ if role in ("user", "system") and content:
474
+ if isinstance(content, str):
475
+ parts.append(content)
476
+ elif isinstance(content, list):
477
+ for item in content:
478
+ if isinstance(item, dict) and item.get("type") == "text":
479
+ parts.append(item.get("text", ""))
480
+ elif isinstance(item, str):
481
+ parts.append(item)
482
+ elif hasattr(m, "role") and hasattr(m, "content"):
483
+ role = getattr(m, "role", "")
484
+ content = getattr(m, "content", "")
485
+ if role in ("user", "system") and content:
486
+ parts.append(str(content))
487
+ return "\n".join(parts)[:4000]