Skip to content

Commit f11df02

Browse files
authored
Merge pull request #3 from nickroci/chore/code-quanlity-2
Chore/code quanlity 2
2 parents 7a550d6 + 734c3ba commit f11df02

83 files changed

Lines changed: 11327 additions & 3578 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
concurrency:
9+
group: ci-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
build:
14+
name: ${{ matrix.package }}
15+
runs-on: ubuntu-latest
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
include:
20+
- package: daemon
21+
python-version: "3.10"
22+
- package: tools/search
23+
python-version: "3.10"
24+
- package: src
25+
python-version: "3.12"
26+
defaults:
27+
run:
28+
working-directory: ${{ matrix.package }}
29+
steps:
30+
- uses: actions/checkout@v4
31+
32+
- uses: astral-sh/setup-uv@v6
33+
with:
34+
enable-cache: true
35+
cache-dependency-glob: ${{ matrix.package }}/uv.lock
36+
37+
- name: Install dependencies
38+
run: uv sync --locked
39+
40+
- name: Lint (ruff)
41+
run: uv run ruff check .
42+
43+
- name: Format check (ruff)
44+
run: uv run ruff format --check .
45+
46+
- name: Type check (pyright)
47+
run: uv run pyright
48+
49+
- name: Tests (pytest)
50+
run: uv run pytest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ prior-art/
1212
# Per-machine Claude Code hook config; written by /ultan-install
1313
.claude/
1414
local/
15+
.coverage

.pre-commit-config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,23 @@ repos:
3333
entry: bash -c 'cd daemon && uv run --frozen pyright "${@#daemon/}"' --
3434
files: ^daemon/.*\.py$
3535
pass_filenames: true
36+
stages: [pre-commit]
3637

3738
- id: pyright-tools-search
3839
name: pyright (tools/search)
3940
language: system
4041
entry: bash -c 'cd tools/search && uv run --frozen pyright "${@#tools/search/}"' --
4142
files: ^tools/search/.*\.py$
4243
pass_filenames: true
44+
stages: [pre-commit]
4345

4446
- id: pyright-src
4547
name: pyright (src)
4648
language: system
4749
entry: bash -c 'cd src && uv run --frozen pyright "${@#src/}"' --
4850
files: ^src/.*\.py$
4951
pass_filenames: true
52+
stages: [pre-commit]
5053

5154
# Pytest per package — runs at pre-push, not pre-commit. Full suite,
5255
# only when something in that package changed since the last push.

daemon/agent_mem_daemon/__main__.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,19 @@
2323
import sys
2424
import threading
2525
from pathlib import Path
26+
from types import FrameType
2627

