deepparallel 0.3.1__tar.gz → 0.4.1__tar.gz

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 (56) hide show
  1. {deepparallel-0.3.1 → deepparallel-0.4.1}/PKG-INFO +4 -1
  2. {deepparallel-0.3.1 → deepparallel-0.4.1}/README.md +1 -0
  3. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/__init__.py +1 -1
  4. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/cli.py +46 -0
  5. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/renderer.py +55 -5
  6. deepparallel-0.4.1/deepparallel/research/__init__.py +6 -0
  7. deepparallel-0.4.1/deepparallel/research/conduit.py +156 -0
  8. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel.egg-info/PKG-INFO +4 -1
  9. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel.egg-info/SOURCES.txt +3 -0
  10. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel.egg-info/requires.txt +3 -0
  11. {deepparallel-0.3.1 → deepparallel-0.4.1}/pyproject.toml +2 -1
  12. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_renderer.py +32 -10
  13. deepparallel-0.4.1/tests/test_research.py +27 -0
  14. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/agent.py +0 -0
  15. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/backend.py +0 -0
  16. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/branding.py +0 -0
  17. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/config.py +0 -0
  18. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/fusion.py +0 -0
  19. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/licensing.py +0 -0
  20. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/registry.json +0 -0
  21. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/supply_chain.py +0 -0
  22. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/system_prompt.txt +0 -0
  23. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/__init__.py +0 -0
  24. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/codeast.py +0 -0
  25. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/edit.py +0 -0
  26. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/files.py +0 -0
  27. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/registry.py +0 -0
  28. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/sandbox.py +0 -0
  29. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/search.py +0 -0
  30. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/shell.py +0 -0
  31. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/vision.py +0 -0
  32. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel/tools/web.py +0 -0
  33. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel.egg-info/dependency_links.txt +0 -0
  34. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel.egg-info/entry_points.txt +0 -0
  35. {deepparallel-0.3.1 → deepparallel-0.4.1}/deepparallel.egg-info/top_level.txt +0 -0
  36. {deepparallel-0.3.1 → deepparallel-0.4.1}/setup.cfg +0 -0
  37. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_agent.py +0 -0
  38. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_backend.py +0 -0
  39. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_backend_chat.py +0 -0
  40. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_backend_stream.py +0 -0
  41. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_branding.py +0 -0
  42. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_cli.py +0 -0
  43. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_config.py +0 -0
  44. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_fusion.py +0 -0
  45. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_issuer_signer.py +0 -0
  46. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_licensing.py +0 -0
  47. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_supply_chain.py +0 -0
  48. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_tool_registry.py +0 -0
  49. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_tools_codeast.py +0 -0
  50. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_tools_edit.py +0 -0
  51. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_tools_files.py +0 -0
  52. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_tools_sandbox.py +0 -0
  53. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_tools_search.py +0 -0
  54. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_tools_shell.py +0 -0
  55. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_tools_vision.py +0 -0
  56. {deepparallel-0.3.1 → deepparallel-0.4.1}/tests/test_tools_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepparallel
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
5
5
  Author-email: Michael Crowe <michael@crowelogic.com>
6
6
  License: Apache-2.0
@@ -19,6 +19,8 @@ Requires-Dist: cryptography>=42.0.0
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: pytest>=8.0.0; extra == "dev"
21
21
  Requires-Dist: ruff>=0.6.0; extra == "dev"
22
+ Provides-Extra: research
23
+ Requires-Dist: numpy>=1.24; extra == "research"
22
24
 
23
25
  # DeepParallel
24
26
 
