diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3e9c56b --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,92 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +**aaron.tuor@pnnl.gov**. All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +1. **Correction** — Community Impact: use of inappropriate language or other + behavior deemed unprofessional or unwelcome. Consequence: a private, written + warning, providing clarity around the nature of the violation and an + explanation of why the behavior was inappropriate. A public apology may be + requested. +2. **Warning** — Community Impact: a violation through a single incident or + series of actions. Consequence: a warning with consequences for continued + behavior. No interaction with the people involved for a specified period. +3. **Temporary Ban** — Community Impact: a serious violation of community + standards. Consequence: a temporary ban from any sort of interaction or + public communication with the community for a specified period. +4. **Permanent Ban** — Community Impact: demonstrating a pattern of violation of + community standards. Consequence: a permanent ban from any sort of public + interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +[homepage]: https://www.contributor-covenant.org diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..43819ba --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing to DSAgt + +Thanks for your interest in improving DSAgt. This guide covers the mechanics; +for how the codebase is organized and why, see the +[Developer Guide](https://ai-modcon.github.io/dsagt/developer/) (source: +`docs/developer.md`). + +## Getting started + +DSAgt develops on [`uv`](https://github.com/astral-sh/uv) with Python 3.12 or +3.13. + +```bash +git clone https://github.com/AI-ModCon/dsagt.git +cd dsagt +uv sync --all-groups # runtime + dev + docs dependencies +source .venv/bin/activate # so `dsagt` and the helpers are on PATH +``` + +Work on a feature branch off `main`; open a pull request when it's ready. + +## Tests + +Run the unit suite before opening a PR (`python -m pytest`, **not** bare +`pytest` — the bare binary can resolve the wrong interpreter): + +```bash +uv run --no-sync python -m pytest -m "not integration" -q +``` + +Run a single file while iterating: + +```bash +uv run --no-sync python -m pytest tests/test_config.py -q +``` + +Integration tests (`-m integration`) hit real embedding/LLM providers and need +`EMBEDDING_*` / `LLM_*` credentials in the environment; they're excluded from CI +and the default local run. + +## Lint & format + +CI enforces both on `src/` and `tests/` (contributor scripts under +`use_cases/` are exempt): + +```bash +uv run ruff check src tests +uv run black src tests # drop the path to format everything you touched +``` + +## Pull requests + +- Keep PRs focused and reasonably small; describe the intent, not just the diff. +- Update `docs/` and `CHANGELOG.md` when behavior changes. +- Make sure `ruff`, `black --check`, and the non-integration tests pass. +- This is pre-1.0, dev-stage code: prefer clean removal over back-compat shims. + +## Reporting issues + +Open a GitHub issue with steps to reproduce, expected vs actual behavior, your +OS/Python version, and the agent platform involved. For security issues, follow +[SECURITY.md](SECURITY.md) instead of opening a public issue. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..40197d6 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,34 @@ +# Security Policy + +## Reporting a vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Report privately through GitHub's +[private vulnerability reporting](https://github.com/AI-ModCon/dsagt/security/advisories/new) +(the **Security** tab → *Report a vulnerability*). If that isn't available to +you, email the maintainers at **aaron.tuor@pnnl.gov**. + +Please include: + +- a description of the issue and its impact, +- steps to reproduce (or a proof of concept), +- affected version / commit, and +- any suggested remediation. + +The maintainers will acknowledge your report, keep you updated on progress, and +credit you in the fix unless you prefer to remain anonymous. + +## Scope + +DSAgt executes CLI codes and installs skills that the agent registers, and it +runs code from external skill catalogs the user chooses to sync. It does **not** +sandbox that code — see the "Risks" section of [agent-card.md](../agent-card.md). +Reports most relevant to this project include: path-traversal or arbitrary +file write/delete from untrusted skill or code specs, injection via indexed +knowledge-base documents, and unintended handling of credentials. + +## Supported versions + +DSAgt is pre-1.0 and dev-stage; security fixes are applied to the latest +development line (currently the `0.2.x` series on `main`). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..192b86e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + python-version: "3.12" + - name: Install dev tools + run: uv sync --group dev + - name: Ruff + run: uv run ruff check src tests + - name: Black + run: uv run black --check src tests + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12"] + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install (all groups) + run: uv sync --all-groups + # `python -m pytest`, not bare `pytest`, per CLAUDE.md. Integration tests + # need real EMBEDDING_*/LLM_* credentials and are excluded here. + - name: Tests (non-integration) + run: uv run --no-sync python -m pytest -m "not integration" -q diff --git a/CLAUDE.md b/CLAUDE.md index 3bedbae..a911517 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ The codebase separates **commands** (entry points with argparse, launched as CLI Entry points (`pyproject.toml` `[project.scripts]`): `dsagt` → `dsagt.commands.cli:main`, `dsagt-run` → `dsagt.commands.run_code:main`, `dsagt-server` → `dsagt.mcp.server:main`. -**Bundled assets** (shipped as `package-data`): +**Built-in assets** (declared as `package-data`): - `src/dsagt/codes/` — built-in codes as skill-standard dirs (`/SKILL.md`), served from the package (never copied into projects). - `src/dsagt/skills/` — built-in skills (e.g., `skill-creator`) the agent discovers via `search_skills`. - `src/dsagt/dsagt_instructions.md` — agent-agnostic system instructions injected into per-agent files at init. @@ -78,12 +78,16 @@ Distilled from working on this codebase; `knowledge.py` is the reference example **Comments state the real reason, at the point they explain it.** A lazy import is justified *at the import site* with its actual cause, not as an "this is absent" note in the import block citing a stale rationale. If the reason changes, fix the comment. +**No change-narration in comments.** A comment describes what the code does and why it exists *now* — never how it used to work, what changed, or paradigms no longer in the tree. Ban breadcrumbs like "previously…", "was formerly…", "no longer uses…", "moved from…", "(not a `.get` default)", "instead of the old…". Git carries the history; the comment describes the present. State the hazard/intent directly ("session_id is null outside a minted session, and ChromaDB rejects null metadata") rather than contrasting with a prior version ("… beats coercing to `[]` like before"). + **Import hygiene on hot paths.** Modules on frequently-invoked paths (`dsagt-run` runs per tool call) must not transitively drag heavy modules in for *annotation-only* type hints. Use `from __future__ import annotations` + a `TYPE_CHECKING`-guarded import (verify the module doesn't introspect annotations at runtime first). Keep cold start lean; lazy-import the heavy leaf (llama_index) at its single use site. **Naming.** Prefer concise domain names (`APIEmbedder`/`LocalEmbedder`, not `…EmbeddingClient`). **Module docstrings (major modules).** Open with a title line + 3–5 sentences: what the module does, the capabilities it backs, the design motivations. Follow with an **ASCII-art UML class map** — one consistent notation throughout (`knowledge.py` uses `◇` holds · `◆` owns · `▷` inherits). Treat the class-map diagram as a deliverable of any **major module refactor** — refresh it whenever the class structure changes substantially. +**Prose register (docs, comments, changelog, commit messages).** Plain, accurate, direct — no anthropomorphism, no code-jockey slang, no advertising gloss. Concretely: files/modules are *located in* / *defined in* / *stored in*, never "live in"; DSAgt *provides* / *includes* things, it does not "ship" or "provision" them; use *built-in*, not "bundled"; drop marketing gloss ("out of the box", "seamless", "with nothing to remember", "blazing"). State what a thing does, not how nice it is. Changelogs and commit messages record real behavior changes — pure renames and doc-only churn are noise, keep them out. This applies to this file too. + ## BYOA artifacts `dsagt init --agent X --location ` writes, in the project dir: diff --git a/README.md b/README.md index 5bfa956..4e5c8d4 100644 --- a/README.md +++ b/README.md @@ -237,4 +237,4 @@ Each launch gets a session id that every span carries, so you can filter the tra | `dsagt smoke-test [--agent claude\|goose\|codex\|opencode\|cline]` | End-to-end install verification | -For tests, troubleshooting, and other developer-facing material, see [developer.md](developer.md). +For tests, troubleshooting, and other developer-facing material, see [docs/developer.md](docs/developer.md). diff --git a/docs/assets/ai-readiness.png b/docs/assets/ai-readiness.png new file mode 100644 index 0000000..3b523f3 Binary files /dev/null and b/docs/assets/ai-readiness.png differ diff --git a/docs/assets/code-registry.png b/docs/assets/code-registry.png new file mode 100644 index 0000000..3924dc0 Binary files /dev/null and b/docs/assets/code-registry.png differ diff --git a/docs/assets/knowledge-base.png b/docs/assets/knowledge-base.png new file mode 100644 index 0000000..8cadecf Binary files /dev/null and b/docs/assets/knowledge-base.png differ diff --git a/docs/assets/memory.png b/docs/assets/memory.png new file mode 100644 index 0000000..7b5c679 Binary files /dev/null and b/docs/assets/memory.png differ diff --git a/docs/assets/observability.png b/docs/assets/observability.png new file mode 100644 index 0000000..529f7aa Binary files /dev/null and b/docs/assets/observability.png differ diff --git a/docs/assets/pipeline-builder.png b/docs/assets/pipeline-builder.png new file mode 100644 index 0000000..71ac3b0 Binary files /dev/null and b/docs/assets/pipeline-builder.png differ diff --git a/docs/assets/program-overview.png b/docs/assets/program-overview.png new file mode 100644 index 0000000..2657ae0 Binary files /dev/null and b/docs/assets/program-overview.png differ diff --git a/docs/assets/provenance.png b/docs/assets/provenance.png new file mode 100644 index 0000000..fcd4128 Binary files /dev/null and b/docs/assets/provenance.png differ diff --git a/docs/assets/use-cases.png b/docs/assets/use-cases.png new file mode 100644 index 0000000..1b04827 Binary files /dev/null and b/docs/assets/use-cases.png differ diff --git a/docs/developer.md b/docs/developer.md index 98aeb8d..0d842eb 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -1,24 +1,74 @@ # Developer Guide -Material for contributors and developers. +How the DSAgt codebase is set up and how to work in it. For contribution +mechanics (branch/PR flow, commit style), see +[CONTRIBUTING.md](https://github.com/AI-ModCon/dsagt/blob/main/.github/CONTRIBUTING.md). + +## Setup + +DSAgt develops on [uv](https://github.com/astral-sh/uv) with Python 3.12 or 3.13: + +```bash +git clone https://github.com/AI-ModCon/dsagt.git +cd dsagt +uv sync --all-groups # runtime + dev + docs dependencies +source .venv/bin/activate # so dsagt / dsagt-run / dsagt-server are on PATH +``` ## Tests +Use `python -m pytest`, not bare `pytest` (the bare binary can resolve the wrong +interpreter): + +```bash +uv run --no-sync python -m pytest -m "not integration" -q # unit suite (~640 tests) +uv run --no-sync python -m pytest tests/test_config.py -q # a single file +uv run --no-sync python -m pytest -m integration -v # integration (needs creds) +``` + +Integration tests hit real embedding/LLM providers and need `EMBEDDING_*` / +`LLM_*` credentials in the environment; they're excluded from CI and the default +local run. + +## Lint & format + +CI enforces both on `src/` and `tests/` (scientific scripts under `use_cases/` +are exempt): + ```bash -uv run python -m pytest -m "not integration" # unit tests, no creds required -uv run python -m pytest -m integration -v # integration tests (require real credentials / models) +uv run ruff check src tests +uv run black src tests # omit the paths to format everything you touched ``` -For per-flow hand-tests (CLI, VS Code extensions), see the scripts under [`tests/manual_walkthroughs/`](https://github.com/AI-ModCon/dsagt/tree/main/tests/manual_walkthroughs/). +## Docs + +The site is MkDocs (Material). `mkdocs.yml` at the repo root is the site config; +`docs/` holds the pages. The `.github/workflows/docs.yml` workflow builds the +site with `--strict` on every PR and deploys it to GitHub Pages from `main`. + +```bash +uv run mkdocs serve # live preview at http://127.0.0.1:8000 +uv run mkdocs build --strict # what CI runs +``` + +## Codebase orientation + +The [Architecture](architecture.md) page is the map of the system — the +capabilities, the single `dsagt-server` MCP layout, and the observability and +memory design. `CLAUDE.md` at the repo root records the house coding and prose +conventions (it doubles as instructions for AI coding agents working in the +repo); read it before a substantial change. ## Troubleshooting -**Agent command not found.** The agent CLI is not installed or is not on PATH. See the [supported agents table](index.md#supported-agents). +**Agent command not found.** The agent CLI isn't installed or isn't on PATH — +see the [supported agents](index.md#supported-agents). -**MCP server not connecting.** Verify the server command resolves: +**MCP server not connecting.** Confirm the entry point resolves: ```bash uv run which dsagt-server ``` -If missing, reinstall: `pip install --force-reinstall "git+https://github.com/AI-ModCon/dsagt.git"`. \ No newline at end of file +If it's missing, reinstall: +`pip install --force-reinstall "git+https://github.com/AI-ModCon/dsagt.git"`. diff --git a/docs/knowledge-base.md b/docs/knowledge-base.md index 753793d..27929db 100644 --- a/docs/knowledge-base.md +++ b/docs/knowledge-base.md @@ -2,6 +2,8 @@ The knowledge base is DSAgt's catalog of **domain knowledge** — reference corpora and your own documents — that the agent searches to ground its work on scientific data-processing and AI-readiness evaluation. +![DSAgt knowledge base](assets/knowledge-base.png) + ## Domain-knowledge collections | Collection | Source | Populated by | diff --git a/docs/memory.md b/docs/memory.md index f03a2fc..3589109 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -2,6 +2,8 @@ DSAgt gives the agent two kinds of persistent memory backed by the project's vector store: **explicit memory** — facts the user confirms — and opt-in **episodic memory** — an automatic record of session turns. Both are retrievable by the agent with `kb_search` / `kb_get_memories` MCP tools. +![DSAgt memory](assets/memory.png) + ## Explicit memory Explicit memories are facts the user confirms during a session. The agent saves them via `kb_remember`, which writes to both the ChromaDB collection and `/.dsagt/explicit_memories.yaml`. It fetches them via `kb_get_memories` on demand — typically when you ask it to recall something — so they are not auto-loaded at session start. @@ -12,6 +14,15 @@ When episodic memory is enabled, DSAgt reads the agent's transcript as the sessi Retrieval over `session_memory` filters first to a session, then by regex over the query's key terms, before a final **recency-weighted** semantic ranking: a newer turn edges out a stale one as a bounded boost, so a corrected fact wins by recency while a strongly-relevant old turn is never buried. +## Comparison with platform-native memory + +Agent platforms provide their own memory: instruction files such as `CLAUDE.md` and `.goosehints`, and on some platforms notes the agent saves for itself. That memory is agent-curated (the model decides what is worth saving), stored as prose files loaded whole into context, and tied to one platform's format. DSAgt memory differs on each point: + +- **Mechanical capture.** Episodic memory records every completed turn from the transcript; nothing depends on the model choosing to save it. +- **Retrieval on demand.** Memories are recalled by search with recency weighting, not loaded whole into context, so the record can grow without consuming the context window. +- **One store across agents.** The same collections and YAML files serve all five supported platforms, so memory persists across a switch of agent. +- **Auditable facts.** Explicit memories are user-confirmed and durable, with superseded entries kept in a history file. + ## Try it ```bash @@ -37,6 +48,7 @@ And **episodic** memory (captured automatically — no `remember` step): The agent recalls the decision from `session_memory` even though you never explicitly stored it. Confirm the collection materialized: + ```bash ls ~/dsagt-projects/demo/kb_index/session_memory/ ``` \ No newline at end of file diff --git a/docs/observability.md b/docs/observability.md index d6f58e3..45c37dc 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -1,6 +1,10 @@ # Observability -DSAgt logs traces to a **MLflow** via an SQLite file at `~/dsagt-projects//mlflow.db`. To view in the MLflow UI: +DSAgt logs traces to a **MLflow** via an SQLite file at `~/dsagt-projects//mlflow.db`. + +![DSAgt observability](assets/observability.png) + +To view in the MLflow UI: ```bash dsagt traces # rund dsagt mlflow ui --backend-store-uri sqlite:////mlflow.db diff --git a/docs/provenance.md b/docs/provenance.md index 2df3093..340be38 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -2,10 +2,14 @@ DSAgt makes every data operation a reproducible, auditable step. The agent registers a **code** — a CLI executable — and every run of that code is wrapped for provenance capture, so the whole pipeline can later be reconstructed from the record. +![DSAgt provenance](assets/provenance.png) + ## Codes Codes are CLI executables defined as markdown files with YAML frontmatter under `/codes/`. The agent registers new codes via the MCP server's `save_code_spec` tool and finds existing ones via `search_registry`. +![DSAgt code registry](assets/code-registry.png) + A code spec includes: - A YAML frontmatter block describing the executable, parameters, dependencies, and tags. diff --git a/docs/use-cases/index.md b/docs/use-cases/index.md index 9efc564..9d4bad4 100644 --- a/docs/use-cases/index.md +++ b/docs/use-cases/index.md @@ -1,6 +1,8 @@ # Use Cases -End-to-end walkthroughs for representative scientific and data-readiness scenarios live in [`use_cases/`](https://github.com/AI-ModCon/dsagt/tree/main/use_cases/). Each covers data acquisition, code registration, pipeline construction, and agent-driven execution against a real dataset. +End-to-end walkthroughs for representative scientific and data-readiness scenarios are located in [`use_cases/`](https://github.com/AI-ModCon/dsagt/tree/main/use_cases/). Each covers data acquisition, code registration, pipeline construction, and agent-driven execution against a real dataset. + +![DSAgt use cases](../assets/use-cases.png) diff --git a/pyproject.toml b/pyproject.toml index 3fa8d5c..0f40469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "dsagt" dynamic = ["version"] description = "DataSmith Agent - AI-assisted data pipeline builder" readme = "README.md" -requires-python = ">=3.12,<3.14" +requires-python = ">=3.12,<3.13" license = "Apache-2.0" license-files = ["LICENSE"] authors = [ @@ -30,6 +30,7 @@ dependencies = [ "sentence-transformers==5.4.0", # local embeddings & reranking "transformers==4.47.0", # pin for sentence-transformers "rank-bm25>=0.2.2", # sparse keyword retrieval + "ruyaml>=0.91.0", ] [project.scripts] @@ -40,8 +41,8 @@ dsagt-server = "dsagt.mcp.server:main" [dependency-groups] dev = [ "pytest>=8.0", - "black>=24.0", - "ruff>=0.5.0", + "black==26.5.1", + "ruff==0.15.20", ] docs = [ "mkdocs-material>=9.5", @@ -90,3 +91,16 @@ required-environments = [ "sys_platform == 'darwin' and platform_machine == 'x86_64'", "sys_platform == 'linux' and platform_machine == 'x86_64'" ] + +[tool.ruff] +target-version = "py312" + +[tool.ruff.lint.per-file-ignores] +# E402 (imports not at top) is intentional in these modules: __init__.py sets +# OpenMP/SSL env vars that must be in place before heavy libs import; +# knowledge.py defers embedding-backend imports; the integration tests guard +# their imports behind a credentials/skip check. +"src/dsagt/__init__.py" = ["E402"] +"src/dsagt/knowledge.py" = ["E402"] +"tests/test_knowledge_integration.py" = ["E402"] +"tests/test_dependency_integration.py" = ["E402"] diff --git a/src/dsagt/__init__.py b/src/dsagt/__init__.py index df446b9..2141e2a 100644 --- a/src/dsagt/__init__.py +++ b/src/dsagt/__init__.py @@ -35,6 +35,7 @@ # inject_into_ssl() must run before any ssl.SSLContext is constructed. try: import truststore as _truststore + _truststore.inject_into_ssl() del _truststore except ImportError: diff --git a/src/dsagt/agents/base.py b/src/dsagt/agents/base.py index 21c2ef6..22798ea 100644 --- a/src/dsagt/agents/base.py +++ b/src/dsagt/agents/base.py @@ -380,7 +380,7 @@ def owned_artifacts(self, working_dir: Path) -> list[Path]: def runtime_env(self, config: dict) -> dict[str, str]: """Dsagt-owned env vars the agent process needs at runtime (BYOA). - Default is empty: DSAGT no longer forces any telemetry env on the + Default is empty: DSAGT sets no telemetry env on the agent (agent traces are recovered post-hoc from the on-disk transcript, not by native OTel emission). Subclasses override only to set per-project state-dir env (``CLINE_DIR``, diff --git a/src/dsagt/agents/cline.py b/src/dsagt/agents/cline.py index 2bdff83..0de3cda 100644 --- a/src/dsagt/agents/cline.py +++ b/src/dsagt/agents/cline.py @@ -53,8 +53,8 @@ observability still work via dsagt-run / MCP-server spans. MCP config: cline 3.x loads whatever file ``CLINE_MCP_SETTINGS_PATH`` -names (hand-written files work — the old "only ``cline mcp add`` is -loaded" behavior is gone), with the schema +names — a hand-written file works, not only one written by ``cline mcp +add`` — with the schema ``{"mcpServers": {: {"transport": {type, command, args, env}}}}``. The env block rides in ``transport.env`` so dsagt MCP-server children get ``MLFLOW_TRACKING_URI``, ``DSAGT_PROJECT_DIR``, ``EMBEDDING_*``. diff --git a/src/dsagt/agents/codex.py b/src/dsagt/agents/codex.py index 5c293ab..3433129 100644 --- a/src/dsagt/agents/codex.py +++ b/src/dsagt/agents/codex.py @@ -68,7 +68,7 @@ def _render_codex_config(mcp_env: dict) -> str: set on the codex CLI directly (``--dangerously-bypass-approvals-and-sandbox``) instead of here. - No ``[otel]`` block: DSAGT no longer forces codex's native telemetry + No ``[otel]`` block: DSAGT doesn't touch codex's native telemetry (nor the ``log_user_prompt`` privacy override). Codex's conversation history is recovered post-hoc from its on-disk session rollout. """ diff --git a/src/dsagt/commands/info.py b/src/dsagt/commands/info.py index 80f04fc..19876ae 100644 --- a/src/dsagt/commands/info.py +++ b/src/dsagt/commands/info.py @@ -249,7 +249,7 @@ def _row_source_for(tags: dict, metadata: dict) -> str: dispatch shell / ``code_execute_span`` / the background emitters. Agent traces carry ``dsagt.agent`` metadata, stamped by ``MLflowSink``. Neither overlaps, so the bucket is a direct lookup; anything else (a stray trace - with neither — which should no longer happen now background work is tagged) + with neither — background work is tagged, so this shouldn't occur) is ``"unknown"``. """ src = (tags or {}).get("dsagt.source") @@ -425,8 +425,8 @@ def _load_traces(mlflow_db: Path, project_name: str): def _report(project_name: str, config: dict, traces) -> dict: """Build the structured report dict. CLI formats it; --json prints it.""" agent_header = config.get("agent", "-") - # BYOA: dsagt no longer records the agent's LLM model (the agent talks to - # its provider directly). Surface the embedding model dsagt configures. + # BYOA: dsagt doesn't record the agent's LLM model (the agent talks to its + # provider directly), so surface the embedding model dsagt configures. model_header = config.get("embedding", {}).get("model", "-") if traces is None or traces.empty: diff --git a/src/dsagt/commands/setup_core_kb.py b/src/dsagt/commands/setup_core_kb.py index 8231967..7af750b 100644 --- a/src/dsagt/commands/setup_core_kb.py +++ b/src/dsagt/commands/setup_core_kb.py @@ -1,9 +1,8 @@ """ -Knowledge-base asset builder — the engine behind ``dsagt init``'s KB -provisioning. No longer a standalone command (``dsagt setup-kb`` was -retired); ``session._provision_kb`` calls :func:`resolve_assets` + -:func:`ensure_assets` to build the requested assets into the shared -``~/dsagt-projects/kb_index/`` once, then copies them per project. +Knowledge-base asset builder — the engine behind ``dsagt init``'s KB setup. +``session._provision_kb`` calls :func:`resolve_assets` + :func:`ensure_assets` +to build the requested assets into the shared ``~/dsagt-projects/kb_index/`` +once, then copies them per project. Asset namespace (the ``--include`` / ``--exclude`` selectors on ``dsagt init``): - ``tools`` bundled tool specs (cheap, local) diff --git a/src/dsagt/knowledge.py b/src/dsagt/knowledge.py index ab3204b..18973ce 100644 --- a/src/dsagt/knowledge.py +++ b/src/dsagt/knowledge.py @@ -202,10 +202,10 @@ class LocalEmbedder(Embedder): backend = "local" #: Default local model. ``bge-small-en-v1.5`` (33M params, ~130 MB - #: on disk, ~250 MB resident) is ~3× faster and ~3× smaller than the - #: ``bge-base`` variant we used previously, with ~2 nDCG@10 points - #: lower MTEB retrieval score — a hard-to-notice difference for - #: typical DSAGT KB sizes (single-digit thousands of chunks). + #: on disk, ~250 MB resident) is ~3× faster and ~3× smaller than + #: ``bge-base`` for ~2 nDCG@10 points lower MTEB retrieval score — a + #: hard-to-notice difference for typical DSAGT KB sizes (single-digit + #: thousands of chunks). #: Override via ``embedding.model`` in ``.dsagt/config.yaml`` (e.g. #: ``BAAI/bge-large-en-v1.5`` for higher quality at the cost of #: ~10× memory and ~5× CPU). @@ -394,8 +394,8 @@ def __init__( base_url or os.getenv("EMBEDDING_BASE_URL") or os.getenv("OPENAI_BASE_URL") ) # EMBEDDING_API_KEY is the canonical name; LLM_API_KEY/OPENAI_API_KEY - # are legacy fallbacks from when the embedding endpoint shared the - # LLM endpoint's auth. + # are accepted as fallbacks for setups where the embedding endpoint + # shares the LLM endpoint's auth. self.api_key = ( api_key or os.getenv("EMBEDDING_API_KEY") diff --git a/src/dsagt/mcp/server.py b/src/dsagt/mcp/server.py index 5bc94a8..7010476 100644 --- a/src/dsagt/mcp/server.py +++ b/src/dsagt/mcp/server.py @@ -1,19 +1,17 @@ -"""DSAGT MCP Server — the single merged registry + knowledge server. - -Supersedes the two former servers (``dsagt-registry-server`` + -``dsagt-knowledge-server``). Both previously constructed their own -:class:`~dsagt.knowledge.KnowledgeBase` — two embedders, two Chroma accesses, -and a write-here/read-there hazard on the ``skills_catalog__*`` collections -(synced by knowledge, searched by registry). Merging into one process gives one -embedder, one Chroma owner, one ``init_tracing``, and one MCP server per agent. - -The heavy/risky work is already offloaded out of the event loop (``run_command`` -→ ``dsagt-run`` subprocess; ``kb_ingest`` → background job thread), so collapsing -the two processes costs little isolation. - -Tool *definitions* and *handlers* live in their concern modules +"""DSAGT MCP Server — the single ``dsagt-server`` (registry + knowledge). + +One process, one :class:`~dsagt.knowledge.KnowledgeBase`, one ``init_tracing``, +one MCP server per agent — a single embedder and a single Chroma owner back +every concern. Single ownership matters for the ``skills_catalog__*`` +collections, which are written under the skill concern and read under the +registry concern: one owner removes any write-here/read-there hazard across +them. Heavy/risky work runs off the event loop (``run_command`` → +``dsagt-run`` subprocess; ``kb_ingest`` → background job thread), so one process +costs little isolation. + +Tool *definitions* and *handlers* are defined in their concern modules (:mod:`~dsagt.mcp.registry_tools` / :mod:`~dsagt.mcp.knowledge_tools` / -:mod:`~dsagt.mcp.memory_tools` / :mod:`~dsagt.mcp.skill_tools`); this module only +:mod:`~dsagt.mcp.memory_tools` / :mod:`~dsagt.mcp.skill_tools`); this module composes their ``(tools, handlers)`` under one dispatch shell (:func:`build_dispatch_server`) and owns the shared-KB startup. The factory imports are *lazy* (inside :func:`create_dsagt_server` / :func:`main`) so the @@ -21,11 +19,6 @@ cycle. See ``design-notes/skills-catalog-server-merge.md`` §2. - -Backward compatibility is **rebuild-not-migrate**: a project created against the -old two-server layout adopts this by re-running ``dsagt start`` (which -regenerates the per-agent MCP config to a single ``dsagt`` server). See the -upgrade note in the README. """ import os diff --git a/src/dsagt/memory.py b/src/dsagt/memory.py index 8350b4b..4e0b1cb 100644 --- a/src/dsagt/memory.py +++ b/src/dsagt/memory.py @@ -71,9 +71,9 @@ def _load(self) -> list[dict]: return [] data = yaml.safe_load(text) if not isinstance(data, list): - # Out-of-contract: a non-list payload means the file is corrupt or - # hand-edited wrong. Surfacing beats silently coercing to [] and - # then overwriting the original on the next _save (silent data loss). + # A non-list payload means the file is corrupt or hand-edited. + # Raising keeps the next _save from silently overwriting (and + # losing) its real contents. raise ValueError( f"{self._path} is not a list of memories " f"(got {type(data).__name__}); inspect and fix the file." diff --git a/src/dsagt/provenance.py b/src/dsagt/provenance.py index 3ae5310..079658a 100644 --- a/src/dsagt/provenance.py +++ b/src/dsagt/provenance.py @@ -265,9 +265,9 @@ def execution_metadata(record: dict) -> dict: meta: dict = {} meta["code_name"] = record.get("code_name") or "unknown" - # ``or`` (not a .get default): a record written outside a minted session - # stores session_id: null, and ChromaDB rejects a None metadata value — - # which would poison the whole batch and re-fail every heartbeat. + # A code run outside a minted session stores session_id: null, and ChromaDB + # rejects a null metadata value — which would fail the whole batch add and + # re-fail every heartbeat. Coerce null to "unknown". meta["session_id"] = record.get("session_id") or "unknown" if execution and execution.get("return_code") is not None: diff --git a/src/dsagt/registry.py b/src/dsagt/registry.py index 4e425ff..2e07dad 100644 --- a/src/dsagt/registry.py +++ b/src/dsagt/registry.py @@ -14,9 +14,9 @@ runnable command in context at invocation time, alongside MCP discovery via ``search_registry``. When registered, executables are wrapped with dsagt-run + uv run --with. -The wrapper lives *inside* the stored shell command by design: execution -used to be dispatched by MCP-server tools, but agents routinely sidestepped -those with their own bash tools, losing provenance. Baking dsagt-run into +The wrapper is baked *inside* the stored shell command by design: agents +routinely run their own bash tools, sidestepping any MCP-mediated execution, +so provenance has to be captured at the shell boundary. Baking dsagt-run into the command the agent copies makes the bash path harmless — the residual failure mode is an agent reconstructing the command from memory and dropping the wrapper, which is why specs render the exact runnable command @@ -56,11 +56,6 @@ #: they can be evicted and refreshed on dsagt upgrade without touching #: agent-registered entries. CODES_COLLECTION = "codes" -#: Legacy installed-skills collection. No longer written or read: installed -#: skills are natively auto-discovered by every supported agent, so skill -#: search covers only the *catalog* tier below. Kept as a name for back-compat -#: and ``dsagt info`` display of any pre-existing index. -SKILLS_COLLECTION = "skills" #: External skill catalogs (fetched from GitHub repos) live in their own #: per-source collections named ``skills_catalog__``. Keeping each @@ -75,12 +70,6 @@ def catalog_collection(slug: str) -> str: return f"{CATALOG_COLLECTION_PREFIX}{slug}" -#: Backwards-compat aliases — kept so external code that imported the -#: previous names still resolves. New code should use the names above. -TOOL_REGISTRY_COLLECTION = CODES_COLLECTION -SKILL_REGISTRY_COLLECTION = SKILLS_COLLECTION - - # --------------------------------------------------------------------------- # Helpers (codes only) # --------------------------------------------------------------------------- @@ -582,8 +571,7 @@ def save_skill( Returns "added" or "updated". Does **not** index into a KB: saved skills land in ``/skills/`` where every supported agent natively auto-discovers them, so search only covers the - not-yet-installed *catalog* tier (see ``SkillRouter``). The old - ``skills`` collection is no longer read by anything. + not-yet-installed *catalog* tier (see ``SkillRouter``). """ name = spec.get("name") if not name: diff --git a/src/dsagt/session.py b/src/dsagt/session.py index e167a87..6b03faf 100644 --- a/src/dsagt/session.py +++ b/src/dsagt/session.py @@ -700,11 +700,11 @@ def remove_project(project_name: str, keep_files: bool = False) -> Path: def catch_up_extraction(pdir: Path, config: dict) -> dict: """Background post-session catch-up — run by the MCP server at startup. - The MCP server owns the session lifecycle now: each launch, it spawns - this against a snapshot taken at startup, so it processes the *previous* - session's trailing trace records, never the live one. This removes the - need for a reliable session-*end* trigger (``dsagt start`` no longer runs - any extraction) and gives bare-launched agents full parity. + The MCP server owns the session lifecycle: each launch, it spawns this + against a snapshot taken at startup, so it processes the *previous* + session's trailing trace records, never the live one. This means no + reliable session-*end* trigger is required, and bare-launched agents get + full parity. Two phases, both best-effort: diff --git a/src/dsagt/traces.py b/src/dsagt/traces.py index de1093a..674ccb4 100644 --- a/src/dsagt/traces.py +++ b/src/dsagt/traces.py @@ -1404,9 +1404,9 @@ def collect(self, *, include_last: bool = False) -> int: continue try: consumer.write(trace.subset(emit_ids)) - # Persist acks inside the same try: if it raised outside, - # a post-write failure would propagate out of collect() and - # the next pass would re-emit these turns as duplicates. + # Ack within the same per-consumer try as the write, so a + # failure is isolated to this consumer and turns already + # written can't re-emit as duplicates on the next pass. self._save_acks( consumer.name, acks | {key_by_span[s] for s in emit_ids} ) diff --git a/tests/test_knowledge_base.py b/tests/test_knowledge_base.py index a85be5c..1ac7059 100644 --- a/tests/test_knowledge_base.py +++ b/tests/test_knowledge_base.py @@ -760,7 +760,7 @@ def test_ingest_empty_folder(self, kb, tmp_path): def test_ingest_custom_file_types(self, kb, source_folder): """Custom file_types filters which files are processed.""" - result = kb.ingest(source_folder, file_types=["txt"]) + kb.ingest(source_folder, file_types=["txt"]) # Only .txt files should be processed chunks_path = kb.index_dir / "test_docs" / "chunks.jsonl" @@ -850,7 +850,6 @@ def test_search_with_rerank(self, kb_with_data): mock_st = MagicMock() mock_st.CrossEncoder.return_value = mock_reranker - with patch.dict(sys.modules, {"sentence_transformers": mock_st}): # Ensure the lazy import triggers kb_with_data._reranker = None diff --git a/tests/test_knowledge_integration.py b/tests/test_knowledge_integration.py index e125c03..5590565 100755 --- a/tests/test_knowledge_integration.py +++ b/tests/test_knowledge_integration.py @@ -222,7 +222,9 @@ def test_list_after_ingest(self, kb_server): class TestSetupRuntimeKB: - def test_runtime_search_after_setup(self, tmp_path, smoke_test_dir, embedding_config): + def test_runtime_search_after_setup( + self, tmp_path, smoke_test_dir, embedding_config + ): """End-to-end: a KB pointed at a runtime dir provisioned by setup_runtime_kb can search the copied collections with real embeddings. diff --git a/tests/test_memory.py b/tests/test_memory.py index d09467d..c78e895 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -108,7 +108,7 @@ def test_superseded_entry_in_history(self, mem, tmp_path): def test_supersede_preserves_other_entries(self, mem): r1 = mem.remember("fact one") - r2 = mem.remember("fact two") + mem.remember("fact two") mem.remember("fact one updated", supersedes=r1["entry_id"]) entries = mem.get_all() diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 129f9f3..79aaee8 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -357,7 +357,7 @@ def test_full_pipeline_with_deps(self, tmp_path): # Steps in correct order lines = script.splitlines() - step_lines = [l for l in lines if l.startswith("# Step")] + step_lines = [line for line in lines if line.startswith("# Step")] assert "fastp" in step_lines[0] assert "megahit" in step_lines[1] assert "quast" in step_lines[2] diff --git a/tests/test_registry_server.py b/tests/test_registry_server.py index cc9ef41..3699bdd 100644 --- a/tests/test_registry_server.py +++ b/tests/test_registry_server.py @@ -553,8 +553,8 @@ class TestToolIndexing: """Tests for KB-backed tool registration and search.""" def test_save_tool_indexes_into_kb(self, tmp_path): - """Saving a tool indexes it into the registered_tools collection.""" - from dsagt.registry import TOOL_REGISTRY_COLLECTION + """Saving a code indexes it into the codes collection.""" + from dsagt.registry import CODES_COLLECTION server, reg, kb = _make_server_with_kb(tmp_path) @@ -569,7 +569,7 @@ def test_save_tool_indexes_into_kb(self, tmp_path): }, ) - results = kb.search("filter", collection=TOOL_REGISTRY_COLLECTION) + results = kb.search("filter", collection=CODES_COLLECTION) assert len(results) > 0 assert any("csv-filter" in r["chunk"].get("text", "") for r in results) diff --git a/tests/test_trace_scan.py b/tests/test_trace_scan.py index be4d2b8..b36df5c 100644 --- a/tests/test_trace_scan.py +++ b/tests/test_trace_scan.py @@ -351,9 +351,7 @@ def test_catch_up_traces_noop_without_previous_or_transcript(tmp_path): # Previous session exists but recorded no transcript (SQLite agent / too # short) → skip rather than risk reading the new session's transcript. - write_state( - proj, {"sessions": [{"id": 1}, {"id": 2}], "memory_cursor": {}} - ) + write_state(proj, {"sessions": [{"id": 1}, {"id": 2}], "memory_cursor": {}}) assert _catch_up_traces(proj, config, MagicMock()) == 0 @@ -405,9 +403,7 @@ def test_emitted_traces_land_in_the_store(scan_env): ) collector.collect(include_last=True) # emit both turns exp = mlflow.get_experiment_by_name("proj") - traces = mlflow.search_traces( - locations=[exp.experiment_id], return_type="list" - ) + traces = mlflow.search_traces(locations=[exp.experiment_id], return_type="list") sessions = {t.info.trace_metadata.get("mlflow.trace.session") for t in traces} assert len(traces) == 2 assert sessions == {"proj:s"} diff --git a/tests/test_traces_cline.py b/tests/test_traces_cline.py index c2e26d6..044ff22 100644 --- a/tests/test_traces_cline.py +++ b/tests/test_traces_cline.py @@ -163,7 +163,5 @@ def test_end_to_end_through_the_sink(mlflow_sqlite): ids = MLflowSink(mlflow_sqlite, "clineproj").write(_translate()) assert len(ids) == 2 exp = mlflow.get_experiment_by_name("clineproj") - traces = mlflow.search_traces( - locations=[exp.experiment_id], return_type="list" - ) + traces = mlflow.search_traces(locations=[exp.experiment_id], return_type="list") assert len(traces) == 2 diff --git a/tests/test_traces_codex.py b/tests/test_traces_codex.py index 4cbef4a..0c1daa2 100644 --- a/tests/test_traces_codex.py +++ b/tests/test_traces_codex.py @@ -203,9 +203,7 @@ def test_end_to_end_through_the_sink(mlflow_sqlite): ids = MLflowSink(mlflow_sqlite, "codexproj").write(trace) assert len(ids) == 2 # one MLflow trace per turn exp = mlflow.get_experiment_by_name("codexproj") - traces = mlflow.search_traces( - locations=[exp.experiment_id], return_type="list" - ) + traces = mlflow.search_traces(locations=[exp.experiment_id], return_type="list") assert len(traces) == 2 # the tool-bearing turn rendered llm + tool spans under its agent root shapes = {len(t.data.spans) for t in traces} diff --git a/tests/test_traces_goose.py b/tests/test_traces_goose.py index 4cac896..b9084a6 100644 --- a/tests/test_traces_goose.py +++ b/tests/test_traces_goose.py @@ -161,7 +161,5 @@ def test_end_to_end_through_the_sink(mlflow_sqlite): ids = MLflowSink(mlflow_sqlite, "gooseproj").write(_translate()) assert len(ids) == 2 exp = mlflow.get_experiment_by_name("gooseproj") - traces = mlflow.search_traces( - locations=[exp.experiment_id], return_type="list" - ) + traces = mlflow.search_traces(locations=[exp.experiment_id], return_type="list") assert len(traces) == 2 diff --git a/tests/test_traces_opencode.py b/tests/test_traces_opencode.py index 93e4eaa..a73b542 100644 --- a/tests/test_traces_opencode.py +++ b/tests/test_traces_opencode.py @@ -164,7 +164,5 @@ def test_end_to_end_through_the_sink(mlflow_sqlite): ids = MLflowSink(mlflow_sqlite, "ocproj").write(_translate()) assert len(ids) == 2 exp = mlflow.get_experiment_by_name("ocproj") - traces = mlflow.search_traces( - locations=[exp.experiment_id], return_type="list" - ) + traces = mlflow.search_traces(locations=[exp.experiment_id], return_type="list") assert len(traces) == 2