deepparallel 0.5.7__tar.gz → 0.6.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 (92) hide show
  1. {deepparallel-0.5.7 → deepparallel-0.6.0}/PKG-INFO +1 -1
  2. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/__init__.py +1 -1
  3. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/agent.py +19 -5
  4. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cli.py +36 -2
  5. deepparallel-0.6.0/deepparallel/mesh.py +165 -0
  6. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/serve.py +148 -2
  7. deepparallel-0.6.0/deepparallel/session.py +132 -0
  8. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/PKG-INFO +1 -1
  9. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/SOURCES.txt +3 -0
  10. {deepparallel-0.5.7 → deepparallel-0.6.0}/pyproject.toml +1 -1
  11. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_cli.py +5 -0
  12. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_serve.py +22 -16
  13. deepparallel-0.6.0/tests/test_serve_session.py +40 -0
  14. {deepparallel-0.5.7 → deepparallel-0.6.0}/README.md +0 -0
  15. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/backend.py +0 -0
  16. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/branding.py +0 -0
  17. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cockpit.py +0 -0
  18. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cockpit_observe.py +0 -0
  19. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cockpit_panel.py +0 -0
  20. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cockpit_sim.py +0 -0
  21. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/config.py +0 -0
  22. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/crowe_id.py +0 -0
  23. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/dsml.py +0 -0
  24. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/fusion.py +0 -0
  25. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/licensing.py +0 -0
  26. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/memory.py +0 -0
  27. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/registry.json +0 -0
  28. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/renderer.py +0 -0
  29. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/research/__init__.py +0 -0
  30. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/research/conduit.py +0 -0
  31. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/research/provider.py +0 -0
  32. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/routing.example.json +0 -0
  33. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/routing.py +0 -0
  34. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/supply_chain.py +0 -0
  35. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/system_prompt.txt +0 -0
  36. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/__init__.py +0 -0
  37. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/codeast.py +0 -0
  38. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/edit.py +0 -0
  39. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/files.py +0 -0
  40. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/git_ops.py +0 -0
  41. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/mcp.py +0 -0
  42. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/memory.py +0 -0
  43. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/registry.py +0 -0
  44. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/sandbox.py +0 -0
  45. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/search.py +0 -0
  46. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/shell.py +0 -0
  47. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/vision.py +0 -0
  48. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/web.py +0 -0
  49. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/userinput.py +0 -0
  50. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/dependency_links.txt +0 -0
  51. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/entry_points.txt +0 -0
  52. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/requires.txt +0 -0
  53. {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/top_level.txt +0 -0
  54. {deepparallel-0.5.7 → deepparallel-0.6.0}/setup.cfg +0 -0
  55. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_agent.py +0 -0
  56. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_backend.py +0 -0
  57. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_backend_chat.py +0 -0
  58. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_backend_stream.py +0 -0
  59. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_branding.py +0 -0
  60. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_cockpit.py +0 -0
  61. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_cockpit_panel.py +0 -0
  62. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_cockpit_sim.py +0 -0
  63. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_config.py +0 -0
  64. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_config_file.py +0 -0
  65. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_crowe_backend.py +0 -0
  66. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_crowe_gateway_backend.py +0 -0
  67. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_crowe_id_auth.py +0 -0
  68. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_crowe_payment_required.py +0 -0
  69. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_dsml.py +0 -0
  70. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_fusion.py +0 -0
  71. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_git_ops.py +0 -0
  72. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_issuer_signer.py +0 -0
  73. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_licensing.py +0 -0
  74. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_memory.py +0 -0
  75. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_renderer.py +0 -0
  76. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_research.py +0 -0
  77. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_research_provider.py +0 -0
  78. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_routing.py +0 -0
  79. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_spinner_color.py +0 -0
  80. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_supply_chain.py +0 -0
  81. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tool_registry.py +0 -0
  82. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_codeast.py +0 -0
  83. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_edit.py +0 -0
  84. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_files.py +0 -0
  85. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_mcp.py +0 -0
  86. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_sandbox.py +0 -0
  87. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_search.py +0 -0
  88. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_shell.py +0 -0
  89. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_vision.py +0 -0
  90. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_web.py +0 -0
  91. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_userinput.py +0 -0
  92. {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_userinput_paste.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepparallel
3
- Version: 0.5.7
3
+ Version: 0.6.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
@@ -1,3 +1,3 @@
1
1
  """DeepParallel CLI package."""
2
2
 
3
- __version__ = "0.5.7"
3
+ __version__ = "0.6.0"
@@ -255,16 +255,29 @@ def _supply_chain_note(name: str, args: dict) -> str | None:
255
255
  return None
256
256
 
257
257
 
258
- def _approved(name, args, interactive, auto_approve, renderer, guardian=None) -> bool:
258
+ def _approved(name, args, interactive, auto_approve, renderer, guardian=None, mesh_fn=None) -> bool:
259
259
  forced = name in _GATED_PATH_TOOLS and _outside_cwd(args)
260
260
  sc_note = _supply_chain_note(name, args) if name in _EDIT_TOOLS else None
261
- # A hallucinated dependency overrides auto-approve: always surface it.
262
- if auto_approve and not forced and not sc_note:
261
+ # The verification mesh reviews every edit. A BUG verdict blocks the action
262
+ # and overrides auto-approve, exactly like a hallucinated dependency does.
263
+ mesh_result = None
264
+ if mesh_fn is not None and name in _EDIT_TOOLS:
265
+ try:
266
+ mesh_result = mesh_fn(_guardian_review_content(name, args))
267
+ except Exception: # noqa: BLE001 - the gate is best-effort, never fatal
268
+ mesh_result = None
269
+ blocked = bool(mesh_result and mesh_result.get("severity") == "bug")
270
+ # A hallucinated dependency or a BUG verdict overrides auto-approve.
271
+ if auto_approve and not forced and not sc_note and not blocked:
263
272
  return True
264
273
  if not interactive:
265
274
  return False
266
275
  title, detail = _describe(name, args)
267
- if guardian is not None and name in _EDIT_TOOLS:
276
+ if mesh_result is not None:
277
+ from deepparallel.mesh import format_panel
278
+
279
+ detail = f"{detail}\n\nVerification mesh:\n{format_panel(mesh_result)}"
280
+ elif guardian is not None and name in _EDIT_TOOLS:
268
281
  verdict = _guardian_verdict(guardian, name, args)
269
282
  if verdict:
270
283
  detail = f"{detail}\n\nGuardian: {verdict}"
@@ -307,6 +320,7 @@ def run_agent(
307
320
  max_steps: int | None = None,
308
321
  stream: bool = False,
309
322
  guardian=None,
323
+ mesh_fn=None,
310
324
  on_event=None,
311
325
  ) -> str:
312
326
  steps = max_steps if max_steps is not None else settings.max_steps
@@ -346,7 +360,7 @@ def run_agent(
346
360
  elif "__parse_error__" in args:
347
361
  result = json.dumps({"error": "invalid JSON arguments"})
348
362
  elif meta.dangerous and not _approved(
349
- name, args, interactive, auto_approve, renderer, guardian
363
+ name, args, interactive, auto_approve, renderer, guardian, mesh_fn
350
364
  ):
351
365
  result = json.dumps({"error": "denied by user"})
352
366
  else:
@@ -177,6 +177,22 @@ def _build_guardian(settings: Settings) -> Backend | None:
177
177
  return backend_for_deployment(settings, settings.guardian_deployment)
178
178
 
179
179
 
180
+ def _build_mesh(settings: Settings):
181
+ """The verification mesh: a parallel reviewer panel that gates every edit.
182
+
183
+ Supersedes the single guardian when available. Paid (Pro+) feature; returns
184
+ a callable ``mesh_fn(content) -> result`` or None on Free / when disabled.
185
+ """
186
+ if not settings.guardian_enabled:
187
+ return None
188
+ ok, _ = licensing.check_feature("mesh")
189
+ if not ok:
190
+ return None
191
+ from deepparallel import mesh
192
+
193
+ return lambda content: mesh.mesh_review(settings, content)
194
+
195
+
180
196
  def _system_prompt() -> str:
181
197
  """System prompt with the long-term memory index injected (recall tier 1)."""
182
198
  return load_system_prompt() + memory.index_block()
@@ -293,7 +309,8 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
293
309
  """Interactive agentic loop: tools enabled, conversation persists. The dial
294
310
  (/fast //fuse //escalate //deep) swaps the active fusion mode live."""
295
311
  registry = get_registry()
296
- guardian = _build_guardian(settings)
312
+ mesh_fn = _build_mesh(settings)
313
+ guardian = None if mesh_fn else _build_guardian(settings)
297
314
  system = _system_prompt()
298
315
  messages: list[dict] = [{"role": "system", "content": system}]
299
316
  mode = settings.fusion_mode if settings.fusion_mode in ("reason", "escalate") else "off"
@@ -385,6 +402,7 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
385
402
  auto_approve=auto,
386
403
  stream=True,
387
404
  guardian=guardian,
405
+ mesh_fn=mesh_fn,
388
406
  on_event=observer if cockpit_on else None,
389
407
  )
390
408
  if mode in ("reason", "escalate"):
@@ -518,6 +536,7 @@ def run(
518
536
  renderer,
519
537
  interactive=False,
520
538
  auto_approve=settings.auto_approve,
539
+ mesh_fn=_build_mesh(settings),
521
540
  )
522
541
  except Exception as e: # noqa: BLE001 - surface as friendly message
523
542
  branding.error(_translate_error(e))
@@ -614,10 +633,25 @@ def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
614
633
  branding.error("provide a PATH or --diff (with a diff on stdin)")
615
634
  sys.exit(3)
616
635
  _require_ready(settings) # validates creds / exits if missing
636
+ glyphs = {"safe": branding.CHECK, "risky": "!", "bug": branding.CROSS}
637
+ mesh_ok, _ = licensing.check_feature("mesh")
638
+ if mesh_ok and settings.guardian_enabled:
639
+ # Pro: the full verification mesh, one independent reviewer per lens.
640
+ from deepparallel import mesh as _mesh
641
+
642
+ result = _mesh.mesh_review(settings, content)
643
+ for r in result["lenses"]:
644
+ g = glyphs.get(r["severity"], "?")
645
+ console.print(f" {g} [dim]{r['label']}[/] {r['verdict'] or '(no verdict)'}")
646
+ verdict = result["verdict"]
647
+ glyph = glyphs.get(result["severity"], "?")
648
+ console.print(f"[bold]{glyph} {result['severity'].upper()}[/] {verdict or '(no verdict)'}")
649
+ sys.exit(verdict_exit_code(verdict))
650
+ # Free / BYOK: a single independent reviewer.
617
651
  guardian = backend_for_deployment(settings, settings.guardian_deployment)
618
652
  verdict = guardian_review(guardian, content[:8000])
619
653
  severity = verdict_severity(verdict)
620
- glyph = {"safe": branding.CHECK, "risky": "!", "bug": branding.CROSS}.get(severity, "?")
654
+ glyph = glyphs.get(severity, "?")
621
655
  console.print(f"[bold]{glyph} {severity.upper()}[/] {verdict or '(no verdict)'}")
622
656
  sys.exit(verdict_exit_code(verdict))
623
657
 
@@ -0,0 +1,165 @@
1
+ """The verification mesh: N independent models review a proposed change in
2
+ parallel, then a guardian synthesizes the final gate verdict.
3
+
4
+ This generalizes the single-model ``guardian_review`` into the multi-lens panel
5
+ the hosted /review endpoint already uses (correctness, security, edge cases,
6
+ maintainability) and is the gate the agent loop consults before any mutating
7
+ action. It reuses the same parallel fan-out shape as ``fusion.deep_query`` and
8
+ emits :mod:`deepparallel.session` events so a trace view can render the panel.
9
+
10
+ Pure review: it never executes anything. It returns structured verdicts and a
11
+ decision (allow | hold | block) the caller enforces.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from concurrent.futures import ThreadPoolExecutor, as_completed
17
+
18
+ from deepparallel import session
19
+ from deepparallel.backend import backend_for_deployment
20
+
21
+ # Four independent lenses, mirroring the hosted reviewer panel. Each model is
22
+ # asked for exactly one verdict line so the result is cheap to parse and gate.
23
+ _VERDICT_SHAPE = (
24
+ " Reply with exactly one line: 'VERDICT: safe', 'VERDICT: risky: <reason>', "
25
+ "or 'VERDICT: bug: <reason>'. No preamble, no markdown."
26
+ )
27
+ LENSES = (
28
+ {"key": "correctness", "label": "Correctness",
29
+ "sys": "Independently review this proposed change for correctness and logic errors."},
30
+ {"key": "security", "label": "Security",
31
+ "sys": "Independently review this proposed change for security vulnerabilities "
32
+ "(injection, auth, secrets, unsafe input)."},
33
+ {"key": "edge", "label": "Edge cases",
34
+ "sys": "Independently review this proposed change for edge cases and failure "
35
+ "modes the author likely missed."},
36
+ {"key": "maintainability", "label": "Maintainability",
37
+ "sys": "Independently review this proposed change for clarity and "
38
+ "maintainability problems."},
39
+ )
40
+
41
+ _SEVERITY_RANK = {"safe": 0, "unknown": 1, "risky": 2, "bug": 3}
42
+
43
+
44
+ def verdict_severity(verdict: str | None) -> str:
45
+ """Classify a verdict string into safe | risky | bug | unknown."""
46
+ if not verdict:
47
+ return "unknown"
48
+ head = verdict.strip().lower()
49
+ for sev in ("safe", "risky", "bug"):
50
+ if head.startswith(sev):
51
+ return sev
52
+ return "unknown"
53
+
54
+
55
+ def mesh_decision(severity: str) -> str:
56
+ """Map a severity to a gate decision: allow | hold | block."""
57
+ return {"bug": "block", "risky": "hold"}.get(severity, "allow")
58
+
59
+
60
+ def _parse_verdict(msg: dict | None) -> str | None:
61
+ if not msg:
62
+ return None
63
+ text = (msg.get("content") or msg.get("reasoning_content") or "").strip()
64
+ for line in text.splitlines():
65
+ s = line.strip()
66
+ if s.upper().startswith("VERDICT:"):
67
+ return s.split(":", 1)[1].strip()
68
+ return text.splitlines()[0][:120] if text else None
69
+
70
+
71
+ def _review_one(backend, lens: dict, content: str) -> str | None:
72
+ prompt = (
73
+ f"You are an independent code reviewer. {lens['sys']}{_VERDICT_SHAPE}\n\n"
74
+ f"Proposed change:\n{content}\n\nVerdict:"
75
+ )
76
+ messages = [{"role": "user", "content": prompt}]
77
+ for _ in range(2): # one retry: reviewers run against a sometimes-flaky API
78
+ try:
79
+ return _parse_verdict(backend.chat(messages, [], 0.0, 256))
80
+ except Exception: # noqa: BLE001 - one lens failing must not sink the gate
81
+ continue
82
+ return None
83
+
84
+
85
+ def _reviewer_chains(settings):
86
+ """One backend per lens, drawn from the configured council deployments so
87
+ the lenses are model-diverse; falls back to the guardian deployment."""
88
+ deps = list(settings.parallel_deployments) or [settings.guardian_deployment]
89
+ return [
90
+ (lens, backend_for_deployment(settings, deps[i % len(deps)]))
91
+ for i, lens in enumerate(LENSES)
92
+ ]
93
+
94
+
95
+ def _guardian_synthesis(settings, content: str, lenses: list[dict]) -> str | None:
96
+ blocks = "\n".join(f"{r['label']}: {r['verdict'] or '(no verdict)'}" for r in lenses)
97
+ prompt = (
98
+ "Several reviewers independently checked a proposed code change; their "
99
+ "verdicts are below. Issue the final gate verdict, weighing the most "
100
+ f"severe valid concern.{_VERDICT_SHAPE}\n\n"
101
+ f"Reviewer verdicts:\n{blocks}\n\nThe change:\n{content}\n\nFinal verdict:"
102
+ )
103
+ messages = [{"role": "user", "content": prompt}]
104
+ for _ in range(2):
105
+ try:
106
+ v = _parse_verdict(backend_for_deployment(settings, settings.guardian_deployment)
107
+ .chat(messages, [], 0.0, 256))
108
+ if v:
109
+ return v
110
+ except Exception: # noqa: BLE001 - degrade to the worst lens verdict
111
+ break
112
+ return _worst_verdict(lenses)
113
+
114
+
115
+ def _worst_verdict(lenses: list[dict]) -> str | None:
116
+ """Fallback final verdict: the single most severe lens verdict."""
117
+ ranked = sorted(lenses, key=lambda r: _SEVERITY_RANK.get(r["severity"], 1), reverse=True)
118
+ return ranked[0]["verdict"] if ranked else None
119
+
120
+
121
+ def mesh_review(settings, content: str, *, on_event=None, max_chars: int = 8000) -> dict:
122
+ """Run the parallel verification mesh over ``content``.
123
+
124
+ Returns ``{lenses, verdict, severity, decision}`` where ``lenses`` is a list
125
+ of ``{key, label, verdict, severity}`` and ``decision`` is allow|hold|block.
126
+ ``on_event`` (optional) receives :mod:`deepparallel.session` events.
127
+ """
128
+ content = (content or "")[:max_chars]
129
+ chains = _reviewer_chains(settings)
130
+ if on_event:
131
+ on_event(session.verify_start(content.splitlines()[0][:80] if content else "change"))
132
+
133
+ results: list[dict | None] = [None] * len(chains)
134
+ with ThreadPoolExecutor(max_workers=min(8, max(1, len(chains)))) as pool:
135
+ futures = {
136
+ pool.submit(_review_one, backend, lens, content): (i, lens)
137
+ for i, (lens, backend) in enumerate(chains)
138
+ }
139
+ for future in as_completed(futures):
140
+ i, lens = futures[future]
141
+ v = future.result()
142
+ sev = verdict_severity(v)
143
+ results[i] = {"key": lens["key"], "label": lens["label"], "verdict": v, "severity": sev}
144
+ if on_event:
145
+ on_event(session.verdict(lens["label"], v, sev))
146
+
147
+ lenses = [r for r in results if r is not None]
148
+ final = _guardian_synthesis(settings, content, lenses)
149
+ fsev = verdict_severity(final)
150
+ decision = mesh_decision(fsev)
151
+ if on_event:
152
+ on_event(session.gate(decision, final, fsev))
153
+ return {"lenses": lenses, "verdict": final, "severity": fsev, "decision": decision}
154
+
155
+
156
+ def format_panel(result: dict) -> str:
157
+ """A compact multi-line rendering of a mesh result for a confirm card / CLI."""
158
+ glyph = {"safe": "✓", "risky": "!", "bug": "✗", "unknown": "?"}
159
+ lines = [
160
+ f" {glyph.get(r['severity'], '?')} {r['label']}: {r['verdict'] or '(no verdict)'}"
161
+ for r in result.get("lenses", [])
162
+ ]
163
+ final = result.get("verdict") or "(no verdict)"
164
+ lines.append(f" = Gate [{result.get('decision', 'allow').upper()}]: {final}")
165
+ return "\n".join(lines)
@@ -21,12 +21,72 @@ persona/scrub layer stay identical, so the IDE never has to change.
21
21
  from __future__ import annotations
22
22
 
23
23
  import json
24
+ import os
24
25
  import re
25
26
  import time
27
+ import uuid
26
28
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
29
+ from pathlib import Path
27
30
 
28
- from deepparallel.backend import backend_for_deployment
31
+ from deepparallel import memory, session as S
32
+ from deepparallel.backend import backend_for_deployment, resolve_backend
29
33
  from deepparallel.config import load_system_prompt, resolve_settings
34
+ from deepparallel.renderer import Renderer
35
+
36
+
37
+ def _sessions_dir() -> Path:
38
+ d = Path(os.environ.get("DEEPPARALLEL_SESSIONS_DIR", "~/.config/deepparallel/sessions")).expanduser()
39
+ d.mkdir(parents=True, exist_ok=True)
40
+ return d
41
+
42
+
43
+ class SessionRenderer(Renderer):
44
+ """Maps the agent loop's renderer calls onto the canonical session schema,
45
+ so /v1/session emits the exact same events as the in-process run."""
46
+
47
+ def __init__(self, send):
48
+ self._send = send # send(event_dict) -> stamps seq + writes SSE
49
+ self._step = 0
50
+ self._last_tool = "tool"
51
+
52
+ def answer(self, text: str) -> None:
53
+ if text:
54
+ self._send(S.answer(text))
55
+
56
+ def answer_stream(self, chunks) -> str:
57
+ full = ""
58
+ for c in chunks:
59
+ full += c
60
+ self._send(S.token(c))
61
+ return full
62
+
63
+ def tool_start(self, name: str, args_preview: str) -> None:
64
+ self._step += 1
65
+ self._last_tool = name
66
+ self._send(S.step(self._step, intent=name))
67
+ self._send(S.tool_call(name, args_preview))
68
+
69
+ def tool_result(self, ok: bool, summary: str, duration_s: float) -> None:
70
+ self._send(S.tool_result(self._last_tool, summary, ok))
71
+ self._send(S.step_done(self._step))
72
+
73
+ def confirm(self, action_title: str, detail: str) -> bool:
74
+ # Non-interactive: auto-approve. The mesh gate (a BUG verdict) is what
75
+ # actually blocks an edit, and it streams verdict/gate events of its own.
76
+ return True
77
+
78
+ def error(self, msg: str) -> None:
79
+ self._send(S.error(msg))
80
+
81
+ def reasoning(self, text: str) -> None:
82
+ if text:
83
+ self._send(S.token(text, channel="reasoning"))
84
+
85
+ def welcome(self, *args, **kwargs) -> None:
86
+ pass # no banner on the wire
87
+
88
+ def attribution_footer(self, *args, **kwargs) -> None:
89
+ pass # no footer on the wire
30
90
  from deepparallel.dsml import DSMLStreamFilter, strip_dsml
31
91
  from deepparallel.routing import (
32
92
  RoutingTable,
@@ -262,10 +322,14 @@ def _make_handler(
262
322
  if self.path.rstrip("/") in ("/health", "/healthz"):
263
323
  self._json(200, {"status": "ok"})
264
324
  return
325
+ if self.path.startswith("/v1/session/"):
326
+ self._replay_session(self.path.rstrip("/").rsplit("/", 1)[-1])
327
+ return
265
328
  self._json(404, {"error": {"message": "not found"}})
266
329
 
267
330
  def do_POST(self):
268
- if self.path.rstrip("/") != "/v1/chat/completions":
331
+ path = self.path.rstrip("/")
332
+ if path not in ("/v1/chat/completions", "/v1/session"):
269
333
  self._json(404, {"error": {"message": "not found"}})
270
334
  return
271
335
  length = int(self.headers.get("Content-Length", 0))
@@ -274,6 +338,9 @@ def _make_handler(
274
338
  except json.JSONDecodeError:
275
339
  self._json(400, {"error": {"message": "invalid JSON body"}})
276
340
  return
341
+ if path == "/v1/session":
342
+ self._run_session(req)
343
+ return
277
344
 
278
345
  model = req.get("model") or "crowelm-apex"
279
346
  # Tier routing: principal tier from the bearer (paid) or its absence
@@ -357,6 +424,85 @@ def _make_handler(
357
424
  },
358
425
  )
359
426
 
427
+ def _run_session(self, req):
428
+ """Run a full plan->act->verify agent session and stream the canonical
429
+ session.py event schema as SSE. The first end-to-end session over HTTP."""
430
+ prompt = str(req.get("prompt") or req.get("input") or "").strip()
431
+ if not prompt:
432
+ self._json(400, {"error": {"message": "missing 'prompt'"}})
433
+ return
434
+ from deepparallel import licensing, mesh as M
435
+ from deepparallel.agent import run_agent
436
+ from deepparallel.tools import get_registry
437
+
438
+ self.send_response(200)
439
+ self.send_header("Content-Type", "text/event-stream")
440
+ self.send_header("Cache-Control", "no-cache")
441
+ self.send_header("Connection", "keep-alive")
442
+ self.end_headers()
443
+
444
+ sid = "sess_" + uuid.uuid4().hex[:12]
445
+ transcript: list[dict] = []
446
+
447
+ def sink(ev):
448
+ transcript.append(ev)
449
+ self.wfile.write(S.to_sse(ev).encode())
450
+ self.wfile.flush()
451
+
452
+ stream = S.EventStream(sink) # stamps seq, swallows write errors if client drops
453
+ send = stream.emit
454
+
455
+ try:
456
+ send(S.start(sid, goal=prompt))
457
+ sys_prompt = load_system_prompt() + memory.index_block()
458
+ messages = [
459
+ {"role": "system", "content": sys_prompt},
460
+ {"role": "user", "content": prompt},
461
+ ]
462
+ backend = resolve_backend(settings)
463
+ registry = get_registry()
464
+ mesh_fn = None
465
+ if getattr(settings, "guardian_enabled", False) and licensing.check_feature("mesh")[0]:
466
+ mesh_fn = lambda content: M.mesh_review(settings, content, on_event=send)
467
+ renderer = SessionRenderer(send)
468
+ run_agent(
469
+ backend, registry, messages, settings, renderer,
470
+ interactive=False, auto_approve=True, stream=True,
471
+ max_steps=req.get("max_steps"), mesh_fn=mesh_fn,
472
+ )
473
+ send(S.done())
474
+ except Exception as e: # noqa: BLE001 - report, then close cleanly
475
+ try:
476
+ send(S.error(f"{type(e).__name__}: {e}"))
477
+ send(S.done("error"))
478
+ except Exception:
479
+ pass
480
+ try:
481
+ (_sessions_dir() / (sid + ".jsonl")).write_text(
482
+ "\n".join(json.dumps(e) for e in transcript), encoding="utf-8"
483
+ )
484
+ except Exception:
485
+ pass
486
+
487
+ def _replay_session(self, sid):
488
+ """Replay a stored session transcript as SSE (resumption / sharing)."""
489
+ safe = re.sub(r"[^a-zA-Z0-9_]", "", sid or "")
490
+ path = _sessions_dir() / (safe + ".jsonl")
491
+ if not safe or not path.is_file():
492
+ self._json(404, {"error": {"message": "session not found"}})
493
+ return
494
+ self.send_response(200)
495
+ self.send_header("Content-Type", "text/event-stream")
496
+ self.send_header("Cache-Control", "no-cache")
497
+ self.end_headers()
498
+ try:
499
+ for line in path.read_text(encoding="utf-8").splitlines():
500
+ if line.strip():
501
+ self.wfile.write(("data: " + line + "\n\n").encode())
502
+ self.wfile.flush()
503
+ except (BrokenPipeError, OSError):
504
+ pass
505
+
360
506
  return Handler
361
507
 
362
508
 
@@ -0,0 +1,132 @@
1
+ """Canonical DeepParallel agent-session event schema.
2
+
3
+ One event vocabulary shared by every surface (CLI, hosted API, desktop, web).
4
+ It generalizes the hosted /review SSE stream (start / token / verdict / done)
5
+ into a full agent-session stream so a single trace view can render a run on any
6
+ surface: the council plan, each tool step, and the verification-mesh verdicts.
7
+
8
+ Events are plain dicts with a ``type`` discriminator and a monotonic ``seq``
9
+ (assigned by an :class:`EventStream`). ``to_sse`` serializes one event to a
10
+ Server-Sent-Events frame so the same producer feeds an HTTP stream or a local
11
+ renderer.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from itertools import count
18
+ from typing import Any, Callable, Iterable
19
+
20
+ # --- event factories ---------------------------------------------------------
21
+ # Each returns a bare dict; an EventStream stamps `seq`. Keep payloads small and
22
+ # JSON-serializable so they cross the wire unchanged.
23
+
24
+
25
+ def start(session_id: str | None = None, goal: str | None = None) -> dict:
26
+ return _e("start", session_id=session_id, goal=goal)
27
+
28
+
29
+ def plan(text: str, agreement: dict | None = None) -> dict:
30
+ """The council's synthesized plan (and optional cross-model agreement)."""
31
+ return _e("plan", text=text, agreement=agreement)
32
+
33
+
34
+ def chain(name: str, content: str) -> dict:
35
+ """One council chain's raw answer (a single parallel reasoning stream)."""
36
+ return _e("chain", name=name, content=content)
37
+
38
+
39
+ def step(index: int, intent: str | None = None) -> dict:
40
+ return _e("step", index=index, intent=intent)
41
+
42
+
43
+ def token(text: str, channel: str = "content") -> dict:
44
+ return _e("token", text=text, channel=channel)
45
+
46
+
47
+ def tool_call(name: str, preview: str) -> dict:
48
+ return _e("tool_call", name=name, preview=preview)
49
+
50
+
51
+ def tool_result(name: str, summary: str, ok: bool) -> dict:
52
+ return _e("tool_result", name=name, summary=summary, ok=ok)
53
+
54
+
55
+ def verify_start(action: str) -> dict:
56
+ """The verification mesh has begun reviewing a proposed action."""
57
+ return _e("verify_start", action=action)
58
+
59
+
60
+ def verdict(lens: str, verdict_text: str | None, severity: str) -> dict:
61
+ """One independent reviewer's verdict for a single lens."""
62
+ return _e("verdict", lens=lens, verdict=verdict_text, severity=severity)
63
+
64
+
65
+ def gate(decision: str, verdict_text: str | None, severity: str) -> dict:
66
+ """The guardian's final gate: decision is allow | hold | block."""
67
+ return _e("gate", decision=decision, verdict=verdict_text, severity=severity)
68
+
69
+
70
+ def step_done(index: int) -> dict:
71
+ return _e("step_done", index=index)
72
+
73
+
74
+ def answer(text: str) -> dict:
75
+ return _e("answer", text=text)
76
+
77
+
78
+ def error(message: str) -> dict:
79
+ return _e("error", message=message)
80
+
81
+
82
+ def done(status: str = "ok") -> dict:
83
+ return _e("done", status=status)
84
+
85
+
86
+ def _e(type_: str, **fields: Any) -> dict:
87
+ """Build an event, dropping None-valued fields to keep frames compact."""
88
+ ev = {"type": type_}
89
+ ev.update({k: v for k, v in fields.items() if v is not None})
90
+ return ev
91
+
92
+
93
+ # --- serialization / streaming ----------------------------------------------
94
+
95
+ EVENT_TYPES = (
96
+ "start", "plan", "chain", "step", "token", "tool_call", "tool_result",
97
+ "verify_start", "verdict", "gate", "step_done", "answer", "error", "done",
98
+ )
99
+
100
+
101
+ def to_sse(event: dict) -> str:
102
+ """Serialize one event to a Server-Sent-Events ``data:`` frame."""
103
+ return "data: " + json.dumps(event, ensure_ascii=False) + "\n\n"
104
+
105
+
106
+ class EventStream:
107
+ """Stamps a monotonic ``seq`` on each event and forwards it to a sink.
108
+
109
+ The sink is any ``callable(dict) -> None`` (write to an HTTP response, push
110
+ to a renderer, append to a JSONL trace). Use as ``emit(session.plan(...))``.
111
+ """
112
+
113
+ def __init__(self, sink: Callable[[dict], None] | None = None):
114
+ self._sink = sink
115
+ self._seq = count()
116
+
117
+ def emit(self, event: dict) -> dict:
118
+ event = {**event, "seq": next(self._seq)}
119
+ if self._sink is not None:
120
+ try:
121
+ self._sink(event)
122
+ except Exception: # noqa: BLE001 - observation must never break a run
123
+ pass
124
+ return event
125
+
126
+ __call__ = emit
127
+
128
+
129
+ def sse_stream(events: Iterable[dict]) -> Iterable[str]:
130
+ """Adapt an iterable of events into SSE frames."""
131
+ for ev in events:
132
+ yield to_sse(ev)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepparallel
3
- Version: 0.5.7
3
+ Version: 0.6.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
@@ -15,11 +15,13 @@ deepparallel/dsml.py
15
15
  deepparallel/fusion.py
16
16
  deepparallel/licensing.py
17
17
  deepparallel/memory.py
18
+ deepparallel/mesh.py
18
19
  deepparallel/registry.json
19
20
  deepparallel/renderer.py
20
21
  deepparallel/routing.example.json
21
22
  deepparallel/routing.py
22
23
  deepparallel/serve.py
24
+ deepparallel/session.py
23
25
  deepparallel/supply_chain.py
24
26
  deepparallel/system_prompt.txt
25
27
  deepparallel/userinput.py
@@ -71,6 +73,7 @@ tests/test_research.py
71
73
  tests/test_research_provider.py
72
74
  tests/test_routing.py
73
75
  tests/test_serve.py
76
+ tests/test_serve_session.py
74
77
  tests/test_spinner_color.py
75
78
  tests/test_supply_chain.py
76
79
  tests/test_tool_registry.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepparallel"
7
- version = "0.5.7"
7
+ version = "0.6.0"
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" }
@@ -138,6 +138,9 @@ class _GuardianStub:
138
138
 
139
139
  def test_review_file_safe_exits_zero(monkeypatch, tmp_path):
140
140
  _set_azure_env(monkeypatch)
141
+ # Test the single-guardian exit-code contract deterministically, regardless of
142
+ # any ambient license (the multi-model mesh path has its own coverage).
143
+ monkeypatch.setattr(licensing, "check_feature", lambda feature, *a, **k: (feature != "mesh", ""))
141
144
  f = tmp_path / "ok.py"
142
145
  f.write_text("def add(a, b):\n return a + b\n")
143
146
  monkeypatch.setattr(climod, "resolve_backend", lambda s: _GuardianStub("safe"))
@@ -149,6 +152,7 @@ def test_review_file_safe_exits_zero(monkeypatch, tmp_path):
149
152
 
150
153
  def test_review_bug_exits_two(monkeypatch, tmp_path):
151
154
  _set_azure_env(monkeypatch)
155
+ monkeypatch.setattr(licensing, "check_feature", lambda feature, *a, **k: (feature != "mesh", ""))
152
156
  f = tmp_path / "bad.py"
153
157
  f.write_text("def f(u):\n return u.balance\n")
154
158
  monkeypatch.setattr(climod, "resolve_backend", lambda s: _GuardianStub("bug: null deref"))
@@ -162,6 +166,7 @@ def test_review_bug_exits_two(monkeypatch, tmp_path):
162
166
 
163
167
  def test_review_diff_from_stdin(monkeypatch):
164
168
  _set_azure_env(monkeypatch)
169
+ monkeypatch.setattr(licensing, "check_feature", lambda feature, *a, **k: (feature != "mesh", ""))
165
170
  monkeypatch.setattr(climod, "resolve_backend", lambda s: _GuardianStub("risky: no test"))
166
171
  monkeypatch.setattr(
167
172
  climod, "backend_for_deployment", lambda s, d: _GuardianStub("risky: no test")
@@ -49,29 +49,34 @@ def _ids(payload):
49
49
  return [entry["id"] for entry in payload["data"]]
50
50
 
51
51
 
52
- def test_models_payload_lists_only_configured_public_aliases():
52
+ # The gateway lists the full public CroweLM catalog (a fixed alias set),
53
+ # independent of which upstream deployments the local settings configured. The
54
+ # security-critical invariant: no raw upstream deployment name ever leaks.
55
+ _RAW_MARKERS = ("DeepSeek", "Kimi", "grok", "gpt-", "Llama", "Mistral", "Granite", "Qwen", "Cohere", "command-")
56
+
57
+
58
+ def test_models_payload_lists_public_aliases_no_raw_leak():
53
59
  ids = _ids(_models_payload(_settings()))
54
60
 
55
- assert ids == ["crowelm-apex", "crowelm-reason", "crowelm-flash"]
56
- assert "crowelm-quasar" not in ids
57
- assert "DeepSeek-V4-Pro" not in ids
61
+ assert {"crowelm-apex", "crowelm-reason", "crowelm-flash"} <= set(ids)
62
+ assert all(i.startswith("crowelm-") for i in ids)
63
+ assert not any(marker in i for i in ids for marker in _RAW_MARKERS)
58
64
 
59
65
 
60
- def test_models_payload_includes_additional_configured_aliases():
61
- settings = _settings(
62
- parallel_deployments=("DeepSeek-V4-Pro", "Kimi-K2-6"),
63
- judge_deployment="grok-4-3",
66
+ def test_models_payload_is_stable_across_configured_deployments():
67
+ base = _ids(_models_payload(_settings()))
68
+ other = _ids(
69
+ _models_payload(
70
+ _settings(parallel_deployments=("DeepSeek-V4-Pro", "Kimi-K2-6"), judge_deployment="grok-4-3")
71
+ )
64
72
  )
65
73
 
66
- ids = _ids(_models_payload(settings))
67
-
68
- assert "crowelm-apex" in ids
69
- assert "crowelm-eclipse" in ids
70
- assert "crowelm-quasar" in ids
71
- assert "crowelm-titan" not in ids
74
+ # the public catalog does not depend on local deployment config
75
+ assert base == other
76
+ assert "crowelm-apex" in base
72
77
 
73
78
 
74
- def test_models_endpoint_returns_filtered_model_list():
79
+ def test_models_endpoint_returns_public_catalog():
75
80
  settings = _settings(
76
81
  parallel_deployments=("DeepSeek-V4-Pro", "Kimi-K2-6"),
77
82
  reasoner_deployment="",
@@ -91,7 +96,8 @@ def test_models_endpoint_returns_filtered_model_list():
91
96
  handler_cls.do_GET(handler)
92
97
 
93
98
  assert captured["code"] == 200
94
- assert _ids(captured["payload"]) == ["crowelm-apex", "crowelm-eclipse"]
99
+ ids = _ids(captured["payload"])
100
+ assert "crowelm-apex" in ids and all(i.startswith("crowelm-") for i in ids)
95
101
 
96
102
 
97
103
  def test_chat_accepts_public_alias_and_raw_deployment_for_compatibility():
@@ -0,0 +1,40 @@
1
+ """The /v1/session endpoint maps the agent loop onto the canonical session
2
+ schema. These tests are deterministic (no model calls): they exercise the
3
+ renderer-to-event mapping and the request validation directly."""
4
+
5
+ from deepparallel import serve, session
6
+
7
+
8
+ def test_session_renderer_emits_canonical_schema():
9
+ events = []
10
+ r = serve.SessionRenderer(events.append)
11
+
12
+ r.tool_start("write_file", "calc.py")
13
+ r.tool_result(True, "wrote 42 bytes", 0.12)
14
+ full = r.answer_stream(iter(["hel", "lo"]))
15
+ r.answer("final")
16
+
17
+ assert full == "hello"
18
+ types = [e["type"] for e in events]
19
+ # an act step: tool_call -> tool_result, bracketed by step / step_done
20
+ assert types.index("step") < types.index("tool_call") < types.index("tool_result")
21
+ assert "step_done" in types
22
+ # streamed tokens, one event per chunk
23
+ assert types.count("token") == 2
24
+ # the tool_result carries the tool name and ok flag
25
+ tr = next(e for e in events if e["type"] == "tool_result")
26
+ assert tr["name"] == "write_file" and tr["ok"] is True
27
+
28
+
29
+ def test_session_renderer_confirm_auto_approves():
30
+ # non-interactive: confirm never blocks (the mesh gate does the blocking)
31
+ r = serve.SessionRenderer(lambda e: None)
32
+ assert r.confirm("write_file", "diff") is True
33
+
34
+
35
+ def test_session_event_factories_roundtrip_sse():
36
+ frame = session.to_sse(session.gate("block", "bug: sqli", "bug"))
37
+ assert frame.startswith("data: ") and frame.endswith("\n\n")
38
+ import json
39
+ payload = json.loads(frame[len("data: "):].strip())
40
+ assert payload["type"] == "gate" and payload["decision"] == "block" and payload["severity"] == "bug"
File without changes
File without changes