deepparallel 0.2.0__tar.gz → 0.4.0__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.2.0 → deepparallel-0.4.0}/PKG-INFO +30 -3
  2. {deepparallel-0.2.0 → deepparallel-0.4.0}/README.md +27 -2
  3. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/__init__.py +1 -1
  4. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/agent.py +43 -3
  5. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/backend.py +18 -2
  6. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/cli.py +89 -1
  7. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/config.py +1 -1
  8. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/licensing.py +24 -0
  9. deepparallel-0.4.0/deepparallel/research/__init__.py +6 -0
  10. deepparallel-0.4.0/deepparallel/research/conduit.py +156 -0
  11. deepparallel-0.4.0/deepparallel/supply_chain.py +130 -0
  12. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/PKG-INFO +30 -3
  13. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/SOURCES.txt +6 -0
  14. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/requires.txt +3 -0
  15. {deepparallel-0.2.0 → deepparallel-0.4.0}/pyproject.toml +2 -1
  16. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_agent.py +118 -0
  17. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_backend_chat.py +25 -0
  18. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_cli.py +34 -0
  19. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_config.py +1 -1
  20. deepparallel-0.4.0/tests/test_issuer_signer.py +54 -0
  21. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_licensing.py +24 -0
  22. deepparallel-0.4.0/tests/test_research.py +27 -0
  23. deepparallel-0.4.0/tests/test_supply_chain.py +71 -0
  24. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/branding.py +0 -0
  25. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/fusion.py +0 -0
  26. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/registry.json +0 -0
  27. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/renderer.py +0 -0
  28. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/system_prompt.txt +0 -0
  29. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/__init__.py +0 -0
  30. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/codeast.py +0 -0
  31. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/edit.py +0 -0
  32. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/files.py +0 -0
  33. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/registry.py +0 -0
  34. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/sandbox.py +0 -0
  35. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/search.py +0 -0
  36. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/shell.py +0 -0
  37. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/vision.py +0 -0
  38. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/web.py +0 -0
  39. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/dependency_links.txt +0 -0
  40. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/entry_points.txt +0 -0
  41. {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/top_level.txt +0 -0
  42. {deepparallel-0.2.0 → deepparallel-0.4.0}/setup.cfg +0 -0
  43. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_backend.py +0 -0
  44. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_backend_stream.py +0 -0
  45. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_branding.py +0 -0
  46. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_fusion.py +0 -0
  47. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_renderer.py +0 -0
  48. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tool_registry.py +0 -0
  49. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_codeast.py +0 -0
  50. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_edit.py +0 -0
  51. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_files.py +0 -0
  52. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_sandbox.py +0 -0
  53. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_search.py +0 -0
  54. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_shell.py +0 -0
  55. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_vision.py +0 -0
  56. {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepparallel
3
- Version: 0.2.0
3
+ Version: 0.4.0
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,13 +19,27 @@ 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
 
25
- A focused command-line interface for the DeepParallel model, served via Crowe Logic.
27
+ A multi-model agentic coding CLI with cross-model Guardian review. Many models
28
+ check the work; you ship with a second opinion. Served via Crowe Logic.
26
29
 
27
30
  ## Install
28
31
 
32
+ uv tool install deepparallel # recommended
33
+ # or
34
+ pipx install deepparallel --python python3.13
35
+
36
+ Then run `deepparallel` (or the short alias `dp`).
37
+
38
+ Note: on systems with a broken Homebrew Python 3.14, bare `pipx install` may
39
+ fail to find a usable interpreter - use `uv tool install` or pin `--python`.
40
+
41
+ ### From source
42
+
29
43
  uv venv
30
44
  uv pip install -p .venv -e .
31
45
 
@@ -53,6 +67,19 @@ Set the backend env vars (a `.env` file in the working directory is loaded autom
53
67
  deepparallel doctor # diagnose config + reachability
54
68
  deepparallel run --no-tools "..." # plain chat, no tools
55
69
  deepparallel run --yes "..." # auto-approve tool actions
70
+ deepparallel review <file|--diff> # cross-model review as a CI gate (Pro)
71
+ deepparallel audit <file> # supply-chain gate: catch hallucinated deps (Pro)
72
+ deepparallel research conduit # latent-relay research demo (needs [research] extra)
73
+
74
+ ## Supply-chain gate
75
+
76
+ LLMs invent package names that don't exist — and attackers register those names
77
+ ("slopsquatting"). `deepparallel audit <file>` extracts the dependencies a file
78
+ introduces (imports, requirements.txt, package.json) and checks each against the
79
+ real registry (PyPI / npm), flagging any the registry has never seen. Exit 0
80
+ clean, 2 if a likely-hallucinated package is found — so it gates a commit or PR.
81
+ The Guardian also runs this automatically on every edit: a hallucinated import
82
+ forces a confirmation even under `--yes`.
56
83
 
57
84
  ## Fusion (stacking models for stronger output)
58
85
 
@@ -107,7 +134,7 @@ timeboxed local subprocess.
107
134
  | `DEEPPARALLEL_API_VERSION` | Azure API version | `2024-08-01-preview` |
108
135
  | `FOUNDRY_BASE_URL` / `FOUNDRY_API_KEY` | control-plane transport | (required for foundry) |
109
136
  | `DEEPPARALLEL_TEMPERATURE` | default sampling temperature | `0.4` |
110
- | `DEEPPARALLEL_MAX_TOKENS` | response cap | `2048` |
137
+ | `DEEPPARALLEL_MAX_TOKENS` | response cap (large enough to write whole files) | `8192` |
111
138
  | `DEEPPARALLEL_THINK` | surface reasoning stream | `0` (answer-only) |
112
139
  | `DEEPPARALLEL_TOOLS` | enable agent tools | `1` (on) |
113
140
  | `DEEPPARALLEL_AUTO_APPROVE` | auto-approve mutating tools | `0` (off) |
@@ -1,9 +1,21 @@
1
1
  # DeepParallel
2
2
 
3
- A focused command-line interface for the DeepParallel model, served via Crowe Logic.
3
+ A multi-model agentic coding CLI with cross-model Guardian review. Many models
4
+ check the work; you ship with a second opinion. Served via Crowe Logic.
4
5
 
5
6
  ## Install
6
7
 
8
+ uv tool install deepparallel # recommended
9
+ # or
10
+ pipx install deepparallel --python python3.13
11
+
12
+ Then run `deepparallel` (or the short alias `dp`).
13
+
14
+ Note: on systems with a broken Homebrew Python 3.14, bare `pipx install` may
15
+ fail to find a usable interpreter - use `uv tool install` or pin `--python`.
16
+
17
+ ### From source
18
+
7
19
  uv venv
8
20
  uv pip install -p .venv -e .
9
21
 
@@ -31,6 +43,19 @@ Set the backend env vars (a `.env` file in the working directory is loaded autom
31
43
  deepparallel doctor # diagnose config + reachability
32
44
  deepparallel run --no-tools "..." # plain chat, no tools
33
45
  deepparallel run --yes "..." # auto-approve tool actions
46
+ deepparallel review <file|--diff> # cross-model review as a CI gate (Pro)
47
+ deepparallel audit <file> # supply-chain gate: catch hallucinated deps (Pro)
48
+ deepparallel research conduit # latent-relay research demo (needs [research] extra)
49
+
50
+ ## Supply-chain gate
51
+
52
+ LLMs invent package names that don't exist — and attackers register those names
53
+ ("slopsquatting"). `deepparallel audit <file>` extracts the dependencies a file
54
+ introduces (imports, requirements.txt, package.json) and checks each against the
55
+ real registry (PyPI / npm), flagging any the registry has never seen. Exit 0
56
+ clean, 2 if a likely-hallucinated package is found — so it gates a commit or PR.
57
+ The Guardian also runs this automatically on every edit: a hallucinated import
58
+ forces a confirmation even under `--yes`.
34
59
 
35
60
  ## Fusion (stacking models for stronger output)
36
61
 
@@ -85,7 +110,7 @@ timeboxed local subprocess.
85
110
  | `DEEPPARALLEL_API_VERSION` | Azure API version | `2024-08-01-preview` |
86
111
  | `FOUNDRY_BASE_URL` / `FOUNDRY_API_KEY` | control-plane transport | (required for foundry) |
87
112
  | `DEEPPARALLEL_TEMPERATURE` | default sampling temperature | `0.4` |
88
- | `DEEPPARALLEL_MAX_TOKENS` | response cap | `2048` |
113
+ | `DEEPPARALLEL_MAX_TOKENS` | response cap (large enough to write whole files) | `8192` |
89
114
  | `DEEPPARALLEL_THINK` | surface reasoning stream | `0` (answer-only) |
90
115
  | `DEEPPARALLEL_TOOLS` | enable agent tools | `1` (on) |
91
116
  | `DEEPPARALLEL_AUTO_APPROVE` | auto-approve mutating tools | `0` (off) |
@@ -1,3 +1,3 @@
1
1
  """DeepParallel CLI package."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.4.0"
@@ -18,6 +18,17 @@ _MAX_TOOL_RESULT = 50_000
18
18
  _GATED_PATH_TOOLS = ("write_file", "edit_file")
19
19
  _EDIT_TOOLS = ("write_file", "edit_file", "ast_replace_symbol")
20
20
 
21
+ # Shown back to the model when a tool call arrives on a response that the API
22
+ # cut off at the token limit (finish_reason=length). The call's arguments, and
23
+ # any file body inside them, are incomplete, so we never apply it.
24
+ _TRUNCATION_HINT = (
25
+ "Your previous output was cut off by the response token limit "
26
+ "(finish_reason=length), so this tool call is incomplete and was NOT applied. "
27
+ "Write large files in smaller pieces: create the file with write_file using "
28
+ "the first portion, then append the rest with edit_file (or split it across "
29
+ "files). Do not emit an entire large file in one tool call."
30
+ )
31
+
21
32
 
22
33
  def _parse_args(raw: str) -> dict:
23
34
  if not raw:
@@ -191,9 +202,30 @@ def _guardian_verdict(guardian, name: str, args: dict) -> str | None:
191
202
  return guardian_review(guardian, _guardian_review_content(name, args))
192
203
 
193
204
 
205
+ def _supply_chain_note(name: str, args: dict) -> str | None:
206
+ """Best-effort: flag hallucinated/slopsquatted deps an edit introduces."""
207
+ content = args.get("new_source") or args.get("new_string") or args.get("content") or ""
208
+ path = args.get("file_path", "")
209
+ if not content or not path:
210
+ return None
211
+ try:
212
+ from deepparallel import supply_chain
213
+
214
+ result = supply_chain.audit(content, path)
215
+ except Exception: # noqa: BLE001 - supply-chain check is best-effort
216
+ return None
217
+ if result["hallucinated"]:
218
+ return "not found on the registry (possible hallucination): " + ", ".join(
219
+ result["hallucinated"]
220
+ )
221
+ return None
222
+
223
+
194
224
  def _approved(name, args, interactive, auto_approve, renderer, guardian=None) -> bool:
195
225
  forced = name in _GATED_PATH_TOOLS and _outside_cwd(args)
196
- if auto_approve and not forced:
226
+ sc_note = _supply_chain_note(name, args) if name in _EDIT_TOOLS else None
227
+ # A hallucinated dependency overrides auto-approve: always surface it.
228
+ if auto_approve and not forced and not sc_note:
197
229
  return True
198
230
  if not interactive:
199
231
  return False
@@ -202,6 +234,8 @@ def _approved(name, args, interactive, auto_approve, renderer, guardian=None) ->
202
234
  verdict = _guardian_verdict(guardian, name, args)
203
235
  if verdict:
204
236
  detail = f"{detail}\n\nGuardian: {verdict}"
237
+ if sc_note:
238
+ detail = f"{detail}\n\nSupply-chain: {sc_note}"
205
239
  return renderer.confirm(title, detail)
206
240
 
207
241
 
@@ -250,6 +284,7 @@ def run_agent(
250
284
  msg = backend.chat(messages, schemas, settings.temperature, settings.max_tokens)
251
285
  streamed = False
252
286
  tool_calls = msg.get("tool_calls")
287
+ truncated = bool(msg.get("_truncated"))
253
288
  if not tool_calls:
254
289
  content = msg.get("content") or ""
255
290
  messages.append({"role": "assistant", "content": content})
@@ -260,13 +295,18 @@ def run_agent(
260
295
  messages.append(
261
296
  {"role": "assistant", "content": msg.get("content"), "tool_calls": tool_calls}
262
297
  )
263
- for tc in tool_calls:
298
+ # A length-truncated response cuts off the final tool call mid-arguments,
299
+ # so its file body is incomplete. Refuse it and tell the model to chunk.
300
+ last_idx = len(tool_calls) - 1
301
+ for i, tc in enumerate(tool_calls):
264
302
  name = tc["function"]["name"]
265
303
  args = _parse_args(tc["function"].get("arguments", ""))
266
304
  meta = registry.get(name)
267
305
  renderer.tool_start(name, _preview(args))
268
306
  start = time.monotonic()
269
- if meta is None:
307
+ if truncated and i == last_idx:
308
+ result = json.dumps({"error": _TRUNCATION_HINT})
309
+ elif meta is None:
270
310
  result = json.dumps({"error": f"unknown tool: {name}"})
271
311
  elif "__parse_error__" in args:
272
312
  result = json.dumps({"error": "invalid JSON arguments"})
@@ -56,6 +56,7 @@ def parse_sse_stream(lines: Iterator[str]):
56
56
  """
57
57
  content_parts: list[str] = []
58
58
  acc: dict[int, dict] = {}
59
+ finish_reason: str | None = None
59
60
  for raw in lines:
60
61
  line = raw.strip()
61
62
  if not line or not line.startswith("data:"):
@@ -70,6 +71,8 @@ def parse_sse_stream(lines: Iterator[str]):
70
71
  choices = obj.get("choices") or []
71
72
  if not choices:
72
73
  continue
74
+ if choices[0].get("finish_reason"):
75
+ finish_reason = choices[0]["finish_reason"]
73
76
  delta = choices[0].get("delta") or {}
74
77
  reasoning = delta.get("reasoning_content")
75
78
  if reasoning:
@@ -95,6 +98,7 @@ def parse_sse_stream(lines: Iterator[str]):
95
98
  "role": "assistant",
96
99
  "content": "".join(content_parts) or None,
97
100
  "tool_calls": tool_calls,
101
+ "_truncated": finish_reason == "length",
98
102
  }
99
103
 
100
104
 
@@ -103,6 +107,18 @@ def _host(url: str) -> str:
103
107
  return f"{p.scheme}://{p.netloc}" if p.netloc else url
104
108
 
105
109
 
110
+ def _message_from_choice(choice: dict) -> dict:
111
+ """Extract the assistant message and flag output-token truncation.
112
+
113
+ `finish_reason == "length"` means the model was cut off mid-output. For a
114
+ tool call that carries file content, that means the arguments (and any file
115
+ body inside them) are incomplete and must not be applied blindly.
116
+ """
117
+ msg = dict(choice.get("message") or {})
118
+ msg["_truncated"] = choice.get("finish_reason") == "length"
119
+ return msg
120
+
121
+
106
122
  class Backend(Protocol):
107
123
  label: str
108
124
 
@@ -172,7 +188,7 @@ class AzureBackend:
172
188
  headers = {"api-key": self._api_key, "content-type": "application/json"}
173
189
  r = httpx.post(self._url, json=payload, headers=headers, timeout=_STREAM_TIMEOUT)
174
190
  r.raise_for_status()
175
- return r.json()["choices"][0]["message"]
191
+ return _message_from_choice(r.json()["choices"][0])
176
192
 
177
193
  def stream_chat_tools(self, messages, tools, temperature, max_tokens):
178
194
  payload = {
@@ -246,7 +262,7 @@ class FoundryBackend:
246
262
  }
247
263
  r = httpx.post(self._url, json=payload, headers=headers, timeout=_STREAM_TIMEOUT)
248
264
  r.raise_for_status()
249
- return r.json()["choices"][0]["message"]
265
+ return _message_from_choice(r.json()["choices"][0])
250
266
 
251
267
  def stream_chat_tools(self, messages, tools, temperature, max_tokens):
252
268
  payload = {
@@ -43,7 +43,7 @@ from deepparallel.config import (
43
43
  missing_required,
44
44
  resolve_settings,
45
45
  )
46
- from deepparallel import licensing
46
+ from deepparallel import licensing, supply_chain
47
47
  from deepparallel.fusion import (
48
48
  EscalationBackend,
49
49
  ReasonAnswerBackend,
@@ -504,6 +504,94 @@ def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
504
504
  sys.exit(verdict_exit_code(verdict))
505
505
 
506
506
 
507
+ @main.command()
508
+ @click.argument("path", required=True)
509
+ @click.pass_context
510
+ def audit(ctx: click.Context, path: str) -> None:
511
+ """Supply-chain gate: check a file's dependencies against the real registry.
512
+
513
+ Flags hallucinated / slopsquatted packages (names PyPI or npm has never
514
+ seen) in source files and manifests. Exit code: 0 clean, 2 if a likely
515
+ hallucinated dependency is found - so it can gate a commit or PR. Paid (Pro+).
516
+ """
517
+ ok, msg = licensing.check_feature("audit")
518
+ if not ok:
519
+ branding.error(msg)
520
+ sys.exit(3)
521
+ try:
522
+ content = Path(path).expanduser().read_text(encoding="utf-8")
523
+ except OSError as e:
524
+ branding.error(f"cannot read {path}: {e}")
525
+ sys.exit(3)
526
+ result = supply_chain.audit(content, path)
527
+ findings = result["findings"]
528
+ if not findings:
529
+ console.print(f"[{branding.DIM}]no external dependencies found in {path}[/]")
530
+ sys.exit(0)
531
+ for f in findings:
532
+ glyph = {"ok": branding.CHECK, "missing": branding.CROSS}.get(f["status"], "?")
533
+ color = {"ok": branding.GREEN_HEX, "missing": branding.RED_HEX}.get(
534
+ f["status"], branding.DIM
535
+ )
536
+ console.print(
537
+ f" [{color}]{glyph}[/] {f['name']} [{branding.DIM}]({f['ecosystem']}) · {f['status']}[/]"
538
+ )
539
+ if result["hallucinated"]:
540
+ console.print(
541
+ f"[bold {branding.RED_HEX}]{branding.CROSS} {len(result['hallucinated'])} "
542
+ f"possibly hallucinated:[/] {', '.join(result['hallucinated'])}"
543
+ )
544
+ sys.exit(2)
545
+ console.print(f"[{branding.GREEN_HEX}]{branding.CHECK} all dependencies verified[/]")
546
+ sys.exit(0)
547
+
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
+
507
595
  @main.command(name="tools")
508
596
  def tools_cmd() -> None:
509
597
  """List the agent tools available to DeepParallel."""
@@ -101,7 +101,7 @@ def resolve_settings() -> Settings:
101
101
  foundry_api_key=os.environ.get("FOUNDRY_API_KEY"),
102
102
  foundry_model=os.environ.get("DEEPPARALLEL_FOUNDRY_MODEL", "DeepSeek-V3-1"),
103
103
  temperature=_float_env("DEEPPARALLEL_TEMPERATURE", 0.4),
104
- max_tokens=_int_env("DEEPPARALLEL_MAX_TOKENS", 2048),
104
+ max_tokens=_int_env("DEEPPARALLEL_MAX_TOKENS", 8192),
105
105
  show_thinking=_bool_env("DEEPPARALLEL_THINK", False),
106
106
  tools_enabled=_bool_env("DEEPPARALLEL_TOOLS", True),
107
107
  auto_approve=_bool_env("DEEPPARALLEL_AUTO_APPROVE", False),
@@ -70,6 +70,30 @@ def verify_token(token: str, pubkey_b64: str | None = None) -> dict | None:
70
70
  return payload
71
71
 
72
72
 
73
+ def sign_token(payload: dict, private_key_b64: str) -> str:
74
+ """Sign a license payload into a `body.signature` token (issuer-side).
75
+
76
+ Requires the private issuance key; verify_token() checks it with the public
77
+ key. Used by the license-issuer (Stripe webhook) and the admin CLI.
78
+ """
79
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
80
+
81
+ priv = Ed25519PrivateKey.from_private_bytes(_b64url_decode_std(private_key_b64))
82
+ body = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=")
83
+ sig = base64.urlsafe_b64encode(priv.sign(body.encode())).decode().rstrip("=")
84
+ return f"{body}.{sig}"
85
+
86
+
87
+ def _b64url_decode_std(s: str) -> bytes:
88
+ """Decode standard OR url-safe base64 (keys are emitted as standard b64)."""
89
+ s = s.strip()
90
+ pad = "=" * (-len(s) % 4)
91
+ try:
92
+ return base64.b64decode(s + pad)
93
+ except Exception: # noqa: BLE001
94
+ return base64.urlsafe_b64decode(s + pad)
95
+
96
+
73
97
  def _license_file_token() -> str | None:
74
98
  path = Path(
75
99
  os.environ.get("DEEPPARALLEL_LICENSE_FILE", "~/.config/deepparallel/license")
@@ -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