@@ -67,6 +69,7 @@ Set the backend env vars (a `.env` file in the working directory is loaded autom
67
69
  deepparallel run --yes "..." # auto-approve tool actions
68
70
  deepparallel review <file|--diff> # cross-model review as a CI gate (Pro)
69
71
  deepparallel audit <file> # supply-chain gate: catch hallucinated deps (Pro)
72
+ deepparallel research conduit # latent-relay research demo (needs [research] extra)
70
73
 
71
74
  ## Supply-chain gate
72
75
 
@@ -45,6 +45,7 @@ Set the backend env vars (a `.env` file in the working directory is loaded autom
45
45
  deepparallel run --yes "..." # auto-approve tool actions
46
46
  deepparallel review <file|--diff> # cross-model review as a CI gate (Pro)
47
47
  deepparallel audit <file> # supply-chain gate: catch hallucinated deps (Pro)
48
+ deepparallel research conduit # latent-relay research demo (needs [research] extra)
48
49
 
49
50
  ## Supply-chain gate
50
51
 
@@ -1,3 +1,3 @@
1
1
  """DeepParallel CLI package."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.4.1"
@@ -546,6 +546,52 @@ def audit(ctx: click.Context, path: str) -> None:
546
546
  sys.exit(0)
547
547
 
548
548
 
549
+ @main.group()
550
+ def research() -> None:
551
+ """Runnable demonstrators of the ideas behind DeepParallel."""
552
+
553
+
554
+ @research.command("conduit")
555
+ def research_conduit() -> None:
556
+ """Conduit: relay a thought between models as a hidden state vs. as a word.
557
+
558
+ Frozen synthetic models relay a continuous meaning down a chain of agents two
559
+ ways and we measure how much survives each hop. The latent channel holds the
560
+ meaning; the word channel (one emitted token per hop) collapses. The same
561
+ result on real open-weight models: crowelogic.com/research/conduit
562
+ """
563
+ try:
564
+ from deepparallel.research import conduit as _conduit
565
+ except ImportError:
566
+ branding.error(
567
+ "this demo needs numpy. Install it with: pip install 'deepparallel[research]'"
568
+ )
569
+ sys.exit(3)
570
+ console.print(f"[{branding.DIM}]running the latent relay (frozen synthetic models)...[/]")
571
+ r = _conduit.demonstrate()
572
+ console.print(
573
+ f"\n[bold]Conduit[/] · meaning retained after K relay hops "
574
+ f"[{branding.DIM}](cosine; 1.0 = perfect)[/]"
575
+ )
576
+ console.print(
577
+ f"[{branding.DIM}] meaning = {r['meaning_dim']}-dim continuous "
578
+ f"word channel = 1 token of {r['vocab']}[/]\n"
579
+ )
580
+ console.print(
581
+ f" [{branding.DIM}]{'hops':>5} {'latent relay':>13} {'word relay':>11} {'gain':>6}[/]"
582
+ )
583
+ for K in r["hops"]:
584
+ lat, wrd = r["latent"][K], r["word"][K]
585
+ console.print(
586
+ f" {K:>5} [{branding.GREEN_HEX}]{lat:>13.3f}[/] "
587
+ f"[{branding.DIM}]{wrd:>11.3f}[/] [{branding.AMBER_HEX}]{lat - wrd:>+6.3f}[/]"
588
+ )
589
+ console.print(
590
+ f"\n[{branding.DIM}] the full hidden state relays more meaning than the single word "
591
+ f"the model would have said.[/]"
592
+ )
593
+
594
+
549
595
  @main.command(name="tools")
550
596
  def tools_cmd() -> None:
551
597
  """List the agent tools available to DeepParallel."""
@@ -25,6 +25,15 @@ from deepparallel import branding
25
25
  _REVEAL_SECONDS = 0.04 # per-line delay for the animated intro (tests set 0)
26
26
 
27
27
 
28
+ def _balance_fences(text: str) -> str:
29
+ """Close a dangling ``` while streaming so a half-arrived code block renders
30
+ as code instead of leaking its fence as literal text. The closing fence is
31
+ cosmetic for the in-progress frame; the final frame has the real one."""
32
+ if text.count("```") % 2 == 1:
33
+ return text + "\n```"
34
+ return text
35
+
36
+
28
37
  class Renderer(ABC):
29
38
  @abstractmethod
30
39
  def welcome(
@@ -146,10 +155,18 @@ class RichRenderer(Renderer):
146
155
  self._console.print(branding.build_transcript_markdown(self._console, text))
147
156
 
148
157
  def answer_stream(self, chunks: Iterable[str]) -> str:
149
- # Inline token streaming: no Live/transient panels, so it never ghosts
150
- # in wide terminals or when tool turns interleave. The marker is printed
151
- # only on the first VISIBLE character, so empty / whitespace-leading
152
- # (tool-only) turns render no stray marker.
158
+ """Stream the answer. On a real terminal, render Markdown live in a panel
159
+ that grows as tokens arrive (headings, code, lists format in place); the
160
+ final frame is the same panel `answer()` would print, so there is no
161
+ double render. On a pipe / non-tty, fall back to raw inline streaming."""
162
+ if self._console.is_terminal:
163
+ return self._stream_live_markdown(chunks)
164
+ return self._stream_inline(chunks)
165
+
166
+ def _stream_inline(self, chunks: Iterable[str]) -> str:
167
+ # Raw token streaming for pipes / non-tty: no Live, never ghosts. The
168
+ # marker is printed only on the first VISIBLE character, so empty /
169
+ # whitespace-leading (tool-only) turns render no stray marker.
153
170
  parts: list[str] = []
154
171
  started = False
155
172
  for c in chunks:
@@ -158,7 +175,7 @@ class RichRenderer(Renderer):
158
175
  self._console.print(c, end="", soft_wrap=True, highlight=False, markup=False)
159
176
  continue
160
177
  if not "".join(parts).strip():
161
- continue # only whitespace so far; hold the marker back
178
+ continue
162
179
  started = True
163
180
  self._console.print(
164
181
  f"[{branding.DP_ACCENT}]{branding.MARK}[/] ", end="", highlight=False
@@ -168,6 +185,39 @@ class RichRenderer(Renderer):
168
185
  self._console.print()
169
186
  return "".join(parts)
170
187
 
188
+ def _stream_live_markdown(self, chunks: Iterable[str]) -> str:
189
+ from rich.live import Live
190
+
191
+ parts: list[str] = []
192
+ started = False
193
+ last_draw = 0.0
194
+ live = Live(
195
+ console=self._console,
196
+ auto_refresh=False, # we drive refreshes; deterministic, no bg thread
197
+ vertical_overflow="visible", # let answers taller than the screen scroll
198
+ )
199
+ try:
200
+ for c in chunks:
201
+ parts.append(c)
202
+ if not started:
203
+ if not "".join(parts).strip():
204
+ continue # hold the panel back until real content arrives
205
+ started = True
206
+ live.start()
207
+ now = time.monotonic()
208
+ if now - last_draw >= 0.06: # throttle to ~16 fps
209
+ live.update(self._answer_panel("".join(parts)), refresh=True)
210
+ last_draw = now
211
+ if started: # final frame: the complete, settled answer
212
+ live.update(self._answer_panel("".join(parts)), refresh=True)
213
+ finally:
214
+ if started:
215
+ live.stop()
216
+ return "".join(parts)
217
+
218
+ def _answer_panel(self, text: str):
219
+ return branding.build_transcript_markdown(self._console, _balance_fences(text))
220
+
171
221
  def reasoning(self, text: str) -> None:
172
222
  self._console.print(branding.build_reasoning_panel(self._console, text))
173
223
 
@@ -0,0 +1,6 @@
1
+ """DeepParallel research demonstrators.
2
+
3
+ Self-contained, runnable illustrations of the ideas behind DeepParallel's
4
+ multi-model approach. These are optional and depend on numpy; install with
5
+ `pip install deepparallel[research]`.
6
+ """
@@ -0,0 +1,156 @@
1
+ """Conduit: latent-state relay between frozen models (runnable demonstrator).
2
+
3
+ The idea behind DeepParallel's multi-model approach, made concrete. When agents
4
+ collaborate by writing words and reading them back, every hand-off squeezes a
5
+ continuous thought through a single emitted token (~log2(vocab) bits). Conduit
6
+ relays the model's hidden state directly through a tiny connector instead.
7
+
8
+ This is a dependency-light demonstration of the mechanism and the information
9
+ loss it avoids: two FROZEN synthetic transformers of different hidden sizes (so
10
+ the connector is genuinely required) relay a continuous meaning vector down a
11
+ chain of agents two ways, and we measure how much meaning survives each hop:
12
+
13
+ latent : h_i -> connector(W) -> next model (continuous)
14
+ word : h_i -> argmax token -> re-embed (one discrete symbol)
15
+
16
+ Needs numpy (`pip install deepparallel[research]`). The full research, including
17
+ the same result on real open-weight models, is at crowelogic.com/research/conduit.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import numpy as np
23
+
24
+
25
+ def _gelu(x):
26
+ return 0.5 * x * (1.0 + np.tanh(0.7978845608 * (x + 0.044715 * x**3)))
27
+
28
+
29
+ def _ln(x):
30
+ return (x - x.mean(-1, keepdims=True)) / (x.std(-1, keepdims=True) + 1e-5)
31
+
32
+
33
+ def _softmax(x):
34
+ e = np.exp(x - x.max(-1, keepdims=True))
35
+ return e / e.sum(-1, keepdims=True)
36
+
37
+
38
+ class _FrozenLM:
39
+ """A tiny frozen decoder-only transformer with its own hidden size + vocab."""
40
+
41
+ def __init__(self, vocab, d, n_layers, seed):
42
+ self.vocab, self.d = vocab, d
43
+ rng = np.random.default_rng(seed)
44
+ s = 1.0 / np.sqrt(d)
45
+ self.E = rng.standard_normal((vocab, d)) * 0.02
46
+ self.U = rng.standard_normal((d, vocab)) * s
47
+ self.layers = [
48
+ {
49
+ "Wq": rng.standard_normal((d, d)) * s,
50
+ "Wk": rng.standard_normal((d, d)) * s,
51
+ "Wv": rng.standard_normal((d, d)) * s,
52
+ "Wo": rng.standard_normal((d, d)) * s,
53
+ "W1": rng.standard_normal((d, 4 * d)) * s,
54
+ "W2": rng.standard_normal((4 * d, d)) * s,
55
+ }
56
+ for _ in range(n_layers)
57
+ ]
58
+
59
+ def _forward(self, x):
60
+ T = x.shape[0]
61
+ mask = np.triu(np.full((T, T), -1e9), k=1)
62
+ for p in self.layers:
63
+ h = _ln(x)
64
+ q, k, v = h @ p["Wq"], h @ p["Wk"], h @ p["Wv"]
65
+ att = _softmax(q @ k.T / np.sqrt(self.d) + mask)
66
+ x = x + (att @ v) @ p["Wo"]
67
+ x = x + _gelu(_ln(x) @ p["W1"]) @ p["W2"]
68
+ return x
69
+
70
+ def hidden_at(self, in_embed):
71
+ return self._forward(in_embed.reshape(1, self.d))[-1]
72
+
73
+ def next_token(self, hidden):
74
+ return int(np.argmax(hidden @ self.U))
75
+
76
+
77
+ def _ridge(X, Y, lam=1e-2):
78
+ Xb = np.hstack([X, np.ones((X.shape[0], 1))])
79
+ return np.linalg.solve(Xb.T @ Xb + lam * np.eye(Xb.shape[1]), Xb.T @ Y)
80
+
81
+
82
+ def _affine(W, x):
83
+ return np.hstack([x, np.ones((x.shape[0], 1))]) @ W
84
+
85
+
86
+ class _Conduit:
87
+ def __init__(self, d_meaning, dims, vocab, seed=7):
88
+ self.dm = d_meaning
89
+ rng = np.random.default_rng(seed)
90
+ self.models = [
91
+ _FrozenLM(vocab, dims[0], 2, seed + 1),
92
+ _FrozenLM(vocab, dims[1], 2, seed + 2),
93
+ ]
94
+ self.P = [rng.standard_normal((d_meaning, m.d)) * 0.5 for m in self.models]
95
+ self.conn, self.readout = {}, {}
96
+
97
+ def _encode(self, mi, z):
98
+ return self.models[mi].hidden_at(z @ self.P[mi])
99
+
100
+ def _relay(self, z, K, path):
101
+ mi = 0
102
+ h = self._encode(mi, z)
103
+ for hop in range(1, K + 1):
104
+ mj = hop % len(self.models)
105
+ if path == "latent":
106
+ in_embed = _affine(self.conn[(mi, mj)], h.reshape(1, -1))[0]
107
+ else:
108
+ tok = self.models[mi].next_token(h)
109
+ in_embed = self.models[mj].E[tok]
110
+ h = self.models[mj].hidden_at(in_embed)
111
+ mi = mj
112
+ return h, mi
113
+
114
+ def fit(self, n_train, fit_hops, seed=0):
115
+ rng = np.random.default_rng(seed)
116
+ Z = rng.standard_normal((n_train, self.dm))
117
+ n = len(self.models)
118
+ for i in range(n):
119
+ Hi = np.array([self._encode(i, z) for z in Z])
120
+ for j in range(n):
121
+ self.conn[(i, j)] = _ridge(Hi, Z @ self.P[j])
122
+ for path in ("latent", "word"):
123
+ finals = {0: [], 1: []}
124
+ targets = {0: [], 1: []}
125
+ for K in fit_hops:
126
+ for z in Z:
127
+ h, m = self._relay(z, K, path)
128
+ finals[m].append(h)
129
+ targets[m].append(z)
130
+ for m in finals:
131
+ if finals[m]:
132
+ self.readout[(path, m)] = _ridge(np.array(finals[m]), np.array(targets[m]))
133
+
134
+ def recover(self, z, K, path):
135
+ h, m = self._relay(z, K, path)
136
+ return _affine(self.readout[(path, m)], h.reshape(1, -1))[0]
137
+
138
+
139
+ def _cos(a, b):
140
+ return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
141
+
142
+
143
+ def demonstrate(hops=(1, 2, 4, 6, 8)) -> dict:
144
+ """Run the relay both ways and return meaning retained per hop count."""
145
+ d_meaning, vocab = 16, 256
146
+ link = _Conduit(d_meaning, dims=[32, 48], vocab=vocab)
147
+ link.fit(n_train=800, fit_hops=hops)
148
+ rng = np.random.default_rng(123)
149
+ test = rng.standard_normal((300, d_meaning))
150
+ out = {"latent": {}, "word": {}, "meaning_dim": d_meaning, "vocab": vocab, "hops": list(hops)}
151
+ for path in ("latent", "word"):
152
+ for K in hops:
153
+ out[path][K] = round(
154
+ float(np.mean([_cos(z, link.recover(z, K, path)) for z in test])), 3
155
+ )
156
+ return out
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepparallel
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
5
5
  Author-email: Michael Crowe <michael@crowelogic.com>
6
6
  License: Apache-2.0
@@ -19,6 +19,8 @@ Requires-Dist: cryptography>=42.0.0
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: pytest>=8.0.0; extra == "dev"
21
21
  Requires-Dist: ruff>=0.6.0; extra == "dev"
22
+ Provides-Extra: research
23
+ Requires-Dist: numpy>=1.24; extra == "research"
22
24
 
23
25
  # DeepParallel
24
26
 
@@ -67,6 +69,7 @@ Set the backend env vars (a `.env` file in the working directory is loaded autom
67
69
  deepparallel run --yes "..." # auto-approve tool actions
68
70
  deepparallel review <file|--diff> # cross-model review as a CI gate (Pro)
69
71
  deepparallel audit <file> # supply-chain gate: catch hallucinated deps (Pro)
72
+ deepparallel research conduit # latent-relay research demo (needs [research] extra)
70
73
 
71
74
  ## Supply-chain gate
72
75
 
@@ -18,6 +18,8 @@ deepparallel.egg-info/dependency_links.txt
18
18
  deepparallel.egg-info/entry_points.txt
19
19
  deepparallel.egg-info/requires.txt
20
20
  deepparallel.egg-info/top_level.txt
21
+ deepparallel/research/__init__.py
22
+ deepparallel/research/conduit.py
21
23
  deepparallel/tools/__init__.py
22
24
  deepparallel/tools/codeast.py
23
25
  deepparallel/tools/edit.py
@@ -39,6 +41,7 @@ tests/test_fusion.py
39
41
  tests/test_issuer_signer.py
40
42
  tests/test_licensing.py
41
43
  tests/test_renderer.py
44
+ tests/test_research.py
42
45
  tests/test_supply_chain.py
43
46
  tests/test_tool_registry.py
44
47
  tests/test_tools_codeast.py
@@ -10,3 +10,6 @@ cryptography>=42.0.0
10
10
  [dev]
11
11
  pytest>=8.0.0
12
12
  ruff>=0.6.0
13
+
14
+ [research]
15
+ numpy>=1.24
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepparallel"
7
- version = "0.3.1"
7
+ version = "0.4.1"
8
8
  description = "DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -26,6 +26,7 @@ dependencies = [
26
26
 
27
27
  [project.optional-dependencies]
28
28
  dev = ["pytest>=8.0.0", "ruff>=0.6.0"]
29
+ research = ["numpy>=1.24"]
29
30
 
30
31
  [project.urls]
31
32
  Homepage = "https://crowelogic.com"
@@ -123,14 +123,27 @@ def test_rich_confirm_shows_detail():
123
123
  assert "added line" in out
124
124
 
125
125
 
126
- def test_rich_answer_stream_returns_text_and_prints_once():
126
+ def test_rich_answer_stream_returns_text_and_renders_live_markdown():
127
127
  buf = io.StringIO()
128
128
  con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
129
129
  r = RichRenderer(console=con)
130
130
  full = r.answer_stream(iter(["Hel", "lo ", "world"]))
131
- assert full == "Hello world"
132
- # inline streaming prints the content exactly once (no transient-Live ghost)
133
- assert buf.getvalue().count("world") == 1
131
+ out = buf.getvalue()
132
+ assert full == "Hello world" # full text preserved for history
133
+ assert "Hello world" in out # rendered in the live answer panel
134
+ assert "◆" in out and "answer" in out # panel marker + title
135
+
136
+
137
+ def test_rich_answer_stream_renders_markdown_formatting():
138
+ buf = io.StringIO()
139
+ con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
140
+ r = RichRenderer(console=con)
141
+ full = r.answer_stream(iter(["# Big\n\n", "body **bold**"]))
142
+ out = buf.getvalue()
143
+ assert full == "# Big\n\nbody **bold**"
144
+ # markdown is rendered, not shown raw: the heading hash is consumed
145
+ assert "Big" in out and "body" in out
146
+ assert "# Big" not in out
134
147
 
135
148
 
136
149
  def test_rich_answer_stream_empty_prints_nothing():
@@ -139,20 +152,29 @@ def test_rich_answer_stream_empty_prints_nothing():
139
152
  r = RichRenderer(console=con)
140
153
  full = r.answer_stream(iter([]))
141
154
  assert full == ""
142
- assert buf.getvalue() == "" # tool-only turns render no answer marker
155
+ assert buf.getvalue() == "" # tool-only turns never start the live panel
143
156
 
144
157
 
145
- def test_rich_answer_stream_whitespace_leading_no_stray_marker():
158
+ def test_rich_answer_stream_whitespace_leading_holds_panel():
146
159
  buf = io.StringIO()
147
160
  con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
148
161
  r = RichRenderer(console=con)
149
162
  full = r.answer_stream(iter(["\n", " ", "\n", "Hello"]))
150
163
  out = buf.getvalue()
151
164
  assert full == "\n \nHello" # full text preserved for history
152
- # marker appears exactly once and immediately precedes the visible text
153
- assert out.count("◆") == 1
154
- assert "◆ Hello" in out
155
- assert "◆ \n" not in out and "◆ \n" not in out # no stray marker on blank lines
165
+ assert "Hello" in out and "◆" in out
166
+
167
+
168
+ def test_rich_answer_stream_inline_when_not_a_terminal():
169
+ # pipes / CI: raw inline streaming, no Live panel (no border characters)
170
+ buf = io.StringIO()
171
+ con = Console(no_color=True, width=80, file=buf, highlight=False) # not a terminal
172
+ r = RichRenderer(console=con)
173
+ full = r.answer_stream(iter(["Hel", "lo"]))
174
+ out = buf.getvalue()
175
+ assert full == "Hello"
176
+ assert "Hello" in out
177
+ assert "─" not in out and "answer" not in out # no panel chrome
156
178
 
157
179
 
158
180
  # ---------------------------------------------------------------- FakeRenderer
@@ -0,0 +1,27 @@
1
+ import pytest
2
+ from click.testing import CliRunner
3
+
4
+ from deepparallel.cli import main
5
+
6
+ np = pytest.importorskip("numpy") # research demos are an optional extra
7
+
8
+ from deepparallel.research import conduit # noqa: E402
9
+
10
+
11
+ def test_demonstrate_latent_beats_word_at_every_hop():
12
+ r = conduit.demonstrate(hops=(1, 4, 8))
13
+ for k in (1, 4, 8):
14
+ assert r["latent"][k] > r["word"][k], f"latent should beat word at hop {k}"
15
+ assert r["latent"][1] > 0.7 # strong signal retained at one hop
16
+
17
+
18
+ def test_demonstrate_word_channel_does_not_improve_with_depth():
19
+ r = conduit.demonstrate(hops=(1, 6))
20
+ assert r["word"][6] <= r["word"][1] + 0.05 # lossy re-serialization, no gain
21
+
22
+
23
+ def test_research_conduit_command_runs_and_prints_table():
24
+ result = CliRunner().invoke(main, ["research", "conduit"])
25
+ assert result.exit_code == 0
26
+ assert "Conduit" in result.output
27
+ assert "latent relay" in result.output
File without changes