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.
- {deepparallel-0.5.5 → deepparallel-0.6.0}/PKG-INFO +1 -1
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/__init__.py +1 -1
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/agent.py +19 -5
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/branding.py +3 -3
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cli.py +56 -8
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/config.py +47 -0
- deepparallel-0.6.0/deepparallel/memory.py +186 -0
- deepparallel-0.6.0/deepparallel/mesh.py +165 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/serve.py +148 -2
- deepparallel-0.6.0/deepparallel/session.py +132 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/__init__.py +2 -0
- deepparallel-0.6.0/deepparallel/tools/git_ops.py +185 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/mcp.py +69 -2
- deepparallel-0.6.0/deepparallel/tools/memory.py +37 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/PKG-INFO +1 -1
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/SOURCES.txt +9 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/pyproject.toml +1 -1
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_cli.py +5 -0
- deepparallel-0.6.0/tests/test_config_file.py +26 -0
- deepparallel-0.6.0/tests/test_git_ops.py +116 -0
- deepparallel-0.6.0/tests/test_memory.py +68 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_serve.py +22 -16
- deepparallel-0.6.0/tests/test_serve_session.py +40 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_mcp.py +37 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/README.md +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/backend.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cockpit.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cockpit_observe.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cockpit_panel.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/cockpit_sim.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/crowe_id.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/dsml.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/fusion.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/licensing.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/registry.json +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/renderer.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/research/__init__.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/research/conduit.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/research/provider.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/routing.example.json +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/routing.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/supply_chain.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/system_prompt.txt +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/codeast.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/edit.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/files.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/registry.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/sandbox.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/search.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/shell.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/vision.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/tools/web.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel/userinput.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/dependency_links.txt +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/entry_points.txt +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/requires.txt +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/deepparallel.egg-info/top_level.txt +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/setup.cfg +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_agent.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_backend.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_backend_chat.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_backend_stream.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_branding.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_cockpit.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_cockpit_panel.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_cockpit_sim.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_config.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_crowe_backend.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_crowe_gateway_backend.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_crowe_id_auth.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_crowe_payment_required.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_dsml.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_fusion.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_issuer_signer.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_licensing.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_renderer.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_research.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_research_provider.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_routing.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_spinner_color.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_supply_chain.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tool_registry.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_codeast.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_edit.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_files.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_sandbox.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_search.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_shell.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_vision.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_tools_web.py +0 -0
- {deepparallel-0.5.5 → deepparallel-0.6.0}/tests/test_userinput.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
#
|
|
262
|
-
|
|
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
|
|
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+
|
|
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 =
|
|
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
|
-
|
|
292
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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)
|