deepparallel 0.5.4__tar.gz → 0.5.7__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.4 → deepparallel-0.5.7}/PKG-INFO +1 -1
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/__init__.py +1 -1
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/branding.py +3 -3
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/cli.py +42 -16
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/cockpit.py +20 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/cockpit_observe.py +9 -2
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/config.py +47 -0
- deepparallel-0.5.7/deepparallel/memory.py +186 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/__init__.py +2 -0
- deepparallel-0.5.7/deepparallel/tools/git_ops.py +185 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/mcp.py +69 -2
- deepparallel-0.5.7/deepparallel/tools/memory.py +37 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel.egg-info/PKG-INFO +1 -1
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel.egg-info/SOURCES.txt +6 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/pyproject.toml +1 -1
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_cockpit.py +41 -0
- deepparallel-0.5.7/tests/test_config_file.py +26 -0
- deepparallel-0.5.7/tests/test_git_ops.py +116 -0
- deepparallel-0.5.7/tests/test_memory.py +68 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tools_mcp.py +37 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/README.md +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/agent.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/backend.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/cockpit_panel.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/cockpit_sim.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/crowe_id.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/dsml.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/fusion.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/licensing.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/registry.json +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/renderer.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/research/__init__.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/research/conduit.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/research/provider.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/routing.example.json +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/routing.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/serve.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/supply_chain.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/system_prompt.txt +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/codeast.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/edit.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/files.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/registry.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/sandbox.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/search.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/shell.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/vision.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/tools/web.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel/userinput.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel.egg-info/dependency_links.txt +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel.egg-info/entry_points.txt +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel.egg-info/requires.txt +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/deepparallel.egg-info/top_level.txt +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/setup.cfg +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_agent.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_backend.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_backend_chat.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_backend_stream.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_branding.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_cli.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_cockpit_panel.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_cockpit_sim.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_config.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_crowe_backend.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_crowe_gateway_backend.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_crowe_id_auth.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_crowe_payment_required.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_dsml.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_fusion.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_issuer_signer.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_licensing.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_renderer.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_research.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_research_provider.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_routing.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_serve.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_spinner_color.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_supply_chain.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tool_registry.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tools_codeast.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tools_edit.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tools_files.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tools_sandbox.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tools_search.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tools_shell.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tools_vision.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_tools_web.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/tests/test_userinput.py +0 -0
- {deepparallel-0.5.4 → deepparallel-0.5.7}/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.
|
|
3
|
+
Version: 0.5.7
|
|
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
|
|
@@ -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
|
|
|
@@ -17,7 +17,6 @@ apply to chat and run.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import os
|
|
20
|
-
import subprocess
|
|
21
20
|
import sys
|
|
22
21
|
from dataclasses import replace
|
|
23
22
|
from pathlib import Path
|
|
@@ -46,7 +45,7 @@ from deepparallel.config import (
|
|
|
46
45
|
missing_required,
|
|
47
46
|
resolve_settings,
|
|
48
47
|
)
|
|
49
|
-
from deepparallel import licensing, supply_chain
|
|
48
|
+
from deepparallel import licensing, memory, supply_chain
|
|
50
49
|
from deepparallel.fusion import (
|
|
51
50
|
EscalationBackend,
|
|
52
51
|
ReasonAnswerBackend,
|
|
@@ -178,6 +177,11 @@ def _build_guardian(settings: Settings) -> Backend | None:
|
|
|
178
177
|
return backend_for_deployment(settings, settings.guardian_deployment)
|
|
179
178
|
|
|
180
179
|
|
|
180
|
+
def _system_prompt() -> str:
|
|
181
|
+
"""System prompt with the long-term memory index injected (recall tier 1)."""
|
|
182
|
+
return load_system_prompt() + memory.index_block()
|
|
183
|
+
|
|
184
|
+
|
|
181
185
|
def _make_renderer(*, force_plain: bool, assume_yes: bool = False) -> Renderer:
|
|
182
186
|
plain = force_plain or _bool_env("DEEPPARALLEL_PLAIN", False) or not sys.stdout.isatty()
|
|
183
187
|
if plain:
|
|
@@ -234,7 +238,7 @@ def _stream_once(backend: Backend, settings: Settings, messages: list[dict]) ->
|
|
|
234
238
|
|
|
235
239
|
def _stream_repl(backend: Backend, settings: Settings) -> None:
|
|
236
240
|
"""Plain-chat interactive loop (no tools)."""
|
|
237
|
-
system =
|
|
241
|
+
system = _system_prompt()
|
|
238
242
|
history: list[tuple[str, str]] = []
|
|
239
243
|
while True:
|
|
240
244
|
try:
|
|
@@ -290,11 +294,18 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
290
294
|
(/fast //fuse //escalate //deep) swaps the active fusion mode live."""
|
|
291
295
|
registry = get_registry()
|
|
292
296
|
guardian = _build_guardian(settings)
|
|
293
|
-
system =
|
|
297
|
+
system = _system_prompt()
|
|
294
298
|
messages: list[dict] = [{"role": "system", "content": system}]
|
|
295
299
|
mode = settings.fusion_mode if settings.fusion_mode in ("reason", "escalate") else "off"
|
|
296
300
|
deep_next = False
|
|
297
301
|
auto = settings.auto_approve
|
|
302
|
+
cockpit = Cockpit(os.path.abspath(os.path.join(".cockpit", "events.jsonl")))
|
|
303
|
+
cockpit_on = cockpit.in_waveterm()
|
|
304
|
+
observer = make_observer(cockpit)
|
|
305
|
+
if cockpit_on:
|
|
306
|
+
cockpit.events_path.parent.mkdir(parents=True, exist_ok=True)
|
|
307
|
+
cockpit.events_path.write_text("")
|
|
308
|
+
cockpit.status("start", "cockpit ready")
|
|
298
309
|
while True:
|
|
299
310
|
bits = ([mode] if mode != "off" else []) + (["auto"] if auto else [])
|
|
300
311
|
tag = f"[{' · '.join(bits)}] " if bits else ""
|
|
@@ -309,7 +320,7 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
309
320
|
break
|
|
310
321
|
if user_msg == "/help":
|
|
311
322
|
branding.info(
|
|
312
|
-
"/quit · /reset · /info · /tools · /auto · /fast //fuse //escalate //deep · prompt"
|
|
323
|
+
"/quit · /reset · /info · /tools · /auto · /memory · /cockpit · /fast //fuse //escalate //deep · prompt"
|
|
313
324
|
)
|
|
314
325
|
continue
|
|
315
326
|
if user_msg in {"/auto", "/yes"}:
|
|
@@ -337,6 +348,26 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
337
348
|
deep_next = True
|
|
338
349
|
branding.info("next prompt runs as a multi-model deep query")
|
|
339
350
|
continue
|
|
351
|
+
if user_msg == "/cockpit":
|
|
352
|
+
cockpit_on = not cockpit_on
|
|
353
|
+
if cockpit_on:
|
|
354
|
+
cockpit.events_path.parent.mkdir(parents=True, exist_ok=True)
|
|
355
|
+
cockpit.events_path.write_text("")
|
|
356
|
+
cockpit._panel_opened = False
|
|
357
|
+
branding.info("cockpit ON - the data window opens when research starts.")
|
|
358
|
+
else:
|
|
359
|
+
cockpit.status("done", "cockpit complete")
|
|
360
|
+
branding.info("cockpit OFF.")
|
|
361
|
+
continue
|
|
362
|
+
if user_msg == "/memory":
|
|
363
|
+
mems = memory.list_memories()
|
|
364
|
+
if mems:
|
|
365
|
+
branding.info(f"{len(mems)} memories saved. Most recent:")
|
|
366
|
+
for m in mems[-8:]:
|
|
367
|
+
console.print(f" [dim]-[/] {m['name']}: {m['description'][:64]}")
|
|
368
|
+
else:
|
|
369
|
+
branding.info("no memories yet; the agent saves them with the remember tool.")
|
|
370
|
+
continue
|
|
340
371
|
|
|
341
372
|
messages.append({"role": "user", "content": user_msg})
|
|
342
373
|
if deep_next:
|
|
@@ -354,12 +385,15 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
354
385
|
auto_approve=auto,
|
|
355
386
|
stream=True,
|
|
356
387
|
guardian=guardian,
|
|
388
|
+
on_event=observer if cockpit_on else None,
|
|
357
389
|
)
|
|
358
390
|
if mode in ("reason", "escalate"):
|
|
359
391
|
branding.info("reasoned by CroweLM Reason · answered by DeepParallel")
|
|
360
392
|
except Exception as e: # noqa: BLE001 - surface as friendly message
|
|
361
393
|
renderer.error(_translate_error(e))
|
|
362
394
|
|
|
395
|
+
if cockpit_on:
|
|
396
|
+
cockpit.status("done", "cockpit complete")
|
|
363
397
|
branding.attribution_footer()
|
|
364
398
|
|
|
365
399
|
|
|
@@ -462,7 +496,7 @@ def run(
|
|
|
462
496
|
if fuse is not None:
|
|
463
497
|
settings = replace(settings, fusion_mode=fuse)
|
|
464
498
|
backend = _require_ready(settings)
|
|
465
|
-
system =
|
|
499
|
+
system = _system_prompt()
|
|
466
500
|
messages = _build_messages([], system, " ".join(prompt))
|
|
467
501
|
if deep:
|
|
468
502
|
_run_deep(settings, messages)
|
|
@@ -508,17 +542,9 @@ def cockpit(ctx: click.Context, assume_yes: bool, prompt: tuple[str, ...]) -> No
|
|
|
508
542
|
open(events_path, "w").close()
|
|
509
543
|
cp = Cockpit(events_path)
|
|
510
544
|
cp.status("start", "cockpit online")
|
|
511
|
-
|
|
512
|
-
try:
|
|
513
|
-
subprocess.Popen(
|
|
514
|
-
["wsh", "run", "--", "dp", "_cockpit-panel", events_path],
|
|
515
|
-
stdout=subprocess.DEVNULL,
|
|
516
|
-
stderr=subprocess.DEVNULL,
|
|
517
|
-
)
|
|
518
|
-
except Exception: # noqa: BLE001 - the panel block is best-effort
|
|
519
|
-
pass
|
|
545
|
+
cp.ensure_panel()
|
|
520
546
|
backend = _wrap_fusion(backend, settings)
|
|
521
|
-
system =
|
|
547
|
+
system = _system_prompt()
|
|
522
548
|
messages = _build_messages([], system, " ".join(prompt))
|
|
523
549
|
renderer = _make_renderer(force_plain=False, assume_yes=settings.auto_approve)
|
|
524
550
|
try:
|
|
@@ -45,6 +45,7 @@ class Cockpit:
|
|
|
45
45
|
|
|
46
46
|
def __init__(self, events_path: str = ".cockpit/events.jsonl"):
|
|
47
47
|
self.events_path = Path(events_path)
|
|
48
|
+
self._panel_opened = False
|
|
48
49
|
|
|
49
50
|
# --- environment -------------------------------------------------------
|
|
50
51
|
|
|
@@ -119,6 +120,25 @@ class Cockpit:
|
|
|
119
120
|
"""Best-effort `wsh notify`. Silent no-op off WaveTerm or on failure."""
|
|
120
121
|
self._run_wsh(["notify", message])
|
|
121
122
|
|
|
123
|
+
def ensure_panel(self) -> None:
|
|
124
|
+
"""Open the live data-window block once per session (idempotent).
|
|
125
|
+
|
|
126
|
+
Spawns `dp _cockpit-panel <abs-path>` in an adjacent WaveTerm block on
|
|
127
|
+
the first call so the cockpit appears only when research actually
|
|
128
|
+
starts; repeat calls and runs outside WaveTerm are no-ops.
|
|
129
|
+
"""
|
|
130
|
+
if self._panel_opened or not self.in_waveterm():
|
|
131
|
+
return
|
|
132
|
+
self._panel_opened = True
|
|
133
|
+
try:
|
|
134
|
+
subprocess.Popen(
|
|
135
|
+
["wsh", "run", "--", "dp", "_cockpit-panel", str(self.events_path.resolve())],
|
|
136
|
+
stdout=subprocess.DEVNULL,
|
|
137
|
+
stderr=subprocess.DEVNULL,
|
|
138
|
+
)
|
|
139
|
+
except Exception: # noqa: BLE001 - the panel block is best-effort
|
|
140
|
+
pass
|
|
141
|
+
|
|
122
142
|
# --- contract event helpers -------------------------------------------
|
|
123
143
|
|
|
124
144
|
def status(self, phase: str, message: str) -> None:
|
|
@@ -31,8 +31,11 @@ def make_observer(cockpit) -> Callable[[str, dict, str], None]:
|
|
|
31
31
|
try:
|
|
32
32
|
if isinstance(obj, dict) and "error" not in obj:
|
|
33
33
|
if name == "web_search":
|
|
34
|
+
results = obj.get("results") or []
|
|
35
|
+
if results:
|
|
36
|
+
cockpit.ensure_panel()
|
|
34
37
|
provider = obj.get("provider", "web")
|
|
35
|
-
for r in
|
|
38
|
+
for r in results[:8]:
|
|
36
39
|
url = r.get("url")
|
|
37
40
|
if not url:
|
|
38
41
|
continue
|
|
@@ -45,11 +48,15 @@ def make_observer(cockpit) -> Callable[[str, dict, str], None]:
|
|
|
45
48
|
elif name == "web_fetch":
|
|
46
49
|
url = obj.get("url") or (args or {}).get("url", "")
|
|
47
50
|
if url:
|
|
51
|
+
cockpit.ensure_panel()
|
|
48
52
|
cockpit.source(url=url, title=obj.get("title", ""), provider="fetch")
|
|
49
53
|
if counters["blocks"] < _MAX_WEB_BLOCKS and cockpit.open_web(url):
|
|
50
54
|
counters["blocks"] += 1
|
|
51
55
|
elif name == "mcp_search":
|
|
52
|
-
|
|
56
|
+
servers = obj.get("servers") or []
|
|
57
|
+
if servers:
|
|
58
|
+
cockpit.ensure_panel()
|
|
59
|
+
for s in servers[:5]:
|
|
53
60
|
cockpit.source(url="", title=s.get("name", ""), provider="mcp")
|
|
54
61
|
cockpit.status("tool", name)
|
|
55
62
|
except Exception: # noqa: BLE001 - observation must never break the loop
|
|
@@ -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,185 @@
|
|
|
1
|
+
"""Git operations tools: status, diff, log, branch, add, commit, clone."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from deepparallel.tools import tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _repo(repo_path: str) -> Path:
|
|
14
|
+
return Path(repo_path).expanduser().resolve()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_repo(repo: Path) -> bool:
|
|
18
|
+
try:
|
|
19
|
+
r = subprocess.run(
|
|
20
|
+
["git", "-C", str(repo), "rev-parse", "--git-dir"],
|
|
21
|
+
capture_output=True, text=True, timeout=10,
|
|
22
|
+
)
|
|
23
|
+
return r.returncode == 0
|
|
24
|
+
except Exception: # noqa: BLE001
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run_git(repo: Path, args: list[str], timeout_seconds: int = 30) -> dict:
|
|
29
|
+
"""Run a git command in repo and return a result dict.
|
|
30
|
+
|
|
31
|
+
Surfaces failures as {"error": ...} instead of raising.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
r = subprocess.run(
|
|
35
|
+
["git", *args],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
timeout=timeout_seconds,
|
|
39
|
+
cwd=str(repo),
|
|
40
|
+
)
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
return {"error": "git executable not found on PATH"}
|
|
43
|
+
except subprocess.TimeoutExpired:
|
|
44
|
+
return {"error": f"git command timed out after {timeout_seconds}s"}
|
|
45
|
+
except Exception as e: # noqa: BLE001 - surface failure to the model
|
|
46
|
+
return {"error": f"{type(e).__name__}: {e}"}
|
|
47
|
+
stdout = r.stdout or ""
|
|
48
|
+
if len(stdout) > 50000:
|
|
49
|
+
stdout = stdout[:50000] + "\n... (output truncated at 50KB)"
|
|
50
|
+
return {
|
|
51
|
+
"output": stdout.strip(),
|
|
52
|
+
"stderr": (r.stderr or "").strip()[:10000],
|
|
53
|
+
"return_code": r.returncode,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@tool(dangerous=False)
|
|
58
|
+
def git_status(repo_path: str) -> str:
|
|
59
|
+
"""Show the working tree status of a git repository.
|
|
60
|
+
|
|
61
|
+
:param repo_path: Path to the git repository.
|
|
62
|
+
"""
|
|
63
|
+
repo = _repo(repo_path)
|
|
64
|
+
if not _is_repo(repo):
|
|
65
|
+
return json.dumps({"error": f"Not a git repository: {repo_path}"})
|
|
66
|
+
return json.dumps(_run_git(repo, ["status", "--porcelain=v2", "--branch"]))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@tool(dangerous=False)
|
|
70
|
+
def git_diff(repo_path: str, staged: bool = False) -> str:
|
|
71
|
+
"""Show changes in the working directory or staging area.
|
|
72
|
+
|
|
73
|
+
:param repo_path: Path to the git repository.
|
|
74
|
+
:param staged: If true, show staged changes; default shows unstaged changes.
|
|
75
|
+
"""
|
|
76
|
+
repo = _repo(repo_path)
|
|
77
|
+
if not _is_repo(repo):
|
|
78
|
+
return json.dumps({"error": f"Not a git repository: {repo_path}"})
|
|
79
|
+
args = ["diff"]
|
|
80
|
+
if staged:
|
|
81
|
+
args.append("--cached")
|
|
82
|
+
return json.dumps(_run_git(repo, args))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@tool(dangerous=False)
|
|
86
|
+
def git_log(repo_path: str, count: int = 10) -> str:
|
|
87
|
+
"""Show recent commit history as one line per commit.
|
|
88
|
+
|
|
89
|
+
:param repo_path: Path to the git repository.
|
|
90
|
+
:param count: Number of commits to show (capped at 50).
|
|
91
|
+
"""
|
|
92
|
+
repo = _repo(repo_path)
|
|
93
|
+
if not _is_repo(repo):
|
|
94
|
+
return json.dumps({"error": f"Not a git repository: {repo_path}"})
|
|
95
|
+
count = max(1, min(int(count), 50))
|
|
96
|
+
return json.dumps(_run_git(repo, ["log", f"-{count}", "--oneline", "--decorate"]))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@tool(dangerous=False)
|
|
100
|
+
def git_branch(repo_path: str) -> str:
|
|
101
|
+
"""List branches and report the current branch.
|
|
102
|
+
|
|
103
|
+
:param repo_path: Path to the git repository.
|
|
104
|
+
"""
|
|
105
|
+
repo = _repo(repo_path)
|
|
106
|
+
if not _is_repo(repo):
|
|
107
|
+
return json.dumps({"error": f"Not a git repository: {repo_path}"})
|
|
108
|
+
listing = _run_git(repo, ["branch", "--list"])
|
|
109
|
+
if "error" in listing:
|
|
110
|
+
return json.dumps(listing)
|
|
111
|
+
current = _run_git(repo, ["rev-parse", "--abbrev-ref", "HEAD"])
|
|
112
|
+
branches = []
|
|
113
|
+
for line in listing["output"].splitlines():
|
|
114
|
+
name = line.replace("*", "", 1).strip()
|
|
115
|
+
if name:
|
|
116
|
+
branches.append(name)
|
|
117
|
+
return json.dumps(
|
|
118
|
+
{
|
|
119
|
+
"branches": branches,
|
|
120
|
+
"current": current.get("output", ""),
|
|
121
|
+
"return_code": listing["return_code"],
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@tool(dangerous=True)
|
|
127
|
+
def git_add(repo_path: str, paths: str = ".") -> str:
|
|
128
|
+
"""Stage files for the next commit.
|
|
129
|
+
|
|
130
|
+
:param repo_path: Path to the git repository.
|
|
131
|
+
:param paths: Space-separated pathspecs to stage (default "." for all).
|
|
132
|
+
"""
|
|
133
|
+
repo = _repo(repo_path)
|
|
134
|
+
if not _is_repo(repo):
|
|
135
|
+
return json.dumps({"error": f"Not a git repository: {repo_path}"})
|
|
136
|
+
pathspecs = shlex.split(paths) if paths.strip() else ["."]
|
|
137
|
+
return json.dumps(_run_git(repo, ["add", "--", *pathspecs]))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@tool(dangerous=True)
|
|
141
|
+
def git_commit(repo_path: str, message: str, add_all: bool = False) -> str:
|
|
142
|
+
"""Create a commit, optionally staging all tracked changes first.
|
|
143
|
+
|
|
144
|
+
:param repo_path: Path to the git repository.
|
|
145
|
+
:param message: The commit message.
|
|
146
|
+
:param add_all: If true, stage all tracked changes before committing.
|
|
147
|
+
"""
|
|
148
|
+
repo = _repo(repo_path)
|
|
149
|
+
if not _is_repo(repo):
|
|
150
|
+
return json.dumps({"error": f"Not a git repository: {repo_path}"})
|
|
151
|
+
args = ["commit", "-m", message]
|
|
152
|
+
if add_all:
|
|
153
|
+
args.append("--all")
|
|
154
|
+
return json.dumps(_run_git(repo, args))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@tool(dangerous=True)
|
|
158
|
+
def git_clone(url: str, target_path: str) -> str:
|
|
159
|
+
"""Clone a git repository into a local path.
|
|
160
|
+
|
|
161
|
+
:param url: The repository URL to clone.
|
|
162
|
+
:param target_path: Local path to clone into.
|
|
163
|
+
"""
|
|
164
|
+
target = Path(target_path).expanduser().resolve()
|
|
165
|
+
try:
|
|
166
|
+
r = subprocess.run(
|
|
167
|
+
["git", "clone", url, str(target)],
|
|
168
|
+
capture_output=True,
|
|
169
|
+
text=True,
|
|
170
|
+
timeout=300,
|
|
171
|
+
)
|
|
172
|
+
except FileNotFoundError:
|
|
173
|
+
return json.dumps({"error": "git executable not found on PATH"})
|
|
174
|
+
except subprocess.TimeoutExpired:
|
|
175
|
+
return json.dumps({"error": "git clone timed out after 300s"})
|
|
176
|
+
except Exception as e: # noqa: BLE001 - surface failure to the model
|
|
177
|
+
return json.dumps({"error": f"{type(e).__name__}: {e}"})
|
|
178
|
+
return json.dumps(
|
|
179
|
+
{
|
|
180
|
+
"output": (r.stdout or "").strip(),
|
|
181
|
+
"stderr": (r.stderr or "").strip()[:10000],
|
|
182
|
+
"return_code": r.returncode,
|
|
183
|
+
"target_path": str(target),
|
|
184
|
+
}
|
|
185
|
+
)
|