@agxnte/reidx 2.0.2
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.
- package/README.md +724 -0
- package/bin/reidx.js +33 -0
- package/package.json +40 -0
- package/pyproject.toml +45 -0
- package/scripts/find-python.js +24 -0
- package/scripts/postinstall.js +35 -0
- package/src/reidx/__init__.py +3 -0
- package/src/reidx/__main__.py +9 -0
- package/src/reidx/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/app/__init__.py +3 -0
- package/src/reidx/app/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/app/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/app/commands.py +221 -0
- package/src/reidx/automation/__init__.py +3 -0
- package/src/reidx/automation/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/automation/__pycache__/exec.cpython-312.pyc +0 -0
- package/src/reidx/automation/exec.py +35 -0
- package/src/reidx/config/__init__.py +10 -0
- package/src/reidx/config/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/loader.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/settings.cpython-312.pyc +0 -0
- package/src/reidx/config/loader.py +104 -0
- package/src/reidx/config/models.py +49 -0
- package/src/reidx/config/settings.py +127 -0
- package/src/reidx/deepreid/__init__.py +8 -0
- package/src/reidx/deepreid/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/deepreid/__pycache__/pipeline.cpython-312.pyc +0 -0
- package/src/reidx/deepreid/pipeline.py +221 -0
- package/src/reidx/diagnostics/__init__.py +0 -0
- package/src/reidx/diagnostics/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/diagnostics/__pycache__/logger.cpython-312.pyc +0 -0
- package/src/reidx/diagnostics/logger.py +56 -0
- package/src/reidx/goals/__init__.py +21 -0
- package/src/reidx/goals/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/goals/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/goals/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/goals/models.py +101 -0
- package/src/reidx/goals/store.py +233 -0
- package/src/reidx/integrations/__init__.py +3 -0
- package/src/reidx/integrations/mcp.py +70 -0
- package/src/reidx/nyx/__init__.py +3 -0
- package/src/reidx/nyx/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/nyx/__pycache__/persona.cpython-312.pyc +0 -0
- package/src/reidx/nyx/persona.py +35 -0
- package/src/reidx/policy/__init__.py +15 -0
- package/src/reidx/policy/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/policy/__pycache__/engine.cpython-312.pyc +0 -0
- package/src/reidx/policy/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/policy/engine.py +101 -0
- package/src/reidx/policy/models.py +35 -0
- package/src/reidx/provider/__init__.py +16 -0
- package/src/reidx/provider/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/anthropic.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/base.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/ollama.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/openai.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/registry.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/stub.cpython-312.pyc +0 -0
- package/src/reidx/provider/_http.py +29 -0
- package/src/reidx/provider/anthropic.py +176 -0
- package/src/reidx/provider/base.py +50 -0
- package/src/reidx/provider/ollama.py +109 -0
- package/src/reidx/provider/openai.py +144 -0
- package/src/reidx/provider/registry.py +88 -0
- package/src/reidx/provider/store.py +141 -0
- package/src/reidx/provider/stub.py +60 -0
- package/src/reidx/provider_manager/__init__.py +40 -0
- package/src/reidx/provider_manager/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/catalog.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/database.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/keychain.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/palette.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/catalog.py +330 -0
- package/src/reidx/provider_manager/database.py +234 -0
- package/src/reidx/provider_manager/keychain.py +102 -0
- package/src/reidx/provider_manager/palette.py +831 -0
- package/src/reidx/runtime/__init__.py +5 -0
- package/src/reidx/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/agent.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/reasoning.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/state.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/subagent.cpython-312.pyc +0 -0
- package/src/reidx/runtime/agent.py +198 -0
- package/src/reidx/runtime/orchestrator.py +244 -0
- package/src/reidx/runtime/reasoning.py +77 -0
- package/src/reidx/runtime/state.py +32 -0
- package/src/reidx/runtime/subagent.py +174 -0
- package/src/reidx/session/__init__.py +4 -0
- package/src/reidx/session/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/session/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/session/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/session/models.py +43 -0
- package/src/reidx/session/store.py +101 -0
- package/src/reidx/tasks/__init__.py +4 -0
- package/src/reidx/tasks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/tasks/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/tasks/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/tasks/models.py +44 -0
- package/src/reidx/tasks/store.py +86 -0
- package/src/reidx/tools/__init__.py +44 -0
- package/src/reidx/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/base.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/registry.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/shell_tool.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/spawn_agent.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
- package/src/reidx/tools/base.py +84 -0
- package/src/reidx/tools/file_tools.py +222 -0
- package/src/reidx/tools/registry.py +45 -0
- package/src/reidx/tools/shell_tool.py +75 -0
- package/src/reidx/tools/spawn_agent.py +185 -0
- package/src/reidx/tools/web_tools.py +216 -0
- package/src/reidx/ui/__init__.py +41 -0
- package/src/reidx/ui/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/app.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/render.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/repl.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/theme.cpython-312.pyc +0 -0
- package/src/reidx/ui/app.py +1343 -0
- package/src/reidx/ui/commands.py +663 -0
- package/src/reidx/ui/render.py +517 -0
- package/src/reidx/ui/repl.py +14 -0
- package/src/reidx/ui/theme.py +126 -0
- package/src/reidx/workflows/__init__.py +4 -0
- package/src/reidx/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/workflows/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/workflows/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/workflows/models.py +19 -0
- package/src/reidx/workflows/store.py +56 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Claude-Code-style settings file support.
|
|
2
|
+
|
|
3
|
+
A settings.json holds an `env` block (applied to os.environ before any provider
|
|
4
|
+
reads credentials) and an optional `reidx` block (project-baked ReidX
|
|
5
|
+
configuration — default_provider, policy, providers, etc.). Same file shape as
|
|
6
|
+
Claude Code's settings.json so existing files work as-is; ReidX-specific
|
|
7
|
+
config lives under `reidx` so unknown keys (`theme`, `effortLevel`, ...) are
|
|
8
|
+
ignored harmlessly.
|
|
9
|
+
|
|
10
|
+
Path resolution (first hit wins):
|
|
11
|
+
1. $REIDCHAT_SETTINGS (explicit override)
|
|
12
|
+
2. ./settings.json (project-local, when it exists)
|
|
13
|
+
3. E:/leech/Reidchat.json (legacy default — the shared Reidchat file)
|
|
14
|
+
|
|
15
|
+
Project-local wins over the shared Reidchat file so a project can bake in a
|
|
16
|
+
different backend, permission mode, or provider set without editing the
|
|
17
|
+
global file. The env block is authoritative for the process (overrides any
|
|
18
|
+
ambient env) — this is how ReidX picks up the Reidchat proxy credentials
|
|
19
|
+
even when the shell's ANTHROPIC_* vars point somewhere else.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from reidx.diagnostics.logger import get_logger
|
|
28
|
+
|
|
29
|
+
log = get_logger("reidx.config.settings")
|
|
30
|
+
|
|
31
|
+
LEGACY_SETTINGS_PATH = Path("E:/leech/Reidchat.json")
|
|
32
|
+
GLOBAL_SETTINGS_PATH = Path.home() / ".reidx" / "settings.json"
|
|
33
|
+
PROJECT_SETTINGS_FILENAME = "settings.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _walk_upward_for_project_settings(start: Path) -> Path | None:
|
|
37
|
+
"""Walk from `start` toward the filesystem root looking for settings.json.
|
|
38
|
+
|
|
39
|
+
Mirrors how git finds `.git` — so launching `reid` from any
|
|
40
|
+
subdirectory of a project still finds that project's baked-in
|
|
41
|
+
settings.json. Stops at the root (path.parent == path).
|
|
42
|
+
"""
|
|
43
|
+
seen: set[Path] = set()
|
|
44
|
+
current = start.resolve()
|
|
45
|
+
while current not in seen:
|
|
46
|
+
seen.add(current)
|
|
47
|
+
candidate = current / PROJECT_SETTINGS_FILENAME
|
|
48
|
+
if candidate.exists():
|
|
49
|
+
return candidate
|
|
50
|
+
parent = current.parent
|
|
51
|
+
if parent == current:
|
|
52
|
+
break
|
|
53
|
+
current = parent
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def settings_path() -> Path:
|
|
58
|
+
"""Resolve the settings file path (first hit wins).
|
|
59
|
+
|
|
60
|
+
Order:
|
|
61
|
+
1. $REIDCHAT_SETTINGS (explicit override)
|
|
62
|
+
2. project settings.json (walk upward from CWD)
|
|
63
|
+
3. ~/.reidx/settings.json (global default)
|
|
64
|
+
4. E:/leech/Reidchat.json (legacy shared file)
|
|
65
|
+
|
|
66
|
+
Returns the last fallback even if it doesn't exist, so `doctor` can
|
|
67
|
+
report it as "missing" rather than crashing on a None.
|
|
68
|
+
"""
|
|
69
|
+
override = os.environ.get("REIDCHAT_SETTINGS", "").strip()
|
|
70
|
+
if override:
|
|
71
|
+
return Path(override)
|
|
72
|
+
project = _walk_upward_for_project_settings(Path.cwd())
|
|
73
|
+
if project is not None:
|
|
74
|
+
return project
|
|
75
|
+
if GLOBAL_SETTINGS_PATH.exists():
|
|
76
|
+
return GLOBAL_SETTINGS_PATH
|
|
77
|
+
return LEGACY_SETTINGS_PATH
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _read_settings(path: Path | None = None) -> dict:
|
|
81
|
+
path = path or settings_path()
|
|
82
|
+
if not path.exists():
|
|
83
|
+
log.debug("settings file not found: %s", path)
|
|
84
|
+
return {}
|
|
85
|
+
try:
|
|
86
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
87
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
88
|
+
log.warning("failed to read settings %s: %s", path, exc)
|
|
89
|
+
return {}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def apply_settings_env(path: Path | None = None) -> dict[str, str]:
|
|
93
|
+
"""Apply a settings file's `env` block to os.environ.
|
|
94
|
+
|
|
95
|
+
Returns the mapping of env vars that were actually applied. Missing or
|
|
96
|
+
invalid files are a no-op.
|
|
97
|
+
"""
|
|
98
|
+
data = _read_settings(path)
|
|
99
|
+
env = data.get("env")
|
|
100
|
+
if not isinstance(env, dict):
|
|
101
|
+
return {}
|
|
102
|
+
|
|
103
|
+
applied: dict[str, str] = {}
|
|
104
|
+
for key, value in env.items():
|
|
105
|
+
if value is None:
|
|
106
|
+
continue
|
|
107
|
+
new = str(value)
|
|
108
|
+
if os.environ.get(key) not in (None, new):
|
|
109
|
+
log.debug("settings override %s (ambient env replaced)", key)
|
|
110
|
+
os.environ[key] = new # the settings file is authoritative
|
|
111
|
+
applied[key] = new
|
|
112
|
+
|
|
113
|
+
if applied:
|
|
114
|
+
log.debug("applied %d env var(s) from settings: %s", len(applied), ", ".join(applied))
|
|
115
|
+
return applied
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def read_reidx_block(path: Path | None = None) -> dict:
|
|
119
|
+
"""Return the settings file's `reidx` block, or {} if absent.
|
|
120
|
+
|
|
121
|
+
Loader merges this into Config below the env-var overrides but above the
|
|
122
|
+
on-disk .reidx/config.json, so `settings.json` is the project's baked-in
|
|
123
|
+
source of truth for anything that isn't an env credential.
|
|
124
|
+
"""
|
|
125
|
+
data = _read_settings(path)
|
|
126
|
+
block = data.get("reidx")
|
|
127
|
+
return block if isinstance(block, dict) else {}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""DeepReid: Researcher -> Planner -> Critic planning-and-review pipeline.
|
|
2
|
+
|
|
3
|
+
See deepreid-spec.md (repo root, one level above ReidX/) for the full
|
|
4
|
+
design. DeepReid never writes files or runs commands — it produces a
|
|
5
|
+
Markdown plan + critique. Building is a separate step.
|
|
6
|
+
|
|
7
|
+
Each role is a fresh, independent Agent + RuntimeState + Session +
|
|
8
|
+
PolicyEngine — not turns on one shared conversation, so each role's context
|
|
9
|
+
window stays clean and the "Planner/Critic have no tools" constraint is real
|
|
10
|
+
(a shared agent could otherwise still see prior tool results). All three
|
|
11
|
+
subagent PolicyEngines run in AUTONOMOUS mode regardless of the caller's
|
|
12
|
+
configured mode: Planner/Critic have zero tools registered (nothing to ever
|
|
13
|
+
approve), and the Researcher's tools are all read-only/search — the
|
|
14
|
+
restricted ToolRegistry is the actual safety boundary, so auto-approving
|
|
15
|
+
inside it is safe and avoids the pipeline hanging on an unattended prompt.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import time
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from reidx.config.models import Config
|
|
26
|
+
from reidx.policy.engine import PolicyEngine
|
|
27
|
+
from reidx.policy.models import PermissionMode
|
|
28
|
+
from reidx.provider.base import BaseProvider
|
|
29
|
+
from reidx.runtime.agent import Agent
|
|
30
|
+
from reidx.runtime.state import RuntimeState
|
|
31
|
+
from reidx.session.models import Session
|
|
32
|
+
from reidx.tools.file_tools import FindFilesTool, GrepFilesTool, ListDirTool, ReadFileTool
|
|
33
|
+
from reidx.tools.registry import ToolRegistry
|
|
34
|
+
from reidx.tools.web_tools import WebSearchTool
|
|
35
|
+
|
|
36
|
+
MAX_REVISION_ROUNDS = 2
|
|
37
|
+
# The Researcher is the only role that calls tools, so it's the only one that
|
|
38
|
+
# can run out of Agent.run_turn's step budget mid-exploration — observed in
|
|
39
|
+
# practice: a broad task ("read the dir") led it to keep calling list_dir/
|
|
40
|
+
# read_file for all of the default 8 steps and never stop to write up
|
|
41
|
+
# Findings, producing "[agent] step budget exhausted" instead of any usable
|
|
42
|
+
# output. Give it real headroom; Planner/Critic have zero tools so they
|
|
43
|
+
# always finish in exactly 1 step regardless of this number.
|
|
44
|
+
RESEARCHER_MAX_STEPS = 20
|
|
45
|
+
|
|
46
|
+
_RESEARCHER_PROMPT = (
|
|
47
|
+
"You are the Researcher in a DeepReid planning pipeline. Investigate the "
|
|
48
|
+
"codebase (read_file/grep_files/find_files/list_dir) and, if useful, the "
|
|
49
|
+
"web (web_search) to gather everything needed to plan the given task. You "
|
|
50
|
+
"cannot write files or run commands — you only look and report.\n\n"
|
|
51
|
+
"You have a limited number of tool calls. Investigate efficiently: prefer "
|
|
52
|
+
"list_dir/find_files/grep_files to survey before read_file-ing whole "
|
|
53
|
+
"files, and stop once you have enough to plan from — you do not need to "
|
|
54
|
+
"read every file. Always end with your Findings list as plain text (no "
|
|
55
|
+
"further tool calls) well before you run out of turns; a partial Findings "
|
|
56
|
+
"list from what you've seen so far is far more useful than none at all.\n\n"
|
|
57
|
+
"Produce a Findings list: one bullet per finding, each citing a "
|
|
58
|
+
"`file:line` reference or a web_search source. Be specific, not vague. Do "
|
|
59
|
+
"not propose a plan yourself; that is the Planner's job."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
_PLANNER_PROMPT = (
|
|
63
|
+
"You are the Planner in a DeepReid planning pipeline. You have no tools — "
|
|
64
|
+
"reason only over the task and the Researcher's Findings given to you. "
|
|
65
|
+
"Produce: numbered implementation steps (each naming the file(s) it "
|
|
66
|
+
"touches and what changes), explicit risks or things you're unsure about, "
|
|
67
|
+
"and open questions for a human. Do not invent findings the Researcher "
|
|
68
|
+
"didn't report."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
_PLANNER_REVISION_PROMPT = (
|
|
72
|
+
"You are the Planner, revising your plan after Critic feedback. You have "
|
|
73
|
+
"no tools. Given the original task, the Researcher's Findings, your prior "
|
|
74
|
+
"Plan, and the Critic's Critique below, produce a revised Plan in the "
|
|
75
|
+
"same format (numbered steps, risks, open questions) that addresses the "
|
|
76
|
+
"critique."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_CRITIC_PROMPT = (
|
|
80
|
+
"You are the Critic in a DeepReid planning pipeline. You have no tools — "
|
|
81
|
+
"reason only over the task, the Researcher's Findings, and the Planner's "
|
|
82
|
+
"Plan given to you. Find: claims in the Plan the Findings don't actually "
|
|
83
|
+
"support, missing edge cases or files the Researcher didn't look at, and "
|
|
84
|
+
"any internal contradictions. Write your Critique, then end your reply "
|
|
85
|
+
"with a literal final line in exactly one of these forms:\n"
|
|
86
|
+
"Verdict: ready to build\n"
|
|
87
|
+
"Verdict: needs revision\n"
|
|
88
|
+
"Verdict: blocked on: <what it's blocked on>"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
_VERDICT_RE = re.compile(r"^\s*Verdict:\s*(.+?)\s*$", re.IGNORECASE | re.MULTILINE)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class DeepReidResult:
|
|
96
|
+
task: str
|
|
97
|
+
findings: str
|
|
98
|
+
plan: str
|
|
99
|
+
critique: str
|
|
100
|
+
verdict: str
|
|
101
|
+
rounds: int
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _researcher_registry() -> ToolRegistry:
|
|
105
|
+
reg = ToolRegistry()
|
|
106
|
+
for tool in (ReadFileTool(), GrepFilesTool(), FindFilesTool(), ListDirTool(), WebSearchTool()):
|
|
107
|
+
reg.register(tool)
|
|
108
|
+
return reg # write_file/patch_file/run_command are never registered at all
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _make_subagent(
|
|
112
|
+
config: Config, provider: BaseProvider, workspace: Path, registry: ToolRegistry, base_prompt: str
|
|
113
|
+
) -> tuple[Agent, RuntimeState]:
|
|
114
|
+
policy = PolicyEngine(config)
|
|
115
|
+
policy.set_mode(PermissionMode.AUTONOMOUS)
|
|
116
|
+
session = Session(title="deepreid", workspace=workspace)
|
|
117
|
+
agent = Agent(provider, registry, policy, base_system_prompt=base_prompt)
|
|
118
|
+
return agent, RuntimeState(session=session)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _run_role(
|
|
122
|
+
config: Config,
|
|
123
|
+
provider: BaseProvider,
|
|
124
|
+
workspace: Path,
|
|
125
|
+
registry: ToolRegistry,
|
|
126
|
+
base_prompt: str,
|
|
127
|
+
user_input: str,
|
|
128
|
+
*,
|
|
129
|
+
max_steps: int | None = None,
|
|
130
|
+
) -> str:
|
|
131
|
+
agent, state = _make_subagent(config, provider, workspace, registry, base_prompt)
|
|
132
|
+
kwargs = {"max_steps": max_steps} if max_steps is not None else {}
|
|
133
|
+
text, _tools = agent.run_turn(state, user_input, approver=lambda _prompt: True, **kwargs)
|
|
134
|
+
return text
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _report(on_progress: Callable[[str], None] | None, stage: str) -> None:
|
|
138
|
+
if on_progress is not None:
|
|
139
|
+
on_progress(stage)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _split_verdict(critic_text: str) -> tuple[str, str]:
|
|
143
|
+
"""Split the Critic's reply into (critique_body, verdict_line).
|
|
144
|
+
|
|
145
|
+
Falls back to treating the whole reply as critique with an empty verdict
|
|
146
|
+
(never crashes on a Critic that ignores the requested format — same
|
|
147
|
+
"never hide the answer" philosophy as split_reasoning)."""
|
|
148
|
+
match = _VERDICT_RE.search(critic_text)
|
|
149
|
+
if match is None:
|
|
150
|
+
return critic_text.strip(), ""
|
|
151
|
+
verdict = match.group(1).strip()
|
|
152
|
+
critique = (critic_text[: match.start()] + critic_text[match.end() :]).strip()
|
|
153
|
+
return critique, verdict
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run_deepreid(
|
|
157
|
+
config: Config,
|
|
158
|
+
provider: BaseProvider,
|
|
159
|
+
workspace: Path,
|
|
160
|
+
task: str,
|
|
161
|
+
on_progress: Callable[[str], None] | None = None,
|
|
162
|
+
) -> DeepReidResult:
|
|
163
|
+
researcher_registry = _researcher_registry()
|
|
164
|
+
no_tools_registry = ToolRegistry()
|
|
165
|
+
|
|
166
|
+
_report(on_progress, "researching")
|
|
167
|
+
findings = _run_role(
|
|
168
|
+
config, provider, workspace, researcher_registry, _RESEARCHER_PROMPT, task,
|
|
169
|
+
max_steps=RESEARCHER_MAX_STEPS,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
_report(on_progress, "planning")
|
|
173
|
+
plan = _run_role(
|
|
174
|
+
config, provider, workspace, no_tools_registry, _PLANNER_PROMPT,
|
|
175
|
+
f"Task: {task}\n\nFindings:\n{findings}",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
_report(on_progress, "reviewing")
|
|
179
|
+
critic_text = _run_role(
|
|
180
|
+
config, provider, workspace, no_tools_registry, _CRITIC_PROMPT,
|
|
181
|
+
f"Task: {task}\n\nFindings:\n{findings}\n\nPlan:\n{plan}",
|
|
182
|
+
)
|
|
183
|
+
critique, verdict = _split_verdict(critic_text)
|
|
184
|
+
|
|
185
|
+
rounds = 1
|
|
186
|
+
while "revision" in verdict.lower() and rounds < MAX_REVISION_ROUNDS:
|
|
187
|
+
_report(on_progress, "revising plan")
|
|
188
|
+
plan = _run_role(
|
|
189
|
+
config, provider, workspace, no_tools_registry, _PLANNER_REVISION_PROMPT,
|
|
190
|
+
f"Task: {task}\n\nFindings:\n{findings}\n\nPrior Plan:\n{plan}\n\nCritique:\n{critique}",
|
|
191
|
+
)
|
|
192
|
+
_report(on_progress, "reviewing revision")
|
|
193
|
+
critic_text = _run_role(
|
|
194
|
+
config, provider, workspace, no_tools_registry, _CRITIC_PROMPT,
|
|
195
|
+
f"Task: {task}\n\nFindings:\n{findings}\n\nPlan:\n{plan}",
|
|
196
|
+
)
|
|
197
|
+
critique, verdict = _split_verdict(critic_text)
|
|
198
|
+
rounds += 1
|
|
199
|
+
|
|
200
|
+
return DeepReidResult(task=task, findings=findings, plan=plan, critique=critique, verdict=verdict, rounds=rounds)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def format_markdown(result: DeepReidResult) -> str:
|
|
204
|
+
return (
|
|
205
|
+
f"# DeepReid: {result.task}\n\n"
|
|
206
|
+
f"## Findings\n{result.findings}\n\n"
|
|
207
|
+
f"## Plan\n{result.plan}\n\n"
|
|
208
|
+
f"## Critique\n{result.critique or '(none)'}\n\n"
|
|
209
|
+
f"## Verdict\n{result.verdict or '(not provided)'} "
|
|
210
|
+
f"({result.rounds} round{'s' if result.rounds != 1 else ''})\n"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def save_deepreid_result(config: Config, result: DeepReidResult) -> Path:
|
|
215
|
+
storage_root = config.storage_root or (Path.home() / ".reidx")
|
|
216
|
+
out_dir = storage_root / "deepreid"
|
|
217
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
run_id = time.strftime("%Y%m%d-%H%M%S")
|
|
219
|
+
path = out_dir / f"{run_id}.md"
|
|
220
|
+
path.write_text(format_markdown(result), encoding="utf-8")
|
|
221
|
+
return path
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Diagnostics: logging + structured event log.
|
|
2
|
+
|
|
3
|
+
Foundation is intentionally simple — a tagged logger and a JSONL event writer for
|
|
4
|
+
runtime actions. Designed to grow into trace mode and token/usage reporting.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_logger(name: str = "reidx") -> logging.Logger:
|
|
16
|
+
logger = logging.getLogger(name)
|
|
17
|
+
if not logger.handlers:
|
|
18
|
+
handler = logging.StreamHandler()
|
|
19
|
+
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))
|
|
20
|
+
logger.addHandler(handler)
|
|
21
|
+
# Default to WARNING so INFO logs don't pollute the interactive UI.
|
|
22
|
+
# Set REIDX_LOG_LEVEL=INFO (or DEBUG) to see verbose logs.
|
|
23
|
+
import os
|
|
24
|
+
|
|
25
|
+
level = os.environ.get("REIDX_LOG_LEVEL", "WARNING").upper()
|
|
26
|
+
logger.setLevel(getattr(logging, level, logging.WARNING))
|
|
27
|
+
return logger
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EventLog:
|
|
31
|
+
"""Append-only JSONL event log for runtime actions (tool calls, decisions, runs)."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, path: Path) -> None:
|
|
34
|
+
self.path = path
|
|
35
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
def write(self, kind: str, data: dict[str, Any] | None = None) -> None:
|
|
38
|
+
record = {
|
|
39
|
+
"ts": datetime.now(UTC).isoformat(),
|
|
40
|
+
"kind": kind,
|
|
41
|
+
"data": data or {},
|
|
42
|
+
}
|
|
43
|
+
with self.path.open("a", encoding="utf-8") as fh:
|
|
44
|
+
fh.write(json.dumps(record, default=str) + "\n")
|
|
45
|
+
|
|
46
|
+
def tail(self, n: int = 50) -> list[dict[str, Any]]:
|
|
47
|
+
if not self.path.exists():
|
|
48
|
+
return []
|
|
49
|
+
lines = self.path.read_text(encoding="utf-8").splitlines()
|
|
50
|
+
out: list[dict[str, Any]] = []
|
|
51
|
+
for line in lines[-n:]:
|
|
52
|
+
try:
|
|
53
|
+
out.append(json.loads(line))
|
|
54
|
+
except json.JSONDecodeError:
|
|
55
|
+
continue
|
|
56
|
+
return out
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from reidx.goals.models import (
|
|
2
|
+
Goal,
|
|
3
|
+
GoalConstraint,
|
|
4
|
+
GoalEvidence,
|
|
5
|
+
GoalNode,
|
|
6
|
+
GoalNodeKind,
|
|
7
|
+
GoalRevision,
|
|
8
|
+
GoalStatus,
|
|
9
|
+
)
|
|
10
|
+
from reidx.goals.store import GoalStore
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Goal",
|
|
14
|
+
"GoalConstraint",
|
|
15
|
+
"GoalEvidence",
|
|
16
|
+
"GoalNode",
|
|
17
|
+
"GoalNodeKind",
|
|
18
|
+
"GoalRevision",
|
|
19
|
+
"GoalStatus",
|
|
20
|
+
"GoalStore",
|
|
21
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Goal models.
|
|
2
|
+
|
|
3
|
+
Goals are durable outcome structures for long-horizon work. They are separate
|
|
4
|
+
from Tasks: a task is one execution record, while a goal captures the desired
|
|
5
|
+
end state, evidence, decomposition, dependencies, and revision trail.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _new_id() -> str:
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
return uuid.uuid4().hex[:10]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GoalStatus(StrEnum):
|
|
22
|
+
DRAFT = "draft"
|
|
23
|
+
ACTIVE = "active"
|
|
24
|
+
BLOCKED = "blocked"
|
|
25
|
+
COMPLETED = "completed"
|
|
26
|
+
ABANDONED = "abandoned"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GoalNodeKind(StrEnum):
|
|
30
|
+
GOAL = "goal"
|
|
31
|
+
SUBGOAL = "subgoal"
|
|
32
|
+
MILESTONE = "milestone"
|
|
33
|
+
HABIT = "habit"
|
|
34
|
+
TASK_REF = "task_ref"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GoalEvidence(BaseModel):
|
|
38
|
+
description: str
|
|
39
|
+
satisfied: bool = False
|
|
40
|
+
note: str = ""
|
|
41
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
42
|
+
satisfied_at: datetime | None = None
|
|
43
|
+
|
|
44
|
+
def satisfy(self, note: str = "") -> None:
|
|
45
|
+
self.satisfied = True
|
|
46
|
+
if note:
|
|
47
|
+
self.note = note
|
|
48
|
+
self.satisfied_at = datetime.now(UTC)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GoalConstraint(BaseModel):
|
|
52
|
+
description: str
|
|
53
|
+
kind: str = "general"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GoalRevision(BaseModel):
|
|
57
|
+
at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
58
|
+
note: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GoalNode(BaseModel):
|
|
62
|
+
id: str = Field(default_factory=_new_id)
|
|
63
|
+
title: str
|
|
64
|
+
kind: GoalNodeKind = GoalNodeKind.SUBGOAL
|
|
65
|
+
status: GoalStatus = GoalStatus.DRAFT
|
|
66
|
+
parent_id: str | None = None
|
|
67
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
68
|
+
evidence: list[GoalEvidence] = Field(default_factory=list)
|
|
69
|
+
constraints: list[GoalConstraint] = Field(default_factory=list)
|
|
70
|
+
owner: str = "user"
|
|
71
|
+
notes: list[str] = Field(default_factory=list)
|
|
72
|
+
task_ids: list[str] = Field(default_factory=list)
|
|
73
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
74
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
75
|
+
|
|
76
|
+
def touch(self) -> None:
|
|
77
|
+
self.updated_at = datetime.now(UTC)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Goal(BaseModel):
|
|
81
|
+
id: str = Field(default_factory=_new_id)
|
|
82
|
+
session_id: str
|
|
83
|
+
title: str
|
|
84
|
+
status: GoalStatus = GoalStatus.DRAFT
|
|
85
|
+
outcome: str = ""
|
|
86
|
+
evidence: list[GoalEvidence] = Field(default_factory=list)
|
|
87
|
+
constraints: list[GoalConstraint] = Field(default_factory=list)
|
|
88
|
+
nodes: list[GoalNode] = Field(default_factory=list)
|
|
89
|
+
active_node_id: str | None = None
|
|
90
|
+
task_ids: list[str] = Field(default_factory=list)
|
|
91
|
+
revisions: list[GoalRevision] = Field(default_factory=list)
|
|
92
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
93
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
94
|
+
|
|
95
|
+
def touch(self) -> None:
|
|
96
|
+
self.updated_at = datetime.now(UTC)
|
|
97
|
+
|
|
98
|
+
def add_revision(self, note: str) -> None:
|
|
99
|
+
if note:
|
|
100
|
+
self.revisions.append(GoalRevision(note=note))
|
|
101
|
+
self.touch()
|