DM-Code-Agent — Auditable Code-Maintenance Agent
A local-first code-maintenance agent whose core is one ReAct loop — inspect, plan, patch, verify against tests — so every step stays in a record you can read.
A local-first code-maintenance agent whose entire core is one ReAct loop: inspect a repository, plan a change, patch it, then verify with tests, keeping an auditable record of every step. Provider-switchable over any OpenAI-compatible endpoint (DeepSeek, OpenAI, Claude or Gemini), with built-in and user-defined skill packs, MCP config examples and a 550-test suite.
Created by TalentDelta Team Member · via TalentDelta
build 1.0.0 · 3676.7 KB · published 2026-09-30 · 277 files
Licence: MIT
Scouted from github.com/hwfengcs/DM-Code-Agent at commit e4530be37e6f972d9735381e524cab4b5f732ddd (2026-08-27). Licence MIT. Upstream credit: hwfengcs. bench_reports excluded as regenerable; every other file byte-identical to the pin. Upstream code and licence stay upstream's.
Brought into the catalogue by TalentDelta Team Member (Scout).
277 files · 5656.9 KB · previews are generated at approval and never executed
dm-code-maintenance-agent/.clinerulestext · 0.3 KB
# Claude Code 永久规则 ## 语言偏好 - 始终使用中文与用户交流 - 除非用户特别要求使用其他语言,否则所有生成的文档、注释、代码说明都使用中文 - 代码变量名、函数名等可以使用英文,但注释和文档说明必须是中文dm-code-maintenance-agent/.env.exampletext · 0.4 KB
# API key configuration example. # Fill in the provider keys you plan to use. # DeepSeek API key (default provider). DEEPSEEK_API_KEY=your_deepseek_api_key_here # OpenAI API key (optional). OPENAI_API_KEY=your_openai_api_key_here # Claude / Anthropic API key (optional). CLAUDE_API_KEY=your_claude_api_key_here # Google Gemini API key (optional). GEMINI_API_KEY=your_gemini_api_key_heredm-code-maintenance-agent/.github/ISSUE_TEMPLATE/bug_report.ymlcode · 2.7 KB
name: Bug report description: Report a defect in DM-Code-Agent. title: "[Bug] " labels: ["bug"] body: - type: markdown attributes: value: | Thanks for reporting a bug. Please fill in as much detail as you can. If the bug touches a specific provider (DeepSeek/OpenAI/Claude/Gemini) or MCP server, mention it in the description. - type: textarea id: summary attributes: label: Summary description: One or two sentences describing the bug. placeholder: "dm-agent crashes when --trace is set and the workspace is read-only." validations: required: true - type: textarea id: reproduction attributes: label: Steps to reproduce description: Minimal reproducible commands or scripts. placeholder: | 1. `pip install -e ".[dev]"` 2. `dm-agent "..." --trace traces/x.jsonl` 3. Observe the error. render: bash validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: What did you expect to happen? validations: required: true - type: textarea id: actual attributes: label: Actual behavior description: What actually happened? Paste tracebacks if available. render: text validations: required: true - type: input id: version attributes: label: DM-Code-Agent version description: Output of `python -c "import dm_agent; print(dm_agent.__version__)"`. placeholder: "1.5.0" validations: required: true - type: input id: python attributes: label: Python version placeholder: "3.11" validations: required: true - type: input id: os attributes: label: Operating system placeholder: "Windows 11 / Ubuntu 22.04 / macOS 14" validations: required: true - type: dropdown id: provider attributes: label: LLM provider (if relevant) options: - "Not relevant" - "DeepSeek" - "OpenAI" - "Claude" - "Gemini" - "Custom base_url" validations: required: false - type: textarea id: trace attributes: label: Trace excerpt (optional) description: | Paste a small excerpt of `traces/*.jsonl` if useful. Strip secrets first. render: json validations: required: false - type: checkboxes id: checks attributes: label: Pre-submission checks options: - label: I removed API keys, file paths, and other secrets from the report. required: true - label: I searched existing issues for duplicates. required: truedm-code-maintenance-agent/.github/ISSUE_TEMPLATE/config.ymlcode · 0.4 KB
blank_issues_enabled: false contact_links: - name: Discussion url: https://github.com/hwfengcs/DM-Code-Agent/discussions about: Ask a question, share a use case, or discuss research direction. - name: Security report url: https://github.com/hwfengcs/DM-Code-Agent/security/advisories/new about: Report a vulnerability privately. See SECURITY.md.dm-code-maintenance-agent/.github/ISSUE_TEMPLATE/feature_request.ymlcode · 2.2 KB
name: Feature request description: Propose a new feature, tool, or behavior change. title: "[Feature] " labels: ["enhancement"] body: - type: markdown attributes: value: | Thanks for proposing a new feature. DM-Code-Agent stays small and readable on purpose, so please describe both the value and the cost. - type: textarea id: problem attributes: label: What problem does this solve? description: A short problem statement. Whose workflow gets better? validations: required: true - type: textarea id: proposal attributes: label: Proposed solution description: | A concrete description of the change. Include CLI flags, public API names, prompt structure, or trace event schema if relevant. validations: required: true - type: textarea id: alternatives attributes: label: Alternatives considered description: Any other designs you considered, and why this one wins. validations: required: false - type: dropdown id: scope attributes: label: Component multiple: true options: - "Core (agent loop, planner, replan)" - "Tools (file/exec/test/lint/AST/code-index)" - "Skills" - "MCP integration" - "Tracing / replay" - "Benchmarks (coding / maintenance / SWE-bench)" - "Evals" - "Memory / context compression / RAG" - "Reflexion / Critic / self-consistency" - "Documentation" - "Other" validations: required: true - type: textarea id: impact attributes: label: Expected impact and trade-offs description: | What will this change cost (latency, tokens, complexity, dependencies)? What measurable improvement will it bring (pass rate, recovery, readability)? validations: required: false - type: checkboxes id: checks attributes: label: Pre-submission checks options: - label: I read CONTRIBUTING.md and AGENTS.md. required: true - label: This change can be implemented without removing existing safety defaults. required: truedm-code-maintenance-agent/.github/PULL_REQUEST_TEMPLATE.mdmarkdown · 1.6 KB
Summary
Type of change
- Bug fix
- New feature / capability
- Refactor (no behavior change)
- Documentation only
- Benchmark / eval change
- Trace schema or replay change
- Tooling / CI
Implementation notes
Verification
Run before requesting review:
python -m compileall dm_agent main.py tests python -m pytest python -m dm_agent.evals.cli --variant full --task direct_finish python -m dm_agent.benchmarks.cli --suite maintenance --list python -m ruff check . python -m black --check .If this change touches benchmarks or live-model behavior:
- Attached relevant
bench_reports/*.mdoreval_reports/*.md. - Documented the model and provider used.
- Documented the random seed / sampling parameters.
Safety checklist
- No API keys,
.env,config.json, ormcp_config.jsonin the diff. - No new default-on dangerous tool replay or shell execution paths.
- Trace defaults still keep full LLM I/O capture opt-in.
Related issues / research log
dm-code-maintenance-agent/.github/workflows/ci.ymlcode · 4.5 KB
name: CI on: push: pull_request: jobs: test: name: Python ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] python-version: ["3.10", "3.11", "3.12"] steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Set up uv # 钉精确版本而不是滚动标签 v9:setup-uv 只推到 v7 为止的滚动标签, # v8/v9 只有 vX.Y.Z 形式,写 @v9 会直接 "unable to resolve action"。 # 钉死版本也与本项目 uv sync --frozen 的可复现精神一致。 uses: astral-sh/setup-uv@v9.0.0 with: python-version: ${{ matrix.python-version }} enable-cache: true # uv.lock 必须与 pyproject.toml 保持同步:改了依赖却忘记重新 uv lock 时, # 这一步直接 fail,而不是让后续步骤装出一份与锁文件不符的依赖。 - name: Verify lockfile is up to date run: uv lock --check # --frozen:严格按 uv.lock 安装,不做任何重解析。 # 这是可复现构建的关键——任意两次 CI run 装到的依赖版本完全一致。 - name: Install package from lockfile run: uv sync --frozen --extra dev - name: Compile Python sources run: uv run --frozen python -m compileall dm_agent main.py - name: Run tests run: uv run --frozen python -m pytest - name: Run deterministic eval smoke test run: uv run --frozen python -m dm_agent.evals.cli --variant full --task direct_finish - name: Run full deterministic eval suite with quality gate run: | uv run --frozen python -m dm_agent.evals.cli --output eval_reports/ci-report.json uv run --frozen python -m dm_agent.evals.gate eval_reports/ci-report.json --min-success-rate 1.0 - name: Guard benchmark manifests against silent task drift run: | uv run --frozen python -m dm_agent.benchmarks.cli --suite coding --manifest-only eval_reports/ci-manifest-coding.json uv run --frozen python -m dm_agent.benchmarks.cli --suite maintenance --manifest-only eval_reports/ci-manifest-maintenance.json uv run --frozen python -m dm_agent.benchmarks.manifest_diff bench_reports/manifest-baseline-coding.json eval_reports/ci-manifest-coding.json uv run --frozen python -m dm_agent.benchmarks.manifest_diff bench_reports/manifest-baseline-maintenance.json eval_reports/ci-manifest-maintenance.json - name: List coding benchmark tasks run: uv run --frozen python -m dm_agent.benchmarks.cli --list - name: Lint run: uv run --frozen python -m ruff check . - name: Format check run: uv run --frozen python -m black --check . - name: Type check run: uv run --frozen python -m mypy dm_agent web: name: Web console frontend runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: web/package-lock.json # npm ci 严格按 package-lock.json 安装,与 Python 侧 uv sync --frozen 同理。 - name: Install frontend dependencies run: npm ci working-directory: web - name: Type check run: npm run typecheck working-directory: web - name: Run frontend tests run: npm run test working-directory: web - name: Build frontend run: npm run build working-directory: web # 前端产物入库(wheel 自带 UI,终端用户不需要 Node),所以必须防「改了源码 # 忘了重新 build」——那会让 pip 装到的界面与仓库源码不一致,且没人会发现。 # 上一步的构建输出就落在 dm_agent/server/static/,这里检查它是否与入库版本一致。 - name: Verify committed build artifacts are up to date run: | if ! git diff --quiet -- dm_agent/server/static; then echo "::error::dm_agent/server/static 与 web/ 源码不一致。请在 web/ 下跑 npm run build 并提交产物。" git diff --stat -- dm_agent/server/static exit 1 fi echo "前端产物与源码一致。"dm-code-maintenance-agent/.github/workflows/release.ymlcode · 7.5 KB
name: Release # 发布到 PyPI。两条路径,职责分开: # # 手动触发(workflow_dispatch) → TestPyPI —— 随便试,装错了重来就行 # 打 tag v* → 正式 PyPI —— 不可逆,版本号用掉不能重传 # # 认证走 **Trusted Publishing(OIDC)**:GitHub 直接向 PyPI 证明"这个 workflow # 确实来自这个仓库",仓库里不存任何长期 token。所以 Secrets 里是空的, # 也就没有"token 泄露"或"忘记轮换"这回事。 # # 前提:在 PyPI 上把本仓库 + 本文件名(release.yml)登记为 trusted publisher。 # 首次发布一个还不存在的包要用 "pending publisher",见 docs/releasing.md。 on: push: tags: ["v*"] workflow_dispatch: inputs: target: description: "发布目标" type: choice options: [testpypi, none] default: testpypi jobs: # 发一个坏包比不发严重得多——版本号不能重传。所以构建前先把 CI 的核心检查重跑一遍, # 不信任"tag 指向的 commit 当初 CI 过了"(分支可能被改过,产物可能没重建)。 build: name: Build and verify runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" # 钉精确版本:setup-uv 的滚动标签只推到 v7,v8/v9 只有 vX.Y.Z 形式, # 写 @v9 会 "unable to resolve action"。 - uses: astral-sh/setup-uv@v9.0.0 with: python-version: "3.12" enable-cache: true - name: Verify lockfile run: uv lock --check - name: Install run: uv sync --frozen --extra dev - name: Test run: uv run --frozen python -m pytest - name: Deterministic eval gate run: | uv run --frozen python -m dm_agent.evals.cli --output eval_reports/release.json uv run --frozen python -m dm_agent.evals.gate eval_reports/release.json --min-success-rate 1.0 - name: Lint / format / types run: | uv run --frozen python -m ruff check . uv run --frozen python -m black --check . uv run --frozen python -m mypy dm_agent # 前端产物入库,wheel 直接打包它。发布前必须确认它与 web/ 源码一致, # 否则 pip 装到的界面和仓库源码对不上,而且没人会发现。 - uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: web/package-lock.json - name: Rebuild frontend and diff against committed artifacts run: | npm ci npm run build working-directory: web - name: Fail if committed artifacts are stale run: | if ! git diff --quiet -- dm_agent/server/static; then echo "::error::dm_agent/server/static 与 web/ 源码不一致,不能发布。" git diff --stat -- dm_agent/server/static exit 1 fi - name: Build wheel and sdist run: uv run --frozen python -m build # 版本号必须与 tag 一致,否则会发出一个"tag 说 v2.0.1、包里写 2.0.0"的版本。 - name: Check version matches tag if: startsWith(github.ref, 'refs/tags/v') run: | TAG="${GITHUB_REF#refs/tags/v}" PKG=$(uv run --frozen python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") if [ "$TAG" != "$PKG" ]; then echo "::error::tag v$TAG 与 pyproject.toml 的 version=$PKG 不一致。" exit 1 fi echo "版本一致:$PKG" - uses: actions/upload-artifact@v4 with: name: dist path: dist/ # 把构建好的 wheel 装进一个干净环境,确认它真的能用。 # 这一步复刻的是人工验收:命令齐不齐、UI 在不在包里、服务起不起得来。 smoke: name: Install from wheel and smoke test needs: build runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] steps: - uses: actions/setup-python@v5 with: python-version: "3.12" - uses: actions/download-artifact@v4 with: name: dist path: dist # 刻意不 checkout 仓库:必须在**包外**测,否则 cwd 会被加进 sys.path, # import 到的是源码树而不是安装的包,配置路径类的 bug 会被完全掩盖。 - name: Install the built wheel shell: bash run: pip install "$(ls dist/*.whl)[web]" - name: Every console script is present and runnable shell: bash run: | for cmd in dm-agent dm-agent-eval dm-agent-bench dm-agent-trace \ dm-agent-economics dm-agent-manifest-diff dm-agent-web; do "$cmd" --help > /dev/null || { echo "::error::$cmd 不可用"; exit 1; } echo " ok $cmd" done - name: Frontend ships inside the wheel shell: bash run: | python - <<'PY' import sys from dm_agent.server.app import default_static_dir d = default_static_dir() assert d is not None and (d / "index.html").is_file(), "wheel 里没有前端产物" print(" ok 前端产物随包分发:", d) PY - name: Config never lands inside site-packages shell: bash run: | python - <<'PY' import pathlib, dm_agent from dm_agent.paths import resolve_config_write_path, user_config_path site = pathlib.Path(dm_agent.__file__).parent.parent for p in (resolve_config_write_path(), user_config_path()): assert site not in p.resolve().parents, f"配置落进了包目录: {p}" print(" ok 配置路径在包外") PY - name: Web console actually serves the UI shell: bash run: | python - <<'PY' from fastapi.testclient import TestClient from dm_agent.server.app import create_app from dm_agent.server.settings import ServerSettings import pathlib, tempfile s = ServerSettings(sessions_dir=pathlib.Path(tempfile.mkdtemp()), token="t") c = TestClient(create_app(s)) r = c.get("/") assert r.status_code == 200 and 'id="root"' in r.text, r.status_code m = c.get("/api/meta", headers={"Authorization": "Bearer t"}) assert m.status_code == 200, m.status_code print(" ok GET / 200,/api/meta 200") PY publish-testpypi: name: Publish to TestPyPI needs: [build, smoke] if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi' runs-on: ubuntu-latest environment: name: testpypi url: https://test.pypi.org/p/dm-code-agent permissions: id-token: write # OIDC,取代长期 token steps: - uses: actions/download-artifact@v4 with: name: dist path: dist - uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ publish-pypi: name: Publish to PyPI needs: [build, smoke] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/dm-code-agent permissions: id-token: write steps: - uses: actions/download-artifact@v4 with: name: dist path: dist - uses: pypa/gh-action-pypi-publish@release/v1dm-code-maintenance-agent/.gitignoretext · 1.6 KB
# Environment and secrets .env *.env .env.local .env.*.local config.json mcp_config.json # Python bytecode and caches __pycache__/ *.py[cod] *$py.class .pytest_cache/ .mypy_cache/ .ruff_cache/ .pyre/ .coverage .coverage.* coverage.xml htmlcov/ # Build and packaging output build/ dist/ *.egg-info/ .eggs/ *.egg MANIFEST # Virtual environments .venv/ venv/ env/ ENV/ virtualenv/ # Test and generated demo output test_output/ eval_reports/ bench_reports/ traces/ reports/ sessions/ .dm-agent-cache/ test_*.txt demo*.txt random*.txt *.log logs/ *.tmp *.temp *.bak # IDEs and editors .idea/ .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json *.code-workspace *.sublime-project *.sublime-workspace *.swp *.swo *~ # OS files .DS_Store Thumbs.db Desktop.ini $RECYCLE.BIN/ *.lnk # Local assistant files CLAUDE.md .claude/ .clinerules # Archives and local databases *.zip *.tar.gz *.rar *.7z *.db *.sqlite *.sqlite3 # Web 控制台前端 # node_modules 不入库;构建产物 dm_agent/server/static/ **入库**,理由见 docs/web.md: # 用户 pip install dm-code-agent[web] 后必须直接就有 UI,不能要求他们装 Node 再构建。 # CI 会重新构建并与入库产物比对,防止「改了源码忘了重新 build」。 web/node_modules/ web/dist/ web/.vite/ web/vite.config.ts.timestamp-* # SWE-bench Verified 子系统:独立 venv、数据缓存与工作区都不入库 .swebench-venv/ swebench_work/ swebench_verified/logs/ dm-agent-*.json logs/run_evaluation/dm-code-maintenance-agent/.pre-commit-config.yamlcode · 0.8 KB
minimum_pre_commit_version: "4.0.0" repos: - repo: local hooks: - id: ruff name: ruff check entry: uv run --frozen --extra dev python -m ruff check . language: system pass_filenames: false always_run: true - id: black name: black check entry: uv run --frozen --extra dev python -m black --check . language: system pass_filenames: false always_run: true - id: mypy name: mypy check entry: uv run --frozen --extra dev python -m mypy dm_agent language: system pass_filenames: false always_run: true - id: pytest name: pytest entry: uv run --frozen --extra dev python -m pytest language: system pass_filenames: false always_run: truedm-code-maintenance-agent/AGENTS.mdmarkdown · 5.0 KB
AGENTS.md
Guidance for coding agents working in this repository.
Project Intent
DM-Code-Agent is a local-first, auditable code maintenance agent. Favor changes that improve real repository maintenance, traceability, reproducibility, and benchmark quality.
Development Rules
- Keep the core agent readable. Prefer small, explicit modules over large abstractions.
- Ask first: does this have to live in the kernel? If a feature only needs to act at a few
fixed points, it belongs in an extension (
docs/extensions.md), not in a new--enable-xxxflag plus anifbranch inReactAgent. - Never delete original data. Compaction, truncation, and summarization may only append a derived record and skip the original when building context — never overwrite or drop it. This is what makes the agent debuggable, auditable, and ablatable.
- Do not introduce network calls into tests unless they are clearly marked as live-model tests.
- Default tests and deterministic evals must run without API keys.
- Treat session/trace files as potentially sensitive. Full LLM I/O capture must remain opt-in;
--tracestays redacted and--trace/--checkpointmust not point at the same file. - Tool replay that mutates files or runs commands must remain explicit and documented.
- Keep benchmark tasks executable with plain pytest hidden tests.
- When changing benchmark behavior, update tests and docs together.
- Do not claim evaluation numbers that were not actually run. The scoreboard is the bundled coding + maintenance benchmark (13 hidden-test tasks). Real SWE-bench / Docker / cross-model scoring is frozen.
- Do not mix a fix into a pure-refactor commit — it destroys the "eval identical field by field" check that makes the refactor verifiable.
Verification
Before considering a change complete, run:
python -m compileall dm_agent main.py tests python -m pytest python -m dm_agent.evals.cli --variant full --task direct_finish python -m dm_agent.benchmarks.cli --suite maintenance --list python -m ruff check . python -m black --check . python -m mypy dm_agentCI installs from
uv.lock(uv sync --frozen --extra dev) and runs the same commands viauv run --frozen. If you change dependencies inpyproject.toml, re-runuv lockand commit the updateduv.lock— CI fails onuv lock --checkotherwise.Important Modules
dm_agent/core/agent.py: ReAct loop and agent assembly. Per-step concerns live in sibling modules and are wired in as collaborators:context_window.py(message building and compression),response_parser.py(fault-tolerant JSON parsing),tool_invoker.py(the validate → hook → backup → execute → truncate chain),completion.py(finish gating and result formatting),replan.py(failure signatures and replanning),persistence.py(checkpoint codec and pre-write backup),observation.py,prompting.py,run_state.py.dm_agent/core/events.py+capabilities.py: lifecycle hooks and the capability contract. Optional behaviors are extensions, not kernel branches.dm_agent/extensions/:ExtensionAPI+ registry, three-source discovery (discovery.py), and the project trust store (trust.py).dm_agent/tracing/: append-only JSONL session log —writer.py(entry ids, privacy tiers),session.py(read-side normalization,rebuild_context),summary.py/analysis.py(deterministic algorithms),render.py(human/JSON/Markdown output),replay.py/fork.py(explicit actions), and the thincli.pyparser/dispatcher.dm_agent/benchmarks/: coding and maintenance benchmark suites.dm_agent/evals/: deterministic and live-model agent evals.dm_agent/tools/: file, execution, test, lint, and code-analysis tools.dm_agent/tools/code_index_tools.py: repository-level Python symbol/dependency index tools.dm_agent/cli/: user-facingdm-agentCLI and outer-layer runtime assembly.main.py: thin compatibility shim forpython main.py.
Architecture in prose (the authoritative source, not the drawio/png):
docs/architecture.md.Layering
Dependencies must point one way only. Ruff's
TID251enforces it in CI; the contract and its rationale live inpyproject.tomlunder[tool.ruff.lint.flake8-tidy-imports.banned-api].clients → tools → tracing → core → extensions → clicore/tools/clients/memory/tracing/evals/benchmarksmust not importdm_agent.cli, and nothing may import the top-levelmainmodule.Style
- Python 3.10+.
- Black line length: 100.
- Ruff lints with
E,F,I,UP,B,SIM,TID,RUF.E501andRUF001-RUF003are disabled on purpose (reasons documented inpyproject.toml); do not add# noqato source files — disable a rule in config with a written justification instead. - mypy runs over
dm_agent;dm_agent.tools.*anddm_agent.clients.*additionally enforcedisallow_untyped_defs. New code in those two packages must be fully annotated. - Keep tests focused on behavior and avoid brittle stdout assertions unless validating CLI UX.
dm-code-maintenance-agent/CHANGELOG.mdmarkdown · 43.9 KB
Changelog
All notable changes to DM-Code-Agent are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Real SWE-bench Verified cross-repo run and Windows harness hardening (2026-08-06)
- Added the archived 20/50 selection manifests, predictions, and official SWE-bench 4.1.0 reports. The deterministic 20-task prefix resolved 11/20 (55%); the nested 50-task run resolved 21/50 (42%), with 0 harness errors.
- Hardened Docker workspace materialization on Windows: safe tar extraction, Git symlink-text semantics, traversal/collision checks, native symlink preservation on non-Windows, and cleanup on Docker launch failure.
- Patched the official evaluation boundary to force UTF-8 text writes and LF shell scripts; this prevents host encoding/line-ending failures from being reported as model failures.
edit_filegains content-anchored editing and a post-edit echo (2026-08)devlog 37 traced half of all benchmark failures to one mechanism:
edit_file'sreplacerunslines[start:end] = [content], so a range that is one line too wide silently swallows adjacent code — and the old return value was just "已替换第 5-6 行", giving the model nothing to check against. It took 2.2 steps on average to notice, then 3–6 more to repair, often damaging the file again on the way.Measured on 30 tasks: edit self-damage went 13 incidents / 6 tasks → 0 / 0, and the model adopted the new mode on 100% of its 47 edits with zero misses — it never fell back to line numbers.
edit_filecalls dropped 25% (no more repairing itself), total steps −7.1%, tokens −3.9%. Write-up indocs/research-log/38-edit-file-precision.md.Added
edit_fileacceptsold_string/new_string: locate the edit by content, not line number. Must match exactly once including whitespace; on zero or multiple matches nothing is written. Mutually exclusive withoperation/line_start/line_end.- Every edit now echoes the resulting lines back with new line numbers, changed lines marked, ±3 lines of context, capped at 40 lines.
.pywrites (bothedit_fileandcreate_file) are checked withast.parse. Reported, never rolled back — a mid-edit state may legitimately not parse. Note this only catches syntax breakage; a swallowed assignment stays valid Python, which is what the echo is for.
Unchanged
- The line-number mode still works exactly as before; existing callers and tests are untouched.
- Tool return values do not enter the manifest, so both suite signatures are unchanged.
Known issue this surfaced
The echo brings file contents into the observation, and
core.observation.is_failure_observationmatches the bare substringerror— so a successful edit that echoesfrom errors import ...is judged a failure and burns a planner call. 17 of 78 replans (22%) in the run were this false positive. The root cause predates this change (read_filehas always behaved this way) and is now the top open item in devlog 38.Step budget is not the bottleneck — a negative result (2026-08)
devlog 36 left "max steps exceeded" as the top failure mode (4 tasks, 3 with passing hidden tests), which looked like a pure budget problem. Raising
--max-stepsfrom each task's own 14–20 to a uniform 30 did not produce an attributable gain:- Only one task ever used more steps than its old ceiling — and it still failed. The three that flipped to PASS used 9 / 13 / 16 steps, below their old limits. The budget was never binding.
- Those flips track edit self-damage, not budget: across the four tasks, self-damage incidents went 7 → 2.
- More budget made self-damage worse globally (13 → 25 incidents, 6 → 9 tasks).
filename_sanitizerwent from a clean 10-step PASS to burning all 30 steps after damaging its own file 6 times. - Cost +30.6% tokens. Pass rate moved 0.733 → 0.800, but with 10 tasks flipping against a measured noise floor of ±3 — not evidence.
The real bottleneck is
edit_file's line-numbered editing: the agent edits using line numbers that went stale after its own previous edit, corrupts the file, then spends 3–6 steps repairing itself. It hits 6/30 tasks and accounts for half of all failures. Full write-up with the trace excerpts indocs/research-log/37-step-budget-and-edit-self-damage.md.No code changed — this entry records an experiment and its raw data (
bench_reports/steps30-20260804.{json,md}).ablation-scope-20260804.json(0.733) remains the reference baseline; the 0.800 run is the top of the noise band, not a new level.Also corrects devlog 36: scope violations are low but non-zero, not absolutely zero (1 occurrence in 60 runs across both experiments).
Scope-constraint ablation: 8 violations to 0 (2026-08)
The 30-task baseline passed 27/30 hidden tests but only scored 15/30. Eight of the fifteen failures were "changed files outside allowed set" — and all eight had already passed their hidden tests. The cause turned out to be a benchmark design flaw, not an agent one:
allowed_changed_filesnever appeared in the prompt, whileMAINTENANCE_PROMPT_SUFFIXactively told the agent to "update tests when the task asks for a regression test".Stating the constraint took violations to zero and pass rate from
0.500to0.733. Thebefore_tool_callguard extension proposed in devlog 35 is no longer needed — details and the honest counter-evidence (hidden-test rate dropped, coding noise floor is ±3 flips) indocs/research-log/36-scope-constraint-ablation.md.Added
dm-agent-bench --declare-allowed-files: appends the task's allowed set to the prompt handed to the agent. Default off — it changes what the benchmark measures, and existing baselines were recorded without it.BenchmarkTask.scoped_prompt(): derived view that appends the constraint. Tasks with no constraint returnpromptverbatim (all 15 coding tasks — the flag is a byte-level no-op for them, which is what makes them a usable noise-floor control).- Reports carry
prompt_policy.declare_allowed_filesat the top level and per-result metadata, so a report states which side of the ablation it is from. bench_reports/ablation-scope-20260804.{json,md}: the experiment arm.
Unchanged (deliberately)
task.promptis untouched, sobenchmark_task_fingerprintand both suite signatures are byte-identical to the checked-in manifest baselines.dm-agent-score-difftherefore compares the two arms directly instead of refusing with exit 2, and no manifest baseline needed regenerating.test_declaring_allowed_files_does_not_change_the_suite_signaturelocks this in.
Benchmark expanded from 13 to 30 tasks (2026-08)
At 13 tasks one flipped task moved the score by ±7.7 points, which made most changes indistinguishable from re-run noise — two runs of the same 6 coding tasks with the same model scored 3/6 and 4/6. At 30 tasks the noise floor is ±3.3. Details in
docs/research-log/34-benchmark-expansion.md.Added
- 9 coding tasks:
parse_duration,merge_intervals,retry_backoff_schedule,csv_row_parser,paginate_cursor,semver_compare,flatten_config,rate_limiter_window,safe_int_parse. - 8 maintenance tasks:
billing_period_boundary,sql_where_builder,idempotent_job_runner,sort_stability_regression,filename_sanitizer,error_propagation_contract,settings_env_precedence,log_redaction. Every one of them carries anallowed_changed_filesconstraint — the 13-task baseline showed three of eight failures were the agent editing the test file instead of the implementation. - Dataset invariants enforced for all 30 tasks in
tests/test_coding_benchmarks.py: hidden tests must fail on the initial workspace, and a task may not list its own hidden tests as editable. docs/benchmarks.mdgained an "Adding a task" section covering both invariants, the solvability check that cannot be unit-tested, and the manifest regeneration step.
Changed (breaking for score comparison)
suite_signaturechanged for both suites;bench_reports/manifest-baseline-*.jsonwere regenerated.bench_reports/baseline-20260803.json(13 tasks) is no longer comparable to a 30-task run.dm-agent-score-diffrefuses the comparison and exits 2 rather than printing a meaningless delta. Re-run the baseline after any task-set change.
Scope reduction: removing what the frozen evaluation can never falsify (2026-08)
An AST scan found essentially no traditional dead code (one 2-line unreferenced function in the whole package). The redundancy was architectural: entire subsystems with tests and docs that could never produce value, because their graduation criteria depended on the real evaluation — which is frozen. Rationale, boundaries, and the recovery path are in
docs/research-log/33-scope-reduction.md.Do not reintroduce these into the kernel. The
AgentCapabilityprotocol and all six lifecycle hooks are unchanged, so any of them can come back as an external extension.Removed (breaking)
- SWE-bench Lite suite —
dm_agent/benchmarks/swebench_lite/, its tests, 202 lines of CLI glue, the--suite swebench_litebranch and eight suite-specific flags (--instance-id,--max-instances,--use-docker,--snapshot-path,--instance-test-timeout,--resume,--resume-from-output), plus thedm-code-agent[swebench]extra. It never worked: Tier-1 baseline was 0.0% resolved and polluted by host verifier noise, the Tier-2 verifier raisedNotImplementedError, and CI never ran it. Historical evidence stays inbench_reports/swebench_lite_*. - Reflexion —
--enable-reflexion,--max-trials,--reflexion-memory-file,core/reflexion.py,capabilities/reflexion_loop.py, and the checkpointreflexion_memoryfield. - Critic —
--enable-critic(bothdm-agentanddm-agent-bench),core/critic.py,capabilities/critic_gate.py. - Self-Consistency —
--self-consistency-runs,--self-consistency-strategy,capabilities/self_consistency.py, and 195 lines of candidate selection / voting / uncertainty logic in the benchmark runner. It was never wired into the main CLI. - Circuit breaker —
--enable-circuit-breaker,--circuit-breaker-threshold,--circuit-breaker-cooldown,core/circuit_breaker.py,capabilities/circuit_breaker_gate.py. --enable-repeated-failure-policy-experimentand thebreak_repeated_failure_loopstrategy. Repeated-failure detection and bookkeeping are kept — only the experimental escape strategy is gone.--enable-evolution— it was only a meta-switch over the two above.--enable-memory-hygieneand--enable-llm-compression, plus the compressor's_add_llm_summary. The latter called an LLM during compaction, which contradicts the "context compaction is locally deterministic" design.dm_agent/extensions/capabilities/andbuiltin_capabilities_for()— with the built-in capability count at zero, this translation layer had no purpose.ReactAgent(capabilities=[...])is now the single entry point for optional capabilities.- Public exports:
CriticAgent,CriticReview,EpisodicMemory,Lesson,Reflector,SelfConsistencyCandidate,SelfConsistencyResult,SelfConsistencyRunner(from bothdm_agentanddm_agent.core). - Dead code and cruft:
context_budget.last_write_step, thebenchmarks/andevals/root-level thin wrappers, the stale hand-writtenevals/tasks.json,check_mcp_env.py.
Changed
dm-agentCLI flags: 35 → 23.- Removing the
swebenchextra dropped 26 transitive dependencies and 1393 lines fromuv.lock. - Three restrictions that existed only for Reflexion are gone:
--conversation-stdinno longer rejects it,--checkpoint/--resumeare no longer mutually exclusive with it, and the server-side conversation-mode special case was removed. Two server argv tests now exercise the full option set instead of excluding Reflexion, which strengthens them. - The scoreboard is now the bundled coding + maintenance benchmark (13 hidden-test tasks,
overall_pass_rate). Measured: DeepSeekpass_rate 0.5 (3/6)on the coding suite.
Kept deliberately
CompletionGate— it never depended on Critic; an emptybefore_finishchain simply passes.- The
on_run_endretry orchestration inrun()— a general mechanism, not Reflexion's. error_kind="critic_rejected", the planner strategy keyed on it, and the observation failure markers — these are the completion-gate veto path that third-party extensions still use.- Read-side compatibility for historical sessions:
tracing/analysis.pycritic classification and the frontend'scritic_reviewrendering. Old session logs contain those entries, and "raw data is never deleted" applies to reading them back. Mem0StyleMemory.add_messages(invalidate_on_success=...)— a public component's capability parameter; its tests were rewritten against the memory layer directly.
Verification
427 tests pass. Across all four batches the deterministic eval (56 runs) lost exactly 15 metadata keys — all belonging to removed features — with no new keys and no changed values; the summary is field-for-field identical with
overall_success_rateat 1.0. Both benchmark manifest signatures are unchanged, historical sessions stillanalyze/viewcorrectly, and ruff / black / mypy /uv lock --check/ the frontend build all pass.Installability: user-level config and
.env(2026-08)Three bugs that made
pip install dm-code-agentunusable outside a cloned repo. All three were invisible under editable installs, where "relative to the package" and "relative to the working directory" happen to coincide. Details and rejected alternatives indocs/research-log/32-user-level-config-and-env.md.Changed (breaking)
config.jsonlookup is now./config.json→~/.dm_agent/config.json. Previously the path wasPath(__file__).parents[2] / "config.json", which resolves tosite-packages/once the package is actually installed — polluting the install root, failing outright on read-only or system Python installs, sharing one config across every project, and leaving the file somewhere the user cannot find. Saves write back to whichever file was read, falling back to user level. Running from a cloned repo is byte-for-byte unchanged, so no migration is required.dm_agent.cli.CONFIG_FILEremoved from the public__all__. Its semantics were wrong and keeping it would invite the same bug. Usedm_agent.paths.resolve_config_read_path()/resolve_config_write_path().
Fixed
- The user's
.envis found again. Bareload_dotenv()callsfind_dotenv(usecwd=False), which searches upward from the calling module's directory — installed, that walk starts insite-packages/dm_agent/cli/and never reaches the user's project, so a.envnext to their code silently did nothing. Now loaded explicitly as./.env→~/.dm_agent/.env, keeping dotenv'soverride=Falseso exported environment variables still win over both files. .envfiles with a UTF-8 BOM now work. Found while verifying the fix above from a real install: dotenv reads as plainutf-8and folds the BOM into the first key name (DEEPSEEK_API_KEY), so the key is set but unreadable and the error still says "missing API key" — undiagnosable in practice. Every common way to create a.envon Windows writes a BOM (Set-Content -Encoding utf8,>redirection, Notepad's "UTF-8"), so files are now read asutf-8-sig, which is identical toutf-8when no BOM is present.- Missing-key errors are actionable. The message now prints resolved absolute paths for all
three options (environment variable,
~/.dm_agent/.env,./.env) plus the provider's key console URL, instead of "set an environment variable" with no indication of where. - The welcome screen prints the config file's full path rather than the bare name
config.json, which after a global install says nothing about which of the two files was read.
Added
dm_agent/paths.py— dependency-free path resolution shared bycliandserver(servermay not importcli; it spawns it). Includes atomic JSON writes and POSIX permission tightening for config files.tests/test_cli_config_paths.py(15 cases), including a regression guard asserting that no resolved config path ever lands inside the package directory.isolate_user_homeautouse fixture redirectingHOMEandUSERPROFILE(WindowsPath.home()reads the latter) into a temp dir. Without it,parse_args()now reaches~/.dm_agent/config.jsonand every test's behaviour would depend on whether the machine running it happens to have that file.
Web console (2026-07)
A browser UI (
dm-agent-web) serving two purposes with one renderer: a local workbench (start tasks, watch steps live, fork from any entry) and a read-only gallery (auditable session viewer needing no API key, hostable as static files). Live runs and historical traces are the same append-only JSONL entry stream, so "what you watch" and "what you audit afterwards" cannot diverge.Added
dm_agent/server/— a second outermost assembler, sibling todm_agent/cli/: read-only audit API, subprocess executor, SSE streaming. Optional[web]extra; the core package stays free of web framework dependencies.- Read-only endpoints reusing
dm_agent.tracingpure functions verbatim (summarize_events/analyze_events/diff_events/fork_session), so the console anddm-agent-tracealways reach the same conclusions — pinned by a field-by-field comparison test rather than by convention. POST /api/runsspawns apython -m dm_agent.clisubprocess. Chosen over in-processReactAgentbecause the kernel has no cancellation interface (a hook raising is documented as equivalent to allowing), and because it makes the web UI a frontend to the CLI rather than a second assembly path that could drift.- SSE live stream tailing the session log itself (
TraceWriterflushes per entry).id:is the line number; append-only guarantees line numbers never shift, so the browser'sLast-Event-IDresumption works without server-side bookkeeping. web/— React 19 + Vite + Tailwind v4 frontend, built intodm_agent/server/static/and committed sopip install dm-code-agent[web]ships a working UI without requiring Node. CI rebuilds and diffs the artifacts.dm_agent/cli/__main__.py(3 lines), fixing the long-standing "python -m dm_agent.clisilently does nothing" pitfall recorded in CLAUDE.md.
Fixed
- Run status no longer equates exit code 0 with success.
dm-agentreturns 0 formax_steps_exceeded(not a CLI failure, just an unfinished agent), which the console initially reported ascompleted. It now also reads the agent's own verdict from the session log'srun_end, adding anincompletestate. Caught by end-to-end manual verification, not by the test suite — which is why the regression test exists now. - SPA deep links returned a JSON 404:
StaticFiles.get_responseraisesHTTPExceptionrather than returning a 404 response, and raises starlette's class, of which FastAPI's same-named class is a subclass — soexcept fastapi.HTTPExceptionnever caught it. - The startup banner (carrying the only clickable token URL) vanished whenever stdout was
redirected:
print()is block-buffered on a pipe anduvicorn.run()never returns. Now flushed explicitly, and encoded as UTF-8 so Chinese text survives Windows redirection instead of being mangled by cp936.
Testing
- 66 new cases across five files, all offline: layering invariants (AST-asserted, because
dm_agent/server/**must be exempt fromTID251to allowfrom .settings import), security model, argv whitelisting fed to the real CLI parser, SSE semantics (partial lines, resumption, trailing entries after process exit), and run lifecycle. tests/conftest.pynow forces a dummy API key on every test. The project constitution requires tests to run without keys; while writing the run-lifecycle tests the first version relied on "no key configured, so the CLI fails fast" — and really spent money.load_dotenv()'sfind_dotenv()searches upward from the calling module's directory (dm_agent/cli/), so a subprocess picks up the repo-root.envregardless of its cwd; and on Windowsos.environ[k] = ""deletes the variable, letting dotenv refill the real key. This is now a machine guarantee instead of something to remember.
Architecture refactor (2026-07, eight steps)
An eight-step restructuring benchmarked against Pi Agent Harness. Every step was required to keep
python -m dm_agent.evals.cliidentical field by field to the pre-refactor baseline; the four default-off paths (Reflexion / Critic / circuit breaker / breaker×guard) were A/B-compared with a dedicated probe script on top of that.Added
- Lifecycle event bus (
dm_agent/core/events.py) with six interceptable hooks:before_tool_call(rewrite arguments in place, or{"block": True, "reason": ...}),after_tool_result(middleware-style observation rewriting),before_llm_request(rewrite messages, carries aphase),before_finish(veto a completion),on_run_start(mutate metadata, append to the system prompt), andon_run_end({"retry": True}to discard the attempt and rerun). Handlers run in registration order; an exception in one handler is isolated, logged as ahook_errorentry, and does not abort the run. - Extension system (
dm_agent/extensions/):ExtensionAPIwithregister_tool/register_skill/register_provider/on, plus three discovery sources in ascending priority — built-ins,dm_agent.extensionsentry points,~/.dm_agent/extensions/*.py, project-local.dm_agent/extensions/*.py, and--extension PATH. Adding a tool, skill, provider, or guard now requires zero changes underdm_agent/. - Project trust model: project-local extensions are never imported without explicit
authorization. Decisions are stored outside the repository in
~/.dm_agent/trusted-projects.json; non-interactive environments skip untrusted projects instead of blocking. New--no-extensionsand--extensionflags (mutually exclusive). - Session tree: every JSONL entry now carries
id(<run-id-prefix>-<seq>) andparent_id, making the run history a navigable tree (schema1.2→2.0, purely additive —event/payloadkeys unchanged). New entry types:message,compaction,checkpoint,fork. - Non-destructive context compaction: folding now appends a
compactionentry (first_kept_entry_id,folded_entry_ids, summary, token estimates) and the request's message list is assembled by skipping the folded range. No original entry is ever deleted.tracing.session.rebuild_context(entries, apply_compaction=False)replays the same session as if compaction never happened — the difference is exactly what was folded, which makes theno_compressionablation attributable instead of just two end-to-end scores. The messages sent to the model are byte-identical to before. dm-agent-trace fork <session.jsonl> --at <entry-id>: branch a new session from any entry. Entries up to the fork point are copied verbatim and aforkentry whoseparent_idpoints back at the fork point is appended.--checkpoint *.jsonl: append-only session-format checkpointing (every step's snapshot is kept) with--resume-at <entry-id>to resume from an earlier entry.--checkpointwith any other suffix keeps the original single-file JSON snapshot, and--resumesniffs both formats, so existing usage is unchanged.uv.lockfor reproducible installs; CI now runsuv sync --frozen --extra devand fails onuv lock --check.- mypy over
dm_agent, withdisallow_untyped_defsenforced fordm_agent.tools.*anddm_agent.clients.*. - Layering contract enforced by ruff
TID251:core/tools/clients/memory/tracing/evals/benchmarksmay not importdm_agent.cli.
Changed
main.pymoved into the package.dm-agentnow points atdm_agent.cli:mainandpy-modules = ["main"]is gone — installing the project no longer puts a top-levelmainmodule intosite-packages. The rootmain.pyis a thin shim forpython main.py.- Ruff rule set widened from
E9 F63 F7 F82toE F I UP B SIM TID RUF, withE501andRUF001–RUF003disabled on purpose (rationale inpyproject.toml). No# noqain source files. - LLM providers, built-in tools, and built-in skills are now registered through the same
ExtensionAPIthird parties use, rather than hard-coded lists. - Optional capabilities moved out of the kernel. Critic, circuit breaker, Reflexion, and
the read-before-edit guard are now event handlers under
dm_agent/extensions/capabilities/(andcore/guards.py). The--enable-*flags are kept as a transitional surface and are internally equivalent to loading the matching capability; behavior and eval output are unchanged. dm_agent/core/agent.pysplit from 1616 to 866 lines. Per-step concerns became sibling modules:run_state,prompting,context_window,response_parser,tool_invoker,observation,completion,replan,persistence.ReactAgentis down to 4 public methods.- Documentation restructured.
README.md19.6 KB → 2.7 KB navigation page; content split intodocs/getting-started.md,docs/cli.md,docs/capabilities.md,docs/project-status.md, and adocs/README.mdindex.MCP_GUIDE.md→docs/mcp.md,SKILL_GUIDE.md→docs/skills.md. Newdocs/architecture.md(prose/mermaid, now the authoritative source over the drawio/png) and a rewrittendocs/extensions.mdwith an API reference, the security model, and three verified runnable examples.
Removed
README_FR.md. Measured at 1,955 characters versus the Chinese README's 14,324, with 5 sections against 19 and no French accents anywhere, it had never actually been maintained.README_EN.mdis kept and synchronized with the slimmed structure; pages underdocs/are Chinese-only, stated explicitly in both READMEs.
Notes
- No evaluation claims changed. Real SWE-bench / Docker / cross-model scoring remains frozen.
- Devlogs:
docs/research-log/29-session-tree.mdcovers the session tree and non-destructive compaction.
Added
- Default-on observation truncation: tool outputs beyond
--max-observation-chars(default 8000) keep head+tail with an explicit[truncated: ...]marker and concreteread_filepaging hints; audited via theobservation_truncatedtrace event andtruncation_countmetadata. - Token-budget compression trigger:
--context-token-budget(default 24000 estimated tokens, chars/4 heuristic) forces early local compression when the pending history grows too large; audited viacontext_budgettrace events. - Default-on read-before-edit guard:
edit_fileis blocked until the target file was read this run (and re-read after any write). Opt out with--disable-edit-guard; audited viaedit_guardtrace events. --enable-memory-hygiene(default off): success observations supersede matching failure memories (importance/score down-weighting plus a staleness note when rendered) and recall queries are anchored to the task text.--enable-llm-compression(default off): folding old context additionally stores one LLM-written summary memory, with silent fallback to the rule-based path on any client failure.- Trace schema 1.1 (additive): new events
observation_truncated,context_budget,edit_guard,memory_invalidation, and anestimated_prompt_tokensfield onllm_call. - Unified retryable LLM error handling:
--llm-max-retries(default 2) retries transient timeouts/429/5xx with exponential backoff for all four providers (DeepSeek keeps its internal loop; benchmark usage wrapper now routes through the retry layer too). - Atomic file writes for
create_file/edit_file(temp file +os.replace, Windows-safe fallback) and per-run pre-write backups under the system temp directory, reported at run end and viafile_backuptrace events. - Configurable MCP request timeout (
"timeout"per server inmcp_config.json) and a single automatic reconnect when a server process dies mid-run. - Run-level checkpoint/resume:
--checkpointsnapshots conversation, steps, metadata, plan, and local memories after every step;--resumecontinues from the snapshot (task argument optional). Max-steps runs write a terminal snapshot resumable with a larger--max-steps. - Progress-carrying replan: regenerated plans keep completed steps with
continued numbering, plan progress is matched against the next pending step
only, and the default (non-adaptive) replan path gains a budget of 5
(
--max-replansoverrides). --reflexion-memory-fileto persist Reflexion lessons across runs.--enable-circuit-breaker(default off): temporarily disables a tool after N consecutive same-kind failures with cooldown and probe recovery, audited viacircuit_breakertrace events.- Recovery success rate (
recovered_runs / runs_with_failures) and per-tag capability breakdowns in eval and benchmark summaries and reports; trace directory analysis aggregates the same rate. - Hallucination proxy signals in
dm-agent-trace analyze/analyze-dir: edits without a prior read, edit-guard blocks, truncated observations, and missing-path references (advisory; not part of the trace-health score). - Repeat-variance stability metrics for
dm-agent-bench --repeat: per-task pass@k, pass^k, and task pass-rate stddev. dm-agent-bench --per-test-credit: advisory node-by-node hidden-test partial credit (strict scoring unchanged) and--manifest-onlyfor keyless manifest generation.- CI quality gates: the full keyless eval suite must stay at 100%
(
dm_agent/evals/gate.py) and benchmark manifests are diffed against checked-in baselines so task drift fails the build. - New keyless eval tasks
truncated_read_paginationandedit_guard_rereaddriven by the newEvalTask.agent_overridesmechanism.
Fixed
- SWE-bench Lite failure-mode analyzer now recognizes the agent's actual
max_steps_exceededterminal status (plus theagent_status_max_steps*failure-reason fallback), so step-budget failures are no longer miscategorized asregression. Frozen resolved/patch-applied numbers are unaffected; see the errata indocs/research-log/01-swebench-baseline.md.
Removed
- Legacy repository-index context support, including its CLI entry point, old context exports, agent opt-in flags, benchmark flags, and the optional dependency extra.
Added
- Mem0-style local context memory: older conversation turns are stored as scoped atomic memories and only relevant memories are injected back into the prompt.
dm-agent-trace analyzefor offline failure-stage attribution, recovery inspection, verification-gap detection, and advisory trace-health grades.cli_config_docs_contract, a multi-file maintenance benchmark task that requires implementation, docs, and regression-test changes for CLI configuration documentation.- Wilson 95% confidence intervals for benchmark strict pass, hidden-test pass, and agent completion rates in JSON summaries and Markdown reports.
- Self-consistency uncertainty metadata for core and benchmark multi-candidate selection, including vote distribution, selected support, score margin, tie detection, disagreement reason, and confidence label.
- Adaptive replanning repeated-failure metadata and
replan_decisiontrace fields for consecutive identical action/error/observation failures. - Benchmark report
manifestprovenance with hidden-contract task fingerprints, variant names, and suite signature. - Compact per-run
trace_analysismetadata in coding/maintenance benchmark reports when--trace-diris enabled. dm-agent-trace analyze-dirfor offline aggregation of trace-health, verification-gap, and failure-stage counts across trace directories.dm-agent-economicsnow carries pass-rate 95% confidence intervals from benchmark reports, with a Wilson fallback for older reports.dm-agent-trace difffor offline comparison of two JSONL traces, including status changes, step/tool/replan deltas, action-sequence divergence, tool-usage deltas, plan changes, and final-answer changes.packaging_ci_contract, a multi-file maintenance benchmark task that repairs Python packaging metadata, dev extras, CI matrix/install commands, CI checks, and regression coverage under deterministic hidden tests.- Patch-fingerprint voting for self-consistency: benchmark candidates now carry a stable workspace-local patch fingerprint, and core/benchmark selection uses it when present before falling back to final-answer text.
dm-agent-manifest-diff, an offline CLI for comparing two benchmark report manifests before score comparisons. It highlights suite-signature, task fingerprint, and variant-name drift without rerunning benchmarks.dm-agent-economicsmanifest guard metadata and Markdown warning when input reports carry different benchmark suite signatures.dm-agent-trace analyze-dir --markdown PATHfor shareable trace-health summaries that avoid raw prompt, observation, tool-output, and final-answer contents.- Default-off repeated-failure policy experiment for adaptive replanning. When explicitly enabled, repeated action/error signatures can select a loop-breaking replan strategy without changing default behavior.
docs/research-log/07-trace-diff.mddocumenting the trace-diff design and the next trace-analysis bets.docs/research-log/08-trace-analyzer.mddocumenting the analyzer rules and why the output remains advisory.docs/research-log/09-maintenance-realism.mddocumenting the benchmark task design and hidden-test contract.docs/research-log/10-benchmark-confidence.mddocumenting the interval design and interpretation limits.docs/research-log/11-self-consistency-uncertainty.mddocumenting the selection-explainability metadata.docs/research-log/12-repeated-failure-signals.mddocumenting the loop-detection signal and why it does not change replanning decisions yet.docs/research-log/13-benchmark-provenance.mddocumenting task fingerprints and suite signatures.docs/research-log/14-benchmark-trace-analysis.mddocumenting trace analyzer integration in benchmark reports.docs/research-log/15-trace-analysis-aggregation.mddocumenting directory aggregation for trace review.docs/research-log/16-economics-uncertainty.mddocumenting confidence-aware offline economics reporting.docs/research-log/17-packaging-ci-maintenance.mddocumenting the packaging/CI maintenance benchmark contract.docs/research-log/18-self-consistency-patch-fingerprint.mddocumenting patch-based self-consistency voting.docs/research-log/19-benchmark-manifest-diff.mddocumenting manifest diff guardrails for benchmark comparisons.docs/research-log/20-economics-manifest-guard.mddocumenting suite-signature warnings in economics reports.docs/research-log/21-trace-analysis-markdown.mddocumenting the trace-directory Markdown report and privacy boundary.docs/research-log/22-repeated-failure-policy-experiment.mddocumenting the default-off repeated-failure policy experiment.
2.0.0 - 2026-05-08
Added
- P6 release materials:
docs/research-log/06-final-writeup.md,docs/research-log/DISTRIBUTION_CHECKLIST.md, anddocs/research-log/INTERVIEW_TALKING_POINTS.md. - README / README_EN final v2 status sections, including the explicit freeze caveat for real SWE-bench, Docker/Tier-2, and cross-model evaluations.
- Release hardening docs in
docs/release-v2.0.0.md. - Default-off benchmark plumbing flags for coding/maintenance runs:
--enable-critic,--self-consistency-runs, and--self-consistency-strategy. - Fresh-workspace self-consistency selection for coding/maintenance benchmark runs, with SWE-bench Lite self-consistency explicitly blocked while real SWE-bench evaluation is frozen.
- P5 Adaptive Replanning + Token Economics:
AdaptiveReplanPolicy,ReplanSignal, andReplanDecisionclassifytool_error,parse_error,test_failure,critic_rejected, andmax_stepsinto deterministic replan strategies. ReactAgent(enable_adaptive_replanning=True, max_replans=N)opt-in strategy metadata plusreplan_decisiontrace events. Existing replan behavior remains unchanged by default.dm_agent/benchmarks/economics.pyand thedm-agent-economicsCLI for offline pass-rate / token / cost-per-success reports from existing benchmark JSON files.- Benchmark CLI fields:
--enable-adaptive-replanning,--max-replans, and--cost-per-1k-tokens. bench_reports/economics.jsonandbench_reports/economics.mdgenerated from the frozen P1 Tier-1 baseline without running new live evaluations.docs/research-log/05-adaptive-and-economics.mdas the Phase 5 log.- P4 Critic + Self-Consistency core:
dm_agent/core/critic.pywithCriticAgent/CriticReview, plusdm_agent/core/self_consistency.pywithSelfConsistencyRunnerand candidate summaries. ReactAgent(critic=...)opt-in completion gate,critic_reviewtrace events, and critic pass/fail counters in run metadata.tests/test_critic.pyandtests/test_self_consistency.pycovering the keyless critic gate and the three selection strategies.docs/research-log/04-critic-and-consistency.mdas the Phase 4 log.- P2 Reflexion implementation scaffold:
dm_agent/core/reflexion.pywithReflector,EpisodicMemory, and bounded lessons, plus default-offReactAgent(enable_reflexion=True, max_trials=N)support for retrying failed trials. - Trace events for Reflexion runs:
trial_start,trial_end, andreflexion. dm-agent-bench --enable-reflexion --max-trials Noptions. SWE-bench Lite uses hidden-test verifier feedback between trials and reportspass_at_1,pass_at_k, andavg_trials.tests/test_reflexion.pycovering the keyless Reflexion flow.
Docs
- Added the Phase 6 final write-up, distribution checklist, and interview talking points.
- Added
docs/research-log/05-adaptive-and-economics.mdas the Phase 5 implementation log and documented that real cross-model SWE-bench economics remain frozen until an allowed live evaluation. - Added
docs/research-log/04-critic-and-consistency.mdas the Phase 4 implementation log. - Added
docs/research-log/02-reflexion.mdas the Phase 2 implementation log.
1.7.1 - P1 SWE-bench Lite baseline
Added
- Published the first SWE-bench Lite DeepSeek Tier-1 baseline report:
bench_reports/swebench_lite_baseline.jsonandbench_reports/swebench_lite_baseline.md. This is a harness/trace baseline, not a leaderboard-comparable score. - SWE-bench Lite CLI resume/checkpoint support:
--resumeand--resume-from-outputreuse completed instance results from an existing JSON report, while--output/--markdownare now checkpointed after each newly completed instance. - DeepSeek API retry protection for transient HTTP/network failures, including
intermittent
400,429, and 5xx responses during long benchmark runs. - Windows-safe subprocess output decoding for benchmark and SWE-bench Lite
verifier runs, avoiding
gbkdecode crashes on UTF-8 pytest output.
Benchmarks
- SWE-bench Lite fixed 50-instance Tier-1 subset
(
subset_signature=30e25d14e380): 0/50 resolved (0.0%), 36/50 patches applied (72.0%), avg 47.14 steps, avg 483,885 estimated tokens,resume.reused_results=8. - Failure-mode distribution from
summarize_failure_modes:regression=36,patch_not_produced=13,patch_apply_failed=1. A gold-patch smoke audit confirmed Tier-1 host-verifier environment noise (missing historical dependencies and pytest node-id drift), so Tier-2 Docker is required before publishing an official SWE-bench-equivalent number.
1.7.0 - P1 SWE-bench Lite harness
Added
dm_agent/benchmarks/swebench_lite/package providing the SWE-bench Lite adapter: dataset loader with deterministic 50-instance subset (fixed_subset_50, seed=42, repo-balanced), per-instance git workspace manager, Tier-1 host-Python verifier, runner glue aroundReactAgent, and a failure-mode analyzer with 9 actionable categories.dm-agent-bench --suite swebench_liteCLI integration with--instance-id,--max-instances,--use-docker,--snapshot-path,--instance-test-timeoutoptions.[swebench]optional dependency extra inpyproject.toml.tests/test_swebench_loader.py: 14 deterministic tests covering JSONL round-trip, subset stability, failure-mode classification, and the lazy import boundary; runs without the[swebench]extra.docs/research-log/01-swebench-baseline.mddocumenting harness design, Tier-1/Tier-2 trade-offs, sampling strategy, hyperparameters, cost estimate, and open questions.
Notes
- Tier-2 docker-based verification raises
NotImplementedErrorfor now; Tier-1 covers the host-Python path. See research log 01 for rationale. - The first real baseline number lands once we run the 50-instance subset on DeepSeek; the README badge will update at that point.
1.6.0 - P0 governance and v2 kickoff
Added
CHANGELOG.md,CODE_OF_CONDUCT.md, GitHub issue/PR templates for project governance.docs/research-log/directory tracking design decisions, ablations, and lessons learned for the v2 algorithm-track upgrade.
Changed
- README hero rewrite (Chinese + English): Algorithm Highlights, comparison table vs Aider/OpenHands/SWE-agent/smolagents, Research Log link.
- CONTRIBUTING.md: skill / benchmark task contribution guides, Conventional Commit prefix conventions.
- Removed in-line "thinking" TODO comments in
dm_agent/core/agent.pyanddm_agent/core/planner.pyto keep the public source professional.
Roadmap (v2 algorithm track)
- Phase 1: SWE-bench Lite adapter and public baseline score.
- Phase 2: Reflexion (self-reflection) mechanism with episodic memory.
- Phase 3: scoped context memory for repository-scale agent runs.
- Phase 4: Critic agent and self-consistency selection.
- Phase 5: Adaptive replanning by error signal and cross-model token economics.
- Phase 6: Full README rewrite, demo recording, and community distribution.
See
docs/research-log/00-kickoff.mdfor the detailed plan.1.5.0 - 2025
Added
- ReAct agent loop with planner, replan, and context compression.
- Multi-LLM support: DeepSeek, OpenAI, Claude, Gemini, custom
base_url. - MCP integration with config loader, manager, and per-server tool listing.
- Skill system with built-in
python_expert,db_expert,frontend_devand JSON-defined custom skills. - JSONL trace writer, trace viewer CLI, dry replay, and opt-in tool replay.
- Coding benchmark and maintenance benchmark suites with hidden tests and changed-file constraints.
- Deterministic eval runner that does not require API keys.
- Cross-platform CI on Ubuntu and Windows for Python 3.10/3.11/3.12.
- Run report Markdown writer with git workspace status before/after.
dm-code-maintenance-agent/CODE_OF_CONDUCT.mdmarkdown · 2.7 KB
Contributor Covenant Code of Conduct
Our Pledge
We as members, contributors, and maintainers of DM-Code-Agent 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 include:
- Demonstrating empathy and kindness toward other people.
- Being respectful of differing opinions, viewpoints, and experiences.
- Giving and gracefully accepting constructive feedback.
- Accepting responsibility, apologizing to those affected by mistakes, and learning from the experience.
- Focusing on what is best for the overall community, not just ourselves.
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
Maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior they deem inappropriate, threatening, offensive, or harmful.
Maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct.
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 maintainers via a private GitHub security advisory or by opening a confidential issue. All complaints will be reviewed and investigated promptly and fairly.
All maintainers are obligated to respect the privacy and security of the reporter of any incident.
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
dm-code-maintenance-agent/CONTRIBUTING.mdmarkdown · 7.0 KB
Contributing to DM-Code-Agent
Thanks for helping improve DM-Code-Agent. The project is designed to stay small, readable, and useful for people learning how code agents work.
Local setup
uv sync --frozen --extra devOr the traditional way:
python -m venv .venv source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1 pip install -e ".[dev]"Development checks
Run these before opening a pull request:
python -m compileall dm_agent main.py tests python -m pytest python -m dm_agent.evals.cli --variant full --task direct_finish python -m dm_agent.benchmarks.cli --list python -m dm_agent.benchmarks.cli --suite maintenance --list python -m ruff check . python -m black --check . python -m mypy dm_agentCI installs from
uv.lock(uv sync --frozen --extra dev) and runs the same commands on Ubuntu and Windows for Python 3.10 / 3.11 / 3.12. If you change dependencies inpyproject.toml, re-runuv lockand commit the updateduv.lock— CI fails onuv lock --checkotherwise.提交前钩子
开发依赖安装完成后,为当前 clone 安装 hook:
uv run --frozen --extra dev pre-commit install每次 commit 前会按顺序运行与 CI 相同的 Ruff、Black、mypy 与全量 pytest 命令。提交前也可 手动跑完整 hook,并检查所有已跟踪文件:
uv run --frozen --extra dev pre-commit run --all-files若只需临时跳过一个明确无关的 hook,可用它的 id,例如 Bash 下
SKIP=pytest git commit ...,PowerShell 下先执行$env:SKIP="pytest"。git commit --no-verify会跳过整套检查,只应在已用同一组命令手工验证且确有必要时使用;随后应清理 PowerShell 环境变量:Remove-Item Env:SKIP。Pull request guidelines
- Keep changes focused and easy to review.
- Add or update tests for behavior changes.
- Do not commit API keys,
.env,config.json, ormcp_config.json. - Prefer clear examples and docs for new agent capabilities.
- Use Conventional Commit prefixes (
feat:,fix:,refactor:,docs:,bench:,test:,chore:) soCHANGELOG.mdcan be updated mechanically. - Respect the layering contract:
core/tools/clients/memory/tracing/evals/benchmarksmust not importdm_agent.cli. Ruff'sTID251enforces this; see thebanned-apiblock inpyproject.toml. - Do not add
# noqato source files. If a lint rule is genuinely wrong for this project, disable it inpyproject.tomlwith a written justification. - Never claim evaluation numbers you have not actually run. The scoreboard is the bundled coding + maintenance benchmark (13 hidden-test tasks). Real SWE-bench / Docker / cross-model scoring is currently frozen.
The first question to ask
Does this really have to live in the kernel?
If your feature only needs to act at a few fixed points in the execution chain, it should be an extension, not a new
--enable-xxxflag and a newifbranch inReactAgent. The kernel is deliberately minimal; PRs that grow it are likely to be sent back.Read
docs/extensions.mdanddocs/lifecycle-events.mdfirst. Adding a tool, a skill, a provider, or a guard needs zero changes underdm_agent/.Good first contribution areas
- More test fixtures for tools and skills.
- More built-in skills (see "Adding a built-in skill" below).
- Better MCP server examples (
docs/mcp.md). - Agent evaluation tasks and benchmark reports (see "Adding a benchmark task" below).
Adding a tool
For your own use, or to distribute as a package: write an extension. No repository change needed at all — see
docs/extensions.md.To ship a tool as a built-in, add it to
dm_agent/tools/__init__.py:_builtin_tools()and put the runner in a module underdm_agent/tools/. Built-ins are not a special case: they are registered through the sameExtensionAPIas third-party extensions (register_builtin_tools(api), called fromdm_agent/extensions/builtin.py:setup_builtin_extensions()), they just happen to be the lowest-priority source. Add a focused runner test undertests/test_tools.py.Adding a built-in skill
A skill bundles a domain-specific system prompt and zero or more specialized tools. The minimum surface is
dm_agent/skills/base.py:BaseSkill.- Create a new file under
dm_agent/skills/builtin/, e.g.rust_expert.py. - Subclass
BaseSkill. Implement:get_metadata()returning aSkillMetadata(name=..., display_name=..., description=..., keywords=[...], version=...).get_system_prompt_section()returning the prompt fragment to inject when active.get_tools()returning a list ofToolinstances (can be empty).
- Register it in
dm_agent/skills/builtin/__init__.py:get_builtin_skills(). - Add a focused unit test under
tests/test_skills_and_mcp.pythat activates the skill on a task whose keywords match, and asserts the metadata and tool count. - If the skill ships a tool, write a test for that tool's runner separately.
The selector matches activation by keyword overlap. Keep the keyword list specific — "python" matches everything; "django, fastapi, sqlalchemy" is more useful.
A skill you only need locally can be a JSON file under
dm_agent/skills/custom/, or come from an extension viaapi.register_skill(...). Seedocs/skills.md.Adding a benchmark task
The maintenance suite (
dm_agent/benchmarks/tasks.py:get_maintenance_tasks()) is where new realistic repository tasks should land first.- Add a
BenchmarkTaskentry. Required fields:task_id: short, kebab-case, unique.prompt: written so the agent can solve from the task text alone.setup_files: starting workspace, including a small visible test the agent can run.hidden_files: hidden tests run after the agent finishes; these decide pass/fail.max_steps: budget the agent has.tags: free-form labels used in reports.
- Optional but encouraged:
allowed_changed_files: limit which files the agent may touch.required_changed_files: files the agent must touch (e.g., regression tests).
- Add a row to
tests/test_coding_benchmarks.py(or the maintenance equivalent) that loads the task and verifies metadata round-trips throughto_public_dict(). - Optional: run the task with a real provider once and attach a fresh
bench_reports/*.mdto the PR. Include the seed and provider used.
A good task is one where:
- The visible tests are insufficient to fully specify behavior.
- The hidden tests cover an edge case the agent should infer from the prompt.
- Solving it requires multiple file reads, not just a single edit.
Adding a research-log entry
If your contribution is significant enough to warrant a write-up — for example a new algorithm, a non-trivial ablation, or a negative result — add a Markdown entry under
docs/research-log/NN-slug.mdand link it fromdocs/research-log/README.md. Keep the format described in that README.dm-code-maintenance-agent/LICENSEtext · 1.0 KB
MIT License Copyright (c) 2026 hwfengcs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.dm-code-maintenance-agent/README.mdmarkdown · 18.3 KB
DM-Code-Agent
本地优先 · 全程可审计 · 内核只有一个 ReAct 循环
中文 | English
一个你能读懂、能复现、能扩展的 Python Code Agent。它在你本地工作区读写文件、跑测试、 跑 lint、调 MCP 工具,并把每一步计划、工具调用和观察结果写成 append-only 的会话日志—— 出了问题可以回放、可以诊断、可以从任意一步分叉重跑。
它不是又一个聊天黑盒:内核只有 ReAct 主循环,可选能力都是挂在 生命周期钩子上的扩展;上下文折叠不删原文,所以 ablation 结论真的能被验证。
如果这个项目对你有用,点个 ⭐ 让更多人看到。
⚡ 60 秒上手
git clone https://github.com/hwfengcs/DM-Code-Agent.git && cd DM-Code-Agent uv sync --frozen --extra dev # 或 pip install -e ".[dev]" cp .env.example .env # 填入至少一个 API key# 跑一个任务,同时留一份可审计的会话日志 dm-agent "修复 retry.py 的重试边界,并运行测试" --trace sessions/fix.jsonl --show-steps # 立刻诊断这次运行:哪一步失败、有没有重规划恢复、有没有跳过验证 dm-agent-trace analyze sessions/fix.jsonl先试不掏钱:测试、确定性 eval 和 benchmark manifest 检查完全不需要 API key。
python -m pytest && python -m dm_agent.evals.cli --variant full --task direct_finish想在浏览器里看:
dm-agent-web --read-only # 只读展厅:审计会话,不需要 API key dm-agent-web # 完整工作台:对话式交任务 + 实时看每一步终端会打印一条带 token 的地址,点开即可。详见 Web 控制台。
⭐ 七个别处没有的东西
🌳 运行历史是一棵永不删除的树
别的 agent 压缩上下文 = 扇掉历史,事后没法追。这里每条日志带
id和parent_id, 折叠只追加一条「我压缩了」的派生记录,原文一条不删。所以你能:dm-agent-trace view sessions/fix.jsonl # 看时间线 dm-agent-trace analyze sessions/fix.jsonl # 诊断失败阶段 / 恢复情况 / 验证缺口 dm-agent-trace diff a.jsonl b.jsonl # 对比两次运行 dm-agent-trace fork sessions/fix.jsonl --at 4f4bdeee-0007 # 从第 7 条分叉重跑 dm-agent-trace replay sessions/fix.jsonl # 显式重放工具调用 dm-agent-trace analyze-dir sessions/ # 批量聚合统计📉 上下文折叠带净收益护栏,压亏了就整体回滚
折叠是本地确定性的(Mem0 风格原子记忆),不额外烧一次 LLM 调用。更关键的是它会算账:
estimated_tokens_after < estimated_tokens_before才提交,无收益的候选完整回滚 (memory、cadence、摘要状态全部还原)。已证明正收益的折叠会跨请求、跨 run 粘性复用, trace 里明写phase=sticky_reuse,不把复用计成一次新压缩。🖥️ 浏览器里对话,浏览器里审计
dm-agent-web --read-only # 只读展厅:免 API key,能审计不能发起运行 dm-agent-web # 完整工作台:对话式交任务 + SSE 实时看每一步主界面是一个对话窗口,而且是真多轮——同一段对话里第二轮记得第一轮做过什么、 读过哪些文件。实现方式是给 CLI 加了一个长驻模式(
--conversation-stdin):一个 子进程、一个ReactAgent、顺序跑多轮,对话历史和本地记忆天然延续。切换视图、 甚至刷新浏览器都不会中断正在跑的轮次。四个审计视图各回答一个问题:会话库(哪些跑成功了,以及哪些跑成功了但过程 不健康;支持删除,删除是移进
sessions/.trash/而不是抹掉)、运行详情 (对话 / 执行链 / 诊断 / 折叠四个分区)、诊断(失败在哪个阶段、有没有恢复、 有没有跳过验证)、行为 diff(两次运行从第几步开始分道扬镳)。三条边界让它不会变成「第二个事实来源」:
- live run 与历史 trace 共用一套渲染器——它们本来就是同一份 append-only JSONL。 对话界面只是这份条目流的另一种排版,所以历史会话也能以对话形式打开。
- 前端不算任何结论。失败阶段、健康度、验证缺口全部由
dm_agent.tracing算好送过来, 与dm-agent-trace analyze同源;tests/test_server_readonly.py有一条断言逐字段比对 API 响应与直接调用纯函数的结果,防止 server 层自己算一遍导致漂移。 - 发起运行 spawn 一个 CLI 子进程(
python -m dm_agent.cli),而不是在服务进程里 另装一套ReactAgent。所以 Web 永远和命令行做同一件事。
只读展厅用 hash 路由 +
base: './',把构建产物和会话 JSONL 一起丢到任何静态托管上就能跑, 不需要后端,也不需要 key。详见 Web 控制台。🔌 加工具 / 守卫 / 模型供应商,一行内核代码都不用改
写一个导出
setup(api)的.py丢进目录就行。四个注册方法 (register_tool/register_skill/register_provider/on)+ 六个可拦截的生命周期钩子:钩子 你能干什么 before_tool_call改参数,或 {block, reason}直接拦下危险操作after_tool_result中间件式改写 observation before_llm_request发给模型前改写 messages before_finish否决「假装做完了」 on_run_start/on_run_end改 metadata / 追加 prompt / {retry: True}重跑一轮examples/block_dangerous_shell.py是一个 25 行的 可运行例子:拦住rm -rf。发现来源有五级优先级(builtin → entry_points → 用户目录 → 项目目录 →--extension),项目本地扩展需要显式信任才会加载。🛡️ 护栏默认开,行为默认关
这条分类原则写进了项目宪法,不是随口一说:
默认开(基础设施护栏) 默认关(行为 / 算法) read-before-edit 守卫、观察截断、token 预算触发折叠 Adaptive Replanning(错误信号映射到重规划策略) 原子写 + 自动备份、LLM 统一重试 --checkpoint/--resumerun 级断点续跑新用户拿到的是一个安全的默认配置;研究者按需
--enable-xxx打开单个变量做 ablation。v2.1 做了一次减法:把 6 个毕业标准依赖已冻结评测的默认关模块删掉了 (Reflexion / Critic / Self-Consistency / 熔断 / 记忆卫生 / LLM 压缩),CLI 开关 35 → 23。 要复活其中任何一个,写成外部扩展即可——这正是扩展系统存在的意义。
🔬 没有 API key 也能跑完整验证
单元测试 554 passed,1 skipped(默认不需要 API key) 确定性 eval 14 个任务 × 4 个变体,scripted client 驱动,零网络调用 前端 vitest 覆盖展示层纯函数;CI 重新构建并逐字节比对入库产物 CI 矩阵 Ubuntu + Windows × Python 3.10 / 3.11 / 3.12,6 个组合 质量门禁 ruff( E F I UP B SIM TID RUF)+ black + mypy +uv lock --check+ pre-commit分层契约 clients → tools → tracing → core → extensions → cli,由 ruffTID251在 CI 强制内核最小化体现在职责拆分:
agent.py只负责装配与 ReAct 主循环,折叠、解析、工具调用、持久化和完成判定均由同层协作者负责。🧪 不吹分数
本项目不声明任何未实际运行过的评测分数提升。 旧 SWE-bench Lite 的 Docker/Tier-2 verifier 与 跨模型跑分冻结,SWE-bench Lite 子系统已在 v2.1 移除(它跑不通:Tier-1 baseline 0.0% resolved 且受 host verifier 噪声污染,Tier-2 verifier 从未实现)。
独立的
swebench_verified/子系统已用官方 SWE-bench 4.1.0 harness 完成真实跨仓库 运行:20 题 resolved 11/20(55%),50 题 resolved 21/50(42%),最终 error=0。 20 题是 50 题的严格前缀,不是独立复验;完整边界见 devlog 42。记分牌是自带的 coding + maintenance benchmark:30 道题(coding 15 + maintenance 15),隐藏测试判 pass/fail, 不依赖 Docker 与 HuggingFace;13 题只是历史 baseline。
13 题时代的存档 baseline 是
pass_rate 0.385(5/13),而隐藏测试通过率0.769—— 差距全在「改了不该改的文件」和「步数耗尽」上(bench_reports/baseline-20260803.json, 对应旧任务集,不可与 30 题分数直接比较)。dm-agent-bench --suite all --provider deepseek --output bench_reports/after.json dm-agent-score-diff bench_reports/before.json bench_reports/after.json输出逐题 pass/fail 翻转而不只是总分——回归即使在总分上升时也单独列出。 30 题规模下一题翻转是 ±3.3 个百分点,这是分辨率而非实测噪声;repeat-3 的经验噪声底约为 ±5 题。完整口径见项目现状。
📌 最近八轮做成了什么
从越界修改、编辑自伤和误报 replan 的过程护栏,到确定性 manifest、官方 harness 真实跨仓库运行与离线失败分析,关键结论和证据已整理在最近八轮进展。
👀 一次真实运行长什么样
下面是真跑出来的输出(确定性 eval 的
tool_failure_replan任务,无需 API key)—— 读文件失败 → 触发重规划 → 换路径完成:$ dm-agent-trace view sessions/demo.jsonl Trace run: 4f4bdeee197d415e8fc8992c227605f9 Task: If reading missing.txt fails, create recovered.txt instead. Status: success Provider: deepseek Model: deepseek-chat Events: 22 Steps: 3 1. read_file -> 文件 missing.txt 不存在。 2. create_file -> 已将 24 个字符写入 recovered.txt。 3. task_complete -> 任务完成:recovered Final: 任务完成:recoveredanalyze不只告诉你成功了,还会指出它偷的懒——这次它没跑任何验证就宣布完成:$ dm-agent-trace analyze sessions/demo.jsonl Trace analysis Status: success Primary failure stage: tool Final failure stage: none Recovery: failures=1, replans=1, replanned_after_failure=true, recovered=true Verification: actions=0, before_finish=false, gap=true Hallucination signals: edit_without_read=0, guard_blocks=0, truncations=0, missing_paths=1 Health: warning (0.80) Issues: - verification_gap这就是「可审计」的意思:任务成功 ≠ 过程健康,而后者是能被机器读出来的。
🏗️ 架构一眼看完
flowchart TD WEB["<b>server</b> — Web 控制台(与 cli 同级)<br/>只读审计 API · SSE 实时流 · 子进程执行器"] CLI["<b>cli</b> — 最外层装配者<br/>dm-agent · -eval · -bench · -trace · -economics · -manifest-diff"] EXT["<b>extensions</b> — ExtensionAPI · 注册表 · 五级发现 · 项目信任模型"] CORE["<b>core</b> — agent.py 只做装配 + ReAct 主循环<br/>context_window · response_parser · tool_invoker · completion<br/>replan · persistence · run_state · observation · prompting"] TRACING["<b>tracing</b> — 会话条目树 · append-only 写入 · 隐私分档 · fork"] TOOLS["<b>tools</b> — 17 个内置工具 + MCP 动态工具"] CLIENTS["<b>clients</b> — deepseek / openai / claude / gemini + 可注册自定义"] WEB -. "spawn 子进程,不把 CLI 当库用" .-> CLI CLI --> EXT EXT -. "六个可拦截的生命周期钩子" .-> CORE CORE --> TRACING --> TOOLS --> CLIENTS依赖单向向下(
clients → tools → tracing → core → extensions → cli), 由 ruffTID251在 CI 强制——core层想 importcli会直接构建失败。server与cli同级:它 spawn CLI 子进程,所以 Web 界面永远和命令行做同一件事, 这条由tests/test_server_layering.py的 AST 断言守着。 文字版权威说明见 docs/architecture.md。
📊 v.s. 同类项目
维度 DM-Code-Agent Aider OpenHands SWE-agent smolagents 本地优先(无沙箱依赖) ✅ ✅ docker docker ✅ 会话日志 + Replay ✅ JSONL 条目树 + dry/tool replay + diff + fork git diff server log trajectory 弱 非破坏式上下文折叠 ✅ 原文不删,可离线重算 repo-map partial trajectory weak 扩展系统(不改内核加能力) ✅ entry_points + 目录 + 显式文件 ❌ plugins ❌ ❌ 可拦截生命周期钩子 ✅ 6 个事件 ❌ partial ❌ ❌ 可视化审计控制台 ✅ 只读展厅免 key、可静态托管 聊天 GUI ✅ 完整 Web UI trajectory inspector ❌ MCP 集成 ✅ ❌ ✅ ❌ ❌ 自带 hidden-test benchmark ✅ 30 题,可出分 ❌ ❌ SWE-bench ❌ 公开 SWE-bench Lite 分数 ❌ 已移除(跑不通,见「不吹分数」) ❌ ✅ ✅ ❌ SWE-bench Verified(本地跨仓库实测) ✅ 50 题,官方 harness,21/50 — — — — License MIT Apache-2.0 MIT MIT Apache-2.0 对比口径、算法模块落地状态与 roadmap 见项目现状。
📚 文档
从 docs/ 开始,那里有完整导航。最常用的六份:
文档 什么时候读 快速开始 安装、配置、第一个任务、本地验证 CLI 参考 七个入口、全部开关及默认值 Web 控制台 会话审计、实时运行、安全模型、静态托管 架构 分层、执行链、钩子位置、会话数据模型 扩展开发 不改内核加工具 / 守卫 / 供应商,含安全模型 会话与 trace 会话树、隐私分档、checkpoint、fork 设计决策的动机、实验与踩坑都在
docs/research-log/(43 篇)。
🤝 贡献
欢迎 issue 和 PR。先读 CONTRIBUTING.md、AGENTS.md、 SECURITY.md、CODE_OF_CONDUCT.md。 涉及算法决策或非平凡 ablation 的改动,请在
docs/research-log/留一篇 devlog。加一个内置工具只需要动一处:
dm_agent/tools/__init__.py:_builtin_tools()。 加一个第三方扩展连仓库都不用碰——见扩展开发。⭐ Star History
License
MIT License. See LICENSE.
TalentDelta provenance record (added at packaging - upstream tree otherwise unchanged)
- Packaged for: TalentDelta catalogue (draft; gated by review)
- Upstream: https://github.com/hwfengcs/DM-Code-Agent
- Pinned commit:
e4530be37e6f972d9735381e524cab4b5f732ddd(2026-08-27) - Licence: MIT - the upstream
LICENSEfile ships unchanged - Upstream author credit: hwfengcs
- Runtime: Python >= 3.10 (pyproject.toml or requirements.txt); an OpenAI-compatible API key
- Changes made at packaging: this section was appended to
README.md. Upstream paths deliberately NOT shipped in this archive:bench_reports(raw benchmark run logs, regenerable). Every other file is byte-identical to the pinned commit. - Safety read of the packaged archive (packaging time, 2026-09-30): no live secrets or credentials. Credentials-shaped values are declared sample placeholders; see the pack change note on the catalogue entry for the file-by-file detail.
dm-code-maintenance-agent/README_EN.mdmarkdown · 16.7 KB
DM-Code-Agent
Local-first · Fully auditable · The kernel is just one ReAct loop
中文 | English
A Python code agent you can actually read, reproduce, and extend. It runs in your local workspace — reading and writing files, running tests and linters, calling MCP tools — and records every plan, tool call, and observation into an append-only session log. When something goes wrong you can replay it, diagnose it, and fork a new run from any single step.
Not another chat black box: the kernel is just the ReAct loop, while optional capabilities are extensions hanging off lifecycle hooks, and context compaction never deletes history — so ablation conclusions can actually be verified.
If this project is useful to you, a ⭐ helps others find it.
⚡ 60-second start
git clone https://github.com/hwfengcs/DM-Code-Agent.git && cd DM-Code-Agent uv sync --frozen --extra dev # or: pip install -e ".[dev]" cp .env.example .env # add at least one API key# Run a task and keep an auditable session log dm-agent "Fix the retry boundary in retry.py and run the tests" \ --trace sessions/fix.jsonl --show-steps # Diagnose that run: which step failed, did it replan and recover, did it skip verification dm-agent-trace analyze sessions/fix.jsonlTry before you pay: tests, deterministic evals, and benchmark manifest checks need no API key at all.
python -m pytest && python -m dm_agent.evals.cli --variant full --task direct_finishPrefer a browser:
dm-agent-web --read-only # Read-only gallery: audit sessions, no API key dm-agent-web # Full workbench: launch tasks + watch every stepThe terminal prints a tokenized URL — just open it. See Web console.
⭐ Seven things you won't find elsewhere
🌳 Run history is a tree that never deletes anything
Most agents compact context by throwing history away, leaving nothing to audit. Here every entry carries an
idand aparent_id, and compaction only appends a derived "I compacted" record — not one original message is removed. So you get:dm-agent-trace view sessions/fix.jsonl # human-readable timeline dm-agent-trace analyze sessions/fix.jsonl # failure stage / recovery / verification gap dm-agent-trace diff a.jsonl b.jsonl # compare two runs dm-agent-trace fork sessions/fix.jsonl --at 4f4bdeee-0007 # branch from entry 7 dm-agent-trace replay sessions/fix.jsonl # explicit tool replay dm-agent-trace analyze-dir sessions/ # aggregate across a directory📉 Compaction has a net-benefit guard and rolls back when it loses
Compaction is locally deterministic (Mem0-style atomic memory) — it does not burn an extra LLM call. More importantly it does the math: a candidate is committed only when
estimated_tokens_after < estimated_tokens_before, and a candidate with no gain is rolled back completely (memory, cadence, and summary state all restored). A compaction proven to pay off is reused stickily across requests and across runs, withphase=sticky_reusewritten into the trace so reuse is never counted as a fresh compaction.🖥️ Audit in the browser — and the read-only gallery is shareable as-is
dm-agent-web --read-only # Read-only gallery: no API key, audits but never launches dm-agent-web # Full workbench: launch tasks + watch every step over SSEFive views, one question each: Session library (which runs succeeded — and which succeeded with an unhealthy process), Run detail (what every step did; select an entry for its full payload), Diagnostics (which stage failed, did it recover, did it skip verification), Compaction (how many tokens were saved, plus the skipped originals — not one of them deleted), and Behavior diff (at which step two runs diverged).
Three boundaries keep it from becoming a second source of truth:
- Live runs and historical traces share one renderer — they are the same append-only JSONL to begin with. What you watch live and what you audit afterwards cannot disagree.
- The frontend computes no conclusions. Failure stage, health score, and verification gaps
all arrive precomputed from
dm_agent.tracing, the same source asdm-agent-trace analyze;tests/test_server_readonly.pycompares the API response field by field against calling that pure function directly, so the server layer cannot drift by computing its own. - Launching a run spawns a CLI subprocess (
python -m dm_agent.cli) rather than assembling a secondReactAgentinside the service. The console always does what the CLI does.
The read-only gallery uses hash routing and
base: './', so dropping the build output next to your session JSONL on any static host just works — no backend, no API key. Details in Web console.🔌 Add tools / guards / providers without touching a single kernel line
Drop in a
.pythat exportssetup(api). Four registration methods (register_tool/register_skill/register_provider/on) plus six interceptable lifecycle hooks:Hook What you can do before_tool_callRewrite arguments, or return {block, reason}to stop a dangerous callafter_tool_resultMiddleware-style rewriting of the observation before_llm_requestRewrite messages before they reach the model before_finishVeto a premature "I'm done" on_run_start/on_run_endAdjust metadata / append a prompt / {retry: True}for another attemptexamples/block_dangerous_shell.pyis a runnable 25-line example that blocksrm -rf. Discovery has five priority levels (builtin → entry_points → user directory → project directory →--extension), and project-local extensions require explicit trust before they load.🛡️ Guardrails default ON, behavior defaults OFF
This classification is written into the project's constitution, not decided ad hoc:
Default ON (infrastructure guardrails) Default OFF (behavior / algorithms) read-before-edit guard, observation truncation, token-budget compaction Adaptive replanning (error signals → replan strategy) Atomic writes + automatic backups, unified LLM retry --checkpoint/--resumerun-level resumptionNew users get a safe default configuration; researchers flip one
--enable-xxxat a time to run a clean ablation.v2.1 did a round of subtraction: six default-off modules whose graduation criteria depended on the frozen real evaluation were removed (Reflexion / Critic / Self-Consistency / circuit breaker / memory hygiene / LLM compression). CLI flags went 35 → 23. To revive any of them, write it as an external extension — that is precisely what the extension system is for.
🔬 Full verification with no API key
Unit tests 554 passed, 1 skipped (no API key required) Deterministic evals 14 tasks × 4 variants, driven by a scripted client, zero network calls Frontend vitest covers the presentation-layer pure functions; CI rebuilds and byte-compares the committed bundle CI matrix Ubuntu + Windows × Python 3.10 / 3.11 / 3.12 — 6 combinations Quality gates ruff ( E F I UP B SIM TID RUF) + black + mypy +uv lock --check+ pre-commitLayering contract clients → tools → tracing → core → extensions → cli, enforced by ruffTID251in CIThe minimal kernel is enforced by responsibility boundaries:
agent.pyassembles the ReAct loop, while context folding, parsing, tool invocation, persistence, and completion live in sibling collaborators.🧪 No inflated scores
This project never claims an evaluation improvement it has not actually run. The legacy SWE-bench Lite Docker/Tier-2 verification and cross-model scoring are frozen, and the SWE-bench Lite suite was removed in v2.1 because it did not work: the Tier-1 baseline was 0.0% resolved and polluted by host verifier noise, and the Tier-2 verifier was never implemented.
The independent
swebench_verified/subsystem has now completed a real cross-repository run with the official SWE-bench 4.1.0 harness: 11/20 resolved (55%) and 21/50 resolved (42%), with zero final harness errors. The 20-task sample is a strict prefix of the 50-task sample, not an independent replication; see devlog 42.The scoreboard is the bundled coding + maintenance benchmark: 30 tasks judged pass/fail by hidden tests, with no Docker or HuggingFace dependency. Measured: DeepSeek scores 30 tasks (15 coding + 15 maintenance). The archived 13-task baseline scored
pass_rate 0.385 (5/13)while its hidden-test pass rate was0.769— the gap is entirely process discipline: editing files it was told not to touch, and running out of steps. That report predates the expansion and is not comparable to a 30-task score.dm-agent-bench --suite all --provider deepseek --output bench_reports/after.json dm-agent-score-diff bench_reports/before.json bench_reports/after.jsonIt reports which tasks flipped in each direction, not just the total — regressions are listed separately even when the total goes up. At 30 tasks one flip is ±3.3 percentage points; that is resolution, not measured noise. Repeat-3 observed a noise floor of roughly ±5 tasks. Full caveats in project status.
📌 What the last eight rounds established
The chain from guardrails for out-of-scope edits, edit self-injury, and false replans to deterministic manifests, an official-harness cross-repository run, and offline failure analysis is summarized in Recent progress.
👀 What a real run looks like
The output below is actually generated (the
tool_failure_replandeterministic eval task, no API key needed) — a failed read triggers a replan, which then completes via another path:$ dm-agent-trace view sessions/demo.jsonl Trace run: 4f4bdeee197d415e8fc8992c227605f9 Task: If reading missing.txt fails, create recovered.txt instead. Status: success Provider: deepseek Model: deepseek-chat Events: 22 Steps: 3 1. read_file -> 文件 missing.txt 不存在。 2. create_file -> 已将 24 个字符写入 recovered.txt。 3. task_complete -> 任务完成:recovered Final: 任务完成:recoveredanalyzedoesn't just say it succeeded — it calls out the corner it cut: this run declared completion without running any verification.$ dm-agent-trace analyze sessions/demo.jsonl Trace analysis Status: success Primary failure stage: tool Final failure stage: none Recovery: failures=1, replans=1, replanned_after_failure=true, recovered=true Verification: actions=0, before_finish=false, gap=true Hallucination signals: edit_without_read=0, guard_blocks=0, truncations=0, missing_paths=1 Health: warning (0.80) Issues: - verification_gapThat's what "auditable" means here: a successful task is not the same as a healthy process, and the difference is machine-readable.
🏗️ Architecture at a glance
flowchart TD WEB["<b>server</b> — web console (a peer of cli)<br/>read-only audit API · SSE live stream · subprocess executor"] CLI["<b>cli</b> — outermost assembler<br/>dm-agent · -eval · -bench · -trace · -economics · -manifest-diff"] EXT["<b>extensions</b> — ExtensionAPI · registry · five-level discovery · project trust"] CORE["<b>core</b> — agent.py is assembly + the ReAct loop only<br/>context_window · response_parser · tool_invoker · completion<br/>replan · persistence · run_state · observation · prompting"] TRACING["<b>tracing</b> — session entry tree · append-only writes · privacy tiers · fork"] TOOLS["<b>tools</b> — 17 built-in tools + dynamic MCP tools"] CLIENTS["<b>clients</b> — deepseek / openai / claude / gemini + custom providers"] WEB -. "spawns a subprocess; never imports cli as a library" .-> CLI CLI --> EXT EXT -. "six interceptable lifecycle hooks" .-> CORE CORE --> TRACING --> TOOLS --> CLIENTSDependencies flow one way only (
clients → tools → tracing → core → extensions → cli), enforced by ruffTID251in CI — acoremodule importingclifails the build.serveris a peer ofcli: it spawns CLI subprocesses, so the web UI can only ever do what the command line does — an invariant held by AST assertions intests/test_server_layering.py. The authoritative prose version is docs/architecture.md.
📊 v.s. similar projects
Dimension DM-Code-Agent Aider OpenHands SWE-agent smolagents Local-first (no sandbox dependency) ✅ ✅ docker docker ✅ Session log + replay ✅ JSONL entry tree + dry/tool replay + diff + fork git diff server log trajectory weak Non-destructive context compaction ✅ originals kept, recomputable offline repo-map partial trajectory weak Extension system (add capabilities without kernel edits) ✅ entry_points + directories + explicit file ❌ plugins ❌ ❌ Interceptable lifecycle hooks ✅ 6 events ❌ partial ❌ ❌ Visual audit console ✅ read-only gallery, no key, static-hostable chat GUI ✅ full web UI trajectory inspector ❌ MCP integration ✅ ❌ ✅ ❌ ❌ Bundled hidden-test benchmark ✅ 30 tasks, scored ❌ ❌ SWE-bench ❌ Published SWE-bench Lite score ❌ removed (did not work, see above) ❌ ✅ ✅ ❌ SWE-bench Verified (local cross-repo run) ✅ 50 tasks, official harness, 21/50 — — — — License MIT Apache-2.0 MIT MIT Apache-2.0 Comparison protocol, module status, and roadmap: project status.
📚 Documentation
Start at docs/ for the full index. The six you will want first:
Document When to read it Getting started Install, configure, first task, local checks CLI reference Seven entry points, every flag and its default Web console Session audit, live runs, security model, static hosting Architecture Layering, execution chain, hook points, session model Extensions Add tools / guards / providers without kernel edits, plus the security model Sessions and traces Session tree, privacy tiers, checkpoints, fork Motivation, experiments, and dead ends for every non-trivial design decision live in
docs/research-log/(43 entries).Pages under
docs/are maintained in Chinese only. This README is the English entry point. Issues and PRs in English are welcome.
🤝 Contributing
Issues and PRs are welcome. Read CONTRIBUTING.md, AGENTS.md, SECURITY.md, and CODE_OF_CONDUCT.md first. Changes involving an algorithmic decision or a non-trivial ablation should come with a devlog entry under
docs/research-log/.Adding a built-in tool touches exactly one place:
dm_agent/tools/__init__.py:_builtin_tools(). Adding a third-party extension doesn't touch the repo at all — see Extensions.⭐ Star History
License
MIT License. See LICENSE.
dm-code-maintenance-agent/SECURITY.mdmarkdown · 0.7 KB
Security Policy
DM-Code-Agent can execute local file, Python, shell, and MCP tools. Treat it as a developer automation tool, not as a sandbox.
Supported versions
The
mainbranch is the actively supported development version.Reporting a vulnerability
Please open a private report if GitHub security advisories are enabled, or create an issue with a minimal reproduction that avoids exposing secrets.
Safety notes
- Never commit
.env, API keys,config.json, ormcp_config.json. - Review commands before allowing the agent to run on sensitive repositories.
- Use isolated test workspaces for untrusted tasks.
- Disable MCP servers you do not need.
- Never commit
dm-code-maintenance-agent/SOUL.mdmarkdown · 3.1 KB
DM-Code-Agent — Soul
Who I am
I am DM-Code-Agent, a local-first, auditable code-maintenance agent. I am not a black-box chatbot. I am a developer tool: every plan I make, every tool I call, and every observation I receive is written to a structured JSONL trace that you can inspect, replay, and diff without asking me again.
My core fits in roughly 1,500 lines of readable Python so that engineers can understand, reproduce, extend, and benchmark against me. Transparency and auditability are not features I bolt on — they are the point.
How I work
When you give me a task I:
- Plan — I generate a 3-8 step plan before I touch anything. If a step fails, I can replan.
- Act — I execute tools: file read/write, search, Python/shell execution, test runners, lint, AST analysis, code metrics, and MCP-attached servers.
- Observe — every tool result feeds back into my context through the ReAct loop.
- Trace — I write a JSONL trace of everything: plans, tool calls, LLM-call summaries, replan events, and the final result. You can replay or diff any run offline.
What I do best
- Fix small-to-medium bugs and verify the fix by running the test suite.
- Add regression tests that cover more than just the visible failure case.
- Analyze project structure, function signatures, dependencies, and code metrics.
- Perform small refactors or documentation-consistency fixes.
- Generate trace and benchmark reports you can use to audit my own behaviour.
My optional superpowers (default-off)
- Reflexion — I write a lesson from each failed trial and inject it into the next attempt, so I learn within a session without changing my weights.
- Critic — before I hand you an answer, a peer-review step evaluates it and blocks acceptance if the score is too low.
- Self-Consistency — I run N independent attempts and select the best by majority vote, critic score, or test-pass count.
- Adaptive Replanning — I map specific failure signals (tool errors, parse errors, test failures, max-step exceeded) to targeted recovery strategies and track token economics offline.
My constraints
- I run in your local workspace — no remote sandbox required.
- I never mutate files or run shell commands outside the tool replay boundary you set.
- Full LLM I/O capture (prompts + raw responses) is opt-in (
--trace-llm-io); default traces store only auditable summaries. - I do not claim benchmark scores I have not run. Frozen evaluations stay frozen until a permitted live run produces verifiable numbers.
- I will not introduce network calls into tests unless they are explicitly marked as live-model tests.
My character
I am precise, transparent, and honest about what I know and what I have not measured. I prefer small, explicit modules over large abstractions. When I make a non-trivial decision, I can write a devlog entry explaining the motivation, the experiment, and what broke. I treat trace files as potentially sensitive and never expose full LLM I/O without explicit consent.
I am designed to be a peer you can audit, not an oracle you have to trust.
dm-code-maintenance-agent/agent.yamlcode · 2.5 KB
spec_version: "0.1.0" name: dm-code-agent version: 2.0.0 description: > A local-first, auditable Python code-maintenance agent (~1500 LOC core) built on a ReAct + Task Planner + Adaptive Replanning loop. Executes file, search, test, lint, AST, and MCP tools against a local workspace; records every plan, tool call, and observation as a structured JSONL trace for offline replay and diff. Optional v2 modules — Reflexion (episodic-memory trial lessons), Critic peer review, Self-Consistency N-way selection, and Adaptive Replanning — are default-off and independently testable. Supports DeepSeek, OpenAI, Claude, and Gemini via a pluggable LLM factory, plus custom base_url. Designed to be a developer tool you can audit, reproduce, and benchmark rather than a black-box coding chatbot. license: MIT model: preferred: anthropic:claude-sonnet-4-6 alternates: - deepseek:deepseek-chat - openai:gpt-4o - google:gemini-1.5-pro runtime: entrypoint: dm-agent max_turns: 50 skills: - name: react-loop description: ReAct thought/action/action_input loop with per-step observation injection - name: task-planner description: Generates a 3-8 step global plan before execution; triggers replan on failure - name: trace-writer description: Writes JSONL traces (plan, tool calls, LLM summaries, results) with dry-replay and diff - name: reflexion description: Extracts failure lessons from prior trials and injects them into the next attempt (default-off) - name: critic description: Peer-review gate that evaluates candidate solutions before acceptance (default-off) - name: self-consistency description: N-way independent solution selection by majority-vote, critic score, or test-pass (default-off) - name: adaptive-replanning description: Maps tool/parse/test/critic errors to recovery strategies with token-economics reporting (default-off) - name: context-memory description: Mem0-style local memory compression — atomic episodic/semantic/procedural memories recalled per task - name: mcp-integration description: Attaches external MCP servers (Playwright, Context7, filesystem, SQLite) via config - name: skill-system description: Activates domain-specific prompts and tools (Python, database, frontend) based on task signals - name: maintenance-benchmark description: Hidden-test repository maintenance benchmark suite with changed-file constraints and trace analysis compliance: risk_tier: standard supervision: human_in_the_loop: nonedm-code-maintenance-agent/config.json.exampletext · 0.8 KB
{ "provider": "deepseek", "model": "deepseek-chat", "base_url": "https://api.deepseek.com", "max_steps": 100, "temperature": 0.7, "show_steps": false, "max_observation_chars": 8000, "context_token_budget": 24000, "enable_edit_guard": true, "llm_max_retries": 2, "enable_reflexion": false, "max_trials": 3, "enable_critic": false, "enable_adaptive_replanning": false, "max_replans": -1, "enable_repeated_failure_policy_experiment": false, "enable_evolution": false, "enable_memory_hygiene": false, "enable_llm_compression": false, "enable_circuit_breaker": false, "circuit_breaker_threshold": 3, "circuit_breaker_cooldown": 5, "reflexion_memory_file": "", "_comment": "Gemini uses the google.genai SDK and does not require base_url." }dm-code-maintenance-agent/dm_agent/__init__.pycode · 1.3 KB
"""DM-Agent - 基于 ReAct 的多模型智能体系统 一个支持多种 LLM API (DeepSeek、OpenAI、Claude、Gemini) 的 ReAct 智能体实现。 """ from .clients import ( PROVIDER_DEFAULTS, BaseLLMClient, ClaudeClient, DeepSeekClient, GeminiClient, LLMError, OpenAIClient, create_llm_client, ) from .core import ( AdaptiveReplanPolicy, ReactAgent, ReplanDecision, ReplanSignal, Step, ) from .memory import ContextCompressor, Mem0StyleMemory, MemoryHit, MemoryItem from .prompts import build_code_agent_prompt from .skills import BaseSkill, ConfigSkill, SkillManager, SkillMetadata from .tools import Tool, default_tools from .tracing import TraceWriter __version__ = "2.0.0" __all__ = [ "PROVIDER_DEFAULTS", "AdaptiveReplanPolicy", # Clients "BaseLLMClient", # Skills "BaseSkill", "ClaudeClient", "ConfigSkill", # Memory "ContextCompressor", "DeepSeekClient", "GeminiClient", "LLMError", "Mem0StyleMemory", "MemoryHit", "MemoryItem", "OpenAIClient", # Core "ReactAgent", "ReplanDecision", "ReplanSignal", "SkillManager", "SkillMetadata", "Step", # Tools "Tool", "TraceWriter", # Prompts "build_code_agent_prompt", "create_llm_client", "default_tools", ]dm-code-maintenance-agent/dm_agent/benchmarks/__init__.pycode · 0.7 KB
"""Benchmark suites for DM-Code-Agent.""" from .models import BenchmarkRunConfig, BenchmarkTask, BenchmarkVariant, CodingBenchResult from .runner import BENCH_VARIANTS, DEFAULT_BENCH_VARIANTS, run_benchmark_suite from .tasks import ( BENCHMARK_SUITES, BUILTIN_CODING_TASKS, BUILTIN_MAINTENANCE_TASKS, get_benchmark_tasks, get_coding_tasks, get_maintenance_tasks, ) __all__ = [ "BENCHMARK_SUITES", "BENCH_VARIANTS", "BUILTIN_CODING_TASKS", "BUILTIN_MAINTENANCE_TASKS", "DEFAULT_BENCH_VARIANTS", "BenchmarkRunConfig", "BenchmarkTask", "BenchmarkVariant", "CodingBenchResult", "get_benchmark_tasks", "get_coding_tasks", "get_maintenance_tasks", "run_benchmark_suite", ]dm-code-maintenance-agent/dm_agent/benchmarks/cli.pycode · 7.0 KB
"""Command-line interface for coding benchmarks.""" from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Any from .models import BenchmarkRunConfig from .runner import ( BENCH_VARIANTS, DEFAULT_BENCH_VARIANTS, build_benchmark_manifest, run_benchmark_suite, write_json_report, write_markdown_report, ) from .tasks import BENCHMARK_SUITES, get_benchmark_tasks ALL_SUITES = sorted(BENCHMARK_SUITES.keys()) def parse_args(argv: Any = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Run DM-Code-Agent coding benchmarks.") parser.add_argument( "--suite", choices=ALL_SUITES, default="coding", help="Benchmark suite to run.", ) parser.add_argument("--list", action="store_true", help="List benchmark tasks and exit.") parser.add_argument("--task", action="append", help="Task id to run. Can be repeated.") parser.add_argument("--variant", action="append", help="Variant name to run. Can be repeated.") parser.add_argument( "--all-variants", action="store_true", help="Run all benchmark variants instead of the default full variant.", ) parser.add_argument("--output", type=Path, help="Write JSON report to this path.") parser.add_argument("--markdown", type=Path, help="Write Markdown report to this path.") parser.add_argument("--provider", default="deepseek", help="Provider for live benchmark runs.") parser.add_argument("--model", help="Model name for live benchmark runs.") parser.add_argument("--base-url", help="Base URL for live benchmark runs.") parser.add_argument("--api-key-env", help="Environment variable containing the API key.") parser.add_argument("--timeout", type=int, default=120, help="LLM request timeout.") parser.add_argument("--temperature", type=float, default=0.0, help="Sampling temperature.") parser.add_argument("--repeat", type=int, default=1, help="Repeat count per task/variant.") parser.add_argument("--max-steps", type=int, help="Override task max steps.") parser.add_argument( "--enable-adaptive-replanning", action="store_true", help="Enable deterministic error-signal-aware replanning. Default is off.", ) parser.add_argument( "--max-replans", type=int, default=-1, help="Maximum replans when adaptive replanning is enabled; -1 means unlimited.", ) parser.add_argument( "--cost-per-1k-tokens", type=float, default=0.0, help="Estimated provider cost per 1K tokens for local economics reports.", ) parser.add_argument( "--declare-allowed-files", action="store_true", help=( "Append each task's allowed_changed_files to the prompt handed to the " "agent. Default is off, because existing baselines were run without it. " "Does not change the suite signature, so reports stay comparable." ), ) parser.add_argument("--test-timeout", type=int, default=30, help="Hidden test timeout.") parser.add_argument( "--per-test-credit", action="store_true", help=( "Advisory: also run hidden tests node-by-node and report partial " "credit (strict scoring unchanged). Default is off." ), ) parser.add_argument( "--manifest-only", type=Path, help="Write the suite manifest JSON to this path and exit (keyless, no runs).", ) parser.add_argument( "--keep-workspaces", action="store_true", help="Keep temporary workspaces and include their paths in the report.", ) parser.add_argument("--workspace-root", help="Directory for kept workspaces.") parser.add_argument("--trace-dir", help="Directory for per-run JSONL traces.") parser.add_argument( "--show-agent-output", action="store_true", help="Show live agent stdout during benchmark runs.", ) return parser.parse_args(argv) def main(argv: Any = None) -> int: args = parse_args(argv) if args.max_replans < -1: print("--max-replans must be -1 or greater.", file=sys.stderr) return 2 validation_error = _validate_feature_args(args) if validation_error: print(validation_error, file=sys.stderr) return 2 if args.manifest_only: tasks = get_benchmark_tasks(args.suite) manifest = build_benchmark_manifest( suite=args.suite, tasks=tasks, variants=BENCH_VARIANTS, ) args.manifest_only.parent.mkdir(parents=True, exist_ok=True) args.manifest_only.write_text( json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8" ) print(f"Manifest written: {args.manifest_only} ({manifest['suite_signature']})") return 0 if args.list: listed_tasks = [task.to_public_dict() for task in get_benchmark_tasks(args.suite)] listed_variants = [variant.__dict__ for variant in BENCH_VARIANTS] print( json.dumps( {"suite": args.suite, "tasks": listed_tasks, "variants": listed_variants}, indent=2, ensure_ascii=False, ) ) return 0 variant_names = args.variant variants: list[Any] | None = None if args.all_variants: variants = BENCH_VARIANTS variant_names = None elif not variant_names: variants = DEFAULT_BENCH_VARIANTS try: report = run_benchmark_suite( suite=args.suite, task_ids=args.task, variants=variants, variant_names=variant_names, config=BenchmarkRunConfig( provider=args.provider, model=args.model, base_url=args.base_url, api_key_env=args.api_key_env, timeout=args.timeout, temperature=args.temperature, repeat=args.repeat, max_steps=args.max_steps, test_timeout=args.test_timeout, keep_workspaces=args.keep_workspaces, workspace_root=args.workspace_root, trace_dir=args.trace_dir, quiet=not args.show_agent_output, enable_adaptive_replanning=args.enable_adaptive_replanning, max_replans=args.max_replans, cost_per_1k_tokens=args.cost_per_1k_tokens, declare_allowed_files=args.declare_allowed_files, per_test_credit=args.per_test_credit, ), ) except ValueError as exc: print(str(exc), file=sys.stderr) return 2 if args.output: write_json_report(report, args.output) if args.markdown: write_markdown_report(report, args.markdown) print(json.dumps(report["summary"], indent=2, ensure_ascii=False)) return 0 def _validate_feature_args(args: argparse.Namespace) -> str: return "" if __name__ == "__main__": raise SystemExit(main())dm-code-maintenance-agent/dm_agent/benchmarks/economics.pycode · 12.4 KB · preview truncated
"""Token economics helpers for benchmark reports. The module is intentionally offline-only: it reads existing JSON benchmark reports and never calls providers, price APIs, or SWE-bench infrastructure. """ from __future__ import annotations import argparse import json from collections.abc import Iterable, Sequence from dataclasses import dataclass from pathlib import Path from typing import Any @dataclass(frozen=True) class EconomicsEntry: """One report-level cost/performance row.""" label: str suite: str provider: str model: str total_runs: int successes: int pass_rate: float pass_rate_ci_95: dict[str, float] total_estimated_tokens: int cost_per_1k_tokens: float | None estimated_cost_usd: float | None cost_per_success_usd: float | None avg_tokens_per_run: float tokens_per_success: float | None def to_dict(self) -> dict[str, Any]: return { "label": self.label, "suite": self.suite, "provider": self.provider, "model": self.model, "total_runs": self.total_runs, "successes": self.successes, "pass_rate": self.pass_rate, "pass_rate_ci_95": self.pass_rate_ci_95, "total_estimated_tokens": self.total_estimated_tokens, "cost_per_1k_tokens": self.cost_per_1k_tokens, "estimated_cost_usd": self.estimated_cost_usd, "cost_per_success_usd": self.cost_per_success_usd, "avg_tokens_per_run": self.avg_tokens_per_run, "tokens_per_success": self.tokens_per_success, } def summarize_report( report: dict[str, Any], *, label: str = "", cost_per_1k_tokens: float | None = None, ) -> EconomicsEntry: """Summarize one benchmark report into a cost/performance row.""" suite = str(report.get("suite") or report.get("mode") or "unknown") summary = report.get("summary") or {} results = list(report.get("results") or []) total_runs = int(summary.get("total_runs") or summary.get("total") or len(results)) successes = _success_count(report, summary, total_runs) pass_rate = _safe_rate(successes, total_runs) pass_rate_ci = _pass_rate_ci(summary, successes, total_runs) total_tokens = _total_tokens(results, summary) configured_cost = _resolve_cost_per_1k(report, cost_per_1k_tokens) total_cost = _total_cost(results, total_tokens, configured_cost) cost_per_success = (total_cost / successes) if total_cost is not None and successes else None tokens_per_success = (total_tokens / successes) if successes else None return EconomicsEntry( label=label or _default_label(report), suite=suite, provider=str(report.get("provider") or ""), model=str(report.get("model") or ""), total_runs=total_runs, successes=successes, pass_rate=pass_rate, pass_rate_ci_95=pass_rate_ci, total_estimated_tokens=total_tokens, cost_per_1k_tokens=configured_cost, estimated_cost_usd=total_cost, cost_per_success_usd=cost_per_success, avg_tokens_per_run=(total_tokens / total_runs) if total_runs else 0.0, tokens_per_success=tokens_per_success, ) def build_economics_report( reports: Sequence[dict[str, Any]], *, labels: Sequence[str] | None = None, cost_per_1k_tokens: float | None = None, ) -> dict[str, Any]: """Build a deterministic economics report from existing benchmark JSON payloads.""" labels = labels or [] entries = [ summarize_report( report, label=labels[index] if index < len(labels) else "", cost_per_1k_tokens=cost_per_1k_tokens, ) for index, report in enumerate(reports) ] ranked = sorted( entries, key=lambda item: ( item.cost_per_success_usd is None, item.cost_per_success_usd if item.cost_per_success_usd is not None else float("inf"), -item.pass_rate, item.label, ), ) manifest_guard = _manifest_guard(reports) return { "mode": "benchmark_economics", "summary": { "reports": len(entries), "best_cost_per_success": ( ranked[0].label if ranked and ranked[0].cost_per_success_usd is not None else "" ), "best_pass_rate": max((entry.pass_rate for entry in entries), default=0.0), "manifest_guard": manifest_guard, }, "entries": [entry.to_dict() for entry in entries], "ranking": [entry.label for entry in ranked], } def render_markdown(report: dict[str, Any]) -> str: """Render :func:`build_economics_report` output as Markdown.""" lines = [ "# Benchmark Token Economics", "", "This report is generated from existing benchmark JSON files. It does not run " "live models, SWE-bench, or external price lookups.", "", ] manifest_guard = (report.get("summary") or {}).get("manifest_guard") or {} if manifest_guard.get("warning"): lines.extend( [ "> Warning: source reports have different benchmark suite signatures. " "Cost and pass-rate rankings may not be comparable.", "", ] ) lines.extend( [ "| Label | Suite | Provider | Model | Pass rate (95% CI) | Runs | Avg tokens | Tokens/success | Estimated cost | Cost/success |", "| --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |", ] ) for entry in report.get("entries", []): lines.append( "| {label} | {suite} | {provider} | {model} | {pass_rate} | {total_runs} | " "{avg_tokens_per_run:.0f} | {tokens_per_success} | {estimated_cost} | " "{cost_per_success} |".format( label=entry.get("label", ""), suite=entry.get("suite", ""), provider=entry.get("provider", ""), model=entry.get("model", ""), pass_rate=_format_rate_with_ci( entry.get("pass_rate"), entry.get("pass_rate_ci_95"), ), total_runs=int(entry.get("total_runs", 0)), avg_tokens_per_run=float(entry.get("avg_tokens_per_run", 0.0)), tokens_per_success=_format_number(entry.get("tokens_per_success"), decimals=0), estimated_cost=_format_usd(entry.get("estimated_cost_usd")), cost_per_success=_format_usd(entry.get("cost_per_success_usd")), ) ) ranking = report.get("ranking") or [] if ranking: lines.extend(["", "## Cost-per-success ranking", ""]) for index, label in enumerate(ranking, start=1): lines.append(f"{index}. `{label}`") return "\n".join(lines) + "\n" def load_json_reports(paths: Iterable[Path]) -> list[dict[str, Any]]: reports = [] for path in paths: with path.open("r", encoding="utf-8") as handle: reports.append(json.load(handle)) return reports def write_economics_report(report: dict[str, Any], *, json_path: Path, markdown_path: Path) -> None: json_path.parent.mkdir(parents=True, exist_ok=True) markdown_path.parent.mkdir(parents=True, exist_ok=True) json_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") markdown_path.write_text(render_markdown(report), encoding="utf-8") def parse_args(argv: Any = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Summarize token economics from reports.") parser.add_argument("reports", nargs="+", type=Path, help="Benchmark JSON report paths.") parser.add_argument("--label", action="append", help="Optional label for each report.") parser.add_argument("--output-json", type=Path, default=Path("bench_reports/economics.json")) parser.add_argument("--output-md", type=Path, default=Path("bench_reports/economics.md")) parser.add_argument( "--cost-per-1k-tokens", type=float, help="Override report prices with one explicit USD cost per 1K tokens.", ) return parser.parse_args(argv) def main(argv: Any = None) -> int: args = parse_args(argv) report = build_economics_report( load_json_reports(args.reports), labels=args.label, cost_per_1k_tokens=args.cost_per_1k_tokens, ) write_economics_report(report, json_path=args.output_json, markdown_path=args.output_md) print(render_markdown(report)) return 0 def _success_count(report: dict[str, Any], summary: dict[str, Any], total_runs: int) -> int: if "resolved" in summary: return int(summary.get("resolved") or 0) if "overall_pass_rate" in summary: return round(float(summary.get("overall_pass_rate") or 0.0) * total_runs) return sum(1 for result in report.get("results", []) if result.get("success")) def _total_tokens(results: Sequence[dict[str, Any]], summary: dict[str, Any]) -> int: if "total_estimated_tokens" in summary: return int(summary.get("total_estimated_tokens") or 0) return sum(int(result.get("estimated_tokens") or 0) for result in results) def _resolve_cost_per_1k(report: dict[str, Any], override: float | None) -> float | None: if override is not None: return override economics = report.get("token_economics") or {} value = economics.get("cost_per_1k_tokens") if value is None: return None value = float(value) return value if value > 0 else None def _pass_rate_ci( summary: dict[str, Any], successes: int, total_runs: int, ) -> dict[str, float]: interval = summary.get("overall_pass_rate_ci_95") if isinstance(interval, dict) and {"low", "high"} <= set(interval): return { "low": float(interval.get("low", 0.0)), "high": float(interval.get("high", 0.0)), } return _wilson_interval(successes, total_runs) def _total_cost( results: Sequence[dict[str, Any]], total_tokens: int, cost_per_1k_tokens: float | None, ) -> float | None: explicit = sum(float(result.get("estimated_cost_usd") or 0.0) for result in results) if explicit > 0: return explicit if cost_per_1k_tokens is None: return None return (total_tokens / 1000.0) * cost_per_1k_tokens def _default_label(report: dict[str, Any]) -> str: provider = str(report.get("provider") or "unknown") model = str(report.get("model") or "unknown") suite = str(report.get("suite") or report.get("mode") or "benchmark") return f"{suite}:{provider}:{model}" def _manifest_guard(reports: Sequence[dict[str, Any]]) -> dict[str, Any]: signatures: dict[str, list[str]] = {} missing = [] for index, report in enumerate(reports): label = _default_label(report) or f"report-{index + 1}" manifest = report.get("manifest") or {} signature = "" if isinstance(manifest, dict): signature = str(manifest.get("suite_signature") or "") if signature: signatures.setdefault(signature, []).append(label) else: missing.append(label) return { "warning": len(signatures) > 1, "suite_signature_count": len(signatures), "suite_signatures": signatures, "missing_suite_signature": missing, } def _safe_rate(successes: int, total: int) -> float: return (successes / total) if total else 0.0 def _wilson_interval(successes: int, total: int, *, z: float = 1.96) -> dict[str, float]: if total <= 0: return {"low": 0.0, "high": 0.0} phat = successes / total denominator = 1 + (z * z / total) centre = phat + (z * z / (2 * total)) margin = z * ((phat * (1 - phat) + (z * z / (4 * total))) / total) ** 0.5 return { "low": max(0.0, (centre - margin) / denominator),dm-code-maintenance-agent/dm_agent/benchmarks/manifest_diff.pycode · 6.5 KB
"""Offline diff for benchmark report manifests. The command compares existing benchmark JSON files only. It never runs benchmarks, models, verifiers, or network calls. """ from __future__ import annotations import argparse import json import sys from dataclasses import dataclass from pathlib import Path from typing import Any @dataclass(frozen=True) class ManifestDiff: """Structured comparison of two benchmark manifests.""" compatible: bool suite_signature_match: bool suite_match: bool variant_names_match: bool missing_in_right: list[str] missing_in_left: list[str] changed_fingerprints: list[str] left_signature: str right_signature: str left_suite: str right_suite: str def to_dict(self) -> dict[str, Any]: return { "compatible": self.compatible, "suite_signature_match": self.suite_signature_match, "suite_match": self.suite_match, "variant_names_match": self.variant_names_match, "missing_in_right": self.missing_in_right, "missing_in_left": self.missing_in_left, "changed_fingerprints": self.changed_fingerprints, "left_signature": self.left_signature, "right_signature": self.right_signature, "left_suite": self.left_suite, "right_suite": self.right_suite, } def diff_report_manifests( left_report: dict[str, Any], right_report: dict[str, Any] ) -> ManifestDiff: """Compare the manifest blocks of two benchmark reports.""" left = _manifest(left_report) right = _manifest(right_report) left_fingerprints = _task_fingerprints(left) right_fingerprints = _task_fingerprints(right) left_tasks = set(left_fingerprints) right_tasks = set(right_fingerprints) shared_tasks = left_tasks & right_tasks changed = sorted( task_id for task_id in shared_tasks if left_fingerprints.get(task_id) != right_fingerprints.get(task_id) ) suite_signature_match = _signature(left) == _signature(right) and bool(_signature(left)) suite_match = str(left.get("suite", "")) == str(right.get("suite", "")) variant_names_match = _variant_names(left) == _variant_names(right) missing_in_right = sorted(left_tasks - right_tasks) missing_in_left = sorted(right_tasks - left_tasks) compatible = ( suite_signature_match and suite_match and variant_names_match and not missing_in_right and not missing_in_left and not changed ) return ManifestDiff( compatible=compatible, suite_signature_match=suite_signature_match, suite_match=suite_match, variant_names_match=variant_names_match, missing_in_right=missing_in_right, missing_in_left=missing_in_left, changed_fingerprints=changed, left_signature=_signature(left), right_signature=_signature(right), left_suite=str(left.get("suite", "")), right_suite=str(right.get("suite", "")), ) def render_markdown( diff: ManifestDiff, *, left_label: str = "left", right_label: str = "right" ) -> str: """Render a human-readable manifest diff.""" status = "compatible" if diff.compatible else "different" lines = [ "# Benchmark Manifest Diff", "", f"- Status: `{status}`", f"- Left: `{left_label}`", f"- Right: `{right_label}`", f"- Suite: `{diff.left_suite}` vs `{diff.right_suite}`", f"- Suite signature: `{diff.left_signature or '<missing>'}` vs " f"`{diff.right_signature or '<missing>'}`", f"- Suite signature match: `{diff.suite_signature_match}`", f"- Variant names match: `{diff.variant_names_match}`", "", "## Task Fingerprints", "", ] if diff.compatible: lines.append("Task fingerprints and suite signatures match.") else: lines.extend( [ f"- Missing in right: `{', '.join(diff.missing_in_right) or 'none'}`", f"- Missing in left: `{', '.join(diff.missing_in_left) or 'none'}`", f"- Changed fingerprints: `{', '.join(diff.changed_fingerprints) or 'none'}`", ] ) lines.append("") return "\n".join(lines) def load_json_report(path: Path) -> dict[str, Any]: with path.open("r", encoding="utf-8") as handle: payload = json.load(handle) if not isinstance(payload, dict): raise ValueError(f"{path} is not a JSON object report") return payload def parse_args(argv: Any = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Compare benchmark report manifests offline.") parser.add_argument("left", type=Path, help="Baseline benchmark JSON report.") parser.add_argument("right", type=Path, help="Comparison benchmark JSON report.") parser.add_argument( "--json", action="store_true", help="Print machine-readable JSON instead of Markdown.", ) return parser.parse_args(argv) def main(argv: Any = None) -> int: args = parse_args(argv) try: left = load_json_report(args.left) right = load_json_report(args.right) diff = diff_report_manifests(left, right) except (OSError, ValueError, json.JSONDecodeError) as exc: print(f"Failed to diff benchmark manifests: {exc}", file=sys.stderr) return 2 if args.json: print(json.dumps(diff.to_dict(), indent=2, ensure_ascii=False)) else: print(render_markdown(diff, left_label=str(args.left), right_label=str(args.right))) return 0 if diff.compatible else 1 def _manifest(report: dict[str, Any]) -> dict[str, Any]: manifest = report.get("manifest") if isinstance(manifest, dict): return manifest # Bare manifest files (from `dm-agent-bench --manifest-only`) carry the # fields at top level instead of under a "manifest" key. if "task_fingerprints" in report or "suite_signature" in report: return report return {} def _task_fingerprints(manifest: dict[str, Any]) -> dict[str, str]: fingerprints = manifest.get("task_fingerprints") or {} if not isinstance(fingerprints, dict): return {} return {str(task_id): str(fingerprint) for task_id, fingerprint in fingerprints.items()} def _variant_names(manifest: dict[str, Any]) -> list[str]: return sorted(str(name) for name in manifest.get("variant_names") or []) def _signature(manifest: dict[str, Any]) -> str: return str(manifest.get("suite_signature") or "") if __name__ == "__main__": raise SystemExit(main())dm-code-maintenance-agent/dm_agent/benchmarks/models.pycode · 6.3 KB
"""Data models for coding-agent benchmarks.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Any # 判分强制的改动范围约束,`BenchmarkTask.scoped_prompt` 追加在 prompt 末尾。 # # 三处措辞是照着实际失败样本写的,改动前先看 devlog 36: # - 说明「即使实现完全正确也判失败」——对齐 `_score_run` 的真实行为, # baseline 里 8 道违规题的隐藏测试全部是通过的。 # - 显式点名「新建文件同样算」——Claude 那一轮有一题栽在新建 conftest.py。 # - 显式放行读操作——否则约束会误伤探索步骤。 # 位置必须在 MAINTENANCE_PROMPT_SUFFIX 之后:后者有一句 "update tests when the task # asks for a regression test",会反向鼓励 agent 去动测试文件,靠末尾指令压过它。 SCOPE_CONSTRAINT_TEMPLATE = ( "\n\nSCOPE CONSTRAINT (enforced by the grader): you may only create or modify " "these files: {allowed}. Changing any other file (including creating a new one, " "and including test files not in that list) fails this task even if your " "implementation is completely correct. Reading any file is unrestricted; the " "limit applies to writes only." ) @dataclass(frozen=True) class BenchmarkTask: """One isolated coding task scored by hidden tests.""" task_id: str name: str prompt: str setup_files: dict[str, str] hidden_files: dict[str, str] visible_test_command: list[str] = field( default_factory=lambda: ["{python}", "-m", "pytest", "-q"] ) hidden_test_command: list[str] = field( default_factory=lambda: ["{python}", "-m", "pytest", "-q"] ) max_steps: int = 14 tags: list[str] = field(default_factory=list) allowed_changed_files: list[str] = field(default_factory=list) required_changed_files: list[str] = field(default_factory=list) def to_public_dict(self) -> dict[str, Any]: return { "task_id": self.task_id, "name": self.name, "prompt": self.prompt, "setup_files": sorted(self.setup_files), "hidden_test_count": len(self.hidden_files), "visible_test_command": self.visible_test_command, "hidden_test_command": self.hidden_test_command, "max_steps": self.max_steps, "tags": self.tags, "allowed_changed_files": self.allowed_changed_files, "required_changed_files": self.required_changed_files, } def scoped_prompt(self) -> str: """prompt 追加改动范围约束;无 ``allowed_changed_files`` 时逐字返回原 prompt。 这是**派生视图**,不改 ``self.prompt``——``benchmark_task_fingerprint`` 读的是 字段本身,所以启用约束声明不会动 ``suite_signature``,两侧报告仍可直接 ``dm-agent-score-diff``。这条不变量由 ``tests/test_coding_benchmarks.py`` 守着。 逐题列出实际允许的文件,不能简化成「不要改测试」:有 4 道题的可改范围里 本来就含 ``tests/``(如 ``retry_regression_tests``),一刀切会把它们判错。 """ if not self.allowed_changed_files: return self.prompt allowed = ", ".join(f"`{path}`" for path in self.allowed_changed_files) return self.prompt + SCOPE_CONSTRAINT_TEMPLATE.format(allowed=allowed) @dataclass(frozen=True) class BenchmarkVariant: name: str enable_planning: bool = True enable_skills: bool = True enable_compression: bool = True @dataclass(frozen=True) class BenchmarkRunConfig: provider: str = "deepseek" model: str | None = None base_url: str | None = None api_key_env: str | None = None timeout: int = 120 temperature: float = 0.0 repeat: int = 1 max_steps: int | None = None test_timeout: int = 30 keep_workspaces: bool = False workspace_root: str | None = None trace_dir: str | None = None quiet: bool = True enable_adaptive_replanning: bool = False max_replans: int = -1 cost_per_1k_tokens: float = 0.0 # 把每题的 allowed_changed_files 声明进 prompt。默认关闭:既有 baseline 是在 # 「agent 看不到约束」下跑的,开着会让两侧不可比。见 devlog 36。 declare_allowed_files: bool = False # Advisory: run hidden tests node-by-node for partial credit (score unchanged). per_test_credit: bool = False @dataclass(frozen=True) class CommandResult: command: list[str] returncode: int stdout: str stderr: str duration_seconds: float def to_dict(self) -> dict[str, Any]: return { "command": self.command, "returncode": self.returncode, "stdout": self.stdout, "stderr": self.stderr, "duration_seconds": self.duration_seconds, } @dataclass(frozen=True) class CodingBenchResult: task_id: str task_name: str variant: str success: bool failure_reason: str final_answer: str actions: list[str] steps_count: int tool_calls: int duration_seconds: float prompt_chars: int completion_chars: int estimated_tokens: int estimated_cost_usd: float request_count: int metadata: dict[str, Any] hidden_test: CommandResult changed_files: list[str] = field(default_factory=list) workspace_path: str = "" def to_dict(self) -> dict[str, Any]: return { "task_id": self.task_id, "task_name": self.task_name, "variant": self.variant, "success": self.success, "failure_reason": self.failure_reason, "final_answer": self.final_answer, "actions": self.actions, "steps_count": self.steps_count, "tool_calls": self.tool_calls, "duration_seconds": self.duration_seconds, "prompt_chars": self.prompt_chars, "completion_chars": self.completion_chars, "estimated_tokens": self.estimated_tokens, "estimated_cost_usd": self.estimated_cost_usd, "request_count": self.request_count, "metadata": self.metadata, "hidden_test": self.hidden_test.to_dict(), "changed_files": self.changed_files, "workspace_path": self.workspace_path, }dm-code-maintenance-agent/dm_agent/benchmarks/runner.pycode · 33.9 KB · preview truncated
"""Runner for hidden-test coding benchmarks.""" from __future__ import annotations import hashlib import json import os import statistics import subprocess import sys import tempfile import time from collections.abc import Iterable, Sequence from contextlib import contextmanager, redirect_stdout from io import StringIO from pathlib import Path from typing import Any, cast from dm_agent.clients.llm_factory import PROVIDER_DEFAULTS, create_llm_client from dm_agent.core import ReactAgent from dm_agent.evals.real_runner import PROVIDER_API_KEY_ENV, UsageTrackingClient from dm_agent.skills import SkillManager from dm_agent.tools import default_tools from dm_agent.tracing import TraceWriter, analyze_events, load_trace_events from .models import ( BenchmarkRunConfig, BenchmarkTask, BenchmarkVariant, CodingBenchResult, CommandResult, ) from .tasks import get_benchmark_tasks BENCH_VARIANTS: list[BenchmarkVariant] = [ BenchmarkVariant("full", True, True, True), BenchmarkVariant("no_planning", False, True, True), BenchmarkVariant("no_skills", True, False, True), BenchmarkVariant("no_compression", True, True, False), ] DEFAULT_BENCH_VARIANTS: list[BenchmarkVariant] = [BENCH_VARIANTS[0]] @contextmanager def chdir(path: Path): previous = Path.cwd() os.chdir(path) try: yield finally: os.chdir(previous) def run_benchmark_suite( *, suite: str = "coding", tasks: Sequence[BenchmarkTask] | None = None, variants: Sequence[BenchmarkVariant] | None = None, task_ids: Iterable[str] | None = None, variant_names: Iterable[str] | None = None, config: BenchmarkRunConfig | None = None, ) -> dict[str, Any]: config = config or BenchmarkRunConfig() selected_tasks = list(tasks or get_benchmark_tasks(suite, task_ids)) selected_variants = list(variants or DEFAULT_BENCH_VARIANTS) if task_ids and tasks is not None: allowed_tasks = set(task_ids) selected_tasks = [task for task in selected_tasks if task.task_id in allowed_tasks] if variant_names: allowed_variants = set(variant_names) selected_variants = [ variant for variant in BENCH_VARIANTS if variant.name in allowed_variants ] if config.repeat < 1: raise ValueError("repeat must be at least 1") _validate_benchmark_config(config) if not selected_tasks: raise ValueError("no benchmark tasks selected") if not selected_variants: raise ValueError("no benchmark variants selected") results: list[CodingBenchResult] = [] for repeat_index in range(config.repeat): for variant in selected_variants: for task in selected_tasks: results.append( run_benchmark_task( task, variant, config, repeat_index=repeat_index, suite=suite, ) ) provider = config.provider.lower() defaults = PROVIDER_DEFAULTS.get(provider, {}) return { "mode": f"{suite}_benchmark", "suite": suite, "provider": provider, "model": config.model or defaults.get("model"), "base_url": config.base_url or defaults.get("base_url"), "repeat": config.repeat, "adaptive_replanning": { "enabled": config.enable_adaptive_replanning, "max_replans": config.max_replans, }, # 报告要能自证是哪一组:约束声明只影响传给 agent 的 prompt,不进 manifest, # 所以两组报告的 suite_signature 相同、可直接 score-diff——但必须看得出区别。 "prompt_policy": { "declare_allowed_files": config.declare_allowed_files, }, "token_economics": { "cost_per_1k_tokens": config.cost_per_1k_tokens, }, "summary": summarize_benchmark_results(results, tasks=selected_tasks), "manifest": build_benchmark_manifest( suite=suite, tasks=selected_tasks, variants=selected_variants, ), "results": [result.to_dict() for result in results], "tasks": [task.to_public_dict() for task in selected_tasks], "variants": [variant.__dict__ for variant in selected_variants], } def run_benchmark_task( task: BenchmarkTask, variant: BenchmarkVariant, config: BenchmarkRunConfig, *, repeat_index: int = 0, suite: str = "coding", ) -> CodingBenchResult: _validate_benchmark_config(config) if config.keep_workspaces: root = Path(config.workspace_root) if config.workspace_root else None if root: root.mkdir(parents=True, exist_ok=True) workspace = Path( tempfile.mkdtemp( prefix=f"dm-agent-bench-{task.task_id}-", dir=str(root) if root else None ) ) return _run_benchmark_task_in_workspace( task, variant, config, workspace, repeat_index=repeat_index, cleanup=False, suite=suite, ) with tempfile.TemporaryDirectory(prefix=f"dm-agent-bench-{task.task_id}-") as tmp: return _run_benchmark_task_in_workspace( task, variant, config, Path(tmp), repeat_index=repeat_index, cleanup=True, suite=suite, ) def prepare_workspace( task: BenchmarkTask, workspace: Path, *, include_hidden: bool = False, ) -> None: _write_files(workspace, task.setup_files) if include_hidden: _write_files(workspace, task.hidden_files) def run_hidden_tests(task: BenchmarkTask, workspace: Path, *, timeout: int = 30) -> CommandResult: return run_command(task.hidden_test_command, workspace, timeout=timeout) def run_hidden_tests_per_node( task: BenchmarkTask, workspace: Path, *, timeout: int = 30 ) -> dict[str, Any] | None: """Advisory per-test partial credit for hidden tests. Mirrors the SWE-bench verifier's node-level pattern (collect-only then run each node) without importing the swebench package. Returns None when collection fails so callers fall back to the binary suite result; the strict pass/fail score is never affected. """ test_paths = sorted( path for path in task.hidden_files if path.endswith(".py") and "test" in Path(path).name ) if not test_paths: return None collect = run_command( ["{python}", "-m", "pytest", "--collect-only", "-q", *test_paths], workspace, timeout=timeout, ) if collect.returncode != 0: return None node_ids = [ line.strip() for line in collect.stdout.splitlines() if "::" in line and not line.strip().startswith("=") ] if not node_ids: return None nodes: list[dict[str, Any]] = [] passed = 0 for node_id in node_ids: result = run_command( ["{python}", "-m", "pytest", node_id, "-q", "--no-header", "--tb=short"], workspace, timeout=timeout, ) node_passed = result.returncode == 0 passed += int(node_passed) nodes.append( { "node_id": node_id, "passed": node_passed, "returncode": result.returncode, } ) return { "total": len(node_ids), "passed": passed, "pass_fraction": passed / len(node_ids), "nodes": nodes, } def run_command(command: Sequence[str], cwd: Path, *, timeout: int = 30) -> CommandResult: started_at = time.perf_counter() resolved = _resolve_command(command) try: completed = subprocess.run( resolved, cwd=cwd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, ) return CommandResult( command=resolved, returncode=completed.returncode, stdout=_tail(completed.stdout), stderr=_tail(completed.stderr), duration_seconds=time.perf_counter() - started_at, ) except subprocess.TimeoutExpired as exc: # 上面以 text=True 启动子进程,超时异常携带的 stdout/stderr 运行时必为 str; # typeshed 对 TimeoutExpired 统一标注为 bytes | None,故此处用 cast 声明实际类型 # (cast 是运行时空操作,不改变任何行为)。 timed_out_stdout = cast("str | None", exc.stdout) timed_out_stderr = cast("str | None", exc.stderr) return CommandResult( command=resolved, returncode=124, stdout=_tail(timed_out_stdout or ""), stderr=_tail((timed_out_stderr or "") + f"\nCommand timed out after {timeout}s"), duration_seconds=time.perf_counter() - started_at, ) BENCH_RECOVERY_SIGNAL_KEYS = ( "parse_repair_count", "parse_error_count", "unknown_tool_count", "argument_error_count", "tool_error_count", "replan_count", "edit_guard_block_count", ) def _bench_had_failure_signal(metadata: dict[str, Any]) -> bool: return any(int(metadata.get(key, 0) or 0) > 0 for key in BENCH_RECOVERY_SIGNAL_KEYS) def _repeat_stability(group: Sequence[CodingBenchResult]) -> dict[str, Any] | None: """Aggregate per-task pass stability across --repeat runs (None if single-run).""" repeat_indexes = {result.metadata.get("repeat_index") for result in group} repeat_indexes.discard(None) if len(repeat_indexes) < 2: return None by_task: dict[str, list[bool]] = {} for result in group: by_task.setdefault(result.task_id, []).append(bool(result.success)) per_task: dict[str, Any] = {} for task_id, passes in sorted(by_task.items()): per_task[task_id] = { "runs": len(passes), "passes": sum(passes), "pass_at_k": any(passes), "pass_pow_k": all(passes), "per_repeat_pass": list(passes), } task_rates = [entry["passes"] / entry["runs"] for entry in per_task.values()] return { "repeats": len(repeat_indexes), "per_task": per_task, "pass_at_k_rate": ( sum(1 for entry in per_task.values() if entry["pass_at_k"]) / len(per_task) ), "pass_pow_k_rate": ( sum(1 for entry in per_task.values() if entry["pass_pow_k"]) / len(per_task) ), "task_pass_rate_stddev": (statistics.pstdev(task_rates) if len(task_rates) > 1 else 0.0), } def summarize_benchmark_results( results: Sequence[CodingBenchResult], *, tasks: Sequence[BenchmarkTask] | None = None, ) -> dict[str, Any]: by_variant: dict[str, list[CodingBenchResult]] = {} for result in results: by_variant.setdefault(result.variant, []).append(result) variants: dict[str, Any] = {} for name, group in by_variant.items(): successes = [result for result in group if result.success] hidden_passes = [result for result in group if result.hidden_test.returncode == 0] completed = [result for result in group if result.metadata.get("status") == "success"] runs_with_failures = [ result for result in group if _bench_had_failure_signal(result.metadata) ] recovered_runs = [result for result in runs_with_failures if result.success] variants[name] = { "tasks": len(group), "successes": len(successes), "pass_rate": len(successes) / len(group) if group else 0.0, "pass_rate_ci_95": _wilson_interval(len(successes), len(group)), "hidden_test_pass_rate": len(hidden_passes) / len(group) if group else 0.0, "hidden_test_pass_rate_ci_95": _wilson_interval(len(hidden_passes), len(group)), "agent_completion_rate": len(completed) / len(group) if group else 0.0, "agent_completion_rate_ci_95": _wilson_interval(len(completed), len(group)dm-code-maintenance-agent/dm_agent/benchmarks/score_diff.pycode · 11.1 KB
"""离线对比两份 benchmark 报告:分数差、逐题翻转、token 成本。 这是「改了策略之后到底有没有变好」的判据。只读已有的 JSON 报告, 不跑 benchmark、不调模型、不联网——和 ``manifest_diff`` 同样是纯离线工具。 三条设计约定: - **逐题翻转比总分更有指导性**。总分从 0.46 变 0.54 只说明「多过了一题」, 而「哪题修好了、哪题跑坏了」才指向下一步该看什么。回归(原本过、现在挂) 永远单独列出,即使总分上升。 - **噪声口径直接印在输出里**。一题翻转对应多少百分点由题数决定(30 题下约 3.3), 不写出来的话这个分会骗人——看到���数上涨会以为是改进,其实是一两题的抖动。 - **manifest 不兼容时拒绝比较**。两份报告如果任务集不同,分数差没有意义。 """ from __future__ import annotations import argparse import json import sys from dataclasses import dataclass, field from pathlib import Path from typing import Any # 低于这个翻转数的差异不构成证据(见模块文档第二条)。 NOISE_FLIP_THRESHOLD = 2 @dataclass(frozen=True) class TaskFlip: """一道题在两份报告间的 pass/fail 变化。""" task_id: str variant: str before: bool after: bool @property def is_fix(self) -> bool: return self.after and not self.before @property def is_regression(self) -> bool: return self.before and not self.after @dataclass(frozen=True) class ScoreComparison: """两份 benchmark 报告的结构化对比。""" comparable: bool incomparable_reason: str left_label: str right_label: str left_pass_rate: float right_pass_rate: float left_passed: int right_passed: int total: int fixes: list[TaskFlip] = field(default_factory=list) regressions: list[TaskFlip] = field(default_factory=list) left_tokens: int = 0 right_tokens: int = 0 left_cost_usd: float = 0.0 right_cost_usd: float = 0.0 @property def pass_rate_delta(self) -> float: return self.right_pass_rate - self.left_pass_rate @property def flip_count(self) -> int: return len(self.fixes) + len(self.regressions) @property def within_noise(self) -> bool: """翻转数低于阈值时,差异不构成证据。""" return self.flip_count < NOISE_FLIP_THRESHOLD @property def per_task_points(self) -> float: """一题翻转对应多少个百分点——噪声口径的分母。""" return 100.0 / self.total if self.total else 0.0 def to_dict(self) -> dict[str, Any]: return { "comparable": self.comparable, "incomparable_reason": self.incomparable_reason, "left_label": self.left_label, "right_label": self.right_label, "total": self.total, "left_passed": self.left_passed, "right_passed": self.right_passed, "left_pass_rate": self.left_pass_rate, "right_pass_rate": self.right_pass_rate, "pass_rate_delta": self.pass_rate_delta, "flip_count": self.flip_count, "within_noise": self.within_noise, "per_task_points": self.per_task_points, "fixes": [{"task_id": flip.task_id, "variant": flip.variant} for flip in self.fixes], "regressions": [ {"task_id": flip.task_id, "variant": flip.variant} for flip in self.regressions ], "left_tokens": self.left_tokens, "right_tokens": self.right_tokens, "left_cost_usd": self.left_cost_usd, "right_cost_usd": self.right_cost_usd, } def _outcomes(report: dict[str, Any]) -> dict[tuple[str, str], bool]: """把报告压成 ``(task_id, variant) -> success``。 同一 (task, variant) 有多次 repeat 时按「全过才算过」聚合:repeat 存在的 意义就是暴露不稳定,任意一次挂掉就不该记成通过。 """ outcomes: dict[tuple[str, str], bool] = {} for result in report.get("results", []): key = (str(result.get("task_id", "")), str(result.get("variant", ""))) success = bool(result.get("success")) outcomes[key] = success if key not in outcomes else (outcomes[key] and success) return outcomes def _manifest_signature(report: dict[str, Any]) -> str: manifest = report.get("manifest") if isinstance(manifest, dict): return str(manifest.get("suite_signature", "")) return "" def _totals(report: dict[str, Any]) -> tuple[int, float]: tokens = sum(int(r.get("estimated_tokens", 0) or 0) for r in report.get("results", [])) cost = sum(float(r.get("estimated_cost_usd", 0.0) or 0.0) for r in report.get("results", [])) return tokens, cost def compare_reports( left: dict[str, Any], right: dict[str, Any], *, left_label: str = "before", right_label: str = "after", ) -> ScoreComparison: """对比两份报告。任务集不同时返回 ``comparable=False``。""" left_outcomes = _outcomes(left) right_outcomes = _outcomes(right) shared = sorted(set(left_outcomes) & set(right_outcomes)) reason = "" left_sig, right_sig = _manifest_signature(left), _manifest_signature(right) if left_sig and right_sig and left_sig != right_sig: reason = ( f"任务集不同(suite_signature {left_sig} vs {right_sig});" "分数差无意义,先用 dm-agent-manifest-diff 查清漂移。" ) elif not shared: reason = "两份报告没有共同的 (task_id, variant),无法对比。" left_tokens, left_cost = _totals(left) right_tokens, right_cost = _totals(right) if reason: return ScoreComparison( comparable=False, incomparable_reason=reason, left_label=left_label, right_label=right_label, left_pass_rate=0.0, right_pass_rate=0.0, left_passed=0, right_passed=0, total=0, left_tokens=left_tokens, right_tokens=right_tokens, left_cost_usd=left_cost, right_cost_usd=right_cost, ) fixes: list[TaskFlip] = [] regressions: list[TaskFlip] = [] left_passed = right_passed = 0 for task_id, variant in shared: before = left_outcomes[(task_id, variant)] after = right_outcomes[(task_id, variant)] left_passed += int(before) right_passed += int(after) if before != after: flip = TaskFlip(task_id=task_id, variant=variant, before=before, after=after) (fixes if flip.is_fix else regressions).append(flip) total = len(shared) return ScoreComparison( comparable=True, incomparable_reason="", left_label=left_label, right_label=right_label, left_pass_rate=left_passed / total, right_pass_rate=right_passed / total, left_passed=left_passed, right_passed=right_passed, total=total, fixes=fixes, regressions=regressions, left_tokens=left_tokens, right_tokens=right_tokens, left_cost_usd=left_cost, right_cost_usd=right_cost, ) def _pct(value: float) -> str: return f"{value * 100:.1f}%" def _delta_tokens(before: int, after: int) -> str: if before == 0: return f"{after:,}" ratio = (after - before) / before * 100 return f"{after:,} ({ratio:+.1f}%)" def render_markdown(comparison: ScoreComparison) -> str: lines = ["# Benchmark Score Comparison", ""] lines.append(f"- Left (`{comparison.left_label}`) vs Right (`{comparison.right_label}`)") if not comparison.comparable: lines += ["", f"**无法对比**:{comparison.incomparable_reason}", ""] return "\n".join(lines) lines += [ f"- Tasks compared: `{comparison.total}`", f"- Pass rate: `{_pct(comparison.left_pass_rate)}` → " f"`{_pct(comparison.right_pass_rate)}` " f"(**{comparison.pass_rate_delta * 100:+.1f} pts**)", f"- Passed: `{comparison.left_passed}` → `{comparison.right_passed}`", f"- Tokens: `{comparison.left_tokens:,}` → " f"`{_delta_tokens(comparison.left_tokens, comparison.right_tokens)}`", ] if comparison.left_cost_usd or comparison.right_cost_usd: lines.append( f"- Estimated cost: `${comparison.left_cost_usd:.4f}` → " f"`${comparison.right_cost_usd:.4f}`" ) lines += ["", "## Verdict", ""] if comparison.flip_count == 0: lines.append("**No task changed outcome.** 这次改动没有影响任何一道题的判定。") elif comparison.within_noise: lines.append( f"**在噪声范围内**:只有 {comparison.flip_count} 道题翻转," f"而本套件一题 = {comparison.per_task_points:.1f} 个百分点。" f"少于 {NOISE_FLIP_THRESHOLD} 题的差异**不构成策略有效的证据**——" "同一配置重跑也可能得到这个差。" ) else: lines.append( f"{comparison.flip_count} 道题翻转(一题 = {comparison.per_task_points:.1f} 个百分点)。" ) if comparison.regressions: lines.append("") lines.append( f"**注意:有 {len(comparison.regressions)} 道题从通过变为失败**," "即使总分上升也要先看这些。" ) if comparison.fixes: lines += ["", "## Fixed (fail → pass)", ""] lines += [f"- `{f.task_id}` / `{f.variant}`" for f in comparison.fixes] if comparison.regressions: lines += ["", "## Regressed (pass → fail)", ""] lines += [f"- `{f.task_id}` / `{f.variant}`" for f in comparison.regressions] lines.append("") return "\n".join(lines) def load_report(path: Path) -> dict[str, Any]: with open(path, encoding="utf-8") as handle: data = json.load(handle) if not isinstance(data, dict): raise ValueError(f"{path} is not a benchmark report object.") return data def main(argv: Any = None) -> int: parser = argparse.ArgumentParser( description="Compare two DM-Code-Agent benchmark reports (offline, no model calls)." ) parser.add_argument("left", type=Path, help="Baseline report JSON.") parser.add_argument("right", type=Path, help="New report JSON.") parser.add_argument("--json", action="store_true", help="Emit JSON instead of Markdown.") args = parser.parse_args(argv) try: left = load_report(args.left) right = load_report(args.right) except (OSError, ValueError, json.JSONDecodeError) as exc: print(f"Failed to load reports: {exc}", file=sys.stderr) return 2 comparison = compare_reports( left, right, left_label=args.left.name, right_label=args.right.name ) if args.json: print(json.dumps(comparison.to_dict(), indent=2, ensure_ascii=False)) else: print(render_markdown(comparison)) if not comparison.comparable: return 2 # 有回归就非零退出,方便挂进 CI 或脚本。 return 1 if comparison.regressions else 0 if __name__ == "__main__": raise SystemExit(main())dm-code-maintenance-agent/dm_agent/benchmarks/tasks.pytext · 91.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/cli/__init__.pycode · 9.3 KB
"""dm-agent 命令行入口。 历史上这些代码住在仓库根的顶级 main.py 里,会被 setuptools 装成 site-packages 顶层的 `main` 模块并污染用户命名空间。现按职责拆进本包: ui.py 渲染原语与只读界面(不依赖 Config) config.py Config / config.json 读写 / 高级开关解析 args.py argparse 定义与参数校验 report.py Markdown 运行报告与 git 状态 runner.py agent 装配与单任务执行 interactive.py 交互式菜单模式 依赖方向单向向下:__init__ → interactive → runner → report → config → ui。 """ from __future__ import annotations import sys from pathlib import Path from typing import Any from dm_agent import PROVIDER_DEFAULTS from dm_agent.core.checkpoint import RunCheckpoint from dm_agent.core.persistence import load_resume_state from dm_agent.extensions import ( ExtensionDiscoveryError, ProjectTrustDecision, discover_extensions, ) from dm_agent.paths import load_env_files from .args import parse_args, validate_feature_args from .config import ( Config, format_advanced_feature_status, format_missing_api_key_help, get_api_key_for_provider, load_config_from_file, resolve_advanced_features, save_config_to_file, ) from .interactive import ( configure_settings, execute_task, interactive_mode, multi_turn_conversation, print_welcome, ) from .report import collect_git_status, default_report_path, write_run_report from .runner import ( create_agent, format_agent_context_status, review_completed_run, run_conversation_stdin, run_single_task, ) from .ui import ( UI, Fore, ask_bool_setting, browse_run_steps, configure_console_encoding, create_step_callback, display_completion_screen, display_result, display_step_page, format_duration, format_run_status, format_step_input, print_header, print_menu, print_separator, show_skills, show_tools, ) __all__ = [ "PROVIDER_DEFAULTS", "UI", "Config", "Fore", "ask_bool_setting", "browse_run_steps", "collect_git_status", "configure_console_encoding", "configure_settings", "create_agent", "create_step_callback", "default_report_path", "display_completion_screen", "display_result", "display_step_page", "execute_task", "format_advanced_feature_status", "format_agent_context_status", "format_duration", "format_missing_api_key_help", "format_run_status", "format_step_input", "get_api_key_for_provider", "interactive_mode", "load_config_from_file", "main", "multi_turn_conversation", "parse_args", "print_header", "print_menu", "print_separator", "print_welcome", "resolve_advanced_features", "review_completed_run", "run_conversation_stdin", "run_single_task", "save_config_to_file", "show_skills", "show_tools", "validate_feature_args", "write_run_report", ] def main(argv: Any = None) -> int: """主入口函数""" configure_console_encoding() # 显式按 ./.env → ~/.dm_agent/.env 加载。无参数的 load_dotenv() 是从**调用方模块 # 所在目录**向上找的,全局安装后从 site-packages 往上翻,永远够不到用户的工作目录。 load_env_files() args = parse_args(argv if argv is not None else sys.argv[1:]) feature_error = validate_feature_args(args) if feature_error: print(UI.paint("[ERR] 高级功能参数无效", Fore.RED, bright=True), file=sys.stderr) print(feature_error, file=sys.stderr) return 2 try: discovery = discover_extensions( project_root=Path.cwd(), no_extensions=args.no_extensions, explicit_paths=args.extension_paths, trust_prompt=( _prompt_project_extension_trust if sys.stdin.isatty() and sys.stdout.isatty() else None ), ) except ExtensionDiscoveryError as e: print(UI.paint("[ERR] 扩展加载失败", Fore.RED, bright=True), file=sys.stderr) print(str(e), file=sys.stderr) return 2 _display_extension_diagnostics(discovery.loaded, discovery.skipped) for failure in discovery.failures: UI.status("warn", f"扩展加载失败:{failure.source}", failure.message) extension_registry = discovery.registry if extension_registry.get_provider_factory(args.provider) is None: supported = ", ".join(extension_registry.get_provider_names()) print(UI.paint("[ERR] 不支持的 LLM 提供商", Fore.RED, bright=True), file=sys.stderr) print( f"不支持的提供商: {args.provider}。支持的提供商: {supported}", file=sys.stderr, ) return 2 # 如果没有提供 API 密钥,尝试根据提供商获取 if not args.api_key: args.api_key = get_api_key_for_provider(args.provider) provider_name = args.provider.casefold() # 四家内置供应商保持原有的 API key 前置校验;扩展供应商自行决定是否需要。 if not args.api_key and provider_name in PROVIDER_DEFAULTS: print( UI.paint(f"[ERR] 缺少 {args.provider} API key", Fore.RED, bright=True), file=sys.stderr, ) print(file=sys.stderr) print(format_missing_api_key_help(args.provider), file=sys.stderr) return 2 args.api_key = args.api_key or "" # 获取提供商的默认配置 provider_defaults = PROVIDER_DEFAULTS.get(provider_name, {}) # 如果没有指定 base_url,使用提供商默认值 if not args.base_url: args.base_url = provider_defaults.get("base_url", "") # 如果模型是默认的 deepseek-chat 但提供商不是 deepseek,更新模型 if args.model == "deepseek-chat" and provider_name != "deepseek": args.model = provider_defaults.get("model", args.model) # 创建配置 config = Config( api_key=args.api_key, provider=args.provider, model=args.model, base_url=args.base_url, max_steps=args.max_steps, temperature=args.temperature, show_steps=args.show_steps, max_observation_chars=args.max_observation_chars, context_token_budget=args.context_token_budget, enable_edit_guard=args.enable_edit_guard, llm_max_retries=args.llm_max_retries, enable_adaptive_replanning=args.enable_adaptive_replanning, max_replans=args.max_replans, ) # --resume:从 checkpoint 恢复任务(任务参数可省略) resume_state: RunCheckpoint | None = None if args.resume: try: resume_state = load_resume_state(args.resume, at=args.resume_at or None) except ValueError as e: print(UI.paint("[ERR] checkpoint 加载失败", Fore.RED, bright=True), file=sys.stderr) print(str(e), file=sys.stderr) return 2 if args.task and args.task.strip() != resume_state.task.strip(): print( UI.paint("[ERR] --resume 的任务与命令行任务不一致", Fore.RED, bright=True), file=sys.stderr, ) print("省略任务参数即可沿用 checkpoint 中的原始任务。", file=sys.stderr) return 2 args.task = resume_state.task # 长驻会话模式:任务从 stdin 逐轮进来,共享同一个 agent 的对话历史。 if args.conversation_stdin: return run_conversation_stdin( config, trace_path=args.trace, trace_llm_io=args.trace_llm_io, extension_registry=extension_registry, ) # 如果提供了任务参数,直接执行任务 if args.task: return run_single_task( config, args.task, trace_path=args.trace, trace_llm_io=args.trace_llm_io, report_path=args.report, checkpoint_path=args.checkpoint, resume_state=resume_state, extension_registry=extension_registry, ) # 如果指定了交互模式或没有提供任务,进入交互式菜单 if args.interactive or not args.task: return interactive_mode(config, extension_registry) return 0 def _prompt_project_extension_trust(project_root: Path) -> ProjectTrustDecision: UI.section( "项目扩展安全确认", "项目内 .dm_agent/extensions/*.py 会以当前用户权限执行任意代码。" "仅在你已审查并信任此仓库时加载。", ) UI.status("warn", "待确认项目", str(project_root)) choice = ( UI.ask( "加载项目扩展?o=仅本次,a=始终信任,n=本次跳过,d=始终拒绝", choices=["o", "a", "n", "d"], default="n", ) .strip() .lower() ) return { "o": ProjectTrustDecision.LOAD_ONCE, "a": ProjectTrustDecision.TRUST, "d": ProjectTrustDecision.DENY, }.get(choice, ProjectTrustDecision.SKIP_ONCE) def _display_extension_diagnostics( loaded: list[str], skipped: list[str], ) -> None: external_count = max(0, len(loaded) - 1) if external_count: UI.status("ok", f"已加载 {external_count} 个外部扩展") for source in skipped: UI.status("warn", "项目扩展未加载", source)dm-code-maintenance-agent/dm_agent/cli/__main__.pycode · 0.4 KB
"""让 ``python -m dm_agent.cli`` 可用。 包内一直没有 ``__main__``,所以 ``python -m dm_agent.cli --help`` 会静默什么都不做 (CLAUDE.md 把它记成已知坑)。Web 控制台 spawn 子进程时需要一个不依赖 PATH 上是否 装了 console script 的稳定入口,顺手把这个坑补掉。 """ from __future__ import annotations import sys from . import main if __name__ == "__main__": sys.exit(main())dm-code-maintenance-agent/dm_agent/cli/args.pycode · 8.2 KB
"""命令行参数定义与校验。""" from __future__ import annotations import argparse from pathlib import Path from typing import Any from .config import load_config_from_file def validate_feature_args(args: argparse.Namespace) -> str: """Validate default-off advanced feature CLI arguments.""" if args.max_replans < -1: return "--max-replans must be -1 or greater." if args.max_observation_chars != 0 and args.max_observation_chars < 200: return "--max-observation-chars must be 0 (disabled) or at least 200." if args.context_token_budget < 0: return "--context-token-budget must be 0 (disabled) or greater." if args.llm_max_retries < 0: return "--llm-max-retries must be 0 or greater." if getattr(args, "resume_at", "") and not getattr(args, "resume", None): return "--resume-at 需要与 --resume 一起使用。" conversation_error = _validate_conversation_args(args) if conversation_error: return conversation_error checkpoint_path = getattr(args, "checkpoint", None) trace_path = getattr(args, "trace", None) if checkpoint_path and trace_path and Path(checkpoint_path) == Path(trace_path): # checkpoint 里有完整对话(含模型原始输出),trace 默认是可分享的脱敏档。 # 指向同一个文件会让脱敏档被完整历史悄悄污染,直接拒绝。 return "--trace 与 --checkpoint 不能指向同一个文件(前者默认脱敏,后者含完整对话)。" return "" def _validate_conversation_args(args: argparse.Namespace) -> str: """校验 ``--conversation-stdin`` 的四条前置条件。 这个模式让一个进程用**同一个** ``ReactAgent`` 顺序跑多轮,因此凡是会重置或 分叉对话历史的开关都与它语义冲突,宁可直接拒绝也不要跑出一个「看起来是多轮、 实际上历史被悄悄清掉」的会话。 """ if not getattr(args, "conversation_stdin", False): return "" if getattr(args, "task", None): return "--conversation-stdin 从 stdin 读取任务,不能同时给位置参数任务。" if not getattr(args, "trace", None): # 会话日志是控制台/调用方读取进展的唯一通道(子进程 stdout 只是人类可读日志)。 return "--conversation-stdin 必须配合 --trace,否则没有任何地方能读到每一轮的进展。" if getattr(args, "resume", None) or getattr(args, "resume_at", ""): return "--conversation-stdin 不支持 --resume:resume 恢复的是同一任务的中断点。" return "" def parse_args(argv: Any) -> argparse.Namespace: # 先加载配置文件中的默认值 saved_config = load_config_from_file() parser = argparse.ArgumentParser(description="运行基于 LLM 的 ReAct 智能体来完成任务描述。") parser.add_argument("task", nargs="?", help="智能体要完成的自然语言任务。") parser.add_argument( "--api-key", dest="api_key", default=None, help="API 密钥(默认使用环境变量)。", ) parser.add_argument( "--provider", default=saved_config.get("provider", "deepseek"), help="已注册的 LLM 提供商(内置 deepseek/openai/claude/gemini,默认:deepseek)。", ) parser.add_argument( "--model", default=saved_config.get("model", "deepseek-chat"), help="模型标识符(默认根据提供商选择)。", ) parser.add_argument( "--base-url", dest="base_url", default=saved_config.get("base_url"), help="API 基础 URL(可选,使用提供商默认值)。", ) parser.add_argument( "--max-steps", type=int, default=saved_config.get("max_steps", 100), help="放弃前的最大推理/工具步骤数(默认:100)。", ) parser.add_argument( "--temperature", type=float, default=saved_config.get("temperature", 0.7), help="模型的采样温度(默认:0.7)。", ) parser.add_argument( "--show-steps", action="store_true", default=saved_config.get("show_steps", False), help="打印智能体执行的中间 ReAct 步骤。", ) parser.add_argument( "--max-observation-chars", type=int, default=saved_config.get("max_observation_chars", 8000), help="单条工具观察的字符上限,超出部分截断并附分页提示;0 表示不截断(默认:8000)。", ) parser.add_argument( "--context-token-budget", type=int, default=saved_config.get("context_token_budget", 24000), help="对话历史的估算 token 预算,超限提前触发本地压缩;0 表示只按消息节奏压缩(默认:24000)。", ) parser.add_argument( "--disable-edit-guard", dest="enable_edit_guard", action="store_false", default=saved_config.get("enable_edit_guard", True), help=( "关闭 read-before-edit 守卫(默认开启:首次 edit_file 前必须读过目标文件;" "依赖行号的连续编辑在写后需重读)。" ), ) parser.add_argument( "--llm-max-retries", type=int, default=saved_config.get("llm_max_retries", 2), help="LLM 瞬时故障(超时/断连/429/5xx)的统一重试次数;0 表示不重试(默认:2)。", ) parser.add_argument( "--enable-adaptive-replanning", action="store_true", default=saved_config.get("enable_adaptive_replanning", False), help="启用基于失败信号的自适应重规划。默认关闭。", ) parser.add_argument( "--max-replans", type=int, default=saved_config.get("max_replans", -1), help="自适应重规划最多触发次数;-1 表示不限(默认:-1)。", ) parser.add_argument( "--interactive", action="store_true", help="启动交互式菜单模式。", ) parser.add_argument( "--conversation-stdin", dest="conversation_stdin", action="store_true", help=( '长驻会话模式:从 stdin 按 JSON Lines 读取任务(每行 {"task": "..."}),' "用同一个 agent 顺序执行多轮并共享对话历史;读到 EOF 退出。必须配合 --trace。" ), ) extension_group = parser.add_mutually_exclusive_group() extension_group.add_argument( "--no-extensions", action="store_true", help="关闭 entry point、用户目录、项目目录及显式文件扩展;内置能力仍保留。", ) extension_group.add_argument( "--extension", dest="extension_paths", action="append", type=Path, default=[], metavar="PATH", help="仅本次显式加载一个可信 .py 扩展;可重复指定。", ) parser.add_argument( "--trace", type=Path, help="将本次任务的结构化执行轨迹写入 JSONL 文件。", ) parser.add_argument( "--checkpoint", type=Path, help=( "每步结束后落一份 run 状态,用于崩溃后 --resume 续跑。" "*.jsonl 写成 append-only 会话日志(保留每一步的历史快照,可配合 " "--resume-at 与 dm-agent-trace fork);其他后缀写成单文件 JSON 快照。" ), ) parser.add_argument( "--resume", type=Path, help="从 --checkpoint 生成的文件恢复运行;任务参数可省略(取 checkpoint 内任务)。", ) parser.add_argument( "--resume-at", default="", metavar="ENTRY_ID", help="仅对 JSONL 会话日志有效:从该条目(或之前最近的 checkpoint 条目)恢复;支持唯一前缀。", ) parser.add_argument( "--trace-llm-io", action="store_true", help="在 trace 中包含完整 LLM 输入/输出。仅建议在私有调试时启用。", ) parser.add_argument( "--report", type=Path, help="将本次任务的人类可读运行报告写入 Markdown 文件。", ) return parser.parse_args(argv)dm-code-maintenance-agent/dm_agent/cli/config.pycode · 4.9 KB
"""运行时配置:Config 数据类、config.json 读写与高级开关解析。""" from __future__ import annotations import json import os from dataclasses import dataclass from pathlib import Path from typing import Any from dm_agent.paths import ( atomic_write_json, resolve_config_read_path, resolve_config_write_path, user_env_path, ) from .ui import UI # 缺 key 时告诉用户去哪儿领。纯展示信息,刻意不放进 clients 层的 PROVIDER_DEFAULTS—— # 那是构造客户端用的数据,不该混入面向人的文案。 PROVIDER_CONSOLE_URLS = { "deepseek": "https://platform.deepseek.com/api_keys", "openai": "https://platform.openai.com/api-keys", "claude": "https://console.anthropic.com/settings/keys", "gemini": "https://aistudio.google.com/apikey", } @dataclass class Config: """运行时配置""" api_key: str provider: str = "deepseek" model: str = "deepseek-chat" base_url: str = "https://api.deepseek.com" max_steps: int = 100 temperature: float = 0.7 show_steps: bool = False max_observation_chars: int = 8000 context_token_budget: int = 24000 enable_edit_guard: bool = True llm_max_retries: int = 2 enable_adaptive_replanning: bool = False max_replans: int = -1 def load_config_from_file() -> dict[str, Any]: """读取配置:``./config.json`` 优先,其次 ``~/.dm_agent/config.json``。 两者都不存在时返回空字典,由调用方用硬编码默认值。文件损坏时告警并降级, 绝不让一个坏配置挡住整个 CLI。 """ path = resolve_config_read_path() if path is None: return {} try: with open(path, encoding="utf-8") as f: data: dict[str, Any] = json.load(f) return data except Exception as e: UI.status("warn", "配置文件加载失败,使用默认设置", f"{path}:{e}") return {} def save_config_to_file(config: Config) -> None: """保存配置,写回**读到的那一个**文件;都不存在时落用户级。 用原子写:临时文件 → fsync → ``os.replace``,避免写到一半崩掉留下半个 JSON (下次启动就会走上面那条「加载失败」分支,用户的设置静默丢失)。 """ path = resolve_config_write_path() try: config_data = { "provider": config.provider, "model": config.model, "base_url": config.base_url, "max_steps": config.max_steps, "temperature": config.temperature, "show_steps": config.show_steps, "max_observation_chars": config.max_observation_chars, "context_token_budget": config.context_token_budget, "enable_edit_guard": config.enable_edit_guard, "llm_max_retries": config.llm_max_retries, "enable_adaptive_replanning": config.enable_adaptive_replanning, "max_replans": config.max_replans, } atomic_write_json(path, config_data) UI.status("ok", "配置已保存", str(path)) except Exception as e: UI.status("error", "配置保存失败", str(e)) def format_missing_api_key_help(provider: str) -> str: """缺 key 时的多行提示。 给的是**算出来的绝对路径**而不是「请在 .env 中配置」——全局安装后用户的 ``.env`` 该放哪不再是显然的,泛泛一句提示等于没说。 """ env_var = f"{provider.upper()}_API_KEY" setter = f"set {env_var}=sk-xxx" if os.name == "nt" else f"export {env_var}=sk-xxx" lines = [ "三种配法(任选其一):", f" 1. 环境变量 {setter}", f" 2. 用户级配置 {user_env_path()}", f" 写入 {env_var}=sk-xxx", f" 3. 项目级 {Path.cwd() / '.env'}", ] console_url = PROVIDER_CONSOLE_URLS.get(provider.casefold()) if console_url: lines.append("") lines.append(f"获取 key: {console_url}") return "\n".join(lines) def get_api_key_for_provider(provider: str) -> str | None: """根据提供商获取对应的 API 密钥""" provider_env_map = { "deepseek": "DEEPSEEK_API_KEY", "openai": "OPENAI_API_KEY", "claude": "CLAUDE_API_KEY", "gemini": "GEMINI_API_KEY", } env_var = provider_env_map.get(provider.lower()) return os.getenv(env_var) if env_var else None def resolve_advanced_features(config: Config) -> dict[str, bool]: """Return effective advanced feature switches for one agent run.""" return { "adaptive_replanning": config.enable_adaptive_replanning, } def format_advanced_feature_status(config: Config) -> str: """Compact human-readable summary of advanced feature switches.""" advanced = resolve_advanced_features(config) enabled = [ label for key, label in [ ("adaptive_replanning", "adaptive-replan"), ] if advanced[key] ] return ", ".join(enabled) if enabled else "off"dm-code-maintenance-agent/dm_agent/cli/interactive.pycode · 14.7 KB · preview truncated
"""交互式菜单模式:欢迎页、设置向导、单次任务与多轮对话循环。""" from __future__ import annotations from typing import TYPE_CHECKING from dm_agent import ( PROVIDER_DEFAULTS, LLMError, Tool, create_llm_client, default_tools, ) from dm_agent.mcp import MCPManager, load_mcp_config from dm_agent.paths import resolve_config_read_path from dm_agent.skills import SkillManager from .config import ( Config, format_advanced_feature_status, format_missing_api_key_help, get_api_key_for_provider, save_config_to_file, ) from .report import collect_git_status from .runner import ( create_agent, format_agent_context_status, review_completed_run, ) from .ui import UI, ask_bool_setting, create_step_callback, print_menu, show_skills, show_tools if TYPE_CHECKING: from dm_agent.extensions import ExtensionRegistry def print_welcome() -> None: """打印欢迎界面""" UI.banner("DM-Code-Agent", "Local-first code agent with trace, tools, skills, and memory.") # 打完整路径而不是「config.json」:全局安装后配置可能来自项目级也可能来自用户级, # 只说文件名等于没说。 source = resolve_config_read_path() if source is not None: UI.status("ok", "已加载配置文件", str(source)) else: UI.status("info", "使用默认配置", "max_steps=100 | temperature=0.7") print() def configure_settings(config: Config, extension_registry: ExtensionRegistry | None = None) -> None: """配置设置""" UI.key_values( "当前配置", [ ("Provider", config.provider), ("Model", config.model), ("Base URL", config.base_url), ("Max steps", config.max_steps), ("Temperature", config.temperature), ("Show steps", "是" if config.show_steps else "否"), ("Advanced", format_advanced_feature_status(config)), ("Max replans", config.max_replans), ], ) UI.status("info", "选择要修改的设置", "直接回车跳过") print() config_changed = False provider_names = ( extension_registry.get_provider_names() if extension_registry is not None else list(PROVIDER_DEFAULTS) ) # 修改提供商 provider_input = ( UI.ask( "LLM 提供商", choices=provider_names, default=config.provider, ) .strip() .lower() ) if provider_input and provider_input in provider_names: if provider_input != config.provider: # 尝试获取新提供商的 API 密钥 new_api_key = get_api_key_for_provider(provider_input) if provider_input in PROVIDER_DEFAULTS and not new_api_key: UI.status("error", f"未找到 {provider_input.upper()}_API_KEY") # 给完整路径而不是「请在 .env 中配置」——用户此刻正在切换供应商, # 需要知道那个文件到底该建在哪。 print(format_missing_api_key_help(provider_input)) else: config.provider = provider_input config.api_key = new_api_key or "" # 自动更新默认模型和 base_url defaults = PROVIDER_DEFAULTS.get(provider_input, {}) config.model = defaults.get("model", config.model) config.base_url = defaults.get("base_url", config.base_url) config_changed = True UI.status("ok", f"已更新提供商为 {provider_input}", "模型和 URL 已自动调整") elif provider_input and provider_input not in provider_names: UI.status("error", "无效的提供商") # 修改模型 model_input = UI.ask("模型名称", default=config.model).strip() if model_input and model_input != config.model: config.model = model_input config_changed = True UI.status("ok", f"已更新模型为 {model_input}") # 修改 Base URL base_url_input = UI.ask("Base URL", default=config.base_url).strip() if base_url_input and base_url_input != config.base_url: config.base_url = base_url_input config_changed = True UI.status("ok", f"已更新 Base URL 为 {base_url_input}") # 修改最大步骤数 try: max_steps_input = UI.ask("最大步骤数", default=str(config.max_steps)).strip() if max_steps_input: new_max_steps = int(max_steps_input) if new_max_steps > 0: if new_max_steps != config.max_steps: config.max_steps = new_max_steps config_changed = True UI.status("ok", f"已更新最大步骤数为 {new_max_steps}") else: UI.status("error", "最大步骤数必须大于 0") except ValueError: UI.status("error", "无效的数字") # 修改温度 try: temp_input = UI.ask("温度 (0.0-2.0)", default=str(config.temperature)).strip() if temp_input: new_temp = float(temp_input) if 0.0 <= new_temp <= 2.0: if new_temp != config.temperature: config.temperature = new_temp config_changed = True UI.status("ok", f"已更新温度为 {new_temp}") else: UI.status("error", "温度必须在 0.0 到 2.0 之间") except ValueError: UI.status("error", "无效的数字") # 修改显示步骤 new_show_steps = ask_bool_setting("显示步骤", config.show_steps) if new_show_steps != config.show_steps: config.show_steps = new_show_steps config_changed = True UI.status("ok", "已启用显示步骤" if config.show_steps else "已禁用显示步骤") UI.section( "高级功能", "这些能力会增加模型调用或改变恢复策略;默认关闭,建议按任务显式启用。", ) new_adaptive = ask_bool_setting("启用自适应重规划", config.enable_adaptive_replanning) if new_adaptive != config.enable_adaptive_replanning: config.enable_adaptive_replanning = new_adaptive config_changed = True if config.enable_adaptive_replanning: try: max_replans_input = UI.ask( "最大重规划次数 (-1 表示不限)", default=str(config.max_replans) ).strip() if max_replans_input: new_max_replans = int(max_replans_input) if new_max_replans >= -1: if new_max_replans != config.max_replans: config.max_replans = new_max_replans config_changed = True else: UI.status("error", "最大重规划次数必须为 -1 或更大") except ValueError: UI.status("error", "无效的数字") # 保存配置 if config_changed: print() save_choice = ( UI.ask("是否保存为永久配置?", choices=["y", "n"], default="y").strip().lower() ) if save_choice in ["", "y", "yes", "是"]: save_config_to_file(config) print() def multi_turn_conversation( config: Config, tools: list[Tool], skill_manager: SkillManager | None = None, extension_registry: ExtensionRegistry | None = None, ) -> None: """多轮对话模式""" UI.section( "多轮对话", "同一个 agent 实例会保留当前会话,并使用本地原子记忆整理旧上下文。" "这份记忆只在当前 CLI 进程内连续;输入 exit 退出,输入 reset 重置。", ) try: # 创建客户端和智能体 client = create_llm_client( provider=config.provider, api_key=config.api_key, model=config.model, base_url=config.base_url, respond_retries=config.llm_max_retries, extension_registry=extension_registry, ) step_callback = create_step_callback(config.show_steps) agent = create_agent( config, client, tools, step_callback=step_callback, skill_manager=skill_manager, extension_registry=extension_registry, ) conversation_count = 0 while True: UI.section(f"对话 {conversation_count + 1}") UI.status("info", "当前会话记忆", format_agent_context_status(agent)) task = UI.ask("请输入任务 (exit 退出,reset 重置历史)").strip() if not task: UI.status("error", "任务描述不能为空") continue if task.lower() == "exit": UI.status("info", "退出多轮对话模式") break if task.lower() == "reset": agent.reset_conversation() conversation_count = 0 UI.status("ok", "对话历史和本地记忆已重置") continue try: UI.status("run", "正在执行任务") # 执行任务 git_status_before = collect_git_status() result = agent.run(task) git_status_after = collect_git_status() conversation_count += 1 metadata = result.get("metadata", {}) context_status = ( f"{format_agent_context_status(agent)} | " f"injections={metadata.get('memory_injection_count', 0)}" ) review_completed_run( result, config=config, task=task, context_status=context_status, git_status_before=git_status_before, git_status_after=git_status_after, interactive=True, ) except LLMError as e: UI.status("error", "API 错误", str(e)) except KeyboardInterrupt: UI.status("info", "退出多轮对话模式") break except Exception as e: UI.status("error", "发生错误", str(e)) except Exception as e: UI.status("error", "初始化错误", str(e)) def execute_task( config: Config, tools: list[Tool], skill_manager: SkillManager | None = None, extension_registry: ExtensionRegistry | None = None, ) -> None: """执行任务""" UI.section("执行新任务") task = UI.ask("请输入任务描述").strip() if not task: UI.status("error", "任务描述不能为空") return try: # 创建客户端和智能体 client = create_llm_client( provider=config.provider, api_key=config.api_key, model=config.model, base_url=config.base_url, respond_retries=config.llm_max_retries, extension_registry=extension_registry, ) # 创建步骤回调函数 step_callback = create_step_callback(config.show_steps) agent = create_agent( config, client, tools, step_callback=step_callback, skill_manager=skill_manager, extension_registry=extension_registry, ) UI.status("run", "正在执行任务") # 执行任务 git_status_before = collect_git_status() result = agent.run(task) git_status_after = collect_git_status() review_completed_run( result, config=config, task=task, git_status_before=git_status_before, git_status_after=git_status_after, interactive=True, ) except LLMError as e: UI.status("error", "API 错误", str(e)) except KeyboardInterrupt: UI.status("warn", "任务已被用户中断") except Exception as e: UI.status("error", "发生错误", str(e)) def interactive_mode(config: Config, extension_registry: ExtensionRegistry | None = None) -> int: """交互式菜单模式""" print_welcome() # 初始化 MCP 管理器 mcp_config = load_mcp_config() mcp_manager = MCPManager(mcp_config) # 启动所有启用的 MCP 服务器 UI.status("run", "正在加载 MCP 服务器") started_count = mcp_manager.start_all() if started_count > 0: UI.status("ok", f"成功启动 {started_count} 个 MCP 服务器") else: UI.status("info", "未启用 MCP 服务器") # 获取包含 MCP 工具的工具列表 mcp_tools = mcp_manager.get_tools() tools = default_tools( include_mcp=True, mcp_tools=mcp_tools, extension_registry=extension_registry, ) if mcp_tools: UI.status("ok", f"加载了 {len(mcp_tools)} 个 MCP 工具") # 初始化技能管理器 skill_manager = SkillManager(extension_registry=extension_registry) skill_count = skill_manager.load_all() if skill_count > 0: UI.status("ok", f"加载了 {skill_count} 个技能") else: UI.status("info", "未加载任何技能") try: while True: try: print_menu()dm-code-maintenance-agent/dm_agent/cli/report.pycode · 3.4 KB
"""运行报告:Markdown 报告生成与工作区 git 状态采集。""" from __future__ import annotations import re import subprocess from datetime import datetime from pathlib import Path from typing import Any from .config import Config, format_advanced_feature_status def collect_git_status() -> list[str]: """Return short git status lines for the current workspace, if available.""" try: completed = subprocess.run( ["git", "status", "--short"], capture_output=True, text=True, timeout=10, ) except Exception: return [] if completed.returncode != 0: return [] return [line for line in completed.stdout.splitlines() if line.strip()] def write_run_report( path: Path, *, config: Config, task: str, result: dict[str, Any], trace_path: Path | None = None, git_status_before: list[str] | None = None, git_status_after: list[str] | None = None, ) -> None: """Write a human-readable Markdown report for one agent run.""" metadata = result.get("metadata", {}) steps = result.get("steps", []) path.parent.mkdir(parents=True, exist_ok=True) lines = [ "# DM-Code-Agent Run Report", "", "## Task", "", task, "", "## Runtime", "", f"- Provider: `{config.provider}`", f"- Model: `{config.model}`", f"- Status: `{metadata.get('status', 'unknown')}`", f"- Duration: `{float(metadata.get('duration_seconds', 0.0)):.2f}s`", f"- Steps: `{len(steps)}`", f"- Tool errors: `{metadata.get('tool_error_count', 0)}`", f"- Replans: `{metadata.get('replan_count', 0)}`", f"- Advanced features: `{format_advanced_feature_status(config)}`", ] if trace_path: lines.append(f"- Trace: `{trace_path}`") before = git_status_before or [] after = git_status_after or [] lines.extend(["", "## Workspace Status", ""]) if not before and not after: lines.append("No git status information available.") else: lines.append(f"- Dirty entries before run: `{len(before)}`") lines.append(f"- Dirty entries after run: `{len(after)}`") if before: lines.extend(["", "Before:", ""]) lines.extend(f"- `{line}`" for line in before) if after: lines.extend(["", "After:", ""]) lines.extend(f"- `{line}`" for line in after) lines.extend( [ "", "## Steps", "", "| # | Action | Observation |", "| ---: | --- | --- |", ] ) for index, step in enumerate(steps, start=1): observation = str(step.get("observation", "")).replace("\n", " ") if len(observation) > 180: observation = observation[:177] + "..." lines.append(f"| {index} | `{step.get('action', '')}` | {observation} |") lines.extend( [ "", "## Final Answer", "", str(result.get("final_answer", "")), "", ] ) path.write_text("\n".join(lines), encoding="utf-8") def default_report_path(task: str) -> Path: timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") slug = re.sub(r"[^a-z0-9]+", "-", task.lower()).strip("-")[:32] or "task" return Path(".dm_agent") / "run_reports" / f"{timestamp}-{slug}.md"dm-code-maintenance-agent/dm_agent/cli/runner.pycode · 14.9 KB · preview truncated
"""Agent 装配与单任务执行:把 Config 翻译成 ReactAgent 并跑完一次 run。""" from __future__ import annotations import contextlib import json import sys from pathlib import Path from typing import TYPE_CHECKING, Any from dm_agent import ( LLMError, ReactAgent, Tool, create_llm_client, default_tools, ) from dm_agent.core.checkpoint import RunCheckpoint from dm_agent.mcp import MCPManager, load_mcp_config from dm_agent.skills import SkillManager from dm_agent.tracing import SessionWriter, TraceWriter if TYPE_CHECKING: from dm_agent.extensions import ExtensionRegistry from .config import Config, format_advanced_feature_status, resolve_advanced_features from .report import collect_git_status, default_report_path, write_run_report from .ui import ( UI, Fore, browse_run_steps, create_step_callback, display_completion_screen, ) def review_completed_run( result: dict[str, Any], *, config: Config, task: str, trace_path: Path | None = None, report_path: Path | None = None, context_status: str | None = None, git_status_before: list[str] | None = None, git_status_after: list[str] | None = None, interactive: bool = True, ) -> Path | None: current_report_path = report_path while True: display_completion_screen( result, task=task, context_status=context_status, trace_path=trace_path, report_path=current_report_path, clear_screen=True, review_hint=interactive, ) if not interactive: return current_report_path choice = UI.prompt_line("操作", default="").strip().lower() if choice in {"", "c", "continue"}: return current_report_path if choice in {"v", "view", "steps", "process", "过程"}: browse_run_steps(result) continue if choice in {"s", "save", "report", "保存"}: current_report_path = current_report_path or default_report_path(task) write_run_report( current_report_path, config=config, task=task, result=result, trace_path=trace_path, git_status_before=git_status_before, git_status_after=git_status_after, ) UI.status("ok", "Report 已保存", str(current_report_path)) UI.pause() continue UI.status("warn", "未知操作", "Enter 继续,v 查看步骤,s 保存报告") UI.pause() def create_agent( config: Config, client: Any, tools: list[Tool], *, step_callback: Any = None, skill_manager: SkillManager | None = None, trace_writer: SessionWriter | TraceWriter | None = None, extension_registry: ExtensionRegistry | None = None, ) -> ReactAgent: """Create a ReactAgent with the CLI's default-off advanced switches.""" advanced = resolve_advanced_features(config) return ReactAgent( client, tools, max_steps=config.max_steps, temperature=config.temperature, step_callback=step_callback, skill_manager=skill_manager, trace_writer=trace_writer, max_observation_chars=config.max_observation_chars, context_token_budget=config.context_token_budget, enable_edit_guard=config.enable_edit_guard, enable_adaptive_replanning=advanced["adaptive_replanning"], max_replans=config.max_replans, event_bus=( extension_registry.create_event_bus() if extension_registry is not None else None ), ) def format_agent_context_status(agent: ReactAgent) -> str: stats = agent.get_context_stats() return ( f"history={stats['conversation_messages']} messages | " f"memory={stats['memory_items']} items | " f"compression={'on' if stats['compression_enabled'] else 'off'}" ) def _assemble_agent( config: Config, *, mcp_manager: MCPManager, trace_path: Path | None, trace_llm_io: bool, checkpoint_path: Path | None, extension_registry: ExtensionRegistry | None, ) -> tuple[ReactAgent, SessionWriter | None]: """把 Config 装配成一个可用的 agent(含 MCP 工具、技能、会话日志)。 ``run_single_task`` 与 ``run_conversation_stdin`` 共用这一段。抽出来是因为两条 入口必须装配出**完全一样**的 agent——否则「Web 控制台跑的和命令行跑的是同一件事」 这条保证就会从子进程这一层悄悄漏掉。 """ started_count = mcp_manager.start_all() if started_count > 0: UI.status("ok", f"启动了 {started_count} 个 MCP 服务器") mcp_tools = mcp_manager.get_tools() tools = default_tools( include_mcp=True, mcp_tools=mcp_tools, extension_registry=extension_registry, ) skill_manager = SkillManager(extension_registry=extension_registry) skill_count = skill_manager.load_all() client = create_llm_client( provider=config.provider, api_key=config.api_key, model=config.model, base_url=config.base_url, respond_retries=config.llm_max_retries, extension_registry=extension_registry, ) advanced = resolve_advanced_features(config) trace_writer: SessionWriter | None = None if trace_path or (checkpoint_path and checkpoint_path.suffix.lower() == ".jsonl"): trace_sink = TraceWriter(trace_path, capture_llm_io=trace_llm_io) if trace_path else None trace_writer = SessionWriter(trace_sink) if checkpoint_path: trace_writer.ensure_checkpoint_sink(checkpoint_path) trace_writer.record( "runtime", { "provider": config.provider, "model": config.model, "base_url": config.base_url, "max_steps": config.max_steps, "temperature": config.temperature, "show_steps": config.show_steps, "max_observation_chars": config.max_observation_chars, "context_token_budget": config.context_token_budget, "edit_guard_enabled": config.enable_edit_guard, "mcp_started_count": started_count, "mcp_tool_count": len(mcp_tools), "skill_count": skill_count, "trace_llm_io": trace_llm_io, "adaptive_replanning_enabled": advanced["adaptive_replanning"], "max_replans": config.max_replans, }, ) step_callback = create_step_callback(config.show_steps) agent = create_agent( config, client, tools, step_callback=step_callback, skill_manager=skill_manager, trace_writer=trace_writer, extension_registry=extension_registry, ) return agent, trace_writer def run_single_task( config: Config, task: str, *, trace_path: Path | None = None, trace_llm_io: bool = False, report_path: Path | None = None, checkpoint_path: Path | None = None, resume_state: RunCheckpoint | None = None, extension_registry: ExtensionRegistry | None = None, ) -> int: """运行单个任务(命令行模式)""" # 初始化 MCP mcp_config = load_mcp_config() mcp_manager = MCPManager(mcp_config) trace_writer: SessionWriter | None = None try: agent, trace_writer = _assemble_agent( config, mcp_manager=mcp_manager, trace_path=trace_path, trace_llm_io=trace_llm_io, checkpoint_path=checkpoint_path, extension_registry=extension_registry, ) UI.panel( "Run", ( f"task {task}\n" f"provider {config.provider}\n" f"model {config.model}\n" f"max steps {config.max_steps}\n" f"advanced {format_advanced_feature_status(config)}" ), color=Fore.CYAN, ) git_status_before = collect_git_status() result = agent.run(task, checkpoint_path=checkpoint_path, resume_state=resume_state) git_status_after = collect_git_status() if report_path: write_run_report( report_path, config=config, task=task, result=result, trace_path=trace_path, git_status_before=git_status_before, git_status_after=git_status_after, ) review_completed_run( result, config=config, task=task, trace_path=trace_path, report_path=report_path, git_status_before=git_status_before, git_status_after=git_status_after, interactive=False, ) return 0 except LLMError as e: if trace_writer: trace_writer.record("run_error", {"error_type": "LLMError", "message": str(e)}) print(f"{UI.paint('[ERR] API 错误', Fore.RED, bright=True)} {e}", file=sys.stderr) return 1 except Exception as e: if trace_writer: trace_writer.record( "run_error", {"error_type": type(e).__name__, "message": str(e)}, ) print(f"{UI.paint('[ERR] 发生错误', Fore.RED, bright=True)} {e}", file=sys.stderr) return 1 finally: if trace_writer: trace_writer.close() # 清理 MCP 资源 mcp_manager.stop_all() def _read_conversation_message(line: str) -> dict[str, Any] | None: """把 stdin 上的一行解析成一条控制消息;空行与坏行返回 None。 用 JSON 对象而不是裸文本行:任务描述里完全可能有换行。 """ text = line.strip() if not text: return None try: message = json.loads(text) except json.JSONDecodeError: return None return message if isinstance(message, dict) else None def run_conversation_stdin( config: Config, *, trace_path: Path, trace_llm_io: bool = False, extension_registry: ExtensionRegistry | None = None, ) -> int: """长驻会话模式:一个进程、一个 agent、顺序跑多轮。 这是 Web 控制台多轮对话的执行端。协议刻意做成**单向**的: * 调用方 → 子进程:stdin 上每行一个 JSON 对象,``{"task": "..."}`` 跑一轮, ``{"type": "reset"}`` 清空对话历史与本地记忆。EOF 表示会话结束。 * 子进程 → 调用方:**没有 stdout 协议**。每一轮的进展、结果、失败全都在 ``--trace`` 的会话日志里(``run_start`` / ``run_end`` 本来就在),调用方跟读 那个文件即可。stdout/stderr 保持人类可读日志的原样。 这么设计有两个好处:不需要发明第二套协议,也不会被 agent 自己的打印污染; 而且「实时看到的」与「事后审计到的」仍然是同一份字节。 多轮共享上下文靠的是**同一个 ``ReactAgent`` 实例**——``_run_once`` 会把上一轮 留在 ``conversation_history`` 里的消息通过 ``_adopt_existing_history(kind="carried")`` 补记进会话日志,本地记忆与折叠状态也随实例延续。这正是交互式多轮模式的做法。 """ # stdin 默认按平台 locale 解码(Windows 上是 GBK),中文任务会在这里被打成乱码。 if hasattr(sys.stdin, "reconfigure"): with contextlib.suppress(Exception): sys.stdin.reconfigure(encoding="utf-8", errors="replace") mcp_config = load_mcp_config() mcp_manager = MCPManager(mcp_config) trace_writer: SessionWriter | None = None try: agent, trace_writer = _assemble_agent( config, mcp_manager=mcp_manager, trace_path=trace_path, trace_llm_io=trace_llm_io, checkpoint_path=None, extension_registry=extension_registry, ) except Exception as e: if trace_writer: trace_writer.record( "run_error", {"error_type": type(e).__name__, "message": str(e)}, ) trace_writer.close() mcp_manager.stop_all() print(f"{UI.paint('[ERR] 会话初始化失败', Fore.RED, bright=True)} {e}", file=sys.stderr) return 1 UI.panel( "Conversation", ( f"provider {config.provider}\n" f"model {config.model}\n" f"max steps {config.max_steps}\n" f"advanced {format_advanced_feature_status(config)}\n" f"trace {trace_path}" ), color=Fore.CYAN, ) turn = 0 reason = "eof" try: while True: # 用 readline() 而不是 `for line in sys.stdin`:后者有读前缓冲, # 会把已经写进管道的一行dm-code-maintenance-agent/dm_agent/cli/ui.pycode · 28.2 KB · preview truncated
"""终端渲染层:Rich 优先、colorama 兜底的展示原语与只读界面。 本模块是 CLI 的最底层,不依赖 dm_agent.cli 内的任何其他模块,也不认识 Config。 """ from __future__ import annotations import contextlib import json import os import shutil import sys import textwrap from pathlib import Path from typing import Any, ClassVar from dm_agent.skills import SkillManager from dm_agent.tools import Tool try: from rich import box from rich.console import Console, Group from rich.padding import Padding from rich.panel import Panel from rich.prompt import Prompt from rich.table import Table from rich.text import Text RICH_AVAILABLE = True RICH_CONSOLE = Console(highlight=False, soft_wrap=True) except ImportError: RICH_AVAILABLE = False RICH_CONSOLE = None # 尝试导入 colorama 用于彩色输出 try: from colorama import Fore, Style from colorama import init as colorama_init colorama_init(autoreset=True) COLORS_AVAILABLE = True except ImportError: COLORS_AVAILABLE = False # 如果没有 colorama,定义空的颜色常量 class Fore: GREEN = "" YELLOW = "" RED = "" CYAN = "" MAGENTA = "" BLUE = "" WHITE = "" class Style: BRIGHT = "" DIM = "" RESET_ALL = "" class UI: """Terminal UI layer with Rich rendering and a colorama fallback.""" WIDTH = 88 RICH_STYLES: ClassVar[dict[str, str]] = { "ok": "bold white on green", "error": "bold white on red", "warn": "bold black on yellow", "info": "bold white on blue", "run": "bold white on magenta", } RICH_LABELS: ClassVar[dict[str, str]] = { "ok": "DONE", "error": "ERROR", "warn": "WARN", "info": "INFO", "run": "RUN", } @staticmethod def rich_enabled() -> bool: return RICH_AVAILABLE and RICH_CONSOLE is not None @staticmethod def clear() -> None: if UI.rich_enabled(): RICH_CONSOLE.clear() return os.system("cls" if os.name == "nt" else "clear") @staticmethod def width() -> int: return max(72, min(UI.WIDTH, shutil.get_terminal_size((UI.WIDTH, 20)).columns)) @staticmethod def paint(text: str, color: str = "", *, bright: bool = False, dim: bool = False) -> str: style = "" if bright: style += Style.BRIGHT if dim: style += getattr(Style, "DIM", "") return f"{style}{color}{text}{Style.RESET_ALL}" @staticmethod def rule(label: str = "", *, color: str = Fore.CYAN) -> None: if UI.rich_enabled(): title = Text(f" {label} ", style="bright_black") if label else "" RICH_CONSOLE.rule(title, style="bright_black") return width = UI.width() if label: prefix = f" {label} " line = prefix + "-" * max(width - len(prefix), 0) else: line = "-" * width print(UI.paint(line, color, dim=not label)) @staticmethod def banner(title: str, subtitle: str = "") -> None: if UI.rich_enabled(): heading = Text() if title == "DM-Code-Agent": heading.append("DM", style="bold cyan") heading.append("-Code-Agent", style="bold white") else: heading.append(title, style="bold white") body: Group | Text if subtitle: caption = Text(subtitle, style="bright_black") body = Group(heading, Text(""), caption) else: body = heading RICH_CONSOLE.print( Panel( body, border_style="bright_black", box=box.ROUNDED, padding=(1, 2), expand=True, subtitle=( Text("trace tools skills memory", style="bright_black") if title == "DM-Code-Agent" else None ), ) ) return print() print(UI.paint(title, Fore.GREEN, bright=True)) if subtitle: print(" " + UI.paint(subtitle, Fore.WHITE, dim=True)) print() @staticmethod def section(title: str, subtitle: str = "") -> None: if UI.rich_enabled(): text = Text() text.append(title, style="bold white") if subtitle: text.append("\n") text.append(subtitle, style="bright_black") RICH_CONSOLE.print(Padding(text, (1, 0, 0, 0))) return print() print(UI.paint(title, Fore.CYAN, bright=True)) if subtitle: for line in UI.wrap(subtitle, width=UI.width() - 4): print(" " + UI.paint(line, Fore.WHITE, dim=True)) @staticmethod def panel(title: str, body: str = "", *, color: str = Fore.CYAN) -> None: if UI.rich_enabled(): style = UI._rich_color(color) RICH_CONSOLE.print( Panel( UI._rich_wrapped_text(str(body), width=UI.width() - 10), title=Text(f" {title} ", style=f"bold {style}"), title_align="left", border_style="bright_black", box=box.ROUNDED, padding=(1, 2), expand=True, ) ) return print() print(UI.paint(title, color, bright=True)) if body: for raw_line in str(body).splitlines() or [""]: wrapped = UI.wrap(raw_line, width=UI.width() - 4) or [""] for line in wrapped: print(" " + line) @staticmethod def wrap(text: str, *, width: int | None = None) -> list[str]: return textwrap.wrap( str(text), width=width or UI.width() - 4, replace_whitespace=False, drop_whitespace=False, ) @staticmethod def _rich_wrapped_text(text: str, *, width: int) -> Text: wrapped = Text() target_width = max(36, width) lines: list[str] = [] for raw_line in str(text).splitlines() or [""]: if raw_line.strip(): lines.extend(UI.wrap(raw_line, width=target_width) or [""]) else: lines.append("") for index, line in enumerate(lines): if index: wrapped.append("\n") wrapped.append(line) return wrapped @staticmethod def status(kind: str, message: str, detail: str = "") -> None: if UI.rich_enabled(): style = UI.RICH_STYLES.get(kind, UI.RICH_STYLES["info"]) label = UI.RICH_LABELS.get(kind, kind.upper()) text = Text() text.append(f" {label:<5} ", style=style) text.append(" ") text.append(message, style="white") if detail: text.append(" ") text.append(detail, style="bright_black") RICH_CONSOLE.print(Padding(text, (0, 0, 0, 1))) return palette = { "ok": (Fore.GREEN, "ok"), "error": (Fore.RED, "err"), "warn": (Fore.YELLOW, "warn"), "info": (Fore.CYAN, "info"), "run": (Fore.MAGENTA, "run"), } color, icon = palette.get(kind, palette["info"]) line = f"{UI.paint(icon.ljust(5), color, bright=True)} {message}" if detail: line += UI.paint(f" {detail}", Fore.WHITE, dim=True) print(line) @staticmethod def key_values(title: str, rows: list[tuple[str, Any]]) -> None: if UI.rich_enabled(): table = Table.grid(padding=(0, 3)) table.add_column(style="bright_black", no_wrap=True) table.add_column(style="bold white") for key, value in rows: table.add_row(str(key), str(value)) RICH_CONSOLE.print( Panel( table, title=Text(f" {title} ", style="bold cyan"), title_align="left", border_style="bright_black", box=box.ROUNDED, padding=(1, 2), ) ) return UI.section(title) key_width = max((len(key) for key, _ in rows), default=0) for key, value in rows: print( f" {UI.paint(key.ljust(key_width), Fore.WHITE, dim=True)} " f"{UI.paint(str(value), Fore.YELLOW)}" ) @staticmethod def menu(items: list[tuple[str, str]]) -> None: if UI.rich_enabled(): table = Table.grid(expand=True, padding=(0, 2)) table.add_column(justify="right", no_wrap=True, width=5) table.add_column(style="bold white", no_wrap=True, width=18) table.add_column(style="bright_black", ratio=1) for index, (title, description) in enumerate(items, start=1): badge = Text(f" {index} ", style="bold black on cyan") table.add_row(badge, title, description) RICH_CONSOLE.print( Panel( table, title=Text(" 主菜单 ", style="bold white"), title_align="left", subtitle=Text("输入编号选择操作", style="bright_black"), border_style="bright_black", box=box.ROUNDED, padding=(1, 2), ) ) return UI.section("主菜单", "输入编号选择一个操作") for index, (title, description) in enumerate(items, start=1): badge = UI.paint(f"[{index}]", Fore.GREEN, bright=True) name = UI.paint(title.ljust(14), Fore.WHITE, bright=True) print(f" {badge} {name} {UI.paint(description, Fore.WHITE, dim=True)}") print() @staticmethod def truncate(value: Any, limit: int = 220) -> str: text = str(value) if len(text) <= limit: return text return text[: max(limit - 3, 0)].rstrip() + "..." @staticmethod def ask( prompt: str, *, choices: list[str] | None = None, default: str | None = None, show_choices: bool = True, ) -> str: if UI.rich_enabled(): # 显式标注为异构 dict:默认推断会把值类型收窄成三个分支的联合, # 导致后面按需塞入 default 以及 ** 展开进 Prompt.ask 全部误报 arg-type。 prompt_kwargs: dict[str, Any] = { "choices": choices, "console": RICH_CONSOLE, "show_choices": show_choices, } if default is not None: prompt_kwargs["default"] = default return Prompt.ask(Text(prompt, style="bold cyan"), **prompt_kwargs) suffix = f" [{default}]" if default is not None else "" return input(f"{UI.paint(prompt, Fore.CYAN)}{suffix}: ") @staticmethod def prompt_line(prompt: str, *, default: str = "") -> str: if UI.rich_enabled(): return Prompt.ask( Text(prompt, style="bold cyan"), console=RICH_CONSOLE, default=default, show_default=False, ) suffix = f" [{default}]" if default else "" return input(f"{UI.paint(prompt, Fore.CYAN)}{suffix}: ") @staticmethod def pause(prompt: str = "按 Enter 返回") -> None: UI.prompt_line(prompt, default="") @staticmethod def _rich_color(color: str) -> str: if color == Fore.GREEN: return "green" if color == Fore.YELLOW: return "yellow" if color == Fore.RED: return "red" if color == Fore.MAGENTA: return "magenta" if color == Fore.BLUE: return "blue" return "cyan" def configure_console_encoding(dm-code-maintenance-agent/dm_agent/clients/__init__.pycode · 0.5 KB
"""客户端模块 - 提供各种 LLM API 客户端实现""" from .base_client import BaseLLMClient, LLMError from .claude_client import ClaudeClient from .deepseek_client import DeepSeekClient from .gemini_client import GeminiClient from .llm_factory import PROVIDER_DEFAULTS, create_llm_client from .openai_client import OpenAIClient __all__ = [ "PROVIDER_DEFAULTS", "BaseLLMClient", "ClaudeClient", "DeepSeekClient", "GeminiClient", "LLMError", "OpenAIClient", "create_llm_client", ]dm-code-maintenance-agent/dm_agent/clients/base_client.pycode · 4.5 KB
"""LLM 客户端基类定义。""" from __future__ import annotations import time from abc import ABC, abstractmethod from typing import Any # Provider-agnostic transient-failure status codes (429/5xx plus common # request-conflict/timeout codes). Semantic 4xx errors are never retried. RETRYABLE_STATUS_CODES = frozenset({408, 409, 429, 500, 502, 503, 504}) _RETRYABLE_NAME_TOKENS = ( "timeout", "connection", "ratelimit", "internalserver", "serviceunavailable", "overloaded", ) _RETRYABLE_TEXT_TOKENS = ( "timed out", "timeout", "connection", "rate limit", "overloaded", "temporarily unavailable", "429", "502", "503", "504", ) class LLMError(RuntimeError): """当 LLM API 请求失败时抛出。 ``retryable`` 标记该错误是否为瞬时故障(超时/断连/429/5xx)。 只有 retryable 的 LLMError 才会被 ``complete_with_retry`` 重试。 """ def __init__(self, message: str, *, retryable: bool = False) -> None: super().__init__(message) self.retryable = retryable def classify_retryable_exception(exc: Exception) -> bool: """Best-effort 判断 provider SDK 异常是否为瞬时故障。""" status = getattr(exc, "status_code", None) if not isinstance(status, int): status = getattr(exc, "code", None) if isinstance(status, int) and status in RETRYABLE_STATUS_CODES: return True name = type(exc).__name__.lower() if any(token in name for token in _RETRYABLE_NAME_TOKENS): return True text = str(exc).lower() return any(token in text for token in _RETRYABLE_TEXT_TOKENS) class BaseLLMClient(ABC): """LLM 客户端的抽象基类。""" def __init__( self, api_key: str, *, model: str, base_url: str, timeout: int = 600, respond_retries: int = 2, respond_retry_backoff: float = 1.0, ) -> None: if not api_key: raise ValueError("LLM 客户端需要 API 密钥。") if respond_retries < 0: raise ValueError("respond_retries must be >= 0.") if respond_retry_backoff < 0: raise ValueError("respond_retry_backoff must be >= 0.") self.api_key = api_key self.model = model self.base_url = base_url.rstrip("/") self.timeout = timeout # 统一重试层:仅对 retryable=True 的 LLMError 指数退避重试。 # 自带内部重试的客户端(如 DeepSeek)应传 respond_retries=0 防止双重重试。 self.respond_retries = respond_retries self.respond_retry_backoff = respond_retry_backoff self.total_respond_retries = 0 @abstractmethod def complete( self, messages: list[dict[str, str]], **extra: Any, ) -> dict[str, Any]: """发送聊天式补全请求到 LLM API。 Args: messages: 消息列表,每个消息包含 role 和 content **extra: 额外的参数(如 temperature, max_tokens 等) Returns: API 响应的字典 """ pass @abstractmethod def extract_text(self, data: dict[str, Any]) -> str: """从 API 响应中提取文本内容。 Args: data: API 响应的字典 Returns: 提取的文本内容 """ pass def complete_with_retry( self, messages: list[dict[str, str]], **extra: Any, ) -> dict[str, Any]: """调用 ``complete``,对瞬时故障做有限的指数退避重试。""" attempts = self.respond_retries + 1 for attempt in range(attempts): try: return self.complete(messages, **extra) except LLMError as exc: is_last = attempt == attempts - 1 if not getattr(exc, "retryable", False) or is_last: raise self.total_respond_retries += 1 if self.respond_retry_backoff > 0: time.sleep(self.respond_retry_backoff * (2**attempt)) raise LLMError("LLM request failed after exhausting retry budget.") def respond(self, messages: list[dict[str, str]], **extra: Any) -> str: """返回补全响应的文本部分。 Args: messages: 消息列表 **extra: 额外的参数 Returns: 提取的文本响应 """ data = self.complete_with_retry(messages, **extra) return self.extract_text(data)dm-code-maintenance-agent/dm_agent/clients/claude_client.pycode · 3.3 KB
"""Claude (Anthropic) API 的客户端(使用官方 SDK)。""" from __future__ import annotations from typing import Any try: import anthropic ANTHROPIC_AVAILABLE = True except ImportError: ANTHROPIC_AVAILABLE = False from .base_client import BaseLLMClient, LLMError, classify_retryable_exception class ClaudeClient(BaseLLMClient): """Claude API 的轻量级封装(使用官方 anthropic SDK)。""" def __init__( self, api_key: str, *, model: str = "claude-sonnet-4-5", base_url: str = "", # Claude SDK 不需要 base_url timeout: int = 600, anthropic_version: str = "2023-06-01", respond_retries: int = 2, ) -> None: if not ANTHROPIC_AVAILABLE: raise ImportError("anthropic 未安装。请运行: pip install anthropic") super().__init__( api_key, model=model, base_url=base_url, timeout=timeout, respond_retries=respond_retries, ) self.anthropic_version = anthropic_version # 创建 Anthropic 客户端实例 # 官方 SDK 不需要手动设置 base_url self.client = anthropic.Anthropic(api_key=self.api_key) def complete( self, messages: list[dict[str, str]], **extra: Any, ) -> dict[str, Any]: """向 Claude API 发送消息请求。""" try: # Claude API 要求分离系统消息 system_message = None claude_messages = [] for msg in messages: if msg.get("role") == "system": system_message = msg.get("content", "") else: claude_messages.append(msg) # 调用 Claude API kwargs = { "model": self.model, "messages": claude_messages, "max_tokens": extra.pop("max_tokens", 4096), } if system_message: kwargs["system"] = system_message kwargs.update(extra) response = self.client.messages.create(**kwargs) # 转换为字典格式 return {"response": response} except Exception as e: raise LLMError( f"Claude API 调用失败: {e}", retryable=classify_retryable_exception(e) ) from e def extract_text(self, data: dict[str, Any]) -> str: """从 Claude 响应中提取文本内容。""" if not isinstance(data, dict): raise LLMError("意外的响应负载类型。") # 从响应中提取文本 response = data.get("response") if response: try: # Claude SDK 返回的 content 是一个列表 if hasattr(response, "content") and response.content: # 提取所有文本块 text_parts = [] for block in response.content: if hasattr(block, "text"): text_parts.append(block.text) if text_parts: return "\n".join(text_parts).strip() except Exception as e: raise LLMError(f"无法从 Claude 响应中提取文本: {e}") from e raise LLMError("无法从 Claude 响应中提取文本。")dm-code-maintenance-agent/dm_agent/clients/deepseek_client.pycode · 6.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/clients/gemini_client.pycode · 3.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/clients/llm_factory.pycode · 4.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/clients/openai_client.pycode · 3.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/__init__.pycode · 0.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/agent.pycode · 36.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/capabilities.pycode · 1.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/checkpoint.pycode · 4.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/completion.pycode · 4.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/context_window.pycode · 11.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/events.pycode · 15.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/guards.pycode · 4.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/observation.pycode · 6.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/persistence.pycode · 10.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/planner.pycode · 18.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/prompting.pycode · 2.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/replan.pycode · 8.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/response_parser.pycode · 3.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/run_state.pycode · 4.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/core/tool_invoker.pycode · 6.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/evals/__init__.pycode · 0.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/evals/cli.pycode · 3.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/evals/gate.pycode · 3.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/evals/models.pycode · 2.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/evals/real_runner.pycode · 13.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/evals/runner.pycode · 12.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/evals/scripted_client.pycode · 1.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/evals/tasks.pycode · 12.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/extensions/__init__.pycode · 0.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/extensions/builtin.pycode · 0.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/extensions/discovery.pycode · 8.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/extensions/registry.pycode · 5.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/extensions/trust.pycode · 3.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/mcp/__init__.pycode · 0.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/mcp/client.pycode · 11.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/mcp/config.pycode · 8.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/mcp/manager.pycode · 11.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/memory/__init__.pycode · 0.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/memory/context_budget.pycode · 6.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/memory/context_compressor.pycode · 29.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/paths.pycode · 7.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/prompts/__init__.pycode · 0.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/prompts/code_agent_prompt.pycode · 3.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/prompts/system_prompts.pycode · 0.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/__init__.pycode · 1.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/app.pycode · 5.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/cli.pycode · 6.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/process.pycode · 12.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/routes/__init__.pycode · 0.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/routes/conversations.pycode · 9.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/routes/meta.pycode · 5.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/routes/runs.pycode · 6.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/routes/sessions.pycode · 15.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/runs.pycode · 13.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/security.pycode · 2.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/settings.pycode · 5.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/static/assets/Geist-Variable-Bj2R_7yk.woff2binary · 68.0 KB
Binary — download to inspect.
dm-code-maintenance-agent/dm_agent/server/static/assets/GeistMono-Variable-Dispecij.woff2binary · 69.7 KB
Binary — download to inspect.
dm-code-maintenance-agent/dm_agent/server/static/assets/index-C5MF1qkm.csscode · 34.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/static/assets/index-zWz4BgMR.jstext · 264.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/static/index.htmlcode · 0.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/server/streaming.pycode · 6.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/skills/__init__.pycode · 0.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/skills/base.pycode · 2.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/skills/builtin/__init__.pycode · 0.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/skills/builtin/db_expert.pycode · 4.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/skills/builtin/frontend_dev.pycode · 2.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/skills/builtin/python_expert.pycode · 5.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/skills/custom/.gitkeeptext · 0.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/skills/manager.pycode · 5.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/skills/selector.pycode · 4.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tools/__init__.pycode · 8.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tools/base.pycode · 1.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tools/code_analysis_tools.pycode · 11.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tools/code_index_tools.pycode · 9.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tools/execution_tools.pycode · 6.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tools/file_tools.pycode · 17.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tracing/__init__.pycode · 1.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tracing/analysis.pycode · 13.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tracing/cli.pycode · 6.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tracing/fork.pycode · 3.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tracing/render.pycode · 11.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tracing/replay.pycode · 3.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tracing/session.pycode · 8.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tracing/summary.pycode · 7.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/dm_agent/tracing/writer.pycode · 18.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/README.mdmarkdown · 2.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/architecture-cn.pngbinary · 1147.1 KB
Binary — download to inspect.
dm-code-maintenance-agent/docs/architecture.drawiotext · 20.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/architecture.drawio.pngbinary · 1151.2 KB
Binary — download to inspect.
dm-code-maintenance-agent/docs/architecture.mdmarkdown · 10.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/autonomous-roadmap.mdmarkdown · 4.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/benchmarks.mdmarkdown · 12.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/capabilities.mdmarkdown · 4.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/cli.mdmarkdown · 6.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/extensions.mdmarkdown · 12.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/getting-started.mdmarkdown · 3.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/lifecycle-events.mdmarkdown · 7.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/mcp.mdmarkdown · 9.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/product.mdmarkdown · 1.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/project-overview-simple.drawiotext · 8.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/project-overview-simple.pngbinary · 258.0 KB
Binary — download to inspect.
dm-code-maintenance-agent/docs/project-status.mdmarkdown · 7.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/recent-progress.mdmarkdown · 2.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/release-v2.0.0.mdmarkdown · 1.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/releasing.mdmarkdown · 4.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/00-kickoff.mdmarkdown · 4.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/01-swebench-baseline.mdmarkdown · 13.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/02-reflexion.mdmarkdown · 2.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/04-critic-and-consistency.mdmarkdown · 2.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/05-adaptive-and-economics.mdmarkdown · 4.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/06-final-writeup.mdmarkdown · 3.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/07-trace-diff.mdmarkdown · 3.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/08-trace-analyzer.mdmarkdown · 3.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/09-maintenance-realism.mdmarkdown · 2.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/10-benchmark-confidence.mdmarkdown · 2.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/11-self-consistency-uncertainty.mdmarkdown · 2.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/12-repeated-failure-signals.mdmarkdown · 2.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/13-benchmark-provenance.mdmarkdown · 2.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/14-benchmark-trace-analysis.mdmarkdown · 2.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/15-trace-analysis-aggregation.mdmarkdown · 1.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/16-economics-uncertainty.mdmarkdown · 1.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/17-packaging-ci-maintenance.mdmarkdown · 2.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/18-self-consistency-patch-fingerprint.mdmarkdown · 2.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/19-benchmark-manifest-diff.mdmarkdown · 2.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/20-economics-manifest-guard.mdmarkdown · 2.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/21-trace-analysis-markdown.mdmarkdown · 2.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/22-repeated-failure-policy-experiment.mdmarkdown · 2.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/23-observation-truncation-and-token-budget.mdmarkdown · 6.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/24-memory-hygiene-and-recall.mdmarkdown · 4.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/25-unified-llm-retry-and-atomic-io.mdmarkdown · 3.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/26-run-checkpoint-and-progress-carrying-replan.mdmarkdown · 4.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/27-tool-circuit-breaker-experiment.mdmarkdown · 3.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/28-evals-recovery-capability-and-gates.mdmarkdown · 5.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/29-session-tree.mdmarkdown · 11.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/30-compaction-economics.mdmarkdown · 7.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/31-web-conversation-mode.mdmarkdown · 7.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/32-user-level-config-and-env.mdmarkdown · 8.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/33-scope-reduction.mdmarkdown · 8.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/34-benchmark-expansion.mdmarkdown · 4.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/35-agent-arena-claude-vs-deepseek.mdmarkdown · 9.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/36-scope-constraint-ablation.mdmarkdown · 13.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/37-step-budget-and-edit-self-damage.mdmarkdown · 14.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/38-edit-file-precision.mdmarkdown · 13.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/39-observation-failure-classification.mdmarkdown · 7.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/40-empty-patch-loops.mdmarkdown · 12.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/41-swebench-selection-manifest.mdmarkdown · 5.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/42-swebench-crossrepo-50.mdmarkdown · 5.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/43-swebench-failure-analysis.mdmarkdown · 9.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/DISTRIBUTION_CHECKLIST.mdmarkdown · 1.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/INTERVIEW_TALKING_POINTS.mdmarkdown · 1.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/research-log/README.mdmarkdown · 7.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/skills.mdmarkdown · 11.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/tracing.mdmarkdown · 14.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/web.mdmarkdown · 13.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/docs/改进agent系统.mdmarkdown · 33.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/examples/block_dangerous_shell.pycode · 0.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/main.pycode · 0.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/mcp_config.json.exampletext · 0.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/pyproject.tomlcode · 9.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/requirements.txtcode · 0.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/README.mdmarkdown · 18.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/__init__.pycode · 0.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/_winshim/resource.pycode · 0.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/analyze.pytext · 73.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/dataset.pycode · 5.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/evaluate.pycode · 5.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/predict.pycode · 19.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/progress_guard.pycode · 12.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/run.pycode · 8.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/swebench_verified/selection.pycode · 5.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/conftest.pycode · 12.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_agent_events.pycode · 14.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_agent_guards.pycode · 22.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_agent_internals.pycode · 10.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_checkpoint.pycode · 16.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_cli_config_paths.pycode · 8.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_cli_conversation.pycode · 11.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_cli_ui.pycode · 5.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_coding_benchmarks.pycode · 37.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_context_budget.pycode · 4.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_deepseek_client.pycode · 3.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_evals.pycode · 5.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_extension_discovery.pycode · 8.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_extension_providers.pycode · 4.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_extension_registry.pycode · 6.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_llm_retry.pycode · 3.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_memory_compressor.pycode · 19.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_observation_failure.pycode · 5.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_planner_agent.pycode · 23.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_score_diff.pycode · 5.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_server_cli.pycode · 2.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_server_conversations.pycode · 13.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_server_layering.pycode · 5.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_server_process.pycode · 12.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_server_readonly.pycode · 12.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_server_runs.pycode · 16.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_server_security.pycode · 6.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_server_sessions_delete.pycode · 8.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_server_streaming.pycode · 9.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_session_log.pycode · 28.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_skills_and_mcp.pycode · 4.5 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_swebench_analysis.pycode · 35.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_swebench_selection.pycode · 10.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_swebench_verified.pycode · 26.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_tools.pycode · 14.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_tracing.pycode · 16.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/tests/test_tracing_cli.pycode · 23.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/uv.locktext · 354.1 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/index.htmlcode · 0.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/package-lock.jsontext · 119.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/package.jsoncode · 0.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/App.tsxcode · 4.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/components/ChatTurn.tsxcode · 7.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/components/CompactionView.tsxcode · 6.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/components/Diagnostics.tsxcode · 6.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/components/Sidebar.tsxcode · 6.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/components/Timeline.tsxcode · 6.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/components/ui.tsxcode · 13.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/index.csscode · 7.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/lib/api.tscode · 9.7 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/lib/conversationStore.tscode · 6.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/lib/entries.test.tscode · 7.8 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/lib/entries.tscode · 9.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/lib/paths.tscode · 1.0 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/lib/router.tscode · 2.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/lib/turns.test.tscode · 6.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/lib/turns.tscode · 5.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/lib/types.tscode · 5.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/main.tsxcode · 0.3 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/views/Chat.tsxcode · 21.2 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/views/DiffView.tsxcode · 7.9 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/views/RunDetail.tsxcode · 17.4 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/src/views/SessionList.tsxcode · 15.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/tsconfig.jsoncode · 0.6 KB
No inline preview for this entry.
dm-code-maintenance-agent/web/vite.config.tscode · 0.9 KB
No inline preview for this entry.