deepparallel 0.5.5__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.5 → deepparallel-0.6.0}/PKG-INFO +1 -1
  2. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/__init__.py +1 -1
  3. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/agent.py +19 -5
  4. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/branding.py +3 -3
  5. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cli.py +56 -8
  6. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/config.py +47 -0
  7. deepparallel-0.6.0/deepparallel/memory.py +186 -0
  8. deepparallel-0.6.0/deepparallel/mesh.py +165 -0
  9. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/serve.py +148 -2
  10. deepparallel-0.6.0/deepparallel/session.py +132 -0
  11. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/__init__.py +2 -0
  12. deepparallel-0.6.0/deepparallel/tools/git_ops.py +185 -0
  13. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/mcp.py +69 -2
  14. deepparallel-0.6.0/deepparallel/tools/memory.py +37 -0
  15. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/PKG-INFO +1 -1
  16. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/SOURCES.txt +9 -0
  17. {deepparallel-0.5.5 → deepparallel-0.6.0}/pyproject.toml +1 -1
  18. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_cli.py +5 -0
  19. deepparallel-0.6.0/tests/test_config_file.py +26 -0
  20. deepparallel-0.6.0/tests/test_git_ops.py +116 -0
  21. deepparallel-0.6.0/tests/test_memory.py +68 -0
  22. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_serve.py +22 -16
  23. deepparallel-0.6.0/tests/test_serve_session.py +40 -0
  24. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_mcp.py +37 -0
  25. {deepparallel-0.5.5 → deepparallel-0.6.0}/README.md +0 -0
  26. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/backend.py +0 -0
  27. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cockpit.py +0 -0
  28. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cockpit_observe.py +0 -0
  29. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cockpit_panel.py +0 -0
  30. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cockpit_sim.py +0 -0
  31. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/crowe_id.py +0 -0
  32. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/dsml.py +0 -0
  33. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/fusion.py +0 -0
  34. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/licensing.py +0 -0
  35. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/registry.json +0 -0
  36. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/renderer.py +0 -0
  37. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/research/__init__.py +0 -0
  38. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/research/conduit.py +0 -0
  39. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/research/provider.py +0 -0
  40. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/routing.example.json +0 -0
  41. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/routing.py +0 -0
  42. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/supply_chain.py +0 -0
  43. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/system_prompt.txt +0 -0
  44. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/codeast.py +0 -0
  45. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/edit.py +0 -0
  46. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/files.py +0 -0
  47. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/registry.py +0 -0
  48. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/sandbox.py +0 -0
  49. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/search.py +0 -0
  50. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/shell.py +0 -0
  51. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/vision.py +0 -0
  52. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/web.py +0 -0
  53. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/userinput.py +0 -0
  54. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/dependency_links.txt +0 -0
  55. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/entry_points.txt +0 -0
  56. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/requires.txt +0 -0
  57. {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/top_level.txt +0 -0
  58. {deepparallel-0.5.5 → deepparallel-0.6.0}/setup.cfg +0 -0
  59. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_agent.py +0 -0
  60. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_backend.py +0 -0
  61. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_backend_chat.py +0 -0
  62. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_backend_stream.py +0 -0
  63. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_branding.py +0 -0
  64. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_cockpit.py +0 -0
  65. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_cockpit_panel.py +0 -0
  66. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_cockpit_sim.py +0 -0
  67. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_config.py +0 -0
  68. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_crowe_backend.py +0 -0
  69. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_crowe_gateway_backend.py +0 -0
  70. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_crowe_id_auth.py +0 -0
  71. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_crowe_payment_required.py +0 -0
  72. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_dsml.py +0 -0
  73. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_fusion.py +0 -0
  74. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_issuer_signer.py +0 -0
  75. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_licensing.py +0 -0
  76. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_renderer.py +0 -0
  77. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_research.py +0 -0
  78. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_research_provider.py +0 -0
  79. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_routing.py +0 -0
  80. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_spinner_color.py +0 -0
  81. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_supply_chain.py +0 -0
  82. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tool_registry.py +0 -0
  83. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_codeast.py +0 -0
  84. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_edit.py +0 -0
  85. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_files.py +0 -0
  86. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_sandbox.py +0 -0
  87. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_search.py +0 -0
  88. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_shell.py +0 -0
  89. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_vision.py +0 -0
  90. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_web.py +0 -0
  91. {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_userinput.py +0 -0
  92. {deepparallel-0.5.5 → 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.5
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.5"
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:
@@ -373,12 +373,12 @@ def status_text(
373
373
  body.append(f" {DOT} v{version} {DOT} ", style=DIM)
374
374
  body.append("served via Crowe Logic\n", style=f"bold {CROWE_ACCENT}")
375
375
  body.append(f"{tool_count} tools", style=WHITE_HEX)
376
- body.append(f" {DOT} 5,800+ MCP servers", style=DIM)
377
- body.append(f" {DOT} fusion: {fusion}\n", style=DIM)
376
+ body.append(f" {DOT} MCP registry (local + 5,800+)", style=DIM)
377
+ body.append(f" {DOT} memory {DOT} fusion: {fusion}\n", style=DIM)
378
378
  body.append("Backend: ", style=DIM)
379
379
  body.append(f"{backend_label}\n\n", style="white")
380
380
  body.append("/help", style="bold")
381
- body.append(" /tools /fast //deep //fuse ", style=DIM)
381
+ body.append(" /tools /memory /cockpit /fast //deep //fuse ", style=DIM)
382
382
  body.append("/quit", style="bold")
383
383
  return _gutter(body)
384
384
 
@@ -45,7 +45,7 @@ from deepparallel.config import (
45
45
  missing_required,
46
46
  resolve_settings,
47
47
  )
48
- from deepparallel import licensing, supply_chain
48
+ from deepparallel import licensing, memory, supply_chain
49
49
  from deepparallel.fusion import (
50
50
  EscalationBackend,
51
51
  ReasonAnswerBackend,
@@ -177,6 +177,27 @@ 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
+
196
+ def _system_prompt() -> str:
197
+ """System prompt with the long-term memory index injected (recall tier 1)."""
198
+ return load_system_prompt() + memory.index_block()
199
+
200
+
180
201
  def _make_renderer(*, force_plain: bool, assume_yes: bool = False) -> Renderer:
181
202
  plain = force_plain or _bool_env("DEEPPARALLEL_PLAIN", False) or not sys.stdout.isatty()
182
203
  if plain:
@@ -233,7 +254,7 @@ def _stream_once(backend: Backend, settings: Settings, messages: list[dict]) ->
233
254
 
234
255
  def _stream_repl(backend: Backend, settings: Settings) -> None:
235
256
  """Plain-chat interactive loop (no tools)."""
236
- system = load_system_prompt()
257
+ system = _system_prompt()
237
258
  history: list[tuple[str, str]] = []
238
259
  while True:
239
260
  try:
@@ -288,8 +309,9 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
288
309
  """Interactive agentic loop: tools enabled, conversation persists. The dial
289
310
  (/fast //fuse //escalate //deep) swaps the active fusion mode live."""
290
311
  registry = get_registry()
291
- guardian = _build_guardian(settings)
292
- system = load_system_prompt()
312
+ mesh_fn = _build_mesh(settings)
313
+ guardian = None if mesh_fn else _build_guardian(settings)
314
+ system = _system_prompt()
293
315
  messages: list[dict] = [{"role": "system", "content": system}]
294
316
  mode = settings.fusion_mode if settings.fusion_mode in ("reason", "escalate") else "off"
295
317
  deep_next = False
@@ -315,7 +337,7 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
315
337
  break
316
338
  if user_msg == "/help":
317
339
  branding.info(
318
- "/quit · /reset · /info · /tools · /auto · /cockpit · /fast //fuse //escalate //deep · prompt"
340
+ "/quit · /reset · /info · /tools · /auto · /memory · /cockpit · /fast //fuse //escalate //deep · prompt"
319
341
  )
320
342
  continue
321
343
  if user_msg in {"/auto", "/yes"}:
@@ -354,6 +376,15 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
354
376
  cockpit.status("done", "cockpit complete")
355
377
  branding.info("cockpit OFF.")
356
378
  continue
379
+ if user_msg == "/memory":
380
+ mems = memory.list_memories()
381
+ if mems:
382
+ branding.info(f"{len(mems)} memories saved. Most recent:")
383
+ for m in mems[-8:]:
384
+ console.print(f" [dim]-[/] {m['name']}: {m['description'][:64]}")
385
+ else:
386
+ branding.info("no memories yet; the agent saves them with the remember tool.")
387
+ continue
357
388
 
358
389
  messages.append({"role": "user", "content": user_msg})
359
390
  if deep_next:
@@ -371,6 +402,7 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
371
402
  auto_approve=auto,
372
403
  stream=True,
373
404
  guardian=guardian,
405
+ mesh_fn=mesh_fn,
374
406
  on_event=observer if cockpit_on else None,
375
407
  )
376
408
  if mode in ("reason", "escalate"):
@@ -482,7 +514,7 @@ def run(
482
514
  if fuse is not None:
483
515
  settings = replace(settings, fusion_mode=fuse)
484
516
  backend = _require_ready(settings)
485
- system = load_system_prompt()
517
+ system = _system_prompt()
486
518
  messages = _build_messages([], system, " ".join(prompt))
487
519
  if deep:
488
520
  _run_deep(settings, messages)
@@ -504,6 +536,7 @@ def run(
504
536
  renderer,
505
537
  interactive=False,
506
538
  auto_approve=settings.auto_approve,
539
+ mesh_fn=_build_mesh(settings),
507
540
  )
508
541
  except Exception as e: # noqa: BLE001 - surface as friendly message
509
542
  branding.error(_translate_error(e))
@@ -530,7 +563,7 @@ def cockpit(ctx: click.Context, assume_yes: bool, prompt: tuple[str, ...]) -> No
530
563
  cp.status("start", "cockpit online")
531
564
  cp.ensure_panel()
532
565
  backend = _wrap_fusion(backend, settings)
533
- system = load_system_prompt()
566
+ system = _system_prompt()
534
567
  messages = _build_messages([], system, " ".join(prompt))
535
568
  renderer = _make_renderer(force_plain=False, assume_yes=settings.auto_approve)
536
569
  try:
@@ -600,10 +633,25 @@ def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
600
633
  branding.error("provide a PATH or --diff (with a diff on stdin)")
601
634
  sys.exit(3)
602
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.
603
651
  guardian = backend_for_deployment(settings, settings.guardian_deployment)
604
652
  verdict = guardian_review(guardian, content[:8000])
605
653
  severity = verdict_severity(verdict)
606
- glyph = {"safe": branding.CHECK, "risky": "!", "bug": branding.CROSS}.get(severity, "?")
654
+ glyph = glyphs.get(severity, "?")
607
655
  console.print(f"[bold]{glyph} {severity.upper()}[/] {verdict or '(no verdict)'}")
608
656
  sys.exit(verdict_exit_code(verdict))
609
657
 
@@ -104,7 +104,54 @@ def _int_env(name: str, default: int) -> int:
104
104
  return default
105
105
 
106
106
 
107
+ _TOML_TO_ENV = {
108
+ "backend.backend": "DEEPPARALLEL_BACKEND",
109
+ "backend.deployment": "DEEPPARALLEL_DEPLOYMENT",
110
+ "backend.temperature": "DEEPPARALLEL_TEMPERATURE",
111
+ "backend.max_tokens": "DEEPPARALLEL_MAX_TOKENS",
112
+ "backend.parallel_models": "DEEPPARALLEL_PARALLEL_MODELS",
113
+ "fusion.mode": "DEEPPARALLEL_FUSION",
114
+ "tools.auto_approve": "DEEPPARALLEL_AUTO_APPROVE",
115
+ "memory.enabled": "DEEPPARALLEL_MEMORY",
116
+ "memory.dir": "DEEPPARALLEL_MEMORY_DIR",
117
+ "mcp.config": "DEEPPARALLEL_MCP_CONFIG",
118
+ }
119
+
120
+
121
+ def _apply_config_file() -> None:
122
+ """Layer ~/.config/deepparallel/config.toml under the environment.
123
+
124
+ Sets each known DEEPPARALLEL_* env var only when unset, so precedence is
125
+ explicit env var > config.toml > built-in default. Missing or malformed
126
+ config is ignored. Path override: DEEPPARALLEL_CONFIG.
127
+ """
128
+ import tomllib
129
+
130
+ path = os.environ.get("DEEPPARALLEL_CONFIG") or os.path.expanduser(
131
+ "~/.config/deepparallel/config.toml"
132
+ )
133
+ try:
134
+ with open(path, "rb") as fh:
135
+ data = tomllib.load(fh)
136
+ except (OSError, ValueError):
137
+ return
138
+ for dotted, env_name in _TOML_TO_ENV.items():
139
+ if os.environ.get(env_name) is not None:
140
+ continue
141
+ section, key = dotted.split(".", 1)
142
+ val = (data.get(section) or {}).get(key)
143
+ if val is None:
144
+ continue
145
+ if isinstance(val, bool):
146
+ os.environ[env_name] = "true" if val else "false"
147
+ elif isinstance(val, list):
148
+ os.environ[env_name] = ",".join(str(x) for x in val)
149
+ else:
150
+ os.environ[env_name] = str(val)
151
+
152
+
107
153
  def resolve_settings() -> Settings:
154
+ _apply_config_file()
108
155
  backend = os.environ.get("DEEPPARALLEL_BACKEND", "azure").strip().lower()
109
156
  if backend not in {"azure", "foundry", "crowe", "openai", "ollama"}:
110
157
  backend = "azure"
@@ -0,0 +1,186 @@
1
+ # Copyright 2026, Crowe Logic Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Persistent, continuously-growing memory for DeepParallel.
5
+
6
+ This is what makes CroweLM a learning model in practice: durable facts persist
7
+ across sessions and feed back into context. Two recall tiers:
8
+
9
+ - index: MEMORY.md is injected into the system prompt every session, so the
10
+ model remembers the user and their projects without being re-told.
11
+ - retrieval: recall(query) ranks individual memories by relevance (TF-IDF
12
+ cosine over tokens, stdlib only) for an on-demand, semantic-style pull.
13
+
14
+ Memories are markdown files with frontmatter under the memory directory
15
+ (default ~/.config/deepparallel/memory; override DEEPPARALLEL_MEMORY_DIR).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ import os
22
+ import re
23
+ import time
24
+ from pathlib import Path
25
+
26
+ INDEX_FILE = "MEMORY.md"
27
+ _SLUG_RE = re.compile(r"[^a-z0-9]+")
28
+ _TOK_RE = re.compile(r"[a-z0-9]+")
29
+ _FM_RE = re.compile(r"^---\n(.*?)\n---\n?(.*)$", re.S)
30
+
31
+
32
+ def memory_dir() -> Path:
33
+ raw = os.environ.get("DEEPPARALLEL_MEMORY_DIR") or os.path.expanduser(
34
+ "~/.config/deepparallel/memory"
35
+ )
36
+ return Path(raw)
37
+
38
+
39
+ def enabled() -> bool:
40
+ return os.environ.get("DEEPPARALLEL_MEMORY", "on").strip().lower() not in {
41
+ "0", "off", "false", "no",
42
+ }
43
+
44
+
45
+ def _slugify(name: str) -> str:
46
+ return _SLUG_RE.sub("-", name.strip().lower()).strip("-") or "memory"
47
+
48
+
49
+ def _unique_slug(d: Path, slug: str) -> str:
50
+ cand, i = slug, 2
51
+ while (d / f"{cand}.md").exists():
52
+ cand, i = f"{slug}-{i}", i + 1
53
+ return cand
54
+
55
+
56
+ def load_index() -> str:
57
+ try:
58
+ return (memory_dir() / INDEX_FILE).read_text(encoding="utf-8").strip()
59
+ except OSError:
60
+ return ""
61
+
62
+
63
+ def index_block() -> str:
64
+ """The memory index formatted for system-prompt injection (empty if none)."""
65
+ idx = load_index()
66
+ if not idx or not enabled():
67
+ return ""
68
+ return (
69
+ "\n\nLong-term memory (recalled from past sessions; background context, "
70
+ "verify file/line specifics before relying on them):\n" + idx
71
+ )
72
+
73
+
74
+ def save_memory(content: str, name: str = "", description: str = "", mtype: str = "note") -> str:
75
+ """Write one durable fact as a markdown file and add an index line."""
76
+ d = memory_dir()
77
+ d.mkdir(parents=True, exist_ok=True)
78
+ first_line = content.strip().split("\n", 1)[0]
79
+ title = (name or first_line)[:60].strip() or "memory"
80
+ slug = _unique_slug(d, _slugify(title))
81
+ desc = (description or first_line).replace("\n", " ").strip()[:120]
82
+ body = (
83
+ f"---\nname: {title}\ndescription: {desc}\ntype: {mtype}\n"
84
+ f"created: {int(time.time())}\n---\n\n{content.strip()}\n"
85
+ )
86
+ (d / f"{slug}.md").write_text(body, encoding="utf-8")
87
+ _append_index(d, slug, title, desc)
88
+ return slug
89
+
90
+
91
+ def _append_index(d: Path, slug: str, title: str, desc: str) -> None:
92
+ idx = d / INDEX_FILE
93
+ existing = idx.read_text(encoding="utf-8") if idx.exists() else "# DeepParallel Memory\n\n"
94
+ if f"]({slug}.md)" in existing:
95
+ return
96
+ if not existing.endswith("\n"):
97
+ existing += "\n"
98
+ idx.write_text(existing + f"- [{title}]({slug}.md) - {desc}\n", encoding="utf-8")
99
+
100
+
101
+ def _parse(text: str, fallback: str) -> tuple[str, str, str]:
102
+ m = _FM_RE.match(text)
103
+ if not m:
104
+ return fallback, "", text.strip()
105
+ meta = {}
106
+ for line in m.group(1).splitlines():
107
+ if ":" in line:
108
+ k, v = line.split(":", 1)
109
+ meta[k.strip()] = v.strip()
110
+ return meta.get("name", fallback), meta.get("description", ""), m.group(2).strip()
111
+
112
+
113
+ def list_memories() -> list[dict]:
114
+ d = memory_dir()
115
+ out = []
116
+ if not d.exists():
117
+ return out
118
+ for p in sorted(d.glob("*.md")):
119
+ if p.name == INDEX_FILE:
120
+ continue
121
+ name, desc, body = _parse(p.read_text(encoding="utf-8", errors="replace"), p.stem)
122
+ out.append({"name": name, "description": desc, "content": body, "path": str(p)})
123
+ return out
124
+
125
+
126
+ def _tokenize(text: str) -> list[str]:
127
+ return _TOK_RE.findall(text.lower())
128
+
129
+
130
+ def recall(query: str, limit: int = 5) -> list[dict]:
131
+ """Relevance-ranked memories via TF-IDF cosine over tokens (stdlib only)."""
132
+ mems = list_memories()
133
+ if not mems or not query.strip():
134
+ return []
135
+ docs = [_tokenize(f"{m['name']} {m['description']} {m['content']}") for m in mems]
136
+ df: dict[str, int] = {}
137
+ for toks in docs:
138
+ for t in set(toks):
139
+ df[t] = df.get(t, 0) + 1
140
+ n = len(docs)
141
+
142
+ def vec(toks: list[str]) -> dict[str, float]:
143
+ if not toks:
144
+ return {}
145
+ tf: dict[str, int] = {}
146
+ for t in toks:
147
+ tf[t] = tf.get(t, 0) + 1
148
+ return {t: (c / len(toks)) * math.log((n + 1) / (df.get(t, 0) + 1) + 1) for t, c in tf.items()}
149
+
150
+ qv = vec(_tokenize(query))
151
+ qa = math.sqrt(sum(v * v for v in qv.values()))
152
+ scored = []
153
+ for m, toks in zip(mems, docs):
154
+ dv = vec(toks)
155
+ da = math.sqrt(sum(v * v for v in dv.values()))
156
+ num = sum(qv.get(t, 0.0) * dv.get(t, 0.0) for t in qv)
157
+ score = num / (da * qa) if da and qa else 0.0
158
+ if score > 0:
159
+ scored.append(
160
+ {"name": m["name"], "description": m["description"], "content": m["content"],
161
+ "score": round(score, 4)}
162
+ )
163
+ scored.sort(key=lambda x: x["score"], reverse=True)
164
+ return scored[:limit]
165
+
166
+
167
+ def _rebuild_index(d: Path) -> None:
168
+ lines = ["# DeepParallel Memory", ""]
169
+ for m in list_memories():
170
+ lines.append(f"- [{m['name']}]({Path(m['path']).stem}.md) - {m['description']}")
171
+ (d / INDEX_FILE).write_text("\n".join(lines) + "\n", encoding="utf-8")
172
+
173
+
174
+ def forget(name: str) -> bool:
175
+ d = memory_dir()
176
+ target = d / f"{_slugify(name)}.md"
177
+ if not target.exists():
178
+ for p in d.glob("*.md"):
179
+ if p.stem == name and p.name != INDEX_FILE:
180
+ target = p
181
+ break
182
+ if target.exists() and target.name != INDEX_FILE:
183
+ target.unlink()
184
+ _rebuild_index(d)
185
+ return True
186
+ return False
@@ -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)