2728
from .buffer import DEFAULT_INACTIVITY_SECONDS, DEFAULT_MAX_TURNS, RollingBuffer
2829
from .ingest import DEFAULT_POLL_INTERVAL, JsonlTailer
2930
from .logging_setup import configure as configure_logging
30-
from .paths import ensure_home, events_path, log_path, offset_state_path, pid_path
31+
from .paths import (
32+
ensure_home,
33+
events_path,
34+
knowledge_dir,
35+
log_path,
36+
offset_state_path,
37+
pid_path,
38+
)
3139
from .priming_rpc import PrimingRpcThread
3240
from .scheduler import (
3341
DEFAULT_LIBRARIAN_CONCURRENCY,
@@ -240,35 +248,40 @@ def _build_parser() -> argparse.ArgumentParser:
240248
# ---- main loop ------------------------------------------------------
241249

242250

243-
def _prewarm_indexes(knowledge_dir: Path, log: logging.Logger) -> None:
251+
def _prewarm_indexes(knowledge_root: Path, log: logging.Logger) -> None:
244252
"""Verify the BM25 and embedding indexes match the markdown source
245253
of truth at startup. Rebuild on drift; log either way.
246254
247255
Run synchronously before worker threads start so first retrieval
248256
doesn't pay the rebuild cost. Failures fail-soft to lazy rebuild —
249257
a broken index never blocks daemon start.
258+
259+
Note: bm25 and embeddings live in a sibling ``tools/search`` package
260+
not always on the daemon's ``sys.path``; the imports are kept local
261+
to this function and wrapped in best-effort ``try/except`` so a
262+
missing module degrades to lazy rebuild rather than blocking startup.
250263
"""
251-
if not knowledge_dir.exists():
252-
log.info("startup: knowledge dir %s missing — skipping index prewarm", knowledge_dir)
264+
if not knowledge_root.exists():
265+
log.info("startup: knowledge dir %s missing — skipping index prewarm", knowledge_root)
253266
return
254267
try:
255-
from bm25 import load_or_build as bm25_load
268+
from bm25 import load_or_build as bm25_load # noqa: PLC0415
256269

257-
idx = bm25_load(knowledge_dir)
270+
idx = bm25_load(knowledge_root)
258271
log.info("startup: bm25 index ready (%d docs)", len(idx.docs))
259272
except Exception:
260273
log.exception("startup: bm25 prewarm failed (lazy rebuild on first use)")
261274
try:
262-
from embeddings import load_or_build as emb_load
275+
from embeddings import load_or_build as emb_load # noqa: PLC0415
263276

264-
idx = emb_load(knowledge_dir)
277+
idx = emb_load(knowledge_root)
265278
log.info("startup: embedding index ready (%d docs)", len(idx.docs))
266279
except Exception:
267280
log.exception("startup: embedding prewarm failed (lazy rebuild on first use)")
268281

269282

270283
def _install_signal_handlers(stop_event: threading.Event) -> None:
271-
def handler(signum, _frame):
284+
def handler(signum: int, _frame: FrameType | None) -> None:
272285
# signal.Signals(signum).name avoids hardcoding numbers in the
273286
# log line — clearer when reading post-mortem.
274287
try:
@@ -304,9 +317,7 @@ def run(args: argparse.Namespace) -> int:
304317
# Rebuilds on drift (manual edits, git pull from another machine,
305318
# restore from backup); logs "ready (N docs)" otherwise. Failures
306319
# fail-soft — first retrieval rebuilds.
307-
from .paths import knowledge_dir as _knowledge_dir
308-
309-
_prewarm_indexes(_knowledge_dir(), log)
320+
_prewarm_indexes(knowledge_dir(), log)
310321

311322
stop_event = threading.Event()
312323
_install_signal_handlers(stop_event)

daemon/agent_mem_daemon/_response_parser.py

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,72 @@ class ParseDiagnostic:
4545
error: Optional[str] = field(default=None)
4646

4747

48+
def _strip_code_fence(text: str) -> str:
49+
"""Peel a leading markdown code fence (```json ... ```) if present."""
50+
if not text.startswith("```"):
51+
return text
52+
lines = text.splitlines()
53+
inner = lines[1:]
54+
if inner and inner[-1].lstrip().startswith("```"):
55+
inner = inner[:-1]
56+
return "\n".join(inner).strip()
57+
58+
59+
@dataclass
60+
class _BraceScanner:
61+
"""Mutable state for the balanced-brace walk. Separating it from
62+
``_collect_balanced_blocks`` keeps the loop body straight-line."""
63+
64+
depth: int = 0
65+
in_string: bool = False
66+
escape: bool = False
67+
start: int = -1
68+
69+
70+
def _advance_scanner(state: _BraceScanner, i: int, ch: str) -> Optional[Tuple[int, int]]:
71+
"""Feed one character into the scanner. Returns a ``(start, end)``
72+
slice (inclusive end) when a top-level balanced block just closed,
73+
else ``None``."""
74+
if state.escape:
75+
state.escape = False
76+
return None
77+
if state.in_string:
78+
if ch == "\\":
79+
state.escape = True
80+
elif ch == '"':
81+
state.in_string = False
82+
return None
83+
if ch == '"':
84+
state.in_string = True
85+
return None
86+
if ch == "{":
87+
if state.depth == 0:
88+
state.start = i
89+
state.depth += 1
90+
return None
91+
if ch == "}":
92+
state.depth -= 1
93+
if state.depth == 0 and state.start >= 0:
94+
block = (state.start, i)
95+
state.start = -1
96+
return block
97+
return None
98+
99+
100+
def _collect_balanced_blocks(text: str) -> list[str]:
101+
"""Walk ``text`` left-to-right and return every top-level balanced
102+
``{...}`` substring. String-aware so braces inside JSON strings don't
103+
confuse the depth counter."""
104+
blocks: list[str] = []
105+
state = _BraceScanner()
106+
for i, ch in enumerate(text):
107+
closed = _advance_scanner(state, i, ch)
108+
if closed is not None:
109+
s, e = closed
110+
blocks.append(text[s : e + 1])
111+
return blocks
112+
113+
48114
def extract_json_blob(text: str) -> Optional[str]:
49115
"""Find the LAST top-level balanced ``{...}`` block in ``text``.
50116
@@ -66,47 +132,8 @@ def extract_json_blob(text: str) -> Optional[str]:
66132
if not text or not text.strip():
67133
return None
68134

69-
stripped = text.strip()
70-
71-
# ── Step 1: strip a markdown fence if present ────────────────────
72-
if stripped.startswith("```"):
73-
# Drop the opening fence line (```json or ``` or ```anything).
74-
lines = stripped.splitlines()
75-
inner = lines[1:]
76-
# Drop trailing closing fence if present.
77-
if inner and inner[-1].lstrip().startswith("```"):
78-
inner = inner[:-1]
79-
stripped = "\n".join(inner).strip()
80-
81-
# ── Step 2: collect every top-level balanced { ... } ─────────────
82-
blocks: list[str] = []
83-
depth = 0
84-
in_string = False
85-
escape = False
86-
start = -1
87-
88-
for i, ch in enumerate(stripped):
89-
if escape:
90-
escape = False
91-
continue
92-
if in_string:
93-
if ch == "\\":
94-
escape = True
95-
elif ch == '"':
96-
in_string = False
97-
continue
98-
if ch == '"':
99-
in_string = True
100-
elif ch == "{":
101-
if depth == 0:
102-
start = i
103-
depth += 1
104-
elif ch == "}":
105-
depth -= 1
106-
if depth == 0 and start >= 0:
107-
blocks.append(stripped[start : i + 1])
108-
start = -1
109-
135+
stripped = _strip_code_fence(text.strip())
136+
blocks = _collect_balanced_blocks(stripped)
110137
if blocks:
111138
return blocks[-1]
112139

daemon/agent_mem_daemon/_schemas.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from __future__ import annotations
2626

2727
import json
28-
from typing import Any, Dict, List, Literal, Optional, Union
28+
from typing import Dict, List, Literal, Mapping, Optional, Union, cast
2929

3030
from pydantic import BaseModel, ConfigDict, Field, field_validator
3131
from typing_extensions import Annotated
@@ -309,7 +309,7 @@ class LibrarianInterrupt(BaseModel):
309309
description="The exact applies-when phrase that matched the buffer.",
310310
)
311311
evidence: List[EvidenceItem] = Field(
312-
default_factory=list,
312+
default_factory=list[EvidenceItem],
313313
description=(
314314
"List of EvidenceItem dicts (NOT strings). Each item is "
315315
"{turn_id, role, quote}. Emit at least one verbatim turn quote."
@@ -326,7 +326,7 @@ class LibrarianInterrupt(BaseModel):
326326

327327
@field_validator("evidence", mode="before")
328328
@classmethod
329-
def _coerce_evidence(cls, v):
329+
def _coerce_evidence(cls, v: object) -> object:
330330
"""Tolerate LLM-shape drift on evidence.
331331
332332
The schema asks for ``list[EvidenceItem]`` (dicts), but Sonnet /
@@ -343,11 +343,12 @@ def _coerce_evidence(cls, v):
343343
return []
344344
if isinstance(v, str):
345345
return [{"quote": v}]
346-
if isinstance(v, dict):
347-
return [v]
346+
if isinstance(v, Mapping):
347+
return [cast(Mapping[object, object], v)]
348348
if isinstance(v, list):
349-
out = []
350-
for item in v:
349+
raw = cast(list[object], v)
350+
out: list[object] = []
351+
for item in raw:
351352
if isinstance(item, str):
352353
out.append({"quote": item})
353354
else:
@@ -367,7 +368,7 @@ class LibrarianProposal(BaseModel):
367368
model_config = ConfigDict(extra="ignore")
368369

369370
proposals: List[ProposedAction] = Field(
370-
default_factory=list,
371+
default_factory=list[ProposedAction],
371372
description=(
372373
"Ordered list of curator actions the Librarian wants the "
373374
"Scholar to execute. Each item is a typed action object "
@@ -377,7 +378,7 @@ class LibrarianProposal(BaseModel):
377378
),
378379
)
379380
interrupts: List[LibrarianInterrupt] = Field(
380-
default_factory=list,
381+
default_factory=list[LibrarianInterrupt],
381382
description=(
382383
"Optional list of interrupt-nudge candidates: turns in the "
383384
"buffer that match an existing entry's applies-when phrases. "
@@ -479,7 +480,7 @@ class ScholarReview(BaseModel):
479480
model_config = ConfigDict(extra="ignore")
480481

481482
decisions: List[ScholarDecision] = Field(
482-
default_factory=list,
483+
default_factory=list[ScholarDecision],
483484
description=(
484485
"One ScholarDecision per Librarian proposal, matched by "
485486
"`action_index`. Order need not mirror the proposals — the "
@@ -488,7 +489,7 @@ class ScholarReview(BaseModel):
488489
),
489490
)
490491
interrupts_processed: List[ScholarInterruptDecision] = Field(
491-
default_factory=list,
492+
default_factory=list[ScholarInterruptDecision],
492493
description=(
493494
"One ScholarInterruptDecision per LibrarianInterrupt the "
494495
"Scholar reviewed. Approved decisions are written to "
@@ -513,10 +514,10 @@ class LibrarianResponse(BaseModel):
513514

514515
model_config = ConfigDict(extra="ignore")
515516

516-
candidates: List[Dict[str, Any]] = Field(default_factory=list)
517-
interrupt_candidates: List[Dict[str, Any]] = Field(default_factory=list)
518-
proposals: List[Dict[str, Any]] = Field(default_factory=list)
519-
interrupts: List[Dict[str, Any]] = Field(default_factory=list)
517+
candidates: List[Dict[str, object]] = Field(default_factory=list[Dict[str, object]])
518+
interrupt_candidates: List[Dict[str, object]] = Field(default_factory=list[Dict[str, object]])
519+
proposals: List[Dict[str, object]] = Field(default_factory=list[Dict[str, object]])
520+
interrupts: List[Dict[str, object]] = Field(default_factory=list[Dict[str, object]])
520521

521522

522523
class ScholarResponse(BaseModel):
@@ -527,8 +528,8 @@ class ScholarResponse(BaseModel):
527528

528529
model_config = ConfigDict(extra="ignore")
529530

530-
candidates_processed: List[Dict[str, Any]] = Field(default_factory=list)
531-
interrupts_processed: List[Dict[str, Any]] = Field(default_factory=list)
531+
candidates_processed: List[Dict[str, object]] = Field(default_factory=list[Dict[str, object]])
532+
interrupts_processed: List[Dict[str, object]] = Field(default_factory=list[Dict[str, object]])
532533

533534

534535
# ── Prompt-shape generators ──────────────────────────────────────────

0 commit comments

Comments
 (0)