nexus-lite 0.2.2__py3-none-any.whl
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.
- nexus_lite/__init__.py +190 -0
- nexus_lite/__main__.py +7 -0
- nexus_lite/_db.py +196 -0
- nexus_lite/_proc.py +17 -0
- nexus_lite/artifacts.py +83 -0
- nexus_lite/cli.py +491 -0
- nexus_lite/colab/__init__.py +55 -0
- nexus_lite/colab/__main__.py +9 -0
- nexus_lite/colab/env.py +298 -0
- nexus_lite/colab/tunnel.py +1245 -0
- nexus_lite/compute.py +803 -0
- nexus_lite/core.py +845 -0
- nexus_lite/demos/__init__.py +39 -0
- nexus_lite/demos/__main__.py +7 -0
- nexus_lite/demos/cli.py +251 -0
- nexus_lite/demos/loom_multi_agent.py +689 -0
- nexus_lite/demos/loom_multi_task.py +418 -0
- nexus_lite/demos/loom_radiomics_tasks.py +1175 -0
- nexus_lite/demos/orchestration.py +2232 -0
- nexus_lite/demos/scenarios.py +1808 -0
- nexus_lite/demos/viz.py +1017 -0
- nexus_lite/guard.py +81 -0
- nexus_lite/loom.py +282 -0
- nexus_lite/loom_multi_agent.py +12 -0
- nexus_lite/loom_multi_task.py +12 -0
- nexus_lite/loom_radiomics_tasks.py +12 -0
- nexus_lite/memory/__init__.py +24 -0
- nexus_lite/memory/ltm.py +96 -0
- nexus_lite/memory/mtm.py +269 -0
- nexus_lite/memory/stm.py +756 -0
- nexus_lite/phi.py +35 -0
- nexus_lite/protocol.py +106 -0
- nexus_lite/provider_errors.py +144 -0
- nexus_lite/pty.py +913 -0
- nexus_lite/roles.py +140 -0
- nexus_lite/stm.py +13 -0
- nexus_lite/viz.py +12 -0
- nexus_lite-0.2.2.dist-info/METADATA +377 -0
- nexus_lite-0.2.2.dist-info/RECORD +43 -0
- nexus_lite-0.2.2.dist-info/WHEEL +5 -0
- nexus_lite-0.2.2.dist-info/entry_points.txt +4 -0
- nexus_lite-0.2.2.dist-info/licenses/LICENSE +21 -0
- nexus_lite-0.2.2.dist-info/top_level.txt +1 -0
nexus_lite/__init__.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nexus-lite: Open-Core Kernel for Nexus multi-agent primitives.
|
|
3
|
+
|
|
4
|
+
Eagerly loads only the light modules (`pty`, `roles`, `protocol`); `core` and the memory tiers
|
|
5
|
+
(`nexus_lite.memory.{stm,mtm,ltm}`) load on first attribute access (PEP 562), so
|
|
6
|
+
`import nexus_lite.memory.ltm` pulls in no other tier.
|
|
7
|
+
|
|
8
|
+
Demos/orchestration scenarios (`nexus_lite.demos`) and Colab helpers (`nexus_lite.colab`) are
|
|
9
|
+
outside the kernel; their old top-level names resolve lazily with a DeprecationWarning.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import importlib
|
|
15
|
+
import warnings
|
|
16
|
+
from importlib import metadata as _metadata
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from . import protocol
|
|
20
|
+
from . import pty
|
|
21
|
+
from . import roles
|
|
22
|
+
from .protocol import (
|
|
23
|
+
POINTER_MAX_CHARS,
|
|
24
|
+
format_pointer_line,
|
|
25
|
+
parse_pointer_line,
|
|
26
|
+
pointer_parts,
|
|
27
|
+
sanitize_pointer_token,
|
|
28
|
+
truncate_words,
|
|
29
|
+
)
|
|
30
|
+
from .pty import (
|
|
31
|
+
KEY_MAP,
|
|
32
|
+
KEY_NAMES,
|
|
33
|
+
key_to_bytes,
|
|
34
|
+
keys_to_bytes,
|
|
35
|
+
)
|
|
36
|
+
from .roles import (
|
|
37
|
+
REAL_AGENTS,
|
|
38
|
+
ROLE_FOR_AGENT,
|
|
39
|
+
ROLE_PROMPTS,
|
|
40
|
+
ROLE_TIER,
|
|
41
|
+
TIER_ORDER,
|
|
42
|
+
get_role_prompt,
|
|
43
|
+
resolve_role_dispatch,
|
|
44
|
+
tier_for_role,
|
|
45
|
+
tier_rank,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try: # single source of truth: pyproject.toml via installed metadata
|
|
49
|
+
__version__ = _metadata.version("nexus-lite")
|
|
50
|
+
except _metadata.PackageNotFoundError: # bare source tree / shipped bundle: read pyproject.toml if adjacent
|
|
51
|
+
import re as _re
|
|
52
|
+
from pathlib import Path as _Path
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
_m = _re.search(r'(?m)^version\s*=\s*"([^"]+)"',
|
|
56
|
+
(_Path(__file__).resolve().parent.parent / "pyproject.toml").read_text(encoding="utf-8"))
|
|
57
|
+
except OSError:
|
|
58
|
+
_m = None
|
|
59
|
+
__version__ = _m.group(1) if _m else "0+unknown"
|
|
60
|
+
|
|
61
|
+
_LAZY_MODULES = {"compute", "core", "demos"}
|
|
62
|
+
|
|
63
|
+
_LAZY_SYMBOLS: dict[str, str] = {
|
|
64
|
+
# compute (zero-preinstall remote execution on Turing & Colab)
|
|
65
|
+
"compute_run": "compute",
|
|
66
|
+
"pack_lite_bundle": "compute",
|
|
67
|
+
"pack_self_contained_script": "compute",
|
|
68
|
+
"run_local_sandbox": "compute",
|
|
69
|
+
"run_on_colab": "compute",
|
|
70
|
+
"run_on_turing": "compute",
|
|
71
|
+
"turing_job_status": "compute",
|
|
72
|
+
# core
|
|
73
|
+
"chat": "core",
|
|
74
|
+
"delegate": "core",
|
|
75
|
+
"ensure_opencode": "core",
|
|
76
|
+
"run_pipeline": "core",
|
|
77
|
+
"run_swarm": "core",
|
|
78
|
+
# _db / memory tiers
|
|
79
|
+
"get_workspace": "_db",
|
|
80
|
+
"init": "_db",
|
|
81
|
+
"bus_read": "memory.mtm",
|
|
82
|
+
"bus_send": "memory.mtm",
|
|
83
|
+
"wiki_list": "memory.ltm",
|
|
84
|
+
"wiki_read": "memory.ltm",
|
|
85
|
+
"wiki_search": "memory.ltm",
|
|
86
|
+
"wiki_write": "memory.ltm",
|
|
87
|
+
"stm_list": "memory.stm",
|
|
88
|
+
"stm_pack": "memory.stm",
|
|
89
|
+
"stm_read": "memory.stm",
|
|
90
|
+
"stm_rebuild_index": "memory.stm",
|
|
91
|
+
"stm_search": "memory.stm",
|
|
92
|
+
"stm_write": "memory.stm",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Deprecated top-level names: resolved lazily with a DeprecationWarning.
|
|
96
|
+
_DEPRECATED_SYMBOLS: dict[str, str] = {
|
|
97
|
+
"get_db": "_db",
|
|
98
|
+
"get_db_path": "_db",
|
|
99
|
+
**{n: "demos.scenarios" for n in (
|
|
100
|
+
"run_all_demos", "run_hipaa_privacy_pipeline", "run_lung1_audit_pipeline",
|
|
101
|
+
"run_multimodal_imaging_swarm", "run_opencode_free_demo", "run_phantom_shape_swarm")},
|
|
102
|
+
**{n: "demos.orchestration" for n in (
|
|
103
|
+
"run_agent_interaction", "run_cooperative_problem_solver", "run_multi_tier_swarm")},
|
|
104
|
+
**{n: "demos.viz" for n in ("dashboard", "render_dashboard_html", "watch")},
|
|
105
|
+
**{n: "colab.env" for n in (
|
|
106
|
+
"ColabInitResult", "detect_env", "ensure_colab", "ensure_env", "sync_dotdir_to_drive")},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def __getattr__(name: str) -> Any:
|
|
111
|
+
if name in _LAZY_MODULES:
|
|
112
|
+
mod = importlib.import_module(f".{name}", __name__)
|
|
113
|
+
globals()[name] = mod
|
|
114
|
+
return mod
|
|
115
|
+
if name in _LAZY_SYMBOLS:
|
|
116
|
+
mod_name = _LAZY_SYMBOLS[name]
|
|
117
|
+
mod = importlib.import_module(f".{mod_name}", __name__)
|
|
118
|
+
val = getattr(mod, name)
|
|
119
|
+
globals()[name] = val
|
|
120
|
+
return val
|
|
121
|
+
if name in _DEPRECATED_SYMBOLS:
|
|
122
|
+
mod_name = _DEPRECATED_SYMBOLS[name]
|
|
123
|
+
warnings.warn(
|
|
124
|
+
f"nexus_lite.{name} is deprecated; use nexus_lite.{mod_name}.{name}",
|
|
125
|
+
DeprecationWarning,
|
|
126
|
+
stacklevel=2,
|
|
127
|
+
)
|
|
128
|
+
return getattr(importlib.import_module(f".{mod_name}", __name__), name)
|
|
129
|
+
if name == "stm": # deprecated module alias, see nexus_lite/stm.py
|
|
130
|
+
warnings.warn(
|
|
131
|
+
"nexus_lite.stm is deprecated; import nexus_lite.memory.stm instead",
|
|
132
|
+
DeprecationWarning,
|
|
133
|
+
stacklevel=2,
|
|
134
|
+
)
|
|
135
|
+
return importlib.import_module(".memory.stm", __name__)
|
|
136
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = [
|
|
140
|
+
"pty",
|
|
141
|
+
"compute",
|
|
142
|
+
"core",
|
|
143
|
+
"roles",
|
|
144
|
+
"protocol",
|
|
145
|
+
"compute_run",
|
|
146
|
+
"run_on_turing",
|
|
147
|
+
"run_on_colab",
|
|
148
|
+
"run_local_sandbox",
|
|
149
|
+
"pack_lite_bundle",
|
|
150
|
+
"pack_self_contained_script",
|
|
151
|
+
"turing_job_status",
|
|
152
|
+
"init",
|
|
153
|
+
"get_workspace",
|
|
154
|
+
"bus_send",
|
|
155
|
+
"bus_read",
|
|
156
|
+
"delegate",
|
|
157
|
+
"run_swarm",
|
|
158
|
+
"run_pipeline",
|
|
159
|
+
"stm_write",
|
|
160
|
+
"stm_read",
|
|
161
|
+
"stm_list",
|
|
162
|
+
"stm_search",
|
|
163
|
+
"stm_pack",
|
|
164
|
+
"stm_rebuild_index",
|
|
165
|
+
"wiki_write",
|
|
166
|
+
"wiki_read",
|
|
167
|
+
"wiki_search",
|
|
168
|
+
"wiki_list",
|
|
169
|
+
"chat",
|
|
170
|
+
"ensure_opencode",
|
|
171
|
+
"POINTER_MAX_CHARS",
|
|
172
|
+
"sanitize_pointer_token",
|
|
173
|
+
"truncate_words",
|
|
174
|
+
"format_pointer_line",
|
|
175
|
+
"pointer_parts",
|
|
176
|
+
"parse_pointer_line",
|
|
177
|
+
"KEY_NAMES",
|
|
178
|
+
"KEY_MAP",
|
|
179
|
+
"key_to_bytes",
|
|
180
|
+
"keys_to_bytes",
|
|
181
|
+
"ROLE_PROMPTS",
|
|
182
|
+
"ROLE_FOR_AGENT",
|
|
183
|
+
"REAL_AGENTS",
|
|
184
|
+
"ROLE_TIER",
|
|
185
|
+
"TIER_ORDER",
|
|
186
|
+
"tier_rank",
|
|
187
|
+
"tier_for_role",
|
|
188
|
+
"resolve_role_dispatch",
|
|
189
|
+
"get_role_prompt",
|
|
190
|
+
]
|
nexus_lite/__main__.py
ADDED
nexus_lite/_db.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nexus_lite._db — workspace path + SQLite WAL connection + schema bootstrap.
|
|
3
|
+
|
|
4
|
+
Shared by the memory tiers (stm/mtm/ltm) and the task/session code in core. Stdlib
|
|
5
|
+
only; imports no other nexus_lite module at import time.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import os
|
|
12
|
+
import sqlite3
|
|
13
|
+
import threading
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Iterator, Optional, Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_workspace() -> Path:
|
|
19
|
+
"""Return canonical workspace root Path (.nexus/ or NEXUS_LITE_DIR or NEXUS_LITE_HOME)."""
|
|
20
|
+
env = os.environ.get("NEXUS_LITE_DIR") or os.environ.get("NEXUS_LITE_HOME")
|
|
21
|
+
if env:
|
|
22
|
+
return Path(env).resolve()
|
|
23
|
+
return Path(".nexus").resolve()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_db_path() -> Path:
|
|
27
|
+
"""Return path to SQLite WAL database."""
|
|
28
|
+
return get_workspace() / "lite.db"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def mark_session(name: str, live: bool) -> None:
|
|
32
|
+
"""Best-effort: flip an EXISTING sessions-registry row live/unregistered (lite#8).
|
|
33
|
+
|
|
34
|
+
Update-only and never creates the workspace/DB: a socket with no row already counts as
|
|
35
|
+
live on the bus, so only a stale 'unregistered' row (or a kill) needs a write. Called by
|
|
36
|
+
pty.spawn / pty.kill (edge pty -> _db; _db imports no lite module, so no cycle).
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
if not get_db_path().exists():
|
|
40
|
+
return
|
|
41
|
+
with connect() as con:
|
|
42
|
+
if live:
|
|
43
|
+
con.execute(
|
|
44
|
+
"UPDATE sessions SET status = 'live', last_seen = strftime('%s','now') "
|
|
45
|
+
"WHERE name = ?;", (name,))
|
|
46
|
+
else:
|
|
47
|
+
con.execute("UPDATE sessions SET status = 'unregistered' WHERE name = ?;", (name,))
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_db() -> sqlite3.Connection:
|
|
53
|
+
"""Open SQLite connection configured with WAL journal mode and Row factory.
|
|
54
|
+
|
|
55
|
+
The caller owns the connection and MUST close it (``with con:`` only commits). Prefer
|
|
56
|
+
:func:`connect`, which commits/rolls back and closes.
|
|
57
|
+
"""
|
|
58
|
+
db_p = get_db_path()
|
|
59
|
+
db_p.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
con = sqlite3.connect(str(db_p), timeout=30.0)
|
|
61
|
+
con.row_factory = sqlite3.Row
|
|
62
|
+
con.execute("PRAGMA journal_mode=WAL;")
|
|
63
|
+
con.execute("PRAGMA synchronous=NORMAL;")
|
|
64
|
+
return con
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@contextlib.contextmanager
|
|
68
|
+
def connect() -> Iterator[sqlite3.Connection]:
|
|
69
|
+
"""Yield a connection; commit on success, roll back on error, always close."""
|
|
70
|
+
con = get_db()
|
|
71
|
+
try:
|
|
72
|
+
with con:
|
|
73
|
+
yield con
|
|
74
|
+
finally:
|
|
75
|
+
con.close()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --- schema versioning (PRAGMA user_version) -------------------------------------------
|
|
79
|
+
# v0 = any DB without user_version (the original CREATE TABLE IF NOT EXISTS schema).
|
|
80
|
+
# Table/column names are frozen. Append new steps; never edit old ones.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _m1_stamp_baseline(con: sqlite3.Connection) -> None:
|
|
84
|
+
"""v0 -> v1: baseline schema (created idempotently by _ensure_base_schema); stamp only."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _m2_bus_v1(con: sqlite3.Connection) -> None:
|
|
88
|
+
"""v1 -> v2 (lite#8): messages.seq (backfilled), bus_cursors, bus_meta, sessions.last_seen."""
|
|
89
|
+
cols = {r[1] for r in con.execute("PRAGMA table_info(messages);")}
|
|
90
|
+
if "seq" not in cols:
|
|
91
|
+
con.execute("ALTER TABLE messages ADD COLUMN seq INTEGER;")
|
|
92
|
+
rows = con.execute("SELECT rowid FROM messages ORDER BY timestamp, rowid;").fetchall()
|
|
93
|
+
for i, r in enumerate(rows, 1):
|
|
94
|
+
con.execute("UPDATE messages SET seq = ? WHERE rowid = ?;", (i, r[0]))
|
|
95
|
+
con.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_seq ON messages(seq);")
|
|
96
|
+
con.execute(
|
|
97
|
+
"CREATE TABLE IF NOT EXISTS bus_cursors ("
|
|
98
|
+
"target TEXT PRIMARY KEY, acked_seq INTEGER NOT NULL DEFAULT 0, updated REAL);"
|
|
99
|
+
)
|
|
100
|
+
con.execute("CREATE TABLE IF NOT EXISTS bus_meta (key TEXT PRIMARY KEY, value INTEGER NOT NULL);")
|
|
101
|
+
con.execute("INSERT OR REPLACE INTO bus_meta (key, value) VALUES ('last_seq', ?);", (len(rows),))
|
|
102
|
+
scols = {r[1] for r in con.execute("PRAGMA table_info(sessions);")}
|
|
103
|
+
if "last_seen" not in scols:
|
|
104
|
+
con.execute("ALTER TABLE sessions ADD COLUMN last_seen REAL;")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
MIGRATIONS = [_m1_stamp_baseline, _m2_bus_v1] # index i migrates v<i> -> v<i+1>
|
|
108
|
+
SCHEMA_VERSION = len(MIGRATIONS)
|
|
109
|
+
|
|
110
|
+
_BASE_DDL = (
|
|
111
|
+
"""CREATE TABLE IF NOT EXISTS sessions (
|
|
112
|
+
name TEXT PRIMARY KEY, sock TEXT, cmd TEXT, created_at REAL, status TEXT);""",
|
|
113
|
+
"""CREATE TABLE IF NOT EXISTS messages (
|
|
114
|
+
id TEXT PRIMARY KEY, sender TEXT, target TEXT, kind TEXT, summary TEXT,
|
|
115
|
+
path TEXT, content TEXT, timestamp REAL);""",
|
|
116
|
+
"""CREATE TABLE IF NOT EXISTS tasks (
|
|
117
|
+
id TEXT PRIMARY KEY, caller TEXT, agent_cmd TEXT, status TEXT, rc INTEGER,
|
|
118
|
+
created_at REAL, completed_at REAL, artifact_dir TEXT);""",
|
|
119
|
+
"CREATE VIRTUAL TABLE IF NOT EXISTS wiki_fts USING fts5(slug, content);",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def migrate(con: sqlite3.Connection) -> int:
|
|
124
|
+
"""Bring `con` to SCHEMA_VERSION inside one transaction; refuse a newer DB."""
|
|
125
|
+
cur = con.execute("PRAGMA user_version;").fetchone()[0]
|
|
126
|
+
if cur > SCHEMA_VERSION:
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
f"lite.db schema version {cur} is newer than this nexus_lite supports "
|
|
129
|
+
f"({SCHEMA_VERSION}); upgrade nexus_lite"
|
|
130
|
+
)
|
|
131
|
+
if cur == SCHEMA_VERSION:
|
|
132
|
+
return cur
|
|
133
|
+
con.execute("BEGIN IMMEDIATE;")
|
|
134
|
+
try:
|
|
135
|
+
cur = con.execute("PRAGMA user_version;").fetchone()[0] # re-read under the write lock
|
|
136
|
+
if cur > SCHEMA_VERSION:
|
|
137
|
+
raise RuntimeError(f"lite.db schema version {cur} is newer than {SCHEMA_VERSION}")
|
|
138
|
+
for ddl in _BASE_DDL:
|
|
139
|
+
con.execute(ddl)
|
|
140
|
+
for i in range(cur, SCHEMA_VERSION):
|
|
141
|
+
MIGRATIONS[i](con)
|
|
142
|
+
con.execute(f"PRAGMA user_version = {SCHEMA_VERSION};")
|
|
143
|
+
con.execute("COMMIT;")
|
|
144
|
+
except BaseException:
|
|
145
|
+
con.execute("ROLLBACK;")
|
|
146
|
+
raise
|
|
147
|
+
return SCHEMA_VERSION
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_INIT_DONE: set[str] = set()
|
|
151
|
+
_INIT_LOCK = threading.Lock()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def reset_init() -> None:
|
|
155
|
+
"""Forget memoized init() state (tests / explicit re-init)."""
|
|
156
|
+
with _INIT_LOCK:
|
|
157
|
+
_INIT_DONE.clear()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def init(
|
|
161
|
+
workspace: Optional[Union[str, Path]] = None,
|
|
162
|
+
root: Optional[Union[str, Path]] = None,
|
|
163
|
+
force: bool = False,
|
|
164
|
+
) -> Path:
|
|
165
|
+
"""Initialize workspace layout and SQLite schema; once per (process, db path).
|
|
166
|
+
|
|
167
|
+
`force=True` (or :func:`reset_init`) re-runs it.
|
|
168
|
+
"""
|
|
169
|
+
target = workspace or root
|
|
170
|
+
if target:
|
|
171
|
+
ws = Path(target).resolve()
|
|
172
|
+
os.environ["NEXUS_LITE_HOME"] = str(ws)
|
|
173
|
+
os.environ["NEXUS_LITE_DIR"] = str(ws)
|
|
174
|
+
else:
|
|
175
|
+
ws = get_workspace()
|
|
176
|
+
|
|
177
|
+
key = str(get_db_path())
|
|
178
|
+
if not force and key in _INIT_DONE and Path(key).exists() and (ws / "inbox").is_dir():
|
|
179
|
+
return ws
|
|
180
|
+
|
|
181
|
+
ws.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
(ws / "inbox").mkdir(parents=True, exist_ok=True)
|
|
183
|
+
(ws / "artifacts").mkdir(parents=True, exist_ok=True)
|
|
184
|
+
(ws / "wiki").mkdir(parents=True, exist_ok=True)
|
|
185
|
+
try:
|
|
186
|
+
from .memory import stm as _stm # lazy: keeps _db import-free of the tiers
|
|
187
|
+
|
|
188
|
+
_stm.ensure_project_node(ws)
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
with contextlib.closing(get_db()) as con:
|
|
193
|
+
migrate(con)
|
|
194
|
+
with _INIT_LOCK:
|
|
195
|
+
_INIT_DONE.add(key)
|
|
196
|
+
return ws
|
nexus_lite/_proc.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""nexus_lite._proc — reap intentionally detached children. Stdlib only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def reap_in_background(proc: "subprocess.Popen") -> "subprocess.Popen":
|
|
10
|
+
"""Wait for a detached child on a daemon thread so it never lingers as a zombie.
|
|
11
|
+
|
|
12
|
+
The child keeps running independently (start_new_session=True) and outlives this
|
|
13
|
+
process; the thread only collects its exit status if we are still alive. The thread
|
|
14
|
+
holds a reference, so no 'subprocess still running' ResourceWarning is emitted.
|
|
15
|
+
"""
|
|
16
|
+
threading.Thread(target=proc.wait, name=f"reap-{proc.pid}", daemon=True).start()
|
|
17
|
+
return proc
|
nexus_lite/artifacts.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Task-artifact layout contract (stdlib only, no nexus_lite imports).
|
|
2
|
+
|
|
3
|
+
One per-task directory holds every artifact of a delegated task:
|
|
4
|
+
|
|
5
|
+
<root>/<task_id>/
|
|
6
|
+
prompt.md input prompt output.md worker stdout (lite name)
|
|
7
|
+
result.md same bytes as output.md (code-style readers)
|
|
8
|
+
meta.json {task_id, agent, status, rc, started, finished}
|
|
9
|
+
rc exit code as text spec.json bg runner spec
|
|
10
|
+
(timeout, runner.pid, runner.log, stderr.log, escalation.json, ...)
|
|
11
|
+
|
|
12
|
+
Root: ``<workspace>/artifacts``. The root is an argument; a downstream
|
|
13
|
+
multi-project host passes its own root to ``task_dir``/``ensure_task_dir``.
|
|
14
|
+
|
|
15
|
+
Env exported to workers (one release of overlap):
|
|
16
|
+
NEXUS_TASK_DIR canonical per-task directory
|
|
17
|
+
NEXUS_ARTIFACT_DIR deprecated alias, same value
|
|
18
|
+
Readers use ``task_dir_from_env``: canonical wins, alias accepted.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Mapping, Optional, Union
|
|
26
|
+
|
|
27
|
+
PROMPT = "prompt.md"
|
|
28
|
+
OUTPUT = "output.md"
|
|
29
|
+
RESULT = "result.md"
|
|
30
|
+
META = "meta.json"
|
|
31
|
+
RC = "rc"
|
|
32
|
+
SPEC = "spec.json"
|
|
33
|
+
|
|
34
|
+
TASK_DIR_ENV = "NEXUS_TASK_DIR"
|
|
35
|
+
TASK_DIR_ENV_DEPRECATED = "NEXUS_ARTIFACT_DIR"
|
|
36
|
+
|
|
37
|
+
PathLike = Union[str, "os.PathLike[str]"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def artifacts_root(workspace: PathLike) -> Path:
|
|
41
|
+
"""Default artifact root: <workspace>/artifacts (no env override)."""
|
|
42
|
+
return Path(workspace) / "artifacts"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def task_dir(task_id: str, root: PathLike) -> Path:
|
|
46
|
+
"""Pure path: <root>/<task_id>."""
|
|
47
|
+
return Path(root) / task_id
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_task_dir(task_id: str, root: PathLike) -> Path:
|
|
51
|
+
d = task_dir(task_id, root)
|
|
52
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
return d
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def task_dir_from_env(env: Optional[Mapping[str, str]] = None) -> Optional[Path]:
|
|
57
|
+
"""Per-task dir from env: canonical name wins, deprecated alias accepted."""
|
|
58
|
+
e = os.environ if env is None else env
|
|
59
|
+
v = e.get(TASK_DIR_ENV) or e.get(TASK_DIR_ENV_DEPRECATED)
|
|
60
|
+
return Path(v) if v else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def export_env(env: dict, art_dir: PathLike) -> dict:
|
|
64
|
+
"""Set canonical and deprecated per-task dir vars on ``env`` (in place)."""
|
|
65
|
+
env[TASK_DIR_ENV] = str(art_dir)
|
|
66
|
+
env[TASK_DIR_ENV_DEPRECATED] = str(art_dir)
|
|
67
|
+
return env
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def write_result(art_dir: PathLike, output: str, rc: int) -> None:
|
|
71
|
+
"""Write output.md, result.md (same content) and rc."""
|
|
72
|
+
d = Path(art_dir)
|
|
73
|
+
(d / OUTPUT).write_text(output, encoding="utf-8")
|
|
74
|
+
(d / RESULT).write_text(output, encoding="utf-8")
|
|
75
|
+
(d / RC).write_text(str(rc), encoding="utf-8")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def write_meta(art_dir: PathLike, *, task_id: str, agent: str, status: str,
|
|
79
|
+
rc: int, started: float, finished: float) -> None:
|
|
80
|
+
(Path(art_dir) / META).write_text(
|
|
81
|
+
json.dumps({"task_id": task_id, "agent": agent, "status": status,
|
|
82
|
+
"rc": rc, "started": started, "finished": finished}),
|
|
83
|
+
encoding="utf-8")
|