Compare commits

...

85 commits

Author SHA1 Message Date
jinliyl
99afc2604f
fix(release): harden embedding store and plugins for ReMe 0.4.1.9 (#503)
Some checks are pending
CI / Documentation / Test and build documentation (push) Waiting to run
CI / Python packages / Build and verify distributions (push) Waiting to run
CI / Python quality / Pre-commit (push) Waiting to run
CI / Python tests / Unit Tests - py3.11 (push) Waiting to run
CI / Python tests / Unit Tests - py3.12 (push) Waiting to run
CI / Python tests / Unit Tests - py3.13 (push) Waiting to run
CI / ReMe Studio / Studio checks (push) Waiting to run
CI / TypeScript integrations / Type-check, test, and pack (push) Waiting to run
CI / Windows / CLI smoke - py3.11 (push) Waiting to run
Deploy / Documentation / deploy (push) Blocked by required conditions
Deploy / Documentation / Build documentation (push) Waiting to run
Security / CodeQL / Analyze javascript-typescript (push) Waiting to run
Security / CodeQL / Analyze python (push) Waiting to run
* chore(release): prepare ReMe 0.4.1.9

* refactor(config): remove daily_cookbook and streamline plugin configs

- Delete the entire daily_cookbook.yaml standalone application config
- Remove qwenpaw dependencies verification and related CI workflow steps
- Simplify release workflows by removing qwenpaw verification and enforcing reme-ai >=0.4.1.9
- Update plugin start commands and examples to use 'default' or 'demo' configs instead of daily_cookbook
- Adjust imports and tests related to daily_cookbook removal and injected_job_kwargs enhancements
- Refactor agent wrapper to support injected_job_kwargs for job parameter injection in auto-fin and daily-paper
- Improve daily_paper digest prompt to include configured daily directory and correct historical search constraints
- Update dependency versions in pyproject.toml files to require reme-ai >=0.4.1.9 and remove qwenpaw optional dependencies
- Clean up unused environment variables and obsolete test cases related to daily_cookbook and verification steps

* fix(local_embedding_store): retry batch computation on vector space changes

- Add up to 3 attempts to recompute embedding batch if vector space changes during processing
- Log warnings when maximum retries reached and discard stale results
- Prevent caching results from outdated vector spaces to maintain consistency
- Add tests to verify retry behavior and abort after continuous vector space churn

fix(daily_paper): update digest search logic and tests

- Change search to query existing memory, not only previous articles in daily_dir
- Allow multiple searches outside daily_dir but limit links to dated markdown in daily_dir before today
- Update test assertions to reflect revised search and linking rules

* fix(embedding): retry vector space changes per request
2026-08-28 11:35:04 +08:00
jinliyl
2dd2255760
ci: update release workflow actions and smoke checks (#502)
* ci: update artifact actions for Node 24

* ci: validate Auto Fin package manifest
2026-08-27 18:03:44 +08:00
jinliyl
d8d667c6ac
docs: refresh ReMe Studio preview image (#501) 2026-08-27 17:50:48 +08:00
jinliyl
940a923f06
ci: allow bootstrap release before qwenpaw plugins (#499) 2026-08-27 17:23:53 +08:00
jinliyl
3d2ecc60d2
feat(service): expose MCP through HTTP backend (#498)
* feat(service): expose MCP through HTTP backend

Serve JSON/SSE job endpoints and streamable HTTP MCP from one FastAPI application, sharing the same jobs and application lifecycle. Preserve the standalone MCP backend, add configurable MCP HTTP settings, update startup metadata and integration docs, and cover routing, lifecycle, configuration, and compatibility behavior with unit tests.

* fix(service): preserve MCP request protections

Route the exact MCP path through the complete FastMCP ASGI application so its middleware and state remain active. Reject non-literal MCP paths and validate reserved Job conflicts before tolerant service registration. Add regression coverage for middleware preservation, route syntax, and startup failure.

* fix(service): reject encoded MCP paths

Reject percent signs in mcp_path so ASGI path decoding cannot turn an accepted configuration into an unreachable route. Cover encoded slash, space, and double-encoded slash inputs.
2026-08-27 17:23:11 +08:00
jinliyl
6f38d201b6
ci: harden build and release workflows (#497) 2026-08-27 16:43:05 +08:00
jinliyl
ef3f99f019
refactor(packaging): reorganize published packages (#495)
* refactor(packaging): reorganize published packages

* fix(packaging): install AgentScope extra in wheel smoke

* docs: align package guides and documentation site

* ci(workflow): add core dependency verification step in Python package build

- Add a workflow step to verify released core dependencies by installing the wheel with core extras
- Assert the presence of the static index.html file to ensure proper package contents
- Create and use a temporary virtual environment for isolation during verification
- Keep existing artifacts upload step intact and conditional on inputs.upload_artifacts flag

* fix(ci): update package installation dependencies in Windows workflow

- Change pip install from editable reme_studio and core to only dev and as extras
- Remove installation of reme_studio and core to streamline dependency setup
- Ensure Windows CI uses the correct extras for testing environment

* fix(tests): add missing commas in toml file reads in package version tests

- Added trailing commas in the tomllib.loads calls for auto-fin and daily_paper configs
- Ensured consistent syntax to prevent potential tuple misinterpretation
- Improved readability and correctness of the test setup code

* fix(packaging): protect qwenpaw releases and test Studio health
2026-08-27 14:02:09 +08:00
jinliyl
b78e32ef03
feat(openclaw): align ReMe plugin with current SDK (#493)
Some checks are pending
CI / Documentation / Test and build documentation (push) Waiting to run
CI / Python packages / Build and verify distributions (push) Waiting to run
CI / Python quality / Pre-commit (push) Waiting to run
CI / Python tests / Unit Tests - py3.11 (push) Waiting to run
CI / Python tests / Unit Tests - py3.12 (push) Waiting to run
CI / Python tests / Unit Tests - py3.13 (push) Waiting to run
CI / TypeScript integrations / Type-check, test, and pack (push) Waiting to run
CI / Windows / CLI smoke - py3.11 (push) Waiting to run
Deploy / Documentation / Build documentation (push) Waiting to run
Deploy / Documentation / deploy (push) Blocked by required conditions
Security / CodeQL / Analyze javascript-typescript (push) Waiting to run
Security / CodeQL / Analyze python (push) Waiting to run
Adopt definePluginEntry, before_prompt_build, current manifest contracts, and official OpenClaw SDK types.

Add DSH-aligned memory batching, retryable shutdown flushing, daily Auto Dream scheduling, updated documentation, tests, ClawHub validation, and optional release publishing.
2026-08-26 19:57:57 +08:00
jinliyl
6a6e0b3c29
fix(dsh): deduplicate pending memory guidance (#494) 2026-08-26 19:37:24 +08:00
jinliyl
a457bf7542
docs: reorganize readme around agent integrations (#492) 2026-08-26 19:30:16 +08:00
jinliyl
513fb5b7f4
feat: extract Daily Paper into an independently packaged plugin (#491)
* feat: extract Daily Paper into a plugin

* fix: satisfy clean-environment quality checks

* fix: address daily paper review feedback
2026-08-26 17:32:46 +08:00
jinliyl
1a6b584274
fix(persistence): avoid duplicate index dumps (#489)
* fix(persistence): avoid duplicate index dumps

* fix(persistence): align dumps with component ownership

* fix(persistence): preserve subclass dump hooks
2026-08-26 16:51:00 +08:00
jinliyl
15d12be6b6
fix(index): tolerate invalid text encoding (#490)
* fix(index): tolerate invalid text encoding

* fix(index): preserve text chunker compatibility
2026-08-26 16:16:26 +08:00
jinliyl
626c850ccb
fix(daily-paper): sanitize Unicode surrogates (#487)
Some checks failed
CI / Python tests / Unit Tests - py3.11 (push) Has been cancelled
CI / Documentation / Test and build documentation (push) Has been cancelled
CI / Python quality / Pre-commit (push) Has been cancelled
CI / Python tests / Unit Tests - py3.12 (push) Has been cancelled
CI / Python tests / Unit Tests - py3.13 (push) Has been cancelled
CI / TypeScript integrations / Type-check, test, and pack (push) Has been cancelled
CI / Windows / CLI smoke - py3.11 (push) Has been cancelled
Deploy / Documentation / Build documentation (push) Has been cancelled
Security / CodeQL / Analyze javascript-typescript (push) Has been cancelled
Security / CodeQL / Analyze python (push) Has been cancelled
Deploy / Documentation / deploy (push) Has been cancelled
* fix(daily-paper): sanitize Unicode surrogates

* fix(daily-paper): sanitize analysis workflow state
2026-08-24 18:59:53 +08:00
jinliyl
01ef1a6efb
ci: use trusted publishing for TypeScript package (#486)
* ci: use trusted publishing for TypeScript package

* docs: scope npm announcement to DeepSeek Harness
2026-08-24 15:21:28 +08:00
jinliyl
efcc2b34d1
feat: simplify plugin setup and add management CLI (#485)
Some checks are pending
CI / Documentation / Test and build documentation (push) Waiting to run
CI / Python quality / Pre-commit (push) Waiting to run
CI / Python tests / Unit Tests - py3.11 (push) Waiting to run
CI / Python tests / Unit Tests - py3.12 (push) Waiting to run
CI / Python tests / Unit Tests - py3.13 (push) Waiting to run
CI / Windows / CLI smoke - py3.11 (push) Waiting to run
Deploy / Documentation / Build documentation (push) Waiting to run
Deploy / Documentation / deploy (push) Blocked by required conditions
Security / CodeQL / Analyze javascript-typescript (push) Waiting to run
Security / CodeQL / Analyze python (push) Waiting to run
* feat: simplify plugin setup and add management CLI

* fix: isolate plugin CLI import side effects

* refactor: streamline plugin validation

* fix: route plugin CLI arguments independently

* fix: support standard plugin source layouts
2026-08-23 17:58:20 +08:00
jinliyl
c8e1248769
fix: recover embeddings after transient health check failure (#471)
Some checks failed
CI / Python tests / Unit Tests - py3.12 (push) Has been cancelled
CI / Python tests / Unit Tests - py3.13 (push) Has been cancelled
CI / TypeScript integrations / Type-check, test, and pack (push) Has been cancelled
CI / Windows / CLI smoke - py3.11 (push) Has been cancelled
Deploy / Documentation / Build documentation (push) Has been cancelled
Security / CodeQL / Analyze javascript-typescript (push) Has been cancelled
Security / CodeQL / Analyze python (push) Has been cancelled
CI / Documentation / Test and build documentation (push) Has been cancelled
CI / Python quality / Pre-commit (push) Has been cancelled
CI / Python tests / Unit Tests - py3.11 (push) Has been cancelled
Deploy / Documentation / deploy (push) Has been cancelled
* fix: recover embedding after transient health failure

* refactor(embedding_store): remove provider_success_count and simplify health recovery logic

- Deleted provider_success_count attribute and related methods across embedding and file stores
- Updated _recover_after_real_request to rely solely on is_healthy flag for recovery decisions
- Removed redundant counting logic for provider successes during embedding operations
- Cleaned up health status management to streamline provider recovery detection
- Adjusted unit tests to align with removal of provider_success_count and maintain health checks consistency

* refactor(embedding_store): use default health check timeout

* fix(embedding_store): ensure is_healthy remains unchanged on cache hits

- Updated get_embeddings docstring to clarify cache hits must not alter is_healthy state
- Improved code comment for embedding dimension matching method

* fix(file_store): make embedding recovery race-safe

* ci: use default CodeQL query suite

* fix(file_store): preserve queued embedding rebuilds

* fix(file_store): preserve verified recovery without chunks
2026-08-21 13:58:51 +08:00
jinliyl
8416fd3ac9
feat: add unified TypeScript agent integrations (#483)
* feat: add unified TypeScript agent integrations

* fix: normalize endpoints without regex backtracking

* fix: address TypeScript integration review feedback

* fix: preserve original OpenClaw prompts

* fix: bound pending OpenClaw prompts
2026-08-21 13:57:14 +08:00
jinliyl
f44f52d919
fix(embedding): exclude provider init from health timeout (#484)
Some checks failed
CI / Documentation / Test and build documentation (push) Waiting to run
CI / Python quality / Pre-commit (push) Waiting to run
CI / Python tests / Unit Tests - py3.11 (push) Waiting to run
CI / Python tests / Unit Tests - py3.12 (push) Waiting to run
CI / Python tests / Unit Tests - py3.13 (push) Waiting to run
CI / Windows / CLI smoke - py3.11 (push) Waiting to run
Deploy / Documentation / Build documentation (push) Waiting to run
Deploy / Documentation / deploy (push) Blocked by required conditions
Security / CodeQL / Analyze javascript-typescript (push) Waiting to run
Security / CodeQL / Analyze python (push) Waiting to run
CI / Website / Website checks (push) Has been cancelled
CI / Python packages / Build and verify distributions (push) Has been cancelled
2026-08-21 11:23:14 +08:00
jinliyl
ebcb154e37
fix(search): isolate range dedup state (#465) 2026-08-20 16:15:30 +08:00
jinliyl
39233f4e62
ci: remove Dependabot configuration (#482) 2026-08-20 16:14:52 +08:00
jinliyl
94b7dedc26
Delete .github/README.md 2026-08-20 16:03:55 +08:00
jinliyl
87187c1d25
ci: organize GitHub automation (#466) 2026-08-20 15:56:30 +08:00
jinliyl
f5ec230fef
feat: add DSH memory integration and organize extensions (#461)
* feat: add DSH memory integration and organize extensions

* fix: support newer DSH release candidates

* fix: address DSH integration review feedback

* fix: handle DSH cross-day retry edge cases
2026-08-20 15:31:51 +08:00
jinliyl
2f5fd46b44
refactor: deduplicate entry-point loading (#460)
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
GitHub Pages Check / test-and-build (push) Has been cancelled
Package Check / distributions (push) Has been cancelled
Deploy ReMe documentation / build (push) Has been cancelled
Tests ReMe / Unit Tests - py3.11 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.12 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.13 (push) Has been cancelled
Windows Smoke / CLI smoke - py3.11 (push) Has been cancelled
Deploy ReMe documentation / deploy (push) Has been cancelled
* refactor: deduplicate entry-point loading

* fix: preserve config conflict error priority
2026-08-19 19:57:53 +08:00
jinliyl
618e8cec66
feat: add entry-point plugin system and extract Auto Fin (#459)
* feat: add entry-point plugin system

* fix: harden plugin config and client loading

* docs(workflow): add detailed manual for publishing reme-auto-fin to PyPI

- Provide step-by-step instructions for updating project.version and merging branches
- Explain dependency verification for reme-ai on PyPI during build
- Specify requirements for GitHub Actions secret configuration and version uniqueness
- Describe manual workflow triggering and input of version number
- Recommend publishing order for related projects
- Clarify that only manual dispatch triggers publishing, no automatic triggers on push or tag

* feat: support plugin-defined component types

* refactor: simplify plugin configuration

* fix: isolate plugin loading and defer client fallback

* refactor: freeze built-in component registry

* fix: isolate config entry point loading

* fix: complete auto-fin package metadata
2026-08-19 17:23:23 +08:00
jinliyl
d3aee1adf5
feat(evolve): report auto-dream file changes (#458)
* feat(evolve): report auto-dream content changes

* perf(evolve): use lightweight dream snapshots
2026-08-19 15:44:06 +08:00
jinliyl
6b9a75267b
fix(ci): support AgentScope 2.0.6 initialization (#457)
Some checks are pending
Package Check / distributions (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
2026-08-19 11:33:52 +08:00
jinliyl
c792fd197c
Update agentscope version to 2.0.6
Some checks are pending
Package Check / distributions (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
2026-08-18 11:04:26 +08:00
jinliyl
fd2894f939
fix: harden the 0.4.1.7 release configuration (#456)
Some checks failed
NPM Format / Website checks (push) Has been cancelled
GitHub Pages Check / test-and-build (push) Has been cancelled
Package Check / distributions (push) Has been cancelled
Deploy ReMe documentation / build (push) Has been cancelled
Windows Smoke / CLI smoke - py3.11 (push) Has been cancelled
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Tests ReMe / Unit Tests - py3.11 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.12 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.13 (push) Has been cancelled
Deploy ReMe documentation / deploy (push) Has been cancelled
* fix(packaging): harden the Studio release workflow

* chore(daily-paper): tune scheduled discovery defaults

* fix(docs): link the ReMe blog to GitHub Pages

* fix(docs): increase Chinese hero title spacing

* refactor(docs): share hero title line spacing

* fix(docs): keep desktop hero copy on two lines

* fix(docs): widen the home hero description

* style(docs): loosen hero title line height

* fix(docs): hide Markdown frontmatter in rendered pages

* docs(readme): simplify installation and remove standalone ReMe Studio instructions

- Remove references to separate ReMe Studio package and static build steps
- Clarify that `core` extra includes common integrations including Studio
- Update installation instructions to use `pip install -e ".[core]"`
- Remove detailed Studio usage and frontend development instructions
- Note that Studio is included with `core` and optional via `web` extra
- Simplify Quick Start guide by removing Studio usage step
- Remove mentions of serving Studio with HTTP service when using extras
- Update both English and Chinese README files accordingly

* docs(readme): streamline and clarify memory design and operations

- Remove redundant explanations about core extra installation
- Simplify memory processing flow description for clarity
- Clarify memory workspace directory default and customization
- Condense automatic memory flow to emphasize rebuildable metadata
- Refine search functionality explanation with RRF fusion details
- Shorten and clarify agent integration description, removing redundancy
- Update and simplify the operations command list, removing less common commands
- Revise community and support section for conciseness and clarity
- Maintain parallel updates in both English and Chinese README files

* test(bump_version): add tests for version bumping and consistency checks

- Add dynamic loading of bump_version and package_studio scripts for testing
- Test that studio package and dependencies have matching versions
- Implement fixtures to write temporary version files for testing
- Add test ensuring bump_version updates all relevant files and dependencies
- Add test to reject inconsistent version sources before writing
- Refactor tests to use common REPOSITORY path variable
- Include imports and setup for pytest in test file

feat(bump_version): create script to update ReMe and Studio versions

- Implement version reading from __init__.py and pyproject.toml files
- Validate current versions are consistent across files before updating
- Update version strings atomically to avoid partial writes
- Ensure exact pinning of studio dependency in main package extras
- Validate new version format against a safe pattern
- Provide CLI interface to bump versions from command line
- Raise errors if expected version declarations or pins are missing or duplicated

* fix(release): validate split package publishing

* fix(release): improve validation diagnostics

* fix(release): sync docs and workflow inputs

* fix(release): split PyPI publish jobs
2026-08-13 17:22:00 +08:00
jinliyl
2a05914150
feat: distribute Studio as an optional package (#454) 2026-08-13 11:07:37 +08:00
jinliyl
29eb51d7ba
fix: show resolved service URL and shrink Studio preview asset (#453)
Some checks are pending
NPM Format / Website checks (push) Waiting to run
Deploy ReMe documentation / build (push) Waiting to run
Deploy ReMe documentation / deploy (push) Blocked by required conditions
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* fix: show resolved service address in startup banner

* perf(website): reduce social preview image size

* fix: resolve MCP transport in startup banner
2026-08-12 19:37:31 +08:00
jinliyl
da9a8b7810
fix(docs): make homepage cards use direct links (#452) 2026-08-12 13:28:23 +08:00
jinliyl
dbf2a17da6
docs: expand ReMe documentation site (#451) 2026-08-12 13:18:40 +08:00
jinliyl
64249873ce
fix(website): resolve Dependabot dependency alerts (#450) 2026-08-12 12:20:41 +08:00
jinliyl
28fa636506
fix(docs): use public npm registry for Pages (#449) 2026-08-12 12:03:19 +08:00
jinliyl
52fdd446fb
docs: add standalone GitHub Pages site (#448) 2026-08-12 11:55:35 +08:00
jinliyl
ab66f2bb56
docs: refresh ReMe guides, diagrams, and Studio documentation (#447)
* docs: update ReMe documentation URL

* docs: localize ReMe Studio social image

* docs(AGENTS): update agent guidelines and repository documentation structure

- Clarify coding agent guidance for keeping changes small and consistent
- Revise project principle descriptions for clarity and modern terminology
- Expand repository map with detailed component and folder explanations
- Add configuration and CLI usage instructions, including syntax and merging rules
- Elaborate on component, step registration, and application lifecycle processes
- Define jobs, steps, and state handling conventions for stateless design
- Specify workspace and file safety policies, including path restrictions and locking
- Update validation commands and testing environment recommendations
- Clarify coding and test conventions, including style and dependency policies
- Distinguish documentation boundaries and update website content contribution notes
- Reinforce change guardrails to avoid breaking backward compatibility and data loss
- Improve svg diagram formatting and textual details in auto dream and proactive flow image

* style(docs): fix font-family syntax in SVG style definitions

- Correct quotation marks around font-family names in memory-as-file.svg
- Standardize font-family formatting by removing unnecessary quotes in reme-blog-architecture.svg
- Ensure consistent CSS style formatting within SVG files for better rendering fidelity

* docs: add ReMe blog to news

* style(docs): inline svg styles and improve text formatting

- Convert multiline SVG style tags into single-line for compactness in multiple figures
- Remove redundant line breaks in subtitle text elements for consistency
- Shorten descriptive texts in SVG figures for clarity and conciseness
- Adjust font sizes and text for better readability in SVG elements
- Correct whitespace issues in Chinese markdown document for improved formatting
- Remove unused style blocks from framework structure SVG for cleaner code
2026-08-12 10:59:03 +08:00
jinliyl
215c1f72f2
feat: refine local-first research and memory workflows (#444)
Some checks are pending
NPM Format / Website checks (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* feat: refine local-first research workflows

* fix: delegate structured output tool choice

* refactor(auto-fin): fetch and filter rolling CLS news

* fix(auto-fin): keep imports portable across platforms

* feat(auto-fin): expose CLS fetch controls

* fix(auto-fin): propagate configurable news window

* feat(auto_fin): normalize hybrid wikilinks in report body

- Add _normalize_hybrid_wikilinks method to remove redundant Markdown destinations
- Use regex to identify hybrid wikilinks with optional destinations
- Replace redundant destinations with simpler wikilink format for clarity
- Ensure normalization is failure-safe with exception handling and logging
- Update report body normalization process to apply hybrid wikilink fix
- Add unit tests to verify correct normalization and failure safety behavior

* fix(dream): serialize integration with application-wide asyncio lock

- Add application-wide asyncio.Lock to serialize digest writes during integration
- Update _snapshot_digest to capture metadata per bucket
- Validate bucket association when recovering from file changes
- Add tests ensuring recovery only from the correct bucket
- Add tests confirming integration lock is shared across application context
- Enhance strict topic YAML loading validation in dream utils
- Add tests for strict topic loading rejecting invalid or lossy fields

* fix(cookbook): enable configurable job_tools for digest and merge steps

- Update daily_cookbook.yaml to add job_tools: [memory_search, read] in digest steps
- Modify DailyPaperDigestStep to read job_tools from kwargs instead of fixed list
- Modify AutoFinMergeStep to similarly read job_tools from kwargs
- Update tests to pass job_tools explicitly when invoking these steps
- Remove hardcoded _TOOLS constants and replace with dynamic job_tools handling

* fix: retry incomplete dream receipts

* perf(pdf): increase max PDF pages limit from 20 to 35

- Updated configuration max_pdf_pages from 20 to 35 in daily_cookbook.yaml
- Modified code to extract up to 35 pages instead of 20 in analyze.py
- Updated README and README_ZH to document the increased max_pdf_pages
- Adjusted unit test assertions to reflect new max_pdf_pages limit of 35

* fix memory integration and daily paper links

* docs clarify cookbook tool usage
2026-08-11 23:32:34 +08:00
jinliyl
9533c17d51
feat(web): serve workspace from HTTP service (#446)
* feat(web): add the ReMe workspace frontend

* feat(web): serve workspace from HTTP service

* test(web): satisfy pylint docstring checks

* fix(web): use same-origin API safely

* fix(web): preserve API route semantics
2026-08-11 23:32:24 +08:00
jinliyl
b8f48c8004
feat(web): add the ReMe Studio frontend (#418)
* feat(web): add the ReMe workspace frontend

* fix(web): use public npm registry in lockfile

* fix(web): address workspace review feedback

* fix(web): protect drafts and report file limits

* fix(web): finish chat streams after tab switches

* feat(web): rename frontend to ReMe Studio
2026-08-11 19:47:59 +08:00
imrewce
3924f89bb4
feat(bench): adding eval adapter for proactiveness on Pi-Bench (#439)
* feat(bench): adding eval adapter for proactiveness on Pi-Bench

* Revise README for π-Bench evaluation suite

Updated the README to reflect the new project name and description.

* fix(bench): refining pi-bench scripts according to cr comments

* fix(bench): restore agent builtin tools in prebuilt toolkit
2026-08-11 16:37:54 +08:00
jinliyl
c7dbf31c3f
docs: expand ReMe guides and agent integrations (#445) 2026-08-11 13:31:11 +08:00
imrewce
58276f740b
fix(file_io): auto appending suffix for all related steps (#430)
* fix(file_io): auto appending suffix for all related steps

* fix(file_io): covering boundary cases of potential directory input
2026-08-11 11:09:28 +08:00
imrewce
3095564313
docs: Adding pi-bench related proc performance to blog draft (#443)
* docs(blog): refine proactive section wording in zh reme-blog

* chore(doc): supplementing proc related performance
2026-08-11 10:59:51 +08:00
jinliyl
21057931a9
fix(embedding): isolate caches by vector space (#442)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* fix(embedding): isolate caches by vector space

* fix(embedding): stabilize cache space switching

* Revert "fix(embedding): stabilize cache space switching"

This reverts commit 74193c9a0a.

* fix(embedding): include resolved OpenAI endpoint in cache ID

* fix(embedding): stabilize cache space switching

* fix(embedding): isolate Ollama endpoint caches
2026-08-10 22:42:16 +08:00
Zhaoyang Liu
5a5855f5ff
docs: refine ReMe launch blog (#440)
Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com>
2026-08-10 18:45:06 +08:00
jinliyl
072cb6a55b
feat(daily-paper): add opt-in Hugging Face mirror support (#437)
* feat(daily-paper): add Hugging Face mirror switch

* refactor(daily-paper): simplify the HF mirror switch and warn on ignored env

The switch was a three-state bool|None where None preserved the legacy
environment-driven selection, but no production caller ever passes None --
collect.py always resolves an explicit bool. Collapse it to a plain bool
defaulting to False.

HF_MIRROR_URL no longer redirects traffic on its own, so warn when it is
configured while the mirror stays disabled; a mirror-only setup would
otherwise fall back to the official site with no signal. Both READMEs now
record the behavior change and stop presenting the two mirror variables as
symmetric -- arXiv remains environment-driven while Hugging Face is gated on
the job parameter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(daily-paper): address mirror configuration feedback

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:57:24 +08:00
imrewce
fca42f4e6c
docs(blog): refine proactive section wording in zh reme-blog (#438) 2026-08-10 15:57:16 +08:00
jinliyl
d5e0d2837b
refactor: rebuild auto-fin and daily-paper cookbooks on structured-output agents (#432)
Some checks failed
Tests ReMe / Unit Tests - py3.12 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.13 (push) Has been cancelled
Windows Smoke / CLI smoke - py3.11 (push) Has been cancelled
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Tests ReMe / Unit Tests - py3.11 (push) Has been cancelled
* refactor: rebuild auto-fin and daily-paper cookbooks on structured-output agents

Rework the auto-fin and daily-paper cookbooks to run on structured-output
LLM agents instead of Claude Code agent wrappers, replace the SSH proxy with
data-source mirrors, and rewrite the affected unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(auto_fin): unify JSON output serialization and writing

- Extracted _write_output static method to serialize and write Pydantic models as compact JSON
- Replaced inline JSON dump and write calls with _write_output usage across auto_fin steps
- Added _report_path and _current_report for managing intra-day reports in AutoFinMergeStep
- Updated auto_fin merge step to write output via new _write_output method
- Enhanced news reading with caching in AutoFinHistoryStep
- Refined returns calculation to handle events before close on non-trading days correctly

feat(daily_paper): improve note path resolution and metadata handling

- Introduced iter_note_metadata generator for safe Markdown frontmatter iteration
- Added resolve_unique_note_path to avoid note filename conflicts on disk and in used titles
- Updated analyze, collect, digest, and select steps to use centralized constants and helpers
- Used utc_now_iso for consistent timestamping in metadata
- Replaced direct frontmatter loads with iter_note_metadata in collect and analyze steps
- Replaced hardcoded paper selection count with PAPER_COUNT constant in all relevant places
- Added _MAX_SELECT_ATTEMPTS constant in select step for attempt management
- Improved error messages for filename validation in daily paper title normalization

feat(auto_fin): add multi-run cron schedules for intraday refinement

- Defined three auto_fin cron jobs at 09:30, 11:30, and 18:00 Shanghai time for gradual report updates
- Each intraday run adds evidence cumulatively instead of replacing prior output wholly
- Updated daily_cookbook.yaml to register new cron schedules and remove legacy 12:00 cron

refactor(auto_fin_data): clean ETF code handling and page limits

- Replaced hardcoded DEFAULT_ETF_CODES with required non-empty config value "etf_codes"
- Added constants for major news and fund page limits to control pagination
- Improved ETF name extraction logic to handle missing fields consistently

fix(auto_fin_merge): fix report retrieval and merging logic

- Added support for getting current intra-day report in addition to previous day's report
- Modified merge template to include prior and current report sections for better context
- Adjusted report path handling to consistently use Path objects

test(auto_fin): add coverage for returns calculation and report retrieval

- Added test for returns when event occurs before close on non-trading day, checking next session entry
- Added test for previous and current report retrieval feeding merge context with disk files
- Extended test asserts for auto_fin cron schedule changes in config

style(daily_paper): reorder and cleanup imports

- Reorganized imports in _common.py for clarity and added missing collections.abc.Iterator import
- Cleaned up commented and unused imports across daily_paper steps

* feat: add configurable upstream mirror proxy

* style: format auto-fin data step

* fix: align cookbook mirrors and contracts

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 23:53:14 +08:00
jinliyl
e05b201da9
feat(backend): improve workspace support for web clients (#420)
* feat(backend): improve workspace support for web clients

* fix(config): preserve the default workspace directory

* chore(reme): bump version to 0.4.1.5

- Update __version__ from 0.4.1.4 to 0.4.1.5 in initialization file

* fix(chat): disable builtin tools in read-only mode

* fix(agent): make builtin tools opt-in

* fix(list): tolerate files removed during mtime sort

* fix(chat): expose complete read-only job set
2026-08-07 23:52:56 +08:00
jinliyl
765103a597
docs(blog): add reme blog (#436)
* docs: add Chinese ReMe blog article

* fix(docs): update wiki links and adjust SVG path coordinates

- Removed file extensions from wiki link texts for consistency
- Modified path coordinates for relation lines in SVG illustration
- Added an arrow path with fill color to indicate direction in SVG diagram

* docs(blog): expand ReMe user guide and invite community contributions

- Add detailed descriptions for different ReMe user groups including intelligent agents,
  developers, researchers, engineers, and analysts
- Emphasize user control over data as editable Markdown files instead of black-box storage
- Introduce ReMe's long-term memory infrastructure accessible via multiple interfaces
- Highlight how ReMe can turn scattered information into personal knowledge networks
- Include a new "Welcome Contributions" section encouraging community involvement
- List areas for contribution such as integration, data sources, features, applications,
  documentation, and issue feedback
2026-08-07 17:36:02 +08:00
jinliyl
168b7194ab
docs: add Chinese ReMe blog article (#435) 2026-08-07 17:19:01 +08:00
lichen2015
e7b9274190
fix(stat): return text/markdown for .md files regardless of OS mime registry (#433)
On macOS, mimetypes.guess_type() may not recognize .md files, causing
stat to report application/octet-stream and breaking test assertions.
Explicitly map .md files to text/markdown so the behavior is stable
across platforms.
2026-08-07 16:31:03 +08:00
jinliyl
c5d92a24ab
feat: weave dream wikilinks into contextual prose (#428)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
2026-08-06 17:40:01 +08:00
jinliyl
9218a2d0e3
refactor: derive dialog paths from session_dir (#421)
* refactor: derive dialog paths from session directory

* fix: normalize configured session paths

* fix: align dialog watch paths with writers

* fix: reject absolute session directories
2026-08-06 17:07:14 +08:00
Ziyang Guo
6503e1271c
fix(prompt): default omitted conditional flags to false (#424)
Always apply conditional-line filtering so tagged prompt lines are removed unless the corresponding boolean flag is explicitly true. Add regressions for omitted flags with and without format variables.

Test: pytest tests/unit/test_prompt_handler.py -q
2026-08-06 16:22:59 +08:00
xyf2020
23d4c96c15
refactor(benchmark): isolate per-benchmark assets and simplify LME agentic prompt (#422)
* chore(benchmark): isolate dataset/workspaces/results per benchmark

- Move shared benchmark/{datasets,memory_workspaces,results} into per-benchmark subdirs benchmark/<name>/{dataset,workspaces,results}
- Update beam/longmemeval config.yaml and run.py path defaults
- Relocate longmemeval download.py to benchmark/longmemeval/ (downloads into dataset/ subdir); inline dataset download docs into README
- Update .gitignore: benchmark/*/{dataset,workspaces,results}/
- Move result-{beam,longmemeval}.md to benchmark/results_md/ and drop result- prefix; update README links
- Fix stale path refs in llm_judge.py and logs/demo_search_format.py

* feat(benchmark): add read tool to agentic answer and update BEAM results

- Add 'read' to job_tools in BaseAgenticAnswerStep for file reading capability
- Document read tool usage in lme/agentic_answer.yaml system prompt
- Update result-beam.md with latest evaluation scores (OVERALL: 0.623/0.580)

* feat(auto_memory): add source line-number markers for note traceability

- Add _format_history hook in AutoMemoryStep with line-number annotation
- Override in BeamAutoMemoryStep to prefix each turn with [Ln] for citation
- Add session_file variable to prompt templates for source marker paths
- Simplify repeated extraction rules by referencing system prompt
- Enhance agentic_answer search strategy (multi-search, read tool hint)
- Add warning log on ReadStep failure

* feat(beam): enhance auto_memory with source markers and pilot ingest tooling

* refactor(beam): rename max_chunk_words to max_segment_words, drop one-off pilot scripts

* feat: add CompressorStep and search_v2 dual-mode session compression

- Add CompressorStep (reme/steps/evolve/compressor.py) for direct LLM
  text compression with optional query-guided relevance filtering
- Extend search_v2_step to support query-aware and query-independent
  session transcript compression via _compress injected kwargs
- Refactor _source_format.py: split into render_chunk_entries +
  join_chunk_entries; session chunks now render line-aligned with
  L<n>: prefixes for verbatim/compressed parity
- Add JOB_TOOLS and INJECTED_JOB_KWARGS to BaseAgenticAnswerStep for
  per-subclass tool and parameter injection
- LmeAgenticAnswerStep injects _search._compress payload to enable
  query-aware compression during benchmark evaluation
- Record compression ablation results in result-longmemeval.md
- Add unit tests for CompressorStep and search compression paths

* refactor(compress): relax session compression to lenient format-preserving strategy and update LME results

* refactor(benchmark): make session compression config-driven via compress_session flag

Move session-transcript compression from LME hard-coded injection to a
runtime context flag set by evaluation.compress_session in each
benchmark config. Compression is off by default for both BEAM and LME,
and BaseAgenticAnswerStep now conditionally injects the _search compress
payload only when the flag is truthy.

* feat(lme/auto_memory): add source attribution markers with line numbers

Add _format_history to annotate each turn with [Ln] line numbers and
expose {session_file} in prompts so the agent can emit bare wikilink-style
source markers like [[session/dialog/s1.jsonl#L1-L2,L5-L6]] at the end
of factual entries. Consolidate the per-prompt body/format rules into
references to the system prompt to avoid drift, and add frontmatter-
protection guidance for the edit tool.

* feat: improve agentic answer prompt and update beam 100K results

- Strengthen abstention rule: prohibit extrapolation from related but
  non-direct evidence
- Add multi-angle search after preliminary answer to check for
  conflicting/supplementary/updated information
- Add max-iteration fallback to 'Information not found'
- Update beam.md with 100K results (agentscope 2.0.4.post1, from scratch)
  including per-type token consumption and memory construction stats
- config.yaml: 100K dataset, 20 workers for BEAM evaluation
- run.py: add memory construction token usage tracking (default agent)
- Overall: 0.635 → 0.654 (+0.019), contradiction_resolution: 0.338 → 0.478
  (+0.140), abstention: 0.500 → 0.525 (+0.025)

* feat(read): add session-aware formatting for read tool and update BEAM eval

- Add truncate_session_output in _file_io.py to render jsonl session
  lines as [speaker @ time] content before byte-budget truncation
- Add read_step_format_session flag to ReadStep, honoring injected
  job kwargs (precedence) and YAML fallback
- Inject read_step_format_session=True into BaseAgenticAnswerStep
  so agentic answer reads render session transcripts human-readably
- Refine BEAM agentic_answer prompt: continue multi-angle search
  after preliminary answer, forbid fabrication/extrapolation
- Update BEAM config to 1M variant and add sequential 100K-eval /
  1M-build shell script
- Refresh benchmark/results_md/beam.md with latest results

* chore(config): disable expand_links in beam and lme search_v2 configs

* refactor(beam): drop one-off sequential 100K-eval-then-1M-build script

* fix(benchmark): add compressor job to beam config and fix BEAM clone instructions

- Add compressor job and compressor as_llm component to reme/config/beam.yaml
  (aligned with lme.yaml) so that compress_session: true works for BEAM
- Add graceful degradation guard in search_v2._compress_session_entries:
  when the compressor job is missing from the active config, log a warning
  and skip compression instead of raising 'Job compressor not found'.
  Skipped when there is no app_context so unit tests mocking run_job still
  drive compression behavior.
- Fix BEAM download instructions in README.md/README_ZH.md: add mkdir -p
  before cd benchmark/beam/dataset (the directory is gitignored and absent
  in a fresh clone)

* fix(steps): guard compressor exceptions and fix ReadStep boolean override

1. search_v2: catch per-entry exceptions from run_job('compressor') inside
   compress() so asyncio.gather never propagates a compressor failure (e.g.
   temporary LLM outage). The failing entry keeps its original body while
   remaining entries are still compressed, preserving already-retrieved
   search results.

2. read: replace 'context_value or yaml_value' with an existence check so
   that a runtime-injected False can explicitly disable a YAML-true
   read_step_format_session flag.

Add focused unit tests for both paths.

* fix(search_v2): use existence check for strict_date_filter boolean override

Replace 'context_value or yaml_value' with an existence-based check so
that a runtime-injected False can explicitly disable a YAML-true
strict_date_filter flag, consistent with the read_step_format_session fix.

* refactor(search): simplify strict_date_filter fallback to truthiness-or

* style(test): rename unused param to satisfy pylint W0613

* refactor(benchmark): isolate per-benchmark assets and simplify LME agentic prompt

- Move shared benchmark/README, README_ZH, kill.sh, and results_md/*.md into
  per-benchmark subdirs (benchmark/beam/, benchmark/longmemeval/) so each
  benchmark owns its own docs, scripts, and result snapshots.
- Simplify lme/agentic_answer.yaml system prompt: drop verbose memory-system
  description, keep search strategy, draft tool, and answer rules concise.

* docs(benchmark): update LME README_ZH results to latest eval run

---------

Co-authored-by: sa-buc <jiangniurou.xyf@dail-algo011164204033.ET135>
2026-08-06 15:13:52 +08:00
jinliyl
f31daf1949
Revert "feat(backend): improve workspace support for web clients (#417)" (#419)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
This reverts commit b00eb0a9ea.
2026-08-05 23:17:08 +08:00
jinliyl
b00eb0a9ea
feat(backend): improve workspace support for web clients (#417) 2026-08-05 23:07:05 +08:00
lichen2015
ad4f23e4dc
feat(file_store): add ZvecLocalFileStore backend (#410)
* feat(file_store): add ZvecLocalFileStore backend

- Implement ZvecLocalFileStore with native zvec collection for ANN search.
- Keep JSONL chunks as the source of truth; rebuild collection from chunks
  when sidecar digest/dimension/HNSW M mismatch is detected.
- Add dedicated unit tests in tests/unit/test_zvec_file_store.py.
- Parametrize existing file_store consistency tests to cover both
  LocalFileStore and ZvecLocalFileStore.
- Register the new backend in reme/components/file_store/__init__.py.

* fix(file_store): fix zvec collection sync and content validation, declare zvec dependency
2026-08-05 22:08:30 +08:00
xyf2020
5bc46c88b6
feat(benchmark): enhance session memory retrieval and isolate benchmark assets (#409)
* chore(benchmark): isolate dataset/workspaces/results per benchmark

- Move shared benchmark/{datasets,memory_workspaces,results} into per-benchmark subdirs benchmark/<name>/{dataset,workspaces,results}
- Update beam/longmemeval config.yaml and run.py path defaults
- Relocate longmemeval download.py to benchmark/longmemeval/ (downloads into dataset/ subdir); inline dataset download docs into README
- Update .gitignore: benchmark/*/{dataset,workspaces,results}/
- Move result-{beam,longmemeval}.md to benchmark/results_md/ and drop result- prefix; update README links
- Fix stale path refs in llm_judge.py and logs/demo_search_format.py

* feat(benchmark): add read tool to agentic answer and update BEAM results

- Add 'read' to job_tools in BaseAgenticAnswerStep for file reading capability
- Document read tool usage in lme/agentic_answer.yaml system prompt
- Update result-beam.md with latest evaluation scores (OVERALL: 0.623/0.580)

* feat(auto_memory): add source line-number markers for note traceability

- Add _format_history hook in AutoMemoryStep with line-number annotation
- Override in BeamAutoMemoryStep to prefix each turn with [Ln] for citation
- Add session_file variable to prompt templates for source marker paths
- Simplify repeated extraction rules by referencing system prompt
- Enhance agentic_answer search strategy (multi-search, read tool hint)
- Add warning log on ReadStep failure

* feat(beam): enhance auto_memory with source markers and pilot ingest tooling

* refactor(beam): rename max_chunk_words to max_segment_words, drop one-off pilot scripts

* feat: add CompressorStep and search_v2 dual-mode session compression

- Add CompressorStep (reme/steps/evolve/compressor.py) for direct LLM
  text compression with optional query-guided relevance filtering
- Extend search_v2_step to support query-aware and query-independent
  session transcript compression via _compress injected kwargs
- Refactor _source_format.py: split into render_chunk_entries +
  join_chunk_entries; session chunks now render line-aligned with
  L<n>: prefixes for verbatim/compressed parity
- Add JOB_TOOLS and INJECTED_JOB_KWARGS to BaseAgenticAnswerStep for
  per-subclass tool and parameter injection
- LmeAgenticAnswerStep injects _search._compress payload to enable
  query-aware compression during benchmark evaluation
- Record compression ablation results in result-longmemeval.md
- Add unit tests for CompressorStep and search compression paths

* refactor(compress): relax session compression to lenient format-preserving strategy and update LME results

* refactor(benchmark): make session compression config-driven via compress_session flag

Move session-transcript compression from LME hard-coded injection to a
runtime context flag set by evaluation.compress_session in each
benchmark config. Compression is off by default for both BEAM and LME,
and BaseAgenticAnswerStep now conditionally injects the _search compress
payload only when the flag is truthy.

* feat(lme/auto_memory): add source attribution markers with line numbers

Add _format_history to annotate each turn with [Ln] line numbers and
expose {session_file} in prompts so the agent can emit bare wikilink-style
source markers like [[session/dialog/s1.jsonl#L1-L2,L5-L6]] at the end
of factual entries. Consolidate the per-prompt body/format rules into
references to the system prompt to avoid drift, and add frontmatter-
protection guidance for the edit tool.

* feat: improve agentic answer prompt and update beam 100K results

- Strengthen abstention rule: prohibit extrapolation from related but
  non-direct evidence
- Add multi-angle search after preliminary answer to check for
  conflicting/supplementary/updated information
- Add max-iteration fallback to 'Information not found'
- Update beam.md with 100K results (agentscope 2.0.4.post1, from scratch)
  including per-type token consumption and memory construction stats
- config.yaml: 100K dataset, 20 workers for BEAM evaluation
- run.py: add memory construction token usage tracking (default agent)
- Overall: 0.635 → 0.654 (+0.019), contradiction_resolution: 0.338 → 0.478
  (+0.140), abstention: 0.500 → 0.525 (+0.025)

* feat(read): add session-aware formatting for read tool and update BEAM eval

- Add truncate_session_output in _file_io.py to render jsonl session
  lines as [speaker @ time] content before byte-budget truncation
- Add read_step_format_session flag to ReadStep, honoring injected
  job kwargs (precedence) and YAML fallback
- Inject read_step_format_session=True into BaseAgenticAnswerStep
  so agentic answer reads render session transcripts human-readably
- Refine BEAM agentic_answer prompt: continue multi-angle search
  after preliminary answer, forbid fabrication/extrapolation
- Update BEAM config to 1M variant and add sequential 100K-eval /
  1M-build shell script
- Refresh benchmark/results_md/beam.md with latest results

* chore(config): disable expand_links in beam and lme search_v2 configs

* refactor(beam): drop one-off sequential 100K-eval-then-1M-build script

* fix(benchmark): add compressor job to beam config and fix BEAM clone instructions

- Add compressor job and compressor as_llm component to reme/config/beam.yaml
  (aligned with lme.yaml) so that compress_session: true works for BEAM
- Add graceful degradation guard in search_v2._compress_session_entries:
  when the compressor job is missing from the active config, log a warning
  and skip compression instead of raising 'Job compressor not found'.
  Skipped when there is no app_context so unit tests mocking run_job still
  drive compression behavior.
- Fix BEAM download instructions in README.md/README_ZH.md: add mkdir -p
  before cd benchmark/beam/dataset (the directory is gitignored and absent
  in a fresh clone)

* fix(steps): guard compressor exceptions and fix ReadStep boolean override

1. search_v2: catch per-entry exceptions from run_job('compressor') inside
   compress() so asyncio.gather never propagates a compressor failure (e.g.
   temporary LLM outage). The failing entry keeps its original body while
   remaining entries are still compressed, preserving already-retrieved
   search results.

2. read: replace 'context_value or yaml_value' with an existence check so
   that a runtime-injected False can explicitly disable a YAML-true
   read_step_format_session flag.

Add focused unit tests for both paths.

* fix(search_v2): use existence check for strict_date_filter boolean override

Replace 'context_value or yaml_value' with an existence-based check so
that a runtime-injected False can explicitly disable a YAML-true
strict_date_filter flag, consistent with the read_step_format_session fix.

* refactor(search): simplify strict_date_filter fallback to truthiness-or

* style(test): rename unused param to satisfy pylint W0613

---------

Co-authored-by: sa-buc <jiangniurou.xyf@dail-algo011164204033.ET135>
2026-08-05 19:23:42 +08:00
jinliyl
e256c556ca
feat: add workspace web APIs and star growth report (#416) 2026-08-05 18:03:37 +08:00
Eucalyptus
d2b8872f2e
docs: link ExpG news entry to toolmemory README (#415)
Make "Experience-driven enhancement method" point to the archived benchmark page while keeping the arXiv link.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 17:00:19 +08:00
jinliyl
eac8223387
feat: add frontend-ready wikilink graph APIs (#414) 2026-08-05 16:45:50 +08:00
Eucalyptus
dc7df26e95
docs(benchmark): add toolmemory archive (#413)
* docs(benchmark): archive ExpG tool-use results under toolmemory

Add ToolMemory benchmark materials and link them from the root and
benchmark READMEs so ReMe documents the ExpG tool-use enhancement work.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: point ToolMemory news entry directly to the paper

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(benchmark): address ToolMemory review and pre-commit

Restore benchmark index READMEs, link ExpG to WangCan1178/ExpG instead
of ReMe version notes, and format tool_memory.py for CI hooks.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(benchmark): align ToolMemory client with official ReMe APIs

Drop ExpG-only request fields and non-official metadata handling so the archived client matches add/summary/retrieve Tool Memory endpoints.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(benchmark): fix trailing whitespace in ToolMemory READMEs

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 16:03:03 +08:00
jinliyl
a9ec334adc
feat: simplify wikilink semantics and support line anchors (#412)
* feat: simplify local links and support line anchors

* fix: align line anchor tests with CI lint

* fix: preserve local links across file moves

* fix: encode markdown paths when rewriting links

* refactor(read): keep explicit line range parameters

* fix: simplify legacy link predicate compatibility

* docs: align local link behavior with implementation

* fix: skip unsupported markdown destination escapes

* fix: normalize workspace link paths across platforms

* fix: bound markdown link scanning

* fix: keep local link processing linear

* docs: clarify permissive markdown link parsing

* fix: handle local link processing failures

* refactor: limit file links to wikilink syntax

* docs: align wikilink contract with implementation

* fix: normalize dream and neighbor paths on Windows

* fix: resolve workspace path for neighbor expansion
2026-08-05 11:47:50 +08:00
xyf2020
6b035c6553
feat(evaluation): track job calls and agent token usage in benchmarks (#406)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* feat(counter): extend counter tree utils and record job call statistics

- replace global_counter_next with fetch-and-add style global_counter_add/inc, plus read-only global_counter_get and global_counter_get_all
- record per-job call counts in app_context.metadata via BaseJob._record_call, covering background/cron/stream jobs
- update agentic_answer step and utils exports; add unit tests for job counting and counter utils

* feat(evaluation): add check_job_count interface and report search calls in benchmarks

- Extract _counter_key from BaseJob._record_call for reusable counter lookup
- Add reme.utils.evaluation_interface.check_job_count read-only helper
- Track and report average search calls per query in beam and longmemeval benchmarks

* job counter

* token消耗量统计

* benchmark输出完整token消耗统计

* benchmark统计输出改用标准差

- beam/longmemeval 的工具调用与 token 统计由方差改为标准差输出
- 修复 lint: 局部变量遮蔽 importlib.metadata、补充测试 docstring
- black 格式化

* fix(evaluation): preserve complete token usage metrics

* fix: exclude stream replies from token accounting

* Revert "fix: exclude stream replies from token accounting"

This reverts commit 85bf32064d.

* Reapply "fix: exclude stream replies from token accounting"

This reverts commit 6722c24dc5.

* support agent scope 2.0.5

* feat: support injection_config to disable runtime state injection in benchmarks

- Add InjectionConfig passthrough in AsAgentWrapper.reply()
- Disable inject_runtime_state in BaseAgenticAnswerStep to avoid
  wall-clock time conflicting with benchmark query_time anchors
- Disable inject_runtime_state in beam/lme llm_judge calls

* feat: agentscope dual-version compat & benchmark improvements

- Add version_tuple utility for semantic version comparison
- AsAgentWrapper: version-aware InjectionConfig, max_iters doubling,
  and token usage collection (reply vs reply_stream) for AS>=2.0.5/<2.0.5
- Default inject_runtime_state=False in wrapper to avoid benchmark
  time-anchor conflicts; remove per-callsite injection_config overrides
- longmemeval run.py: support question_ids filter in dataset config
- Fix unused import in test_evaluation_interface; format fixes

* chore: remove temporary flip-test benchmark config

* revert: pin agentscope to 2.0.4.post1 and drop dual-version compat

* fix(evaluation): clarify usage semantics and atomic counters

---------

Co-authored-by: sa-buc <jiangniurou.xyf@dail-algo011164204033.ET135>
Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com>
2026-08-04 11:42:18 +08:00
Sen Huang
3d487d8d45
docs: fix ReMe documentation links (#408)
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Tests ReMe / Unit Tests - py3.11 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.12 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.13 (push) Has been cancelled
Windows Smoke / CLI smoke - py3.11 (push) Has been cancelled
2026-07-31 14:52:30 +08:00
Sen Huang
f3d32e203d
feat: add mail component enum (#405)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* feat: add mail component enum

* fix format

---------

Co-authored-by: jinliyl <6469360+jinliyl@users.noreply.github.com>
2026-07-30 14:48:20 +08:00
Sen Huang
550317c3bf
Revert "feat(plugin): add ReMe integration for Codex (#372)" (#400)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
This reverts commit a367c2ce13.
2026-07-29 18:14:05 +08:00
DiegoCluv7
a367c2ce13
feat(plugin): add ReMe integration for Codex (#372)
* feat(plugin): add ReMe integration for Codex

* fix(plugin): fix Codex plugin port, transcript ingestion, and Windows support

* fix(plugin): correct Codex transcript schema, path validation, and hook fixes

* test(plugin): add MCP round-trip tests

* fix(plugin): rewrite parser and tests.

* fix(plugin): reserve id-less messages, cover marketplace manifest, error handling, path fixes, and main sync

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-29 18:11:29 +08:00
jinliyl
c937be9d94
refactor(auto_fin): normalize data models and selection logic across agents (#396)
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Tests ReMe / Unit Tests - py3.11 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.12 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.13 (push) Has been cancelled
Windows Smoke / CLI smoke - py3.11 (push) Has been cancelled
- Introduce tolerant AutoFinAgentModel base class allowing extra fields in raw Agent outputs
- Replace strict models with tolerant ones for ETF, historical event, market selection, and report outputs
- Remove redundant field validators and allow empty defaults for key string fields
- Enhance historical source path resolution to safely filter invalid or out-of-workspace paths
- Add normalization of whitespace and validation to historical event references before processing
- Implement normalization in Topic and Market Agent selections to eliminate duplicates, blanks, unknowns
- Limit Topic Agent output to top 20 ETFs and ensure sorting and deduplication of events
- Normalize final Markdown report by removing redundant headers and providing safe fallbacks
- Update agent prompts to clarify task constraints and improve instruction consistency
- Add extensive tests for normalization, filtering, and safe source resolution for historical events
2026-07-27 20:15:27 +08:00
xyf2020
4eb2adf961
feat(faiss_file_store): upgrade FAISS to HNSW index with async reindex (#390)
* feat(file_store): upgrade FAISS to HNSW index with async reindex and path constraint

- Replace IndexFlatIP with IndexHNSWFlat for better recall/speed tradeoff
- Add dynamic efSearch (limit * 5) scaled to query request size
- Add async_reindex option: background rebuild with generation-based invalidation
- Extract _delete_nodes() in LocalFileStore for subclass reuse
- Add unit tests for file store consistency

* fix: resolve pylint warnings in faiss store and test file

* refactor(file_store): replace generation-based reindex with event-flag worker

- Replace _reindex_generation/lock/task with a single long-lived worker
  coroutine consuming an asyncio.Event flag; repeated submissions coalesce
- Use local index reference in vector_search to avoid TOCTOU on self._faiss_index
- Pass index explicitly to _set_ef_search for consistency
- Track _index_writes to re-arm reindex after concurrent writes
- Update tests to match new internal API

* fix: resolve pylint too-many-return-statements and implicit-booleaness warnings

* feat(file_store): add refine maintenance hook and incremental embedding backfill

- Add refine() idle-time maintenance hook to BaseFileStore/LocalFileStore
- FaissLocalFileStore: incremental vector add on backfill instead of full rebuild
- Dynamic tombstone compaction threshold scaled by index size
- Add RefineStoreStep with daily cron job (refine_store_cron)
- Enable faiss backend and embedding_store by default in default.yaml
- Add unit tests for faiss index maintenance

* chore(deps): promote faiss-cpu to core dependencies

faiss backend is now the default file_store, so faiss-cpu moves from
the optional [core] extra to the base dependencies list.

* feat: rename refine_store to optimize_index and add vecdb_path_constraint

- Rename refine_store step to optimize_index with cron job scheduling
- Add vecdb_path_constraint to file_store components
- Update default.yaml with optimize_index_cron and faiss backend comment
- Update memory_search docs (en/zh) for FAISS vector management
- Update unit tests for index maintenance

* feat(faiss): add embedding digest to reject stale sidecar after partial dump

Add _chunks_embedding_digest() that computes an order-independent SHA-256
over (chunk_id, float16 embedding) pairs. The digest is written into the
idmap sidecar at dump time and verified at load time. A mismatch means the
sidecar vectors belong to a different chunk generation than the authoritative
JSONL — detectable even when the live-ID set is unchanged (same-ID in-place
update crash window).

Add test_faiss_rejects_stale_sidecar_after_partial_dump reproducing the
crash-between-writes scenario and asserting digest-based rejection.

Compress verbose docstrings/comments in existing tests for pylint line
budget.

---------

Co-authored-by: sa-buc <jiangniurou.xyf@dail-algo011164204033.ET135>
2026-07-27 19:54:38 +08:00
xyf2020
f34dcdb09b
feat(Step tools): add white/black path prefix permission filtering to read, edit, write (#391)
* feat(read): add white/black path prefix permission filtering to ReadStep

* feat: add PrefixCheck mixin for path-prefix permission in file I/O steps

* feat: add injected_job_kwargs mechanism and refine path-prefix permission

* refactor(file_io): consolidate prefix_check into _path module
2026-07-27 17:20:21 +08:00
jinliyl
2f79977df0
refactor(auto_fin): replace similarity with direction classification for historical events (#395)
- Add AutoFinHistoricalDirectionReference model to classify historical events by direction
- Remove AutoFinHistoricalSimilarity and related similarity score usage
- Update AutoFinMarketSelection to handle same and opposite direction event lists
- Adjust AutoFinMarketStep to calculate forecasts based on equal weights and direction signs
- Change market.yaml instructions to require direction classification instead of similarity scoring
- Modify tests to reflect direction-based classification and verify uniqueness across direction groups
- Improve DingTalkWaitStep to support reconnect on server request with proper disconnect reason handling
2026-07-27 11:52:23 +08:00
Amir Fathi
0522135791
fix(file_io): stop ReadStep small-file path over-counting total lines by 1 (#389)
content.split("\n") yields a trailing empty element for any file ending in a
newline, inflating total by 1 and letting a start_line one past real EOF be
silently accepted instead of rejected. Mirrors the trailing-newline correction
default_file_chunker already applies, and matches the large-file path's
line-by-line count.

Fixes #388
2026-07-27 11:01:09 +08:00
jinliyl
11fe50d89c
refactor(auto_fin/history_search): improve historical event resolution and error handling (#394)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
- Separate candidate source file resolution from event resolution logic
- Allow fallback to date-derived daily news file if original source is missing
- Check existence and validity of historical source files more robustly
- Handle multiple candidate source files and aggregate matches before validation
- Gather and log resolution limitations without stopping processing
- Return resolved events along with a list of resolution warnings
- Update related code to consume new return signature and merge limitations
- Add detailed validation on source path relativity and file naming conventions
2026-07-26 19:00:46 +08:00
jinliyl
1687179f84
feat: add Auto Fin cookbook and managed outbound proxy support (#392)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* feat: add ssh proxy

* feat: add ssh proxy

* feat: add ssh proxy

* feat: add ssh proxy

* feat: add prompt

* feat: add agent wrapper

* feat: add agent wrapper

* feat: add agent wrapper

* feat: add tushare skill

* feat: add tushare skill

* feat: add tushare skill

* feat: add none stream

* chore(deps): update dependency versions in pyproject.toml

- Bump claude-agent-sdk from 0.2.123 to 0.2.126
- Upgrade pre-commit to version 4.6.1 or higher
- Upgrade pytest to version 9.1.1 or higher

* feat(agent_wrapper): add session compaction support and unify session commands

- Introduce compact_session method to BaseAgentWrapper and implement it in AsAgentWrapper, CcAgentWrapper, and CodexAgentWrapper
- Add session_command module with SessionCommandResult dataclass and handle_session_command function for /clear and /compact commands
- Update __init__.py exports to include session_command handlers
- Modify DingTalkWaitStep to handle session commands via handle_session_command function
- Remove streaming mode from DingTalkWaitStep and simplify reply handling to final Markdown replies only
- Add unit tests for session compaction methods and session command handling across wrappers and DingTalk integration
- Clean up and remove obsolete streaming and card rendering code from DingTalk wait step
- Adjust daily_cookbook.yaml to remove stream and card_update_interval config entries for DingTalk wait step

* feat(auto_fin): add Auto Fin simulated portfolio cookbook workflow

- Add comprehensive Auto Fin schema exports for multiple models and enums
- Implement base class and helpers for Auto Fin analysis steps
- Create file, state, and formatting utilities for Auto Fin with atomic file writes and locking
- Define Auto Fin pipeline with four analysis agents: backtest, event, portfolio, and US correlation
- Register Auto Fin package in cookbook workflows and schema initialization
- Add detailed documentation in markdown describing the system design, workflow, and data contracts

* feat(outbound_proxy): add application-scoped outbound HTTP proxy components

- Introduce BaseOutboundProxy and OutboundProxyEndpoint as core contracts
- Implement FixedHttpOutboundProxy for external HTTP proxy integration
- Add SshHttpOutboundProxy providing SSH-backed local HTTP proxy tunnels
- Register outbound proxy components in component registry and enumeration
- Update components package to include outbound_proxy module
- Add dependency on pproxy for SSH HTTP proxy bridging
- Include comprehensive unit tests covering proxy lifecycle, validation,
  environment merging, error handling, readiness, and monitoring mechanisms

* refactor(network): replace SSH proxy with explicit HTTP outbound proxy

- Remove SSH proxy helper implementation and references in codebase
- Add support for explicit HTTP proxy URL in arXiv and HuggingFace clients
- Modify clients to use async context manager for consistent resource handling
- Update daily paper steps to forward outbound proxy configuration explicitly
- Change tests to cover new proxy usage model and remove SSH proxy mocks
- Add outbound proxy component configuration in daily_cookbook.yaml
- Ensure proxy URL usage disables environment trust in HTTP clients
- Fix app context component enum access to be defensive against missing keys

* feat(agent_wrapper): add managed proxy support for command environments

- Introduce BaseOutboundProxy binding in BaseAgentWrapper for outbound proxy management
- Add bash_environment and command_proxy_environment properties to apply proxy settings
- Update WorkspaceBackend instantiation in AsAgentWrapper to use bash_environment
- Inject managed proxy export commands into Claude Code Bash commands via hooks
- Enhance CodexAgentWrapper to include managed proxy in shell environment policy
- Modify daily_cookbook.yaml steps to specify outbound_proxy as default where needed
- Add comprehensive unit tests verifying managed proxy injection and environment isolation
- Ensure subprocess_environment remains unchanged while proxy is applied selectively to commands

* refactor(memory): replace search job_tools with memory in daily cookbook config

- Change workspace_dir default from .reme to reme_workspace
- Replace search job_tools with memory across multiple components and jobs
- Update descriptions to reflect long-term memory retrieval instead of search
- Modify system prompts to instruct using memory for retrieving notes
- Adjust unit tests to verify memory job_tools and job presence instead of search
- Ensure consistency in configuration and tests for memory backend usage

* refactor(config): rename memory to memory_search in daily cookbook config

- Change all occurrences of "memory" to "memory_search" in job_tools and job definitions
- Update related system prompts to reflect the new memory_search terminology
- Modify unit tests to assert the presence of memory_search instead of memory
- Ensure consistency across skills, job tools, and backend configurations in multiple components

* feat(auto_fin): add deterministic quantitative research and ranking fusion

- Introduce new schema models: EtfScore, RankingMetrics, ExtremeAnalysis,
  DimensionRanking, and FusionRanking to represent deterministic research outputs
- Add ranking data to event, backtest, us_correlation, and portfolio analysis outputs
- Implement ranking_section renderer to format Top20 scores and diagnostics in Markdown
- Develop AutoFinQuantStep for deterministic ETF ranking using TuShare data, Polars,
  and a custom extremely randomized tree ensemble
- Integrate quantitative rankings into backtest and portfolio analysis steps and reports
- Extend auto_fin pipeline with new quant_enabled and quant_required config options
- Enforce ranking constraints like unique codes, contiguous ranks, and normalized fusion weights
- Update analysis YAMLs with rules limiting data freshness, universe, and ranking usage
- Incorporate ranking outputs into all major markdown report bodies in Auto Fin pipeline
- Add concurrency-limited asynchronous TuShare client to fetch required market data
- Introduce cross-sectional rank correlation and NDCG metrics for ranking quality evaluation

* feat(auto_fin): implement stage-wise notification and reporting for analysis pipeline

- Refactor notification config in daily_cookbook.yaml to support dispatch steps
- Update AutoFinNotificationStep to deduplicate notifications per run stage
- Add _notify_stage method in pipeline to send notifications for each analysis stage
- Implement persistence and notification for event, backtest, US correlation, and portfolio stages
- Modify pipeline flow to persist reports and notify after each stage completion
- Adjust metadata to track notifications and errors per stage
- Update tests to verify stage-wise notification sending and deduplication
- Remove older combined report persistence in favor of modular stage handling

* feat(auto_fin): add outbound proxy support for Tushare API usage

- Introduce BaseOutboundProxy reference in AutoFinPipelineStep and AutoFinQuantStep
- Update TushareResearchClient and trade calendar fetch to accept and use proxy URL
- Create _ProxiedTushareApi adapter to route Tushare requests via explicit HTTP proxy
- Modify create_tushare_api utility to optionally return proxied API client
- Add unit tests covering proxy forwarding and client behavior with managed proxies
- Ensure proxy usage respects explicit proxy URL over environment fallback
- Integrate outbound proxy into data fetching and quantitative research steps

* feat(auto_fin): enforce checkpoint time validation and add state models

- Introduce AnalysisState base class and specific states for event, backtest, and US correlation analyses
- Replace analysis output types with corresponding state classes in run schemas
- Add require_checkpoint_reached method to validate decision_at/data_cutoff against current time
- Enforce checkpoint time checks before analysis steps in event, backtest, portfolio, and quant analyses
- Refactor quant data loading to include adjustment factors and apply price adjustments without fallback
- Update analysis YAML docs to require real-time checkpoint validation and forbid using future data
- Improve portfolio run serialization by excluding redundant legacy fields and nested proposed actions
- Add helper to extract readable sections from persisted checkpoint documents
- Fix event analysis output validation to reject events and sources with future timestamps

* feat(auto_fin): auto-select latest reached checkpoint if none specified

- Extend checkpoint config to accept empty string for auto selection
- Add static method to compute latest checkpoint reached by current time
- Modify pipeline step to auto-select checkpoint based on trade calendar and time
- Adjust force flag default depending on whether checkpoint is explicit or auto
- Log details when checkpoint is auto-selected to improve observability
- Add comprehensive tests for auto checkpoint selection logic and edge cases
- Remove deprecated default and required constraints from force parameter in config

* refactor(auto_fin): unify datetime comparison with compare_datetimes utility

- Replace direct datetime comparisons with compare_datetimes function calls
- Use cmp_to_key with compare_datetimes for sorting datetime tuples and lists
- Update validation logic in backtest, event, analysis, and ledger modules for consistent datetime handling
- Add unit tests to verify handling of naive and aware datetime comparisons in event and backtest validations
- Ensure marked_at and interval_end timestamps are set and compared consistently using compare_datetimes
- Improve correctness of ordering and conditional checks related to timestamps throughout auto_fin steps and ledger code

* feat(auto_fin): add datetime comparison helper for mixed timezone data

- Implement compare_datetimes function to handle naive and aware datetimes
- Ensure naive datetime is interpreted in the known timezone of the counterpart
- Facilitate comparisons between legacy and timezone-aware Auto Fin data
- Add module docstring explaining purpose of the helpers

* docs(auto_fin): enforce unique ETF representative per sub-theme in analysis rules

- Update backtest.yaml to recommend or highlight only one ETF per sub-theme for ETF analyses
- Modify event.yaml to map only one representative ETF per sub-theme, avoiding duplicate recommendations
- Revise portfolio.yaml to restrict holdings/buys to a single ETF per sub-theme, preventing repeated buys of highly overlapping ETFs
- Adjust us_correlation.yaml to retain only one representative A-share ETF per sub-theme for mapping or recommendation
- Add test to verify presence of new sub-theme uniqueness guidance in step prompts

* feat(auto_fin): separate draft model and include deterministic fusion ranking

- Introduce _PortfolioProposalDraft pydantic model for agent-authored fields before ranking
- Discard any "fusion_ranking" data from draft to prevent conflicts with canonical ranking
- Modify AutoFinPortfolioStep to receive draft, enrich with fusion_ranking, and produce final output
- Update tests to use _PortfolioProposalDraft and validate deterministic fusion ranking propagation
- Add async test verifying fusion ranking is correctly set in portfolio output with no errors

* refactor(auto_fin): rewrite and simplify Auto Fin schema and steps

- Remove legacy Auto Fin analysis step modules and helpers
- Replace complex ranking and portfolio models with simplified current-news models
- Update schema to focus on news-case workflow with new domain models
- Remove A-share decision checkpoints and backtest details from schema
- Simplify recommendation and decision output structures
- Clean up deprecated state and utility functions
- Update Auto Fin steps initialization to new pipeline steps only
- Improve uniqueness validation for themes and ETFs in research plan

* feat(auto_fin): implement full local cache and analysis workflow for Auto Fin

- Add AutoFinDataStep to prepare and cache daily TuShare data with lookback
- Add AutoFinAnalysisStep to analyze cached data and generate Markdown report
- Implement detailed time window, ETF filtering, and historical case validation
- Introduce YAML prompts for planning and decision-making steps
- Update .gitignore to include reme_workspace/
- Clean up config and import structure for auto_fin steps
- Remove old pipeline.py and consolidate functionality into new modules
- Use polars for efficient CSV reading and data processing
- Ensure atomic writes and strict JSON serialization for cache files
- Enforce rules on news timing, ETF universe, and historical case usage

* fix(auto_fin): restrict news data source to '财联社' in analysis and cache

- Update analysis templates to specify current news as from '财联社' only
- Modify news fetching functions to filter by source '财联社'
- Add validation method to check cached news source correctness
- Update news caching logic to exclude non-'财联社' news
- Enhance unit tests with multiple sources to ensure filtering works
- Confirm news API calls include source filter parameter as '财联社'

* refactor(auto_fin): convert I/O methods to asynchronous implementations

- Change _news, _dataset, and _theme_data methods to async for improved concurrency
- Move JSONL and CSV reading operations to asynchronous wrappers using asyncio.to_thread
- Remove synchronous _read_jsonl and _read_csv functions, integrate them as static async class methods
- Update cache validation methods to async, awaiting I/O operations accordingly
- Adjust usage of dataset and news retrieval in analysis step to await asynchronous methods
- Add async unit test to validate JSONL reading with unicode line separators
- Preserve existing functionality while enabling non-blocking file and data access

* fix(nx_file_graph): defer networkx import and improve dependency handling

- Move networkx import inside NxFileGraph constructor for lazy loading
- Raise ImportError with original exception context if networkx is missing
- Remove module-level fallback assignment of nx to None
- Expand test to block loading of multiple optional core dependencies eagerly
- Change exception type in test from ModuleNotFoundError to AssertionError
- Update test comments to reflect broader optional dependency checks

* feat(embedding_store): add quota retry delay mechanism for embedding requests

- Introduce quota_retry_delay parameter to configure wait time before retry on quota exhaustion
- Implement detection of insufficient quota errors in LocalEmbeddingStore without external SDK
- Add retry logic with custom delay when quota is insufficient during embedding requests
- Update configuration to set max_retries and quota_retry_delay defaults for embedding store
- Add unit tests covering quota exhaustion retry behavior with delay and opt-in control
- Ensure existing retry behavior remains unchanged if quota_retry_delay is not set

* feat(auto_fin): add detailed logging to analysis and data fetching steps

- Add _preview static method for bounded diagnostic output in analysis.py
- Log prompt start, completion, errors, and validation details in _reply method
- Add info logs for major processing steps in execute method of analysis.py
- Add debug and info logs for cache validation, data fetching, and pagination in data.py
- Log conditions for skipping reports and cache plans in data.py execute method
- Log download summaries and cache writes for news and ETF data
- Improve error logging with exception details in cache validation functions
- Ensure all logs include context such as record counts, paths, and parameters

* refactor(auto_fin): overhaul Auto Fin workflow and schema contracts

- Replace old Auto Fin schema models with comprehensive new data classes
- Remove legacy Auto Fin analysis step in favor of modular agent-based steps
- Introduce AutoFinAgentStep for validating structured agent replies
- Simplify data cleaning and JSONL writing utilities for news cache
- Remove synchronous and asynchronous dataset methods from analysis step
- Redefine Auto Fin analysis configuration for 360-day news retention and multi-step pipeline
- Remove embedded analysis prompt templates and replace with agent-driven logic
- Update __init__.py exports to match new step implementations and remove deprecated classes
- Improve error handling and validation in agent step reply processing
- Clean up redundant imports and unused code in analysis and data preparation modules

* feat(auto_fin): add detailed logging for analysis and data processing steps

- Add timing logs to measure agent prompt processing duration in analysis.py
- Log news cache hits and news write paths with record counts in data.py
- Include detailed info logs for news download start and completion in data.py
- Add start, progress, and completion logs with topic and event counts in history.py
- Log start and completion of merge step including path and ETF count in merge.py
- Add start and done logs with window and news counts in topic.py

* feat(auto_fin): enhance schema and steps with detailed ETF and event modeling

- Replace and add multiple AutoFin schema classes to support detailed ETF selection,
  historical research, market analysis, forecast models, and report output with validation
- Implement Shanghai timezone normalization and strict validation in schema models
- Remove deprecated AutoFin analysis agent step and consolidate reply handling in base step
- Introduce AutoFinStep base class with shared helpers for prompt handling, data fetching,
  logging, and JSONL file operations
- Add AutoFinDataStep to manage daily news data complete with schedule validation, caching,
  and source validation logic
- Update cookbook configuration to customize auto_fin step parameters and simplify
  outbound proxy settings
- Refactor imports and clean unused code for better maintainability

* feat(auto_fin): introduce detailed historical event resolution and market similarity analysis

- Add AutoFinHistoricalEventReference and AutoFinHistoricalSimilarity models for refined event referencing and similarity judgment
- Implement validation to ensure non-empty critical fields and uniqueness of historical news IDs
- Develop method to resolve Agent-selected historical event references from workspace files with strict path and existence checks
- Enrich historical events with market entry and future returns data after resolution
- Redesign market step to calculate similarity-weighted ETF forecasts based on matched historical event similarities
- Enforce validation on matched historical events for uniqueness and proper weight summation
- Simplify merge step output to final Markdown report without YAML frontmatter and redundant fields
- Update user instructions for history search, market, and merge steps to reflect new data structures and responsibilities
- Adjust test suite to cover new schema and step behavior changes, including enhanced validation and JSON output formats

* feat(auto_fin): add new cron jobs and output analysis jsonl

- Add new cron jobs auto_fin_1145_cron and auto_fin_1800_cron with auto_fin_steps
- Change auto_fin_0930_cron schedule to run Monday to Sunday
- Extend merge step to write analysis data to auto_fin_analysis.jsonl
- Update unit tests to verify new cron jobs and their steps configuration

* fix(auto_fin): improve atomic file write and refresh daily index

- Change temporary file naming to include UUID for uniqueness and hidden prefix
- Replace atomic write method from using Path.replace to os.replace with safe unlink
- Add import and use os.replace for safer file replace operation
- Refresh daily index after writing auto finance markdown and JSONL files
- Import and call refresh_day_index in merge step to update file index asynchronously

* docs(cookbook): add optional SSH proxy configuration in README files

- Introduce optional SSH proxy setup in auto-fin and daily_paper cookbooks
- Provide instructions to enable outbound proxy via `daily_cookbook.yaml` and environment variables
- Add `REME_PROXY_IP` and `REME_PROXY_ACCOUNT` environment variables descriptions in multiple README files
- Update English and Chinese README and README_ZH documents with proxy details
- Maintain consistent formatting of environment variable tables across documents

* fix(file_io): include schema_version in hidden metadata keys

- Added "schema_version" to _INDEX_HIDDEN_METADATA_KEYS in _daily_index.py
- Updated _render_notes_block to always include additional keys regardless of schema_version

fix(deps): move pproxy dependency to later in pyproject.toml

- Removed pproxy from early dependencies list
- Added pproxy back near the end of dependency list for better ordering

fix(outbound_proxy): require pproxy package for ssh_http proxy

- Added importlib.util check for pproxy package presence
- Raise RuntimeError if pproxy is not installed when using SSH HTTP outbound proxy
- Improved error message suggests installing reme-ai with 'core' extra

* docs(readme): update News section with new Cookbook workflows

- Clarify introduction of optional Cookbooks with Daily Paper and Auto Fin workflows
- Update English README to reflect both paper discovery and file-native ETF event research
- Revise Chinese README to include financial news and historical market data research capability
- Maintain announcement of paper acceptance at Findings of ACL 2026

* feat(auto_fin): add calculation results to final Markdown output

- Implement _calculation_results to summarize forecast for each ETF analyzed
- Include program-calculated results in the JSON input for the Markdown report
- Update YAML template to incorporate calculation results and adjust recommendation rules
- Refine recommendation logic to rely on event impact judgments combined with calculation outputs
- Modify tests to verify presence of calculation results and updated report content and format

* up prompt

* fix(keyword_index): ignore non-indexable chunks during keyword sync

- Add is_indexable method to base and BM25 keyword index classes to check text tokenizability
- Update local file store to exclude non-indexable chunks from expected document IDs to prevent rebuild
- Fix JSONL chunker to correctly handle Unicode line separator U+2028 inside JSON strings without splitting
- Add test to ensure non-empty but non-indexable chunk does not trigger keyword index rebuild
- Add test to verify U+2028 character does not cause incorrect JSONL record splitting
2026-07-25 18:09:39 +08:00
jinliyl
46adb5ae1e
feat: add daily paper cookbook and DingTalk agent integration (#385)
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Tests ReMe / Unit Tests - py3.11 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.12 (push) Has been cancelled
Tests ReMe / Unit Tests - py3.13 (push) Has been cancelled
Windows Smoke / CLI smoke - py3.11 (push) Has been cancelled
* feat(daily-paper): add daily paper cookbook workflow with schema and tests

- Introduce daily paper schema types (DailyBriefOutput, PaperInfo, PaperNoteOutput, etc.)
- Create daily paper cookbook module with analyze, collect, digest, rank, and select steps
- Add cookbook entry point and integrate into main steps module
- Replace job config export with daily brief output in schema exports
- Add comprehensive unit tests covering pipeline, filtering, and output generation
- Update dependencies including openai-codex and pypdf packages
- Configure standalone daily paper cron job with proper scheduling and routing

* test(daily_paper): update tests to use Claude Code wrapper exclusively

- Add test to verify web search is disallowed by default in Claude Code
- Update imports to include DailyBriefOutput, PaperNoteOutput, and PaperSelection schemas
- Change test name from standalone_config_has_backend_split to reflect Claude Code only usage
- Remove default agent wrapper and configure all steps to use Claude Code wrapper
- Rename select_wrapper to cc_wrapper for clarity and consistency
- Remove duplicate Claude Code wrapper initialization
- Update test assertions to verify output schema usage matches expected sequence
- Remove unused as_llm component from standalone configuration test

* refactor(agent-wrapper): simplify skill resolution logic across all wrappers

- Replace duplicate skill resolution code with centralized _resolve_project_skills method
- Add project_path property with configurable relative path resolution
- Introduce proper validation for skill names and directory existence
- Change Codex wrapper to use project_path instead of workspace_path for skills
- Add SKILL.md requirement validation for project skills
- Remove redundant skill processing logic from individual wrappers

* feat(daily_paper): add daily paper workflow with PDF analysis and brief generation

- Implement shared state management and file helpers for daily-paper steps
- Add PDF download and text extraction capabilities with arXiv integration
- Create paper collection step with Hugging Face weekly/monthly rankings
- Build ranking system using reciprocal-rank fusion with memory keyword scoring
- Add Claude Code integration for paper analysis and detailed note generation
- Implement digest step to create final five-minute brief from detailed notes
- Add configuration for standalone daily cookbook application with cron scheduling
- Create typed schema for paper information, selection, and output formats
- Add atomic file writing with temporary file safety mechanisms
- Implement exclusion logic for previously recommended papers and daily filters

* feat(daily_paper): add DingTalk notification integration and enhance logging

- Integrate DingTalk markdown send step to notify groups about daily paper briefs
- Add comprehensive logging throughout daily paper workflow including start/finish events
- Update daily paper analysis prompt to include code repository context requirement
- Configure DingTalk notification in daily_cookbook.yaml with app credentials
- Add dingtalk-stream dependency for proactive message API integration
- Enhance daily paper README with DingTalk notification section and updated flow chart
- Implement detailed logging for each step including paper processing and agent calls
- Add test coverage for DingTalk markdown sending functionality and configuration
- Update pre-commit config to exclude skills directory from checks
- Add .claude/skills to gitignore for local development environment

* refactor(dingtalk): move dingtalk_stream import to local scope and improve code safety

- Moved global dingtalk_stream import to local scope in send.py to avoid eager loading
- Added dynamic import with error handling for optional dependency cases
- Updated test suite to verify lazy loading behavior works correctly
- Fixed markdown title generation by using safe variable naming in wait.py
- Enhanced test coverage for arxiv PDF download caching functionality
- Updated application context initialization with proper resource directory configuration
- Modified paper metadata to include source PDF path reference in output files

* refactor(daily_paper): remove manifest system and store selection metadata in digest files

- Remove JSON manifest creation and storage functionality
- Store selection data directly in digest file frontmatter instead of separate manifest files
- Add load_saved_selection method to rebuild selection from digest and paper-note metadata
- Update README documentation to reflect new cookbook workflow architecture
- Modify test cases to verify selection metadata in digest files instead of manifest JSON
- Remove unused json import from multiple daily paper modules
- Integrate PaperSelection schema for proper data validation in stored metadata

* docs(daily_paper): add bilingual cookbook guides
2026-07-22 19:17:01 +08:00
xyf2020
630f26b119
feat(search): scoped dedup, session-chunk merge, and unified recall formatting (#384)
* feat(search): add tool_context-scoped chunk dedup with TTL

Introduce _ToolContextDedupMixin shared by search/vector_search/bm25_search
to skip already-seen chunks within one agent tool_context. Per-context state
lives in app_context.metadata with configurable TTL (default 24h).

* feat(search): unify chunk answer rendering with merge and explicit empty messages

- Refactor SearchStep/VectorSearchStep/Bm25SearchStep to share format_chunks_answer for consistent source rendering and adjacent session-chunk merging.

- Distinguish empty results: ALL_RETURNED_MESSAGE when dedup removes everything vs NO_RESULTS_MESSAGE when nothing matched.

- Bump JsonlFileChunker default max_chars to 4000.

- Add unit tests for source-format merge and empty-result messages.

* refactor(config): reorganize file_chunker components and move jsonl max_chars into config

- Register explicit markdown/json/jsonl chunkers in beam.yaml and lme.yaml with markdown options (embed_toc, max_ast_sections, frontmatter handling) and jsonl max_chars=4000.

- Restrict default chunker to txt/log extensions.

- Revert JsonlFileChunker code default max_chars back to 2000; the 4000 value now lives in config.

* chore(benchmark): increase longmemeval num_items from 64 to 500

* refactor(search): split SearchStep into simplified and v2 variants, extract counter utility

- Extract global_counter_next from ApplicationContext into reme/utils/counter.py
  as a standalone function operating on metadata dict with lazy initialization.

- Split SearchStep into two variants:
  - SearchStep (simplified): inline chunk.id dedup, single-branch vector/keyword
    optimization based on vector_weight, inline answer formatting.
  - SearchV2Step (full): preserves _ToolContextDedupMixin with interval-subset-aware
    dedup and format_chunks_answer with session-aware chunk merging.

- Update beam.yaml and lme.yaml to use search_v2_step for benchmark jobs.

- Rename existing search tests to test_search_v2_step_* and add new
  test_search_step_* tests covering the simplified variant.

* fix: normalise missing trailing newline in _build_union_chunk to prevent line collision

* refactor: lazy-init counter tree in ApplicationContext metadata

- Remove hardcoded _counter_tree and _counter_tree_lock initialization
  from ApplicationContext.metadata; rely on lazy initialization in
  reme.utils.counter.global_counter_next on first call
- Set longmemeval num_items back to 500
- Remove obsolete trailing-newline collision tests

---------

Co-authored-by: sa-buc <jiangniurou.xyf@dail-algo011164204033.ET135>
2026-07-22 17:17:23 +08:00
xyf2020
7b1da5a9ee
feat(benchmark): add BEAM & restructure LongMemEval evaluation framework (#375)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* feat(eval): add LongMemEval evaluation framework with tool_defaults date injection

- Add evaluation/longmemeval/ with run.py, config.yaml, and test scripts
- Add reme/config/longmemeval.yaml for evaluation-specific model config
- Add tool_defaults mechanism to as_agent_wrapper for injecting default
  tool kwargs (uses setdefault so LLM-provided values take priority)
- Pass tool_defaults={'daily_write': {'date': day}} in auto_memory to
  ensure notes always use the correct historical date
- Add timestamp interpolation (_interpolate_timestamps) in auto_memory
  for filling missing created_at fields via linear interpolation
- Evaluation pipeline: ingest sessions -> dream -> search -> answer -> judge
- Uses qwen3.6-flash for memory, qwen3.7-max for answer/judge

* chore: gitignore logs/results/demo.py, keep empty dirs

* chore: update .gitignore

* feat(eval): add multiprocessing and session time filtering to longmemeval runner

- Replace async execution with synchronous + multiprocessing for parallel item evaluation - Add filter_future_sessions option to only ingest sessions <= question date - Add question_types filtering in config - Add result summary with binary accuracy and avg score - Update config defaults (oracle variant, 50 items, 32 workers) - Minor code style fixes in agent_wrapper and auto_memory

* feat: add bench_query_step with ReAct agent for benchmark query phase

- Add BenchQueryStep using agent_wrapper with search job tool
- Replace manual search+LLM answer in run.py with bench_query_job
- Remove unused answer LLM config from longmemeval.yaml
- Register benchmark step module in steps/__init__.py

* feat: add start_date/end_date time filter support for search job

- Add _extract_date_from_path to extract validated YYYY-MM-DD from chunk paths
- Add start_date/end_date filtering in _matches_search_filter
- Implement progressive recall in FaissLocalFileStore.vector_search
- Promote start_date/end_date from context to search_filter in SearchStep
- Add start_date/end_date parameters to search job in default.yaml
- Add unit tests for date filter functionality

* fix: validate/normalize date filters and harden _extract_date_from_path

Address three code-review comments on the time_filter search feature:

1. Validate/normalize start_date and end_date before string comparison.
   _matches_search_filter does lexicographic comparison against path_date
   (always canonical YYYY-MM-DD). Raw caller values like '2026-2-28' or
   'abc' would produce silently wrong results. Now SearchStep normalizes
   valid dates via extract_daily_date (with strptime fallback for
   non-zero-padded input) and silently ignores invalid dates with a
   logger.warning, removing them from the filter.

2. Clarify behavior for paths without embedded dates.
   Added optional strict_date_filter parameter (default False). When True
   and at least one date bound is active, chunks whose path yields no date
   (e.g. digest/personal/topic.md) are excluded. When False (default),
   the existing behavior is preserved — dateless paths pass through.

3. Harden _extract_date_from_path against non-standard suffixes.
   Previously parts[1].split('.')[0] accepted '2026-05-18.anything' as a
   valid date. Now only exact 'YYYY-MM-DD' (dir) and 'YYYY-MM-DD.md'
   (day-index) forms are accepted.

* feat(eval): LLM-as-Judge per-type prompt routing, binary-only, progress tracking

- Remove 0-5 score metric, keep only binary (yes/no) classification
- Load per-question-type judge prompts from llm-as-judge.json
  (temporal-reasoning, knowledge-update, single-session-preference, __default__)
- Replace SCORE_JUDGE_PROMPT with type-specific BINARY_JUDGE_PROMPT template
- judge_response(): parameter 'metric' -> 'question_type', returns single 'judgment'
- Summary output: add per-type accuracy breakdown, remove score stats
- Add progress tracking: background thread prints PROGRESS every 10min
- Add FINAL progress line and total elapsed time on completion
- Add --log-level, --reme-log-level, -q CLI arguments
- Parallel mode: pool.map -> pool.imap_unordered for real-time progress
- config.yaml: full oracle (10000 items), 32 workers, all question types
- Add kill.sh (process cleanup) and run_async.sh (background eval launcher)

* docs: add LongMemEval oracle evaluation results (61.6% accuracy)

* feat(bench): add MAX_ITERATION limit to BenchQueryStep and add _auto_memory.yaml

* feat: add golden session benchmark & eval_only mode with refined prompt

- Add benchmark/longmemeval/run_golden_session.py for golden session evaluation
- Refine PROMPTED_SYSTEM_PROMPT: concise answer rule, remove 'Information not found' fallback
- Add eval_only mode to run.py (--eval_only flag)
- Add multiple eval config variants (evalonly, full, test5)
- Add analyze_results.py for result parsing
- Update auto_memory.yaml, longmemeval.yaml, application_config
- Update result-longmemeval.md with latest evaluation results
- Add benchmark results to .gitignore

* update: refine answer prompts and increase max iteration to 6 - Tighten prompted-answer system prompt for more concise output - Comment out 'Information not found' fallback rule - Increase MAX_ITERATION from 5 to 6 in bench_query - Add recall_eval.py - Update evaluation results

* feat(chunker): add dedicated JSON and JSONL file chunkers (cherry-pick from upstream #325)

- Add JsonFileChunker: structure-aware chunking preserving nested key paths,
  optional list-to-dict conversion, size measured by json.dumps() char count
- Add JsonlFileChunker: line-aligned sliding-window chunking with configurable
  overlap, supports char/byte mode switching
- Register both chunkers in default.yaml (json for .json, jsonl for .jsonl)
- Add comprehensive unit tests (21 + 20 test cases)

* feat(service): add CLI service for local job execution (from upstream #334)

- Introduce CliService to execute single jobs locally without serving ports
- Add prepare_start_config and should_precheck_start functions for CLI job setup
- Update reme start command to use CLI service when job argument is provided
- Add show_metadata to client kwargs for optional CLI metadata output
- Add unit tests for CLI service functionality and configuration handling

* feat(steps): add BM25/vector search steps, Python execute step, and draft steps (from upstream #334)

- Add Bm25SearchStep for plain BM25 keyword search with tool_context deduplication
- Add VectorSearchStep for plain vector search with tool_context deduplication
- Add PythonExecuteStep to run Python code in subprocess with timeout handling
- Add AddDraftStep/ReadAllDraftStep for draft accumulation scoped by tool context
- Update SearchStep with tool_context dedup, dynamic default limit via REME_SEARCH_LIMIT env,
  and candidate_multiplier default changed from 3.0 to 5.0
- Add comprehensive unit tests for all new steps

* feat(search): add tool context deduplication and improve search configuration (#321)

* feat(search): add tool context deduplication and improve search configuration

- Modify _make_tool methods to accept and inject tool_context_id parameter
- Add tool_context_id handling in AS and CC agent wrappers
- Increase search candidate multiplier from 3.0 to 5.0 in default config
- Extend HTTP client timeout from 30s to 3600s
- Add tool context deduplication logic to prevent duplicate search results
- Implement TTL-based expiration for seen chunks in tool contexts
- Add comprehensive unit tests for tool context deduplication behavior
- Update .gitignore to exclude longmemeval directory
- Add time import for timestamp functionality in search step

* refactor(search): replace time module with datetime for timestamp generation

- Removed unused time import
- Added static method _now_ts using datetime.timestamp
- Updated clock parameter to use _now_ts method instead of time.time
- Maintained same timestamp precision and functionality

* fix(file_io): fix risk of out-workspace paths (#322)

* fix(file_io): fix risk of out-workspace paths

* chore(file_io): remove unused unittest file

* fix(as_embedding): support both agentscope 2.0.2 and 2.0.3 (#323)

2.0.3 promoted `dimensions` to a required first-class constructor
argument while keeping a backfill from `parameters.dimensions`; 2.0.2
has no such argument and reads `dimensions` from `Parameters`. Keep
`dimensions` in `Parameters` for both versions and, when the model
constructor accepts `dimensions`, pass `dimensions=None` so 2.0.3's
backfill promotes it out of `parameters`.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Bump version to 0.4.0.7

* refactor: delegate LLM-as-Judge to answer_judge_step and update eval config/results

- run.py: replace inline judge logic with judge_response_via_job using app.run_job('answer_judge')
- longmemeval.yaml: expand benchmark configuration
- bench_query.py: enhance benchmark query step
- result-longmemeval.md: update evaluation results
- judge_all_plus_results.json: add judge all-plus results

* refactor: split longmemeval.yaml into lme.yaml/beam.yaml and unify job names

- Split reme/config/longmemeval.yaml into lme.yaml (LongMemEval) and beam.yaml (BEAM)
- Unify job names across both configs: agentic_answer, answer_judge, context_answer
- Update evaluation/longmemeval/run.py and evaluation/beam/run_beam_eval.py to use unified job names
- Update all evaluation config YAMLs to reference lme.yaml
- Add BEAM benchmark step implementations (agentic_answer, context_answer, llm_judge)
- Remove obsolete config_test5.yaml and test_5sessions.py

* eval: BEAM 100K & LongMemEval cleaned-S 评测结果记录

- BEAM 100K eval-only (32并发, 20 case): Agentic 0.631, Prompted 0.468
- LongMemEval final GT (500题): Agentic 89.0%, Prompted 83.6%
- 新增 benchmark/result-beam.md, benchmark/result-longmemeval.md
- benchmark/beam/config.yaml: num_workers=32

* refactor: restructure benchmark directory and clean up gitignore rules

- Consolidate benchmark outputs to benchmark/results/ with .gitkeep
- Remove old benchmark scripts, configs and result files from benchmark/beam/ and benchmark/longmemeval/
- Add datasets/README.md and datasets/README_EN.md with download instructions
- Add datasets/longmemeval/download.py and final_groundtruth_cleaned_s.json
- Add memory_workspaces .gitkeep placeholders
- Restructure .gitignore: fix duplicate entries, add BEAM dataset exclusion, refine logs/results ignore patterns
- Remove stale result-beam.md and result-longmemeval.md from project root

* chore: clean up longmemeval benchmark scripts and update dataset docs

- Remove obsolete longmemeval benchmark runner/stats scripts

- Update datasets/longmemeval README and add Chinese translation

- Clean up final_groundtruth_cleaned_s.json

* docs(benchmark): add reproduction guide for LongMemEval and BEAM

- Add bilingual README for benchmark runners (EN/ZH)

- Cover prerequisites, dataset download, run commands, configs, outputs, logs, and kill.sh

* refactor: migrate auto_memory steps from evolve to benchmark-specific modules

- Split auto_memory into beam and lme benchmark-specific implementations
- Add auto_memory.py and auto_memory.yaml under steps/benchmark/beam and steps/benchmark/lme
- Slim down evolve/auto_memory.py and auto_memory.yaml to shared base only
- Remove obsolete evolve/_auto_memory.yaml
- Update benchmark run.py, config YAMLs, and step __init__.py registrations
- Update llm_judge and context_answer minor adjustments
- Remove outdated test_lme_final_answer_review.py

* revert(as_agent_wrapper): sync with upstream/main

Remove local-only comment to keep file identical with upstream/main.

* style: add trailing commas in benchmark __init__.py __all__ lists

* chore: disable vector_weight range assertion in SearchStep

* chore: add tests/integration/logs/ to .gitignore

* refactor: replace scipy.stats.kendalltau with pure numpy implementation

scipy is not listed in project dependencies. Implement Kendall's tau-b
rank correlation using only numpy to remove the undeclared dependency.

* feat(benchmark): add binary score metrics, update BEAM 1M results, and improve LLM retry/prompt config

- benchmark/beam/run.py: add binary score calculation per rubric item and per-type/overall binary stats
- benchmark/beam/config.yaml: switch to 1M dataset, reduce workers to 18
- benchmark/result-beam.md: add 1M evaluation results with binary scores
- benchmark/result-longmemeval.md: minor formatting
- reme/config/beam.yaml: increase max_retries to 5 and add retry_delay 5.0 for all LLM components
- reme/config/lme.yaml: increase max_retries to 5 and add retry_delay for judge/prompted/bench components
- reme/steps/benchmark/lme/agentic_answer.yaml: improve search strategy and answer rules prompts

* fix(benchmark): fix line-too-long and add pylint disable for main()

* refactor(longmemeval): use single cleaned-S dataset with embedded ground truth

- Switch to agentscope-ai/ReMe_longmemeval_clean_s_v2 HuggingFace source
- Remove separate final_groundtruth_cleaned_s.json (ground truth now in data file)
- Simplify download.py to fetch only longmemeval_s_reme_cleaned.json
- Remove dataset.variant and dataset.ground_truth_path config options
- Update benchmark and datasets READMEs to reflect new workflow
- Update .gitignore for new dataset filename

* fix: rename loop variable to avoid pylint redefined-outer-name warning

* refactor(benchmark): restructure datasets/memory_workspaces into benchmark and simplify auto_memory steps

* refactor(benchmark): extract BaseAgenticAnswerStep into base module

- Add reme/steps/benchmark/base/agentic_answer.py with shared agentic answer logic
- Refactor beam/lme AgenticAnswerStep to inherit from BaseAgenticAnswerStep
- Simplify lme/context_answer.py and update context_answer.yaml
- Update result-longmemeval.md with latest evaluation results (agentic 91.0%)

* refactor(benchmark): remove context_answer steps and unused configs

- Remove beam/lme context_answer job definitions and step implementations
- Remove prompted LLM component from beam.yaml and lme.yaml
- Delete jinli_lme.yaml (no longer needed)
- Simplify benchmark run.py scripts
- Clean up .gitkeep files and update .gitignore
- Remove unused import in search.py

* chore: remove benchmark/results/.gitkeep

---------

Co-authored-by: sa-buc <jiangniurou.xyf@dail-algo011164204033.ET135>
Co-authored-by: jinliyl <6469360+jinliyl@users.noreply.github.com>
Co-authored-by: imrewce <wce@pku.edu.cn>
Co-authored-by: Sen Huang <48879559+ployts@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 19:09:50 +08:00
jinliyl
e7d44f6f3b
refactor(agent): unify agent subprocess env, sessions, skills, and MCP/service jobs (#382)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* feat(config): add environment variable configuration for agent subprocesses

- Add environment field to ApplicationConfig to store variables for agent subprocesses
- Remove dynamic loading of .env files in agent wrappers
- Introduce subprocess_environment property in base agent wrapper
- Pass application-level environment variables to Claude Code and Codex agents
- Load environment variables once at startup and pass to ReMe application
- Remove dependency on load_env utility in agent wrapper implementations
- Update tests to use configured environment instead of dynamic loading
- Remove unused environment loading utilities and related test cases

* refactor(mcp): remove channel notification system and related components

- Removed channel notification step implementation
- Removed claim channel step implementation
- Removed ChannelSink class from MCP service
- Removed channel-related documentation from AGENTS.md
- Removed channel instruction text from MCP service
- Removed all channel-related tests
- Updated application context metadata comment to remove channel sink reference
- Removed channel module initialization and imports

* feat(service): add job whitelisting capability to BaseService

- Add optional jobs parameter to BaseService.__init__ to configure job whitelist
- Store jobs as set in self.jobs attribute for efficient lookup operations
- Modify add_jobs method to filter jobs based on whitelist configuration
- Update documentation in both English and Chinese to describe new feature
- Add comprehensive unit tests for job whitelisting behavior
- Implement flowchart update showing new filtering logic
- Preserve existing enable_serve flag behavior alongside new whitelisting

* refactor(service): enhance service job validation and MCP tool injection

- Add strict validation for service jobs whitelist with detailed error messages
- Implement injected job arguments support for MCP services with conflict detection
- Add tool error handling for unsuccessful responses in MCP services
- Remove duplicate job names in Codex agent wrapper using dict.fromkeys
- Update MCP server argument format from single JSON array to repeated --job flags
- Add comprehensive test coverage for job injection and error handling scenarios
- Update documentation to reflect service job validation and MCP features
- Ensure application cleanup occurs even when service lifespan encounters errors

* feat(agent): update skill handling to preserve existing Claude skills

- Change skills parameter processing to use 'all' instead of filtered list
- Add logic to select project skills without restricting Claude's existing skills
- Update variable naming from 'skills' to 'selected_skills' for clarity
- Modify application context metadata documentation to clarify in-memory state usage
- Add test case to verify configured skills are added without filtering existing skills
- Update internal skill directory handling to use renamed variable consistently

* refactor(agent): restructure agent wrapper components and session storage

- Move CcFileSessionStore to separate module for better organization
- Add SDK package version logging in base agent wrapper
- Update Claude Code agent to use new session store structure with project keys
- Refactor Claude Code agent wrapper to use proper type hints and SDK integration
- Add support for server tool use events in Claude Code message processing
- Improve error handling and resource cleanup in streaming operations
- Update Codex agent wrapper with proper type annotations and configuration
- Remove deprecated system prompt mode handling from Claude Code wrapper
- Fix session path construction for Claude Code transcript storage
- Update dependency injection and configuration handling patterns

* fix(cc_agent_wrapper): resolve Claude Code SDK integration issues

- Added dataclass import and created _BlockState for content block metadata tracking
- Implemented proper MCP server name constant and tool context ID validation
- Fixed tool_context_id injection to prevent duplicate assignment errors
- Resolved skills parameter handling in build_options method
- Enhanced job tools integration with MCP servers mapping validation
- Replaced deprecated block_ids/block_types/tool_call_names with block_states dict
- Updated message_delta to emit USAGE chunks instead of REPLY_END
- Fixed stream result handling to ensure proper REPLY_END emission
- Improved error handling for session mirror failures and rate limits
- Added proper cleanup for expected trailing errors in streams
- Refactored Codex agent wrapper initialization and configuration management
- Removed obsolete system_prompt_mode from default config
- Enhanced test coverage for new block state and error handling features
- Fixed async generator handling with aclosing context manager
- Improved chunk type mapping for Claude Code SDK events

* refactor(tests): remove demo config tests from config parser test suite

- Removed test_demo_config_registers_llm_jobs function and its assertions
- Eliminated verification of LLM demo job configurations
- Removed checks for agent wrapper component settings
- Deleted assertions for model configurations and parameters
- Cleaned up deprecated test cases related to demo config parsing

* refactor(evolve): simplify Claude Code session store path structure

- Removed redundant project key subdirectory from session link generation
- Updated CcFileSessionStore initialization to use direct session directory path
- Maintained existing session layout compatibility for backward compatibility
- Added unit tests to verify session persistence behavior with existing transcripts
- Ensured UUID-based session files remain accessible at expected locations
- Preserved existing session directory structure without additional nesting

* refactor(agent): defer optional Codex SDK imports until first use

- Moved openai-codex imports inside functions to avoid mandatory dependencies
- Added TYPE_CHECKING guard for development time type checking only
- Implemented lazy loading mechanism with _get_async_codex_class function
- Updated AsyncCodex initialization to occur on demand rather than at module level
- Maintained backward compatibility while improving import performance
- Added test case to verify package import works without optional Codex SDK
- Updated agentscope dependency to version 2.0.4.post1 in pyproject.toml

* test(embedded): add compatibility tests for in-process ReMe embedding

- Add test suite for QwenPaw-style embedded configurations
- Verify optional defaults remain preserved in embedded configs
- Ensure in-process application API stays compatible
- Test model injection and lifecycle management compatibility
- Remove obsolete hermes agent plugin tests
- Update CLI import test to cover multiple optional SDKs
- Block claude_agent_sdk and openai_codex during import testing
2026-07-20 23:52:14 +08:00
jinliyl
b4333fbef8
feat(index): add bounded memory-aware batch processing (#381)
* test(background_steps): add comprehensive tests for batch processing and memory management

- Add test for catalog upserts in batches of at most 100 files
- Add test for catalog deletes in batches of at most 100 paths
- Add test for index memory budget reducing batches to one file
- Add test for memory target limiting cumulative batch size
- Add test for invalid batch memory settings rejection
- Add test for continuing after one batch fails
- Add test for yielding to event loop while building batch
- Add test for modified file reusing unchanged embedding
- Add test for reporting memory estimation failure without aborting

feat(update_changes): implement bounded batch processing with memory management

- Add configurable batch parameters with default values
- Implement memory budget calculation based on available system memory
- Add file inspection and memory estimation before processing
- Implement batch flushing when limits are reached
- Add proper error handling for batch operations
- Support async yielding during batch building
- Add comprehensive validation for batch configuration parameters
- Implement memory estimation for indexing operations
- Add batch size limiting for delete operations

* test(steps): add tests for memory estimation failure handling

- Add test case for isolated file processing when memory estimation fails
- Add test case for proper release of flushed items before building next file
- Implement weak reference tracking to verify payload lifetime management
- Create parametrized tests for both source and item memory estimation methods
- Add assertions to verify single-item batch behavior on estimation failures
- Include comprehensive error handling verification for memory budget calculations

* chore(version): bump version to 0.4.1.3

- Update __version__ from 0.4.1.2 to 0.4.1.3 in __init__.py

* feat(index): support batch settings from environment

* refactor(index): use direct batch defaults

* refactor(index): configure memory estimates through step args

* ci: simplify Windows smoke dependencies
2026-07-20 17:25:00 +08:00
Sen Huang
55ef4bd6ad
fix(proactive): expose topics in primary answer (#380) 2026-07-20 16:05:47 +08:00
539 changed files with 81656 additions and 10788 deletions

97
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,97 @@
name: Bug report
description: Report reproducible incorrect or unexpected ReMe behavior
title: "[Bug]: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for helping improve ReMe. Please remove secrets, API keys, and private memory content before submitting.
- type: textarea
id: description
attributes:
label: Description
description: What happened, and what did you expect instead?
placeholder: Describe the observed and expected behavior.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Provide the smallest configuration and command sequence that reproduces the problem.
placeholder: |
1. Configure ...
2. Run ...
3. Observe ...
validations:
required: true
- type: textarea
id: config
attributes:
label: Relevant configuration
description: Include only relevant values and redact credentials, tokens, endpoints, and private paths.
render: yaml
- type: textarea
id: logs
attributes:
label: Logs or traceback
description: Paste relevant output after removing secrets and private workspace content.
render: shell
- type: input
id: reme-version
attributes:
label: ReMe version
placeholder: e.g. 0.4.1.8 or a commit SHA
validations:
required: true
- type: input
id: python-version
attributes:
label: Python version
placeholder: e.g. 3.11.9
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
options:
- Linux
- macOS
- Windows
- Other
validations:
required: true
- type: dropdown
id: area
attributes:
label: Affected area
options:
- CLI or configuration
- HTTP, MCP, or local service
- Memory or workspace files
- Search, catalog, graph, or index
- Model or agent integration
- ReMe Studio
- Plugin or external integration
- Packaging or installation
- Other
validations:
required: true
- type: checkboxes
id: safety
attributes:
label: Data safety
options:
- label: I removed credentials and private memory content from this report.
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: ReMe documentation
url: https://reme.agentscope.io
about: Read the installation, configuration, and usage guides.
- name: Existing issues
url: https://github.com/agentscope-ai/ReMe/issues
about: Search for existing reports and discussions before opening a new issue.

View file

@ -0,0 +1,64 @@
name: Feature request
description: Propose a focused enhancement to ReMe
title: "[Feature]: "
labels: [enhancement]
body:
- type: textarea
id: problem
attributes:
label: Problem
description: What user problem or limitation should this change address?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed behavior
description: Describe the desired behavior and its user-visible contract.
validations:
required: true
- type: dropdown
id: area
attributes:
label: Area
options:
- CLI or configuration
- Jobs or steps
- Memory or workspace files
- Search, catalog, graph, or index
- Service or client
- Model or agent integration
- ReMe Studio
- Plugin or external integration
- Documentation
- Other
validations:
required: true
- type: textarea
id: ownership
attributes:
label: Local-first and compatibility considerations
description: Explain any effect on user-owned files, rebuildable state, configuration, schemas, or service interfaces.
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Describe workarounds or alternative designs you considered.
- type: textarea
id: examples
attributes:
label: Example usage
description: Show the proposed CLI, configuration, API, or UI behavior when useful.
render: shell
- type: checkboxes
id: contribution
attributes:
label: Contribution
options:
- label: I am willing to help implement or test this feature.

53
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: Usage question
description: Ask for help using or configuring ReMe
title: "[Question]: "
labels: [question]
body:
- type: markdown
attributes:
value: Please check the documentation and existing issues before asking a new question.
- type: textarea
id: goal
attributes:
label: What are you trying to achieve?
validations:
required: true
- type: textarea
id: attempted
attributes:
label: What have you tried?
description: Include relevant commands or configuration, with secrets and private memory content removed.
validations:
required: true
- type: input
id: reme-version
attributes:
label: ReMe version
placeholder: e.g. 0.4.1.8 or a commit SHA
- type: dropdown
id: area
attributes:
label: Area
options:
- Installation
- Configuration
- CLI or service usage
- Memory and workspace management
- Search and retrieval
- ReMe Studio
- Plugin or integration
- Other
- type: checkboxes
id: checked
attributes:
label: Before submitting
options:
- label: I checked the [ReMe documentation](https://reme.agentscope.io) and searched existing issues.
required: true
- label: I removed credentials and private memory content.
required: true

35
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,35 @@
## Summary
<!-- Explain the problem and the smallest coherent change that addresses it. -->
## Related issue
<!-- Use "Fixes #123" when applicable. -->
## Contract and data impact
- [ ] No public configuration, schema, CLI, endpoint, streaming, or workspace-layout contract changes
- [ ] No user-owned memory files are deleted or rewritten
- [ ] Derived indexes, catalogs, graphs, caches, and metadata remain rebuildable
<!-- If any item is unchecked, describe the impact and migration or recovery path. -->
## Validation
<!-- List the exact checks run and their results. Explain relevant checks that were not run. -->
- [ ] Focused tests pass
- [ ] Unit tests pass, or omitted tests are explained below
- [ ] `pre-commit run --all-files` passes, or omitted checks are explained below
- [ ] Frontend checks were run when `reme_studio/` changed
## Checklist
- [ ] I reviewed the diff for unrelated changes and sensitive data
- [ ] Tests cover intentional behavior changes
- [ ] Defaults, schemas, and concise documentation were updated together when required
- [ ] Long-lived clients, tasks, services, and executors follow the application lifecycle
## Screenshots or additional notes
<!-- Include UI screenshots, compatibility notes, or follow-up work when relevant. -->

58
.github/workflows/_build-docs.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: _Build documentation
on:
workflow_call:
inputs:
run_tests:
description: Run the documentation test suite before building
required: false
default: true
type: boolean
upload_pages_artifact:
description: Upload the build for a later GitHub Pages deployment job
required: false
default: false
type: boolean
permissions:
contents: read
jobs:
build:
name: Build documentation
runs-on: ubuntu-latest
defaults:
run:
working-directory: github-pages
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '22.22.3'
cache: npm
cache-dependency-path: github-pages/package-lock.json
- name: Install dependencies
run: npm ci
- name: Run tests
if: inputs.run_tests
run: npm test
- name: Build documentation
run: npm run build
- name: Configure Pages
if: inputs.upload_pages_artifact
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
- name: Upload Pages artifact
if: inputs.upload_pages_artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
with:
path: github-pages/dist

View file

@ -0,0 +1,88 @@
name: _Build Python packages
on:
workflow_call:
inputs:
expected_version:
description: Expected release version; omit for a consistency-only check
required: false
default: ''
type: string
upload_artifacts:
description: Upload distributions for later publish jobs
required: false
default: false
type: boolean
permissions:
contents: read
jobs:
distributions:
name: Build Python distributions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.11'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build packaging pytest twine
- name: Validate package versions
if: inputs.expected_version == ''
run: python scripts/bump_version.py --check
- name: Validate release version
if: inputs.expected_version != ''
env:
EXPECTED_VERSION: ${{ inputs.expected_version }}
run: python scripts/bump_version.py --check --expected-version "${EXPECTED_VERSION}"
- name: Run package tests
run: PYTHONPATH=. python -m pytest tests/unit/test_package_versions.py -q
- name: Build and check distributions
run: |
mkdir -p dist/reme
python -m build --outdir dist/reme
python -m twine check dist/reme/*
- name: Verify distributions and isolated installation
run: |
REME_WHEEL="$(pwd)/$(ls dist/reme/reme_ai-[0-9]*.whl)"
python -m zipfile -l "${REME_WHEEL}" | (! grep 'reme/web/')
python -m zipfile -l "${REME_WHEEL}" | (! grep 'reme_studio/')
python -m venv "${RUNNER_TEMP}/reme-package-smoke"
"${RUNNER_TEMP}/reme-package-smoke/bin/python" -m pip install "${REME_WHEEL}[as]"
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/reme-package-smoke/bin/python" -c "import reme"
- name: Verify released core dependencies
if: inputs.expected_version != ''
run: |
REME_WHEEL="$(pwd)/$(ls dist/reme/reme_ai-[0-9]*.whl)"
python -m venv "${RUNNER_TEMP}/reme-core-package-smoke"
"${RUNNER_TEMP}/reme-core-package-smoke/bin/python" -m pip install "${REME_WHEEL}[core]"
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/reme-core-package-smoke/bin/python" - <<'PY'
from reme_studio import static_dir
assert (static_dir() / "index.html").is_file()
PY
- name: Upload ReMe distributions
if: inputs.upload_artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: reme-distributions
path: dist/reme/
if-no-files-found: error

48
.github/workflows/ci-docs.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: CI / Documentation
on:
push:
branches: [main, master, dev, develop]
paths:
- '.github/workflows/ci-docs.yml'
- '.github/workflows/_build-docs.yml'
- 'AGENTS.md'
- 'README.md'
- 'README_ZH.md'
- 'docs/**'
- 'github-pages/**'
- 'reme_studio/README*.md'
- 'reme_studio/public/og.jpg'
- 'typescript/README*.md'
- 'plugins/*/README*.md'
- 'benchmark/*/README*.md'
pull_request:
branches: [main, master, dev, develop]
paths:
- '.github/workflows/ci-docs.yml'
- '.github/workflows/_build-docs.yml'
- 'AGENTS.md'
- 'README.md'
- 'README_ZH.md'
- 'docs/**'
- 'github-pages/**'
- 'reme_studio/README*.md'
- 'reme_studio/public/og.jpg'
- 'typescript/README*.md'
- 'plugins/*/README*.md'
- 'benchmark/*/README*.md'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
documentation:
name: Test and build documentation
uses: ./.github/workflows/_build-docs.yml
with:
run_tests: true

40
.github/workflows/ci-packages.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: CI / Python packages
on:
push:
branches: [main, master, dev, develop]
paths:
- '.github/workflows/ci-packages.yml'
- '.github/workflows/_build-python-packages.yml'
- '.github/workflows/release-python.yml'
- 'pyproject.toml'
- 'README.md'
- 'reme/**'
- 'scripts/bump_version.py'
- 'tests/unit/test_package_versions.py'
- 'LICENSE'
pull_request:
branches: [main, master, dev, develop]
paths:
- '.github/workflows/ci-packages.yml'
- '.github/workflows/_build-python-packages.yml'
- '.github/workflows/release-python.yml'
- 'pyproject.toml'
- 'README.md'
- 'reme/**'
- 'scripts/bump_version.py'
- 'tests/unit/test_package_versions.py'
- 'LICENSE'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
distributions:
name: Build and verify distributions
uses: ./.github/workflows/_build-python-packages.yml

40
.github/workflows/ci-python-quality.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: CI / Python quality
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
pre-commit:
name: Pre-commit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.11'
cache: pip
- name: Update setuptools
run: |
pip install -U setuptools wheel
- name: Install
run: |
pip install -q -e reme_studio -e ".[dev,core]"
pip install -q --no-deps -e plugins/auto-fin -e plugins/daily_paper
- name: Pre-commit starts
run: pre-commit run --all-files

View file

@ -1,4 +1,4 @@
name: Tests ReMe
name: CI / Python tests
on:
push:
@ -11,6 +11,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
unit-tests:
name: Unit Tests - py${{ matrix.python-version }}
@ -21,10 +24,12 @@ jobs:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@ -32,12 +37,14 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -e ".[dev,core]"
pip install -e reme_studio -e ".[dev,core]"
pip install --no-deps -e plugins/auto-fin
pip install -e plugins/daily_paper
pip install coverage
- name: Run unit tests
run: |
coverage run -m pytest tests/unit \
coverage run -m pytest tests/unit plugins/auto-fin plugins/daily_paper \
-v \
--tb=long \
-s \

90
.github/workflows/ci-reme-studio.yml vendored Normal file
View file

@ -0,0 +1,90 @@
name: CI / ReMe Studio
on:
push:
paths:
- "reme_studio/**"
- ".github/workflows/ci-reme-studio.yml"
- ".github/workflows/release-reme-studio.yml"
- "scripts/package_studio.py"
- "tests/unit/test_package_versions.py"
- "pyproject.toml"
- "LICENSE"
pull_request:
paths:
- "reme_studio/**"
- ".github/workflows/ci-reme-studio.yml"
- ".github/workflows/release-reme-studio.yml"
- "scripts/package_studio.py"
- "tests/unit/test_package_versions.py"
- "pyproject.toml"
- "LICENSE"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
studio:
name: Studio checks
runs-on: ubuntu-latest
defaults:
run:
working-directory: reme_studio
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "22.22.3"
cache: npm
cache-dependency-path: reme_studio/package-lock.json
- name: Install dependencies
run: npm ci
- name: Run format check
run: npm run format:check
- name: Run lint
run: npm run lint
- name: Run tests
run: npm test
- name: Verify npm package
run: |
npm pack --pack-destination "${RUNNER_TEMP}"
tar -tzf "${RUNNER_TEMP}"/agentscope-ai-reme_studio-*.tgz | grep '^package/dist-static/index.html$'
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"
- name: Build and verify Python package
working-directory: .
run: |
python -m pip install build packaging pytest twine
PYTHONPATH=. python -m pytest tests/unit/test_package_versions.py -q
python scripts/package_studio.py
python -m build reme_studio --outdir dist/studio
python -m twine check dist/studio/*
STUDIO_WHEEL="$(pwd)/$(ls dist/studio/reme_studio-*.whl)"
python -m venv "${RUNNER_TEMP}/reme-studio-package-smoke"
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" -m pip install "${STUDIO_WHEEL}"
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" - <<'PY'
from reme_studio import static_dir
assert (static_dir() / "index.html").is_file()
PY

51
.github/workflows/ci-typescript.yml vendored Normal file
View file

@ -0,0 +1,51 @@
name: CI / TypeScript integrations
on:
push:
branches: [main, master, dev, develop]
paths:
- '.github/workflows/ci-typescript.yml'
- '.github/workflows/release-typescript.yml'
- 'typescript/**'
pull_request:
branches: [main, master, dev, develop]
paths:
- '.github/workflows/ci-typescript.yml'
- '.github/workflows/release-typescript.yml'
- 'typescript/**'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
package:
name: Type-check, test, and pack
runs-on: ubuntu-latest
defaults:
run:
working-directory: typescript
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '22.22.3'
cache: npm
cache-dependency-path: typescript/package-lock.json
- run: npm ci
- run: npm run format:check
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: npm run test:package
- name: Validate OpenClaw package contract
run: npx --yes clawhub@0.23.3 package validate . --json

51
.github/workflows/ci-windows.yml vendored Normal file
View file

@ -0,0 +1,51 @@
name: CI / Windows
on:
push:
branches: [main, master, dev, develop]
pull_request:
branches: [main, master, dev, develop]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
cli-smoke:
name: CLI smoke - py${{ matrix.python-version }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install package
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -e ".[dev,as]"
- name: Run version job
run: reme start config=tests/fixtures/config/version-smoke.yaml job=version
- name: Run Windows path tests
run: |
python -m pytest `
tests/unit/test_auto_dream.py::test_scan_day_files_includes_nested_md_and_excludes_interests `
tests/unit/test_auto_dream.py::test_dream_extract_matches_posix_catalog_paths `
tests/unit/test_read_with_neighbors.py::test_read_with_neighbors_uses_posix_nested_path `
-v

52
.github/workflows/deploy-docs.yml vendored Normal file
View file

@ -0,0 +1,52 @@
name: Deploy / Documentation
on:
push:
branches: [main]
paths:
- "github-pages/**"
- "docs/**"
- "README.md"
- "README_ZH.md"
- "reme_studio/README*.md"
- "reme_studio/public/og.jpg"
- "typescript/README*.md"
- "plugins/*/README*.md"
- "benchmark/*/README*.md"
- "AGENTS.md"
- ".github/workflows/deploy-docs.yml"
- ".github/workflows/_build-docs.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
name: Build documentation
uses: ./.github/workflows/_build-docs.yml
with:
run_tests: true
upload_pages_artifact: true
permissions:
contents: read
pages: write
id-token: write
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
permissions:
pages: write
id-token: write
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5

View file

@ -1,16 +1,20 @@
name: PR Title Check
name: Policy / PR title
on:
pull_request:
branches: [main, master, dev, develop]
types: [opened, edited, synchronize, reopened]
permissions:
contents: read
pull-requests: read
jobs:
check-pr-title:
runs-on: ubuntu-latest
steps:
- name: Check PR title format
uses: amannn/action-semantic-pull-request@v6.1.1
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View file

@ -1,38 +0,0 @@
name: Pre-commit
on: [ push, pull_request ]
jobs:
run:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: True
matrix:
os: [ ubuntu-latest ]
env:
OS: ${{ matrix.os }}
PYTHON: '3.11'
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Update setuptools
run: |
pip install -U setuptools wheel
- name: Install
run: |
pip install -q -e ".[dev,core]"
- name: Install pre-commit
run: |
pre-commit install
- name: Pre-commit starts
run: |
pre-commit run --all-files > pre-commit.log 2>&1 || true
cat pre-commit.log
if grep -q Failed pre-commit.log; then
echo -e "\e[41m [**FAIL**] Please install pre-commit and format your code first. \e[0m"
exit 1
fi
echo -e "\e[46m ********************************Passed******************************** \e[0m"

View file

@ -1,45 +0,0 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Publish Python Package to Pypi
on:
workflow_dispatch:
release:
types: [published]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel build
- name: Build package
run: python -m build
- name: Test installation
run: |
WHEEL="$(ls dist/*.whl)"
pip install "${WHEEL}[core]"
python -c "import reme; print(reme.__version__)"
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

157
.github/workflows/release-auto-fin.yml vendored Normal file
View file

@ -0,0 +1,157 @@
# 发布操作手册:
# 1. 先将 plugins/auto-fin/pyproject.toml 中的 project.version 更新为待发布版本并合入目标分支。
# 2. 确认插件依赖的 reme-ai 版本已经发布到 PyPI本工作流会在构建阶段验证该依赖可下载。
# 3. 确认 PyPI Trusted Publisher 已绑定本仓库、此工作流和 pypi environment且 PyPI 上不存在相同版本。
# 4. 在 GitHub 仓库的 Actions 页面选择“Release / Auto Fin plugin”点击“Run workflow”。
# 5. 输入与 project.version 完全一致的版本号(例如 0.1.0)后运行;版本也可以带 v 前缀。
#
# 推荐发布顺序reme-ai -> reme-auto-fin -> QwenPaw 更新依赖并通过 plugins: [auto-fin] 启用。
# 当前仅支持 workflow_dispatch 手动触发,不会因 push、tag 或 release 自动发布。
name: Release / Auto Fin plugin
run-name: Publish reme-auto-fin ${{ inputs.version }}
on:
workflow_dispatch:
inputs:
version:
description: Version from plugins/auto-fin/pyproject.toml (for example, 0.1.0)
required: true
type: string
permissions:
contents: read
concurrency:
group: publish-reme-auto-fin
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
env:
RELEASE_VERSION: ${{ inputs.version }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.11'
- name: Install test and build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build packaging pytest pytest-asyncio twine
python -m pip install -e ".[core]"
python -m pip install --no-deps -e plugins/auto-fin
- name: Validate package name and release version
id: package
run: |
python - "${RELEASE_VERSION}" <<'PY'
import os
import sys
import tomllib
from pathlib import Path
from packaging.requirements import Requirement
from packaging.version import Version
project = tomllib.loads(Path("plugins/auto-fin/pyproject.toml").read_text(encoding="utf-8"))["project"]
expected = Version(sys.argv[1].removeprefix("v"))
actual = Version(project["version"])
if project["name"] != "reme-auto-fin":
raise SystemExit(f"Expected project name 'reme-auto-fin', found {project['name']!r}")
if actual != expected:
raise SystemExit(f"Package version is {actual}, but workflow input is {expected}")
requirements = [requirement for requirement in project["dependencies"] if requirement.startswith("reme-ai")]
if len(requirements) != 1:
raise SystemExit(f"Expected one reme-ai dependency, found {requirements!r}")
reme_requirement = Requirement(requirements[0])
if reme_requirement.name != "reme-ai" or reme_requirement.extras:
raise SystemExit(f"Expected a base reme-ai dependency, found {requirements[0]!r}")
if Version("0.4.1.8") in reme_requirement.specifier or Version("0.4.1.9") not in reme_requirement.specifier:
raise SystemExit(f"Expected reme-ai>=0.4.1.9, found {requirements[0]!r}")
with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output:
print(f"reme_requirement={reme_requirement}", file=output)
print(f"Publishing {project['name']} {actual}")
PY
- name: Run Auto Fin tests
run: python -m pytest plugins/auto-fin -q
- name: Require the plugin-enabled ReMe release on PyPI
env:
REME_REQUIREMENT: ${{ steps.package.outputs.reme_requirement }}
run: |
python -m pip download --no-deps \
--dest "${RUNNER_TEMP}/reme-auto-fin-base" \
"${REME_REQUIREMENT}"
- name: Build and check distributions
run: |
mkdir -p dist/auto-fin
python -m build plugins/auto-fin --outdir dist/auto-fin
python -m twine check dist/auto-fin/*
- name: Verify distributions and isolated installation
run: |
AUTO_FIN_WHEEL="$(pwd)/$(ls dist/auto-fin/reme_auto_fin-*.whl)"
AUTO_FIN_SDIST="$(pwd)/$(ls dist/auto-fin/reme_auto_fin-*.tar.gz)"
python -m zipfile -l "${AUTO_FIN_WHEEL}" | grep 'dist-info/licenses/LICENSE'
python -m tarfile -l "${AUTO_FIN_SDIST}" | grep '/LICENSE'
python -m venv "${RUNNER_TEMP}/reme-auto-fin-smoke"
"${RUNNER_TEMP}/reme-auto-fin-smoke/bin/python" -m pip install \
"agentscope[model-ollama]==2.0.7" "${AUTO_FIN_WHEEL}"
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/reme-auto-fin-smoke/bin/python" - <<'PY'
from importlib.metadata import distribution
from reme.plugin_manifest import load_package_manifest
package = distribution("reme-auto-fin")
plugins = {entry.name: entry for entry in package.entry_points if entry.group == "reme.plugins"}
assert plugins["auto-fin"].value == "reme_auto_fin"
manifest = load_package_manifest("reme_auto_fin", plugin_name="auto-fin")
assert set(manifest.backends) == {
"auto_fin_data_step",
"auto_fin_topic_step",
"auto_fin_merge_step",
}
assert set(manifest.application_defaults["jobs"]) == {
"auto_fin",
"auto_fin_cron",
}
PY
- name: Upload distributions
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: reme-auto-fin-${{ inputs.version }}
path: dist/auto-fin/
if-no-files-found: error
publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: reme-auto-fin-${{ inputs.version }}
path: dist/auto-fin
- name: Publish reme-auto-fin
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/auto-fin

View file

@ -0,0 +1,157 @@
# Release checklist:
# 1. Update project.version in plugins/daily_paper/pyproject.toml and merge it into the target branch.
# 2. Publish the required reme-ai version before this plugin; the build verifies that dependency on PyPI.
# 3. Configure PyPI Trusted Publishing for this repository/workflow and its pypi environment.
# 4. Run "Release / Daily Paper plugin" from GitHub Actions with the exact project version (a v prefix is accepted).
#
# Recommended order: reme-ai -> reme-daily-paper -> downstream applications enabling plugins: [daily-paper].
# This workflow is intentionally manual and never publishes from a push, tag, or GitHub release event.
name: Release / Daily Paper plugin
run-name: Publish reme-daily-paper ${{ inputs.version }}
on:
workflow_dispatch:
inputs:
version:
description: Version from plugins/daily_paper/pyproject.toml (for example, 0.1.0)
required: true
type: string
permissions:
contents: read
concurrency:
group: publish-reme-daily-paper
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
env:
RELEASE_VERSION: ${{ inputs.version }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.11'
- name: Install test and build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build packaging pytest pytest-asyncio twine
python -m pip install -e ".[core]"
python -m pip install -e plugins/daily_paper
- name: Validate package name, dependencies, and release version
id: package
run: |
python - "${RELEASE_VERSION}" <<'PY'
import os
import sys
import tomllib
from pathlib import Path
from packaging.requirements import Requirement
from packaging.version import Version
project = tomllib.loads(Path("plugins/daily_paper/pyproject.toml").read_text(encoding="utf-8"))["project"]
expected = Version(sys.argv[1].removeprefix("v"))
actual = Version(project["version"])
if project["name"] != "reme-daily-paper":
raise SystemExit(f"Expected project name 'reme-daily-paper', found {project['name']!r}")
if actual != expected:
raise SystemExit(f"Package version is {actual}, but workflow input is {expected}")
requirements = [Requirement(value) for value in project["dependencies"]]
reme_requirements = [requirement for requirement in requirements if requirement.name == "reme-ai"]
if len(reme_requirements) != 1 or reme_requirements[0].extras:
raise SystemExit(f"Expected one base reme-ai dependency, found {reme_requirements!r}")
if Version("0.4.1.8") in reme_requirements[0].specifier or Version("0.4.1.9") not in reme_requirements[0].specifier:
raise SystemExit(f"Expected reme-ai>=0.4.1.9, found {reme_requirements!r}")
if sum(requirement.name == "pypdf" for requirement in requirements) != 1:
raise SystemExit("Expected exactly one pypdf dependency")
with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output:
print(f"reme_requirement={reme_requirements[0]}", file=output)
print(f"Publishing {project['name']} {actual}")
PY
- name: Run Daily Paper tests
run: python -m pytest plugins/daily_paper -q
- name: Require the plugin-enabled ReMe release on PyPI
env:
REME_REQUIREMENT: ${{ steps.package.outputs.reme_requirement }}
run: |
python -m pip download --no-deps \
--dest "${RUNNER_TEMP}/reme-daily-paper-base" \
"${REME_REQUIREMENT}"
- name: Build and check distributions
run: |
mkdir -p dist/daily-paper
python -m build plugins/daily_paper --outdir dist/daily-paper
python -m twine check dist/daily-paper/*
- name: Verify distributions and isolated installation
run: |
DAILY_PAPER_WHEEL="$(pwd)/$(ls dist/daily-paper/reme_daily_paper-*.whl)"
DAILY_PAPER_SDIST="$(pwd)/$(ls dist/daily-paper/reme_daily_paper-*.tar.gz)"
python -m zipfile -l "${DAILY_PAPER_WHEEL}" | grep 'reme_daily_paper/plugin.yaml'
python -m zipfile -l "${DAILY_PAPER_WHEEL}" | grep 'reme_daily_paper/analyze.yaml'
python -m zipfile -l "${DAILY_PAPER_WHEEL}" | grep 'dist-info/licenses/LICENSE'
python -m tarfile -l "${DAILY_PAPER_SDIST}" | grep '/LICENSE'
python -m venv "${RUNNER_TEMP}/reme-daily-paper-smoke"
"${RUNNER_TEMP}/reme-daily-paper-smoke/bin/python" -m pip install \
"agentscope[model-ollama]==2.0.7" "${DAILY_PAPER_WHEEL}"
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/reme-daily-paper-smoke/bin/python" - <<'PY'
from importlib.metadata import distribution
from reme.plugin_manifest import load_package_manifest
package = distribution("reme-daily-paper")
plugins = {entry.name: entry for entry in package.entry_points if entry.group == "reme.plugins"}
assert plugins["daily-paper"].value == "reme_daily_paper"
manifest = load_package_manifest("reme_daily_paper", plugin_name="daily-paper")
assert set(manifest.backends) == {
"daily_paper_collect_step",
"daily_paper_rank_step",
"daily_paper_select_step",
"daily_paper_analyze_step",
"daily_paper_digest_step",
}
assert set(manifest.application_defaults["jobs"]) == {"daily_paper", "daily_paper_cron"}
PY
- name: Upload distributions
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: reme-daily-paper-${{ inputs.version }}
path: dist/daily-paper/
if-no-files-found: error
publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: reme-daily-paper-${{ inputs.version }}
path: dist/daily-paper
- name: Publish reme-daily-paper
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/daily-paper

47
.github/workflows/release-python.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Release / Python packages
# Configure a PyPI Trusted Publisher for this repository, workflow, and its
# pypi environment before running the manual release.
on:
workflow_dispatch:
inputs:
version:
description: Release version
required: true
type: string
permissions:
contents: read
concurrency:
group: publish-reme-ai
cancel-in-progress: false
jobs:
build:
name: Build and verify distributions
uses: ./.github/workflows/_build-python-packages.yml
with:
expected_version: ${{ inputs.version }}
upload_artifacts: true
publish-reme:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- name: Download ReMe distributions
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: reme-distributions
path: dist/reme
- name: Publish ReMe
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/reme
skip-existing: true

View file

@ -0,0 +1,158 @@
# Release checklist:
# 1. Update reme_studio/pyproject.toml, package.json, and package-lock.json to the same Studio version.
# 2. Configure npm Trusted Publishing and PyPI Trusted Publishing with the pypi environment.
# 3. Run this workflow manually with the exact Studio version.
name: Release / ReMe Studio
run-name: Publish ReMe Studio ${{ inputs.version }} (${{ inputs.npm_tag }})
on:
workflow_dispatch:
inputs:
version:
description: Version from the Studio Python and npm manifests
required: true
type: string
npm_tag:
description: npm distribution tag
required: true
default: latest
type: choice
options:
- next
- latest
permissions:
contents: read
concurrency:
group: publish-reme-studio
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
env:
RELEASE_VERSION: ${{ inputs.version }}
NPM_TAG: ${{ inputs.npm_tag }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "22.22.3"
cache: npm
cache-dependency-path: reme_studio/package-lock.json
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"
- name: Validate Studio package names and version
run: |
python - <<'PY'
import json
import os
import tomllib
from pathlib import Path
studio = Path("reme_studio")
python_manifest = tomllib.loads((studio / "pyproject.toml").read_text(encoding="utf-8"))["project"]
npm_manifest = json.loads((studio / "package.json").read_text(encoding="utf-8"))
expected = os.environ["RELEASE_VERSION"].removeprefix("v")
if python_manifest["name"] != "reme_studio":
raise SystemExit(f"Unexpected Python package name: {python_manifest['name']}")
if npm_manifest["name"] != "@agentscope-ai/reme_studio":
raise SystemExit(f"Unexpected npm package name: {npm_manifest['name']}")
if python_manifest["version"] != expected or npm_manifest["version"] != expected:
raise SystemExit(
f"Studio manifests are {python_manifest['version']} and {npm_manifest['version']}; "
f"workflow input is {expected}",
)
prerelease = "-" in expected
if prerelease != (os.environ["NPM_TAG"] == "next"):
raise SystemExit("Prereleases must use next; stable releases must use latest")
PY
- name: Install dependencies and run checks
working-directory: reme_studio
run: |
npm ci
npm run format:check
npm run lint
npm test
- name: Build Studio distributions
run: |
python -m pip install build twine
mkdir -p dist/studio-python dist/studio-npm
npm pack ./reme_studio --pack-destination dist/studio-npm
python scripts/package_studio.py
python -m build reme_studio --outdir dist/studio-python
python -m twine check dist/studio-python/*
- name: Verify Studio distributions and isolated installation
run: |
STUDIO_WHEEL="$(pwd)/$(ls dist/studio-python/reme_studio-*.whl)"
tar -tzf dist/studio-npm/*.tgz | grep '^package/dist-static/index.html$'
python -m venv "${RUNNER_TEMP}/reme-studio-package-smoke"
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" -m pip install "${STUDIO_WHEEL}"
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" - <<'PY'
from reme_studio import static_dir
assert (static_dir() / "index.html").is_file()
PY
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: reme-studio-${{ inputs.version }}
path: |
dist/studio-python/*
dist/studio-npm/*
if-no-files-found: error
publish-python:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: reme-studio-${{ inputs.version }}
path: dist
- name: Publish ReMe Studio to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/studio-python
skip-existing: true
publish-npm:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
registry-url: https://registry.npmjs.org
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: reme-studio-${{ inputs.version }}
path: dist
- name: Publish ReMe Studio to npm
env:
NPM_TAG: ${{ inputs.npm_tag }}
run: npm publish dist/studio-npm/*.tgz --access public --tag "${NPM_TAG}" --provenance

167
.github/workflows/release-typescript.yml vendored Normal file
View file

@ -0,0 +1,167 @@
# Release checklist:
# 1. Update typescript/package.json and package-lock.json to the release version and merge them.
# 2. Configure npm Trusted Publishing for agentscope-ai/ReMe and this workflow file.
# 3. Run this workflow manually with the exact package version (an optional v prefix is accepted).
# 4. Configure ClawHub Trusted Publishing or CLAWHUB_TOKEN before enabling ClawHub publication.
# 5. Use the `next` tag for prereleases and `latest` only for stable releases.
name: Release / TypeScript integrations
run-name: Publish @agentscope-ai/reme ${{ inputs.version }} (${{ inputs.npm_tag }})
on:
workflow_dispatch:
inputs:
version:
description: Version from typescript/package.json (for example, 0.1.0)
required: true
type: string
npm_tag:
description: npm distribution tag
required: true
default: latest
type: choice
options:
- next
- latest
publish_clawhub:
description: Also publish the verified tarball to ClawHub
required: true
default: false
type: boolean
permissions:
contents: read
concurrency:
group: publish-agentscope-ai-reme
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.validate.outputs.version }}
env:
RELEASE_VERSION: ${{ inputs.version }}
NPM_TAG: ${{ inputs.npm_tag }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '22.22.3'
- name: Validate package name and release version
id: validate
working-directory: typescript
run: |
node --input-type=module <<'JS'
import { appendFileSync, readFileSync } from 'node:fs';
const manifest = JSON.parse(readFileSync('package.json', 'utf8'));
const expected = process.env.RELEASE_VERSION.replace(/^v/, '');
if (manifest.name !== '@agentscope-ai/reme') {
throw new Error(`Unexpected package name: ${manifest.name}`);
}
if (manifest.version !== expected) {
throw new Error(`package.json is ${manifest.version}, workflow input is ${expected}`);
}
const prerelease = manifest.version.includes('-');
const npmTag = process.env.NPM_TAG;
if (prerelease !== (npmTag === 'next')) {
throw new Error(prerelease
? 'Prerelease versions must use the next npm tag'
: 'Stable versions must use the latest npm tag');
}
console.log(`Preparing ${manifest.name}@${manifest.version}`);
appendFileSync(process.env.GITHUB_OUTPUT, `version=${manifest.version}\n`);
JS
- name: Install dependencies
working-directory: typescript
run: npm ci
- name: Type-check and test
working-directory: typescript
run: |
npm run format:check
npm run lint
npm run typecheck
npm test
npm run test:package
npx --yes clawhub@0.23.3 package validate . --json
- name: Pack npm tarball
working-directory: typescript
run: |
mkdir -p "${RUNNER_TEMP}/reme-typescript-package"
npm pack --pack-destination "${RUNNER_TEMP}/reme-typescript-package"
- name: Upload npm tarball
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: agentscope-ai-reme-${{ inputs.version }}
path: ${{ runner.temp }}/reme-typescript-package/*.tgz
if-no-files-found: error
publish:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Set up Node for npm
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '24'
registry-url: https://registry.npmjs.org
- name: Download npm tarball
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: agentscope-ai-reme-${{ inputs.version }}
path: dist/typescript
- name: Reject an existing package version
env:
PACKAGE_VERSION: ${{ inputs.version }}
run: |
PACKAGE_VERSION="${PACKAGE_VERSION#v}"
if npm view "@agentscope-ai/reme@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
echo "@agentscope-ai/reme@${PACKAGE_VERSION} already exists" >&2
exit 1
fi
- name: Publish to npm
env:
NPM_TAG: ${{ inputs.npm_tag }}
run: npm publish dist/typescript/*.tgz --access public --tag "${NPM_TAG}" --provenance
publish-clawhub:
if: ${{ inputs.publish_clawhub }}
needs: build
permissions:
actions: read
contents: read
id-token: write
uses: openclaw/clawhub/.github/workflows/package-publish.yml@87ca030c30f3cfb78ab15c8e66b5ff1469c8f9c8 # v0.23.3
with:
owner: agentscope-ai
family: code-plugin
version: ${{ needs.build.outputs.version }}
tags: ${{ inputs.npm_tag }}
source_repo: ${{ github.repository }}
source_commit: ${{ github.sha }}
source_ref: ${{ github.ref }}
source_path: typescript
package_artifact_name: agentscope-ai-reme-${{ inputs.version }}
wait_for_publication: true
secrets:
clawhub_token: ${{ secrets.CLAWHUB_TOKEN }}

46
.github/workflows/security-codeql.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: Security / CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 1 * * 1'
workflow_dispatch:
permissions:
actions: read
contents: read
packages: read
security-events: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze ${{ matrix.language }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [python, javascript-typescript]
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
with:
languages: ${{ matrix.language }}
build-mode: none
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
with:
category: /language:${{ matrix.language }}

View file

@ -1,38 +0,0 @@
name: Windows Smoke
on:
push:
branches: [main, master, dev, develop]
pull_request:
branches: [main, master, dev, develop]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
cli-smoke:
name: CLI smoke - py${{ matrix.python-version }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install package
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -e ".[core,benchmark]"
- name: Run version job
run: reme start service.backend=cli job=version

26
.gitignore vendored
View file

@ -2,8 +2,8 @@
.DS_Store
.idea/
.vscode/
*.code-workspace
.qoder/
*.code-workspace
# Local environment
.env
@ -30,7 +30,9 @@ htmlcov/
# Packaging / build outputs
build/
dist/
node_modules/
*.egg-info/
typescript/reports/
# Logs / temporary files
*.log
@ -45,6 +47,8 @@ temp*/
# ReMe runtime data
.reme/
reme_workspace/
reme_workspace_auto_fin_real_test*/
vault/
*.db
*.sqlite
@ -55,4 +59,24 @@ docs/_build/
site/
evaluation/
# The pi-Bench suite ships its own trace-history render config, which must
# stay in git even though it lives under an evaluation/ directory.
!benchmark/pibench/config/bench/evaluation/
!benchmark/pibench/config/bench/evaluation/**
datasets/
# Claude Code skills (local only)
.claude/skills/
# Benchmark memory workspaces (created on demand by run.py via mkdir)
benchmark/*/workspaces/
# Benchmark datasets (LongMemEval via download.py, BEAM via git clone)
benchmark/*/dataset/
# Benchmark outputs (created on demand by run.py via mkdir)
benchmark/*/results/
# integration tests outputs
tests/integration/logs/
daily/

View file

@ -1,3 +1,5 @@
exclude: ^skills/
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0

273
AGENTS.md
View file

@ -1,20 +1,19 @@
# AGENTS.md
This file guides coding agents working in the ReMe repository. Keep changes small,
testable, and consistent with the contracts already expressed by the code.
This file guides coding agents working in the ReMe repository. Keep changes small, testable, and consistent with the
contracts expressed by the current code.
## Project Principles
ReMe is a local-first, file-native memory system for agents.
- User-owned memory files are the source of truth.
- Indexes, caches, metadata, and generated state must be rebuildable.
- Prefer transparent formats and behavior over hidden state.
- Preserve user control over storage, configuration, and service boundaries.
- User-owned workspace files are the durable source of truth.
- Indexes, catalogs, graphs, caches, and generated metadata must remain rebuildable.
- Prefer transparent formats and predictable behavior over hidden state.
- Preserve user control over workspace paths, configuration, and service boundaries.
- Keep concepts focused on project intent; let code and schemas describe implementation.
When a proposed convenience conflicts with these principles, favor data ownership,
recoverability, and predictable behavior.
When convenience conflicts with these principles, favor data ownership, recoverability, and explicit behavior.
## Sources of Truth
@ -22,166 +21,194 @@ Use this order when documentation and implementation disagree:
1. Current code and public Pydantic schemas.
2. Tests that describe supported behavior.
3. CLI help and the built-in configuration.
4. Development documentation and historical notes.
3. CLI behavior and the built-in configuration.
4. README files and other development documentation.
Do not copy large implementation descriptions into documentation. Link to the relevant
module or express the stable contract instead. If behavior changes intentionally, update
the code, schema, tests, configuration, and concise documentation together as needed.
Do not duplicate large implementation descriptions in documentation. Express the stable contract and link to the
relevant module where useful. When behavior changes intentionally, update the implementation, schemas, tests, defaults,
and concise documentation together.
## Repository Map
- `reme/reme.py`: CLI entry point and client/server dispatch.
- `reme/application.py`: application assembly, dependency ordering, and lifecycle.
- `reme/components/application_context.py`: application-wide wiring and shared in-memory metadata.
- `reme/components/runtime_context.py`: scratch state shared by steps within one execution.
- `reme/config/default.yaml`: built-in jobs, components, and defaults.
- `reme/schema/`: public and runtime Pydantic contracts.
- `reme/components/`: services, stores, clients, jobs, and component registration.
- `reme/steps/`: executable job steps.
- `tests/unit/`: primary fast validation suite.
- `tests/integration/`: tests that may require real credentials or services.
- `tests/vector/` and `tests/light/`: specialized suites.
- `plugins/reme/`: Claude Code integration.
- `skills/reme_memory/`: skill that communicates with the ReMe service.
- `skills/qwenpaw_memory/`: separate direct-file memory convention; it does not call ReMe.
- `docs/`: pages and assets that support the repository README; not the deployed docs site.
- `reme/reme.py`: CLI entry point; dispatches `start`, `find_reme`, and client calls.
- `reme/application.py`: application assembly, dependency ordering, job execution, and lifecycle.
- `reme/config/config_parser.py`: YAML/JSON loading, environment expansion, dot-notation parsing, and deep config
merging.
- `reme/config/default.yaml`: default service, jobs, steps, and components. Other files in
`reme/config/` are named configuration variants.
- `reme/schema/application_config.py`: typed application, component, and job configuration.
- `reme/schema/`: request, response, streaming, memory, graph, and file contracts.
- `reme/components/application_context.py`: application-wide wiring and in-memory shared state.
- `reme/components/runtime_context.py`: request-scoped data, response, streaming queue, and stop event.
- `reme/components/base_component.py`: component lifecycle, dependency binding, and workspace helpers.
- `reme/components/component_registry.py`: the frozen built-in registry template and application-local registry factory.
- `reme/components/job/`: base, stream, background, and cron job implementations.
- `reme/components/service/`: local CLI, HTTP, and MCP service backends.
- `reme/components/`: agent wrappers, model adapters, stores, catalogs, graphs, indexes, clients, tokenizers, and
outbound proxies.
- `reme/steps/`: registered job steps grouped by common, file I/O, index, evolve, cookbook, benchmark, and transfer
concerns.
- `reme/utils/`: shared utilities, including service discovery, logging, web-static resolution, session I/O, token
accounting, and wikilink handling.
- `tests/unit/`: primary fast, isolated validation suite.
- `tests/integration/`: service/model tests that may need credentials or external processes.
- `reme_studio/`: ReMe Studio frontend source plus the independently published `reme_studio` Python package and
`@agentscope-ai/reme_studio` npm static distribution.
- `typescript/`: the independently published `@agentscope-ai/reme` package, including the shared TypeScript client and
DeepSeek Harness and OpenClaw adapters.
- `plugins/`: installable ReMe extensions, such as Auto Fin.
- `integrations/`: adapters that connect ReMe to external agent hosts, such as Claude Code, DSH, and Hermes Agent.
- `skills/`: standalone skills; `reme_memory` calls ReMe, while other skills may use separate tools or direct-file
conventions.
- `benchmark/` and `cookbook/`: runnable evaluations and example workflows.
- `docs/`: README-linked supporting pages and figures.
## Development Setup
ReMe requires Python 3.11 or newer.
ReMe requires Python 3.11 or newer. Install the editable development environment with:
```bash
pip install -e ".[dev,core]"
pip install -e reme_studio -e ".[dev,core]"
```
Before changing behavior, inspect the adjacent implementation, schemas, configuration,
and focused tests. Follow existing patterns unless the task explicitly calls for a new
contract or architecture.
Before changing behavior, inspect the adjacent implementation, schema, built-in config, and focused tests. Follow
existing async and typing patterns unless the task explicitly requires a new contract.
## Change Workflow
## Configuration and CLI Contracts
1. Identify the narrowest supported contract affected by the request.
2. Read the relevant implementation and tests before editing.
3. Make the smallest coherent change; avoid unrelated cleanup.
4. Update related schemas, defaults, registrations, and imports when required.
5. Add or adjust focused tests for observable behavior.
6. Run proportionate validation and report anything not run.
- CLI syntax is `reme ACTION key=value ...`; leading `-` or `--` on arguments is accepted.
- Nested overrides use dot notation. Values support null, booleans, numbers, JSON collections, and quoted JSON strings;
leading-zero numeric-looking values remain strings.
- `config=<name-or-path>` loads a discovered config name or a `.yaml`, `.yml`, or `.json` file. With no explicit config
path, `default` is loaded when available.
- Config files expand `${VAR}` and `${VAR:-default}` recursively. An undefined variable without a default is an error.
- CLI/config overrides are deep-merged over the loaded file. Do not silently change this merge behavior or stable
configuration keys.
- `ApplicationConfig` normalizes `workspace_dir` to an expanded absolute path. `session_dir`
must remain workspace-relative; standard transcripts live under `{session_dir}/dialog`.
- `reme start` runs the configured service. `reme start job=<name> ...` switches to the one-shot CLI service and runs
the job through the normal application lifecycle.
- Other actions use a client selected from the running service configuration when discoverable, otherwise from local
config. Client-selection arguments must not leak into the job payload.
Component and step discovery depends on registration imports:
## Registration and Application Lifecycle
- Components use `R.register(...)` in `reme/components/component_registry.py`.
- Component packages must be reachable through `reme/components/__init__.py`.
- Step modules must be reachable through `reme/steps/__init__.py`.
Component and Step discovery is import-driven:
Adding an implementation without its registration import can leave it undiscoverable at
runtime. Treat the implementation, registry entry, and import side effect as one change.
- Implementations declare a non-`BASE` `component_type` and register with `@R.register("backend")`
or `R.register(Class, "backend")`.
- Component packages must be imported through `reme/components/__init__.py`.
- Step packages/modules must be reachable through their package `__init__.py` chain and ultimately
`reme/steps/__init__.py`.
- Adding an implementation without its registration import leaves it undiscoverable at runtime. Treat implementation,
registration, import side effect, defaults, and tests as one change.
Do not silently change stable CLI flags, configuration keys, workspace layouts, serialized
schemas, or service interfaces. When such a change is required, preserve compatibility
where practical and make the migration explicit.
`Application` validates config through `ApplicationContext`, creates workspace directories, instantiates the service,
configured components, and jobs, and then manages lifecycle as follows:
## Step State Model
- Components start in topological dependency order. Missing required dependencies and cycles fail explicitly; optional
dependencies may resolve to `None`.
- Jobs start after components in this order: base jobs, stream jobs, background jobs, then cron jobs.
- Shutdown closes everything in reverse start order and then shuts down the optional thread pool.
- If startup fails, already-started resources are closed.
- `BaseComponent.start()` and `close()` are lock-protected and idempotent. Dependencies created by a standalone
`default_factory` are owned and closed by the parent component.
Treat every Step as stateless. `BaseJob` stores Step specifications and builds fresh Step
instances for each Job invocation. A Step instance must not use `self` or class variables to
retain mutable runtime state between calls.
Keep async clients, tasks, executors, and services under this lifecycle. Do not introduce an untracked long-lived
resource.
Place state according to its lifetime:
## Jobs, Steps, and State
- Constructor fields on `self`: immutable Step configuration and resolved dependencies only.
- `self.context` (`RuntimeContext`): request data and intermediate results for one Job
execution; sequential Steps share this context.
- `self.app_context.metadata`: in-memory state that must be shared across Step or Job
invocations for the lifetime of the Application.
- Workspace files or a dedicated Component/store: durable state that must survive an
Application restart.
`BaseJob` resolves configured Step classes during job startup and constructs fresh Step instances for every invocation.
Job-level kwargs are merged into each `RuntimeContext`, with call-time kwargs taking precedence. Sequential Steps in one
invocation share the same `RuntimeContext` and `Response`.
Use narrow, namespaced keys in `app_context.metadata`, following existing patterns such as
`tool_contexts` and `channel_sink`. The ApplicationContext is shared, so account for
concurrent access when values are mutable. New Step code must not fall back to `self.kwargs`
or another Step field to emulate shared state when `app_context` is absent; tests of shared
state should construct an `ApplicationContext`. If shared state grows into a stable
service-level contract or needs its own lifecycle, locking, or persistence, promote it to a
typed ApplicationContext field or a dedicated Component instead of expanding an ad hoc
metadata bucket.
Treat Step instances as invocation-scoped:
Do not use `Response.metadata` as a state store. It is request-scoped output for callers and
diagnostics, distinct from `ApplicationContext.metadata`.
- Constructor fields and `self.kwargs` hold Step configuration and resolved dependencies. They may be cached or adjusted
during that one invocation, but must not be relied on across Job calls.
- `self.context.data` holds request inputs and intermediate values shared by sequential Steps.
- `self.context.response.answer`, `success`, and `metadata` are request-scoped output. Because the same response travels
through the Step chain, later Steps may consume metadata produced earlier, but it is not application-lifetime or
durable storage.
- `self.app_context.metadata` holds in-memory state shared across Job/Step invocations for the life of one
`Application`, such as counters, tool-context state, session maps, or locks.
- Workspace files or a dedicated Component/store hold durable state that must survive restart.
Use narrow, namespaced keys in `app_context.metadata` and protect shared mutable values against concurrent access. The
search/draft helpers intentionally mirror tool-context state into
`self.kwargs` only when no `ApplicationContext` exists for standalone use and unit tests; do not generalize that
compatibility fallback into persistent runtime state. If shared state becomes a stable service contract or needs
dedicated lifecycle, locking, or persistence, promote it to a typed context field or Component.
Additional Step contracts:
- `Ref` dependencies resolve in this order: Step kwargs, current `RuntimeContext`, then the named application component.
The value is cached only on the current Step instance and cleared before each call.
- `input_mapping` and `output_mapping` copy keys within `RuntimeContext.data`; missing sources are ignored.
- Dispatched Steps receive the current `RuntimeContext`, so their data and response are shared.
- Base jobs convert uncaught Step errors into `Response(success=False)`; stream jobs emit an error chunk and always a
terminal `DONE`; background jobs let errors reach their supervisor.
- Background jobs are never service-exposed. MCP also skips stream jobs. Respect `enable_serve`
and any configured service job allowlist.
## Workspace and File Safety
- Application startup creates the workspace plus configured metadata, session, memory-session, resource, daily, and
digest directories.
- File-operation paths are resolved against the workspace and must stay inside it. Home-relative paths are unsupported,
traversal escapes are rejected, and `_allowed_paths` restrictions fail closed when invalid.
- Preserve per-path locking, encoding detection, byte limits, truncation behavior, and optimistic
`expected_mtime` checks when modifying file operations.
- Do not bypass the existing file steps or stores in a way that weakens workspace containment.
- Never write test state into the repository's `.reme/`; use `tmp_path` or another isolated workspace.
- Do not delete or rewrite user memory to repair an index or make a test pass. Rebuild derived state from source files
instead.
## Validation
Use the narrowest useful check while iterating, then broaden it according to risk.
Run a focused test:
Focused test:
```bash
pytest tests/unit/path/to/test_file.py -v
```
Run the main unit suite:
Main unit suite:
```bash
pytest tests/unit -v --tb=long -s --log-cli-level=WARNING
```
Run repository formatting and lint checks when the change warrants it:
Repository formatting and lint checks:
```bash
pre-commit run --all-files
```
Formatting and lint configuration is authoritative. Python code currently uses a maximum
line length of 120 for Black and Flake8, with Pylint also run by pre-commit.
Black and Flake8 use a 120-character line limit and Python 3.11 formatting; Pylint is also run by pre-commit. If
`reme_studio/` changes, use its Node 22.13+ scripts and run the proportionate checks from that directory, such as
`npm run format:check`, `npm run lint`, or `npm test`.
Integration tests may contact real services and require credentials such as
`LLM_API_KEY` or `EMBEDDING_API_KEY`. Do not run credentialed or externally mutating tests
automatically. Run them only when the task requires them and the user has supplied or
authorized the necessary environment.
Integration tests may contact real model providers, services, or agent subprocesses and can require credentials. Do not
run credentialed or externally mutating tests automatically; run them only when the task requires them and the necessary
environment has been supplied or authorized. Mock network, model, and subprocess boundaries in unit tests.
## Coding and Test Conventions
- Target Python 3.11+ and follow the surrounding typing and async style.
- Steps are stateless. If a step needs to persist state, store it in
`self.app_context.metadata` rather than on the step instance.
- Keep public schemas explicit and backward-compatible where practical.
- Close async clients, services, tasks, and other lifecycle resources deterministically.
- Prefer clear failures over silently falling back to corrupt or ambiguous state.
- Keep indexes and caches derivable from user-owned source files.
- Use `tmp_path` or another isolated temporary workspace in tests.
- Never write test state into the repository's `.reme/` directory.
- Mock network or model boundaries in unit tests.
- Do not commit `.env` files, credentials, runtime memory, logs, indexes, or caches.
## Documentation Boundaries
ReMe's local docs and the deployed documentation site have separate responsibilities.
- Keep `docs/` focused on content and assets used by `README.md` and `README_ZH.md`.
- Preserve README-linked pages under `docs/en/` and `docs/zh/`, including their relative
paths, unless the README is updated in the same change.
- Keep README-required images under `docs/figure/`.
- Keep the README's main documentation index pointed at `docs.agentscope.io` or the
`agentscope-ai/docs` repository, following the existing link style.
- Do not treat local README-supporting pages as the source for the deployed website.
The separate `agentscope-ai/docs` repository owns website content, navigation, versioning,
and deployment. Public ReMe pages live there under `reme/<version>/`. Make website changes
in that repository and follow its existing version-management conventions.
Do not add website build configuration or deployment workflows to ReMe unless the task
explicitly changes this repository boundary.
## Agent Guardrails
## Change Guardrails
- Preserve unrelated user changes in a dirty working tree.
- Do not edit generated output when the source can be changed instead.
- Do not delete or rewrite user data to make a test pass.
- Avoid broad refactors unless they are necessary for the requested outcome.
- Do not introduce dependencies without a concrete need and repository-level justification.
- Treat network access, real credentials, and external service mutations as opt-in.
- State which validations passed and which were not run in the final handoff.
- Make the smallest coherent change and avoid unrelated cleanup or broad refactors.
- Do not edit generated output when the source can be changed instead. The publish workflow builds
`reme_studio/dist-static` and stages it under `reme_studio/src/reme_studio/static`; change `reme_studio/` source for
frontend work.
- Do not silently change CLI flags, configuration keys, workspace layouts, serialized schemas, endpoint shapes,
streaming termination, or service interfaces. Preserve compatibility where practical and document intentional
migrations.
- Do not introduce dependencies without a concrete repository-level need.
- Do not commit `.env` files, credentials, runtime memory, logs, indexes, caches, benchmark outputs, or generated
Studio distributions.
- State which validations passed and which relevant checks were not run in the final handoff.
If a requirement is ambiguous, first infer intent from nearby code, tests, and schemas. Ask
the user only when the remaining choice would materially alter a public contract, user data,
or external system.
If a requirement is ambiguous, infer intent from nearby code, schemas, defaults, and tests. Ask the user only when the
remaining choice would materially alter a public contract, user data, or an external system.

377
README.md
View file

@ -1,5 +1,5 @@
<p align="center">
<img src="docs/figure/reme_logo.png" alt="ReMe Logo" width="50%">
<img src="https://raw.githubusercontent.com/agentscope-ai/ReMe/main/docs/figure/reme_logo.png" alt="ReMe Logo" width="50%">
</p>
<p align="center">
@ -8,6 +8,7 @@
<a href="https://pepy.tech/project/reme-ai/"><img src="https://img.shields.io/pypi/dm/reme-ai" alt="PyPI Downloads"></a>
<a href="https://github.com/agentscope-ai/ReMe"><img src="https://img.shields.io/github/commit-activity/m/agentscope-ai/ReMe?style=flat-square" alt="GitHub commit activity"></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-black" alt="License"></a>
<a href="https://reme.agentscope.io"><img src="https://img.shields.io/badge/docs-ReMe-blue" alt="Documentation"></a>
<a href="./README.md"><img src="https://img.shields.io/badge/English-Click-yellow" alt="English"></a>
<a href="./README_ZH.md"><img src="https://img.shields.io/badge/简体中文-点击查看-orange" alt="简体中文"></a>
<a href="https://github.com/agentscope-ai/ReMe"><img src="https://img.shields.io/github/stars/agentscope-ai/ReMe?style=social" alt="GitHub Stars"></a>
@ -19,45 +20,44 @@
</p>
<p align="center">
<strong>An agent memory layer that turns conversations and resources into readable, editable, searchable Markdown memory.</strong><br>
<strong>A local-first, self-evolving personal knowledge base for AI agents.</strong><br>
</p>
> Previous versions: [0.3.x](https://github.com/agentscope-ai/ReMe/tree/reme_v3) ·
> [0.2.x](https://github.com/agentscope-ai/ReMe/tree/v0.2.0.6) ·
> [MemoryScope](https://github.com/agentscope-ai/ReMe/tree/memoryscope_branch)
🧠 ReMe is a local-first memory layer for **AI agents**. It turns conversations and resources into file-based long-term
memory, then continuously indexes, links, and consolidates that memory for future recall.
## ✨ Why ReMe?
## ✨ Core Ideas
🧠 ReMe turns conversations and resources into readable, editable, searchable, and interconnected Markdown memory. Agents
such as QwenPaw and DeepSeek Harness can share the same workspace to retrieve, maintain, and evolve knowledge, while
users retain control of the durable files.
- **Memory as File**: Markdown files with frontmatter and wikilinks serve as memory nodes that both users and agents can
read and write directly.
- **Self-evolving knowledge base**: Auto Memory, Auto Resource, and Auto Dream progressively transform conversations and
resources into long-term memories, while automatically building wikilink relationships.
- **Progressive hybrid search**: ReMe combines wikilinks, BM25, and embeddings for hybrid retrieval across keyword
matching, semantic recall, and relationship expansion.
- **Agent-friendly integration**: SKILL.md + CLI integration makes it easy for different agents to read, write,
maintain, and reuse memory.
- **Memory as File, File as Memory**: ReMe stores durable memory as ordinary Markdown with frontmatter and wikilinks.
Users and agents can inspect, edit, move, sync, and back it up with familiar tools, while indexes and generated
metadata remain rebuildable.
- **Self-evolving knowledge base**: ReMe progressively turns conversations and resources into daily notes and long-term
knowledge, preserving sources while refining facts, preferences, procedures, and relationships over time.
- **Recall is precise and context-aware.** BM25, optional embeddings, and wikilink expansion retrieve relevant
line-level passages and their relationships without loading the entire knowledge base into the agent context.
- **One memory workspace works across agents.** Personal assistants, coding agents, and other agent runtimes can share
the same local workspace through native integrations, SKILL.md, CLI, HTTP, MCP, or Python APIs.
<p align="center">
<img src="docs/figure/design-philosophy.svg" alt="ReMe Design Philosophy" width="92%">
</p>
## 🔭 Use Cases
- **Personal assistants**: Give personal assistants such as
[QwenPaw](https://github.com/agentscope-ai/QwenPaw), [OpenClaw](https://github.com/openclaw/openclaw), and
[Hermes](https://github.com/nousresearch/hermes-agent) a user-editable long-term memory layer.
- **Coding agents**: Preserve coding style, project background, repository decisions, and workflow
experience across sessions when integrating with coding agents such as [Claude Code](plugins/reme).
- **LLM Wiki**: Turn conversations, notes, and resources into a searchable, traceable, and linked Markdown
knowledge base that both users and agents can maintain.
- **Self-evolving agents**: Support agents that learn from experience by saving successful paths, failed attempts,
reusable procedures, and periodic reflections as memory.
## 📰 News
## 📰 Latest Updates
- [2026.08] - Published [`@agentscope-ai/reme`](https://www.npmjs.com/package/@agentscope-ai/reme), providing native
ReMe memory integrations for DeepSeek Harness and OpenClaw plus a shared TypeScript HTTP client.
- [2026.08] - Published the [ReMe blog](https://agentscope-ai.github.io/ReMe/?doc=en-reme-blog), an end-to-end introduction to its local-first memory
architecture, self-evolving workflows, hybrid search, proactive discovery, and benchmark results.
- [2026.08] - [Experience-driven enhancement method](https://reme.agentscope.io/?doc=toolmemory-en) of agent tool-use execution built
on ReMe is available on [arXiv:2608.03403](https://arxiv.org/abs/2608.03403).
- [2026.07] - Introduced optional plugins: [Daily Paper](https://reme.agentscope.io/?doc=daily-paper-en) for paper discovery and
analysis, and [Auto Fin](https://reme.agentscope.io/?doc=auto-fin-en) for researching the latest 24 hours of topic-related CLS news
with local-memory search and validated historical wikilinks.
- [2026.07] - Our
paper [Remember Me, Refine Me: A Dynamic Procedural Memory Framework for Experience-Driven Agent Evolution](https://aclanthology.org/2026.findings-acl.829/)
has been accepted to Findings of ACL 2026.
@ -79,33 +79,14 @@ Install from source:
```bash
git clone https://github.com/agentscope-ai/ReMe.git
cd ReMe
pip install -e ".[core]"
pip install -e reme_studio -e ".[core]"
cd reme_studio
npm ci
npm run build:static
cd ..
```
### Environment Variables
Configure environment variables when you want LLM-powered memory evolution or embedding retrieval. Embeddings are
disabled by default, so the default setup does not start an embedding model or require an embedding API key.
```bash
cat > .env <<'EOF'
# Optional: used only after embedding components are explicitly enabled in the config.
# EMBEDDING_API_KEY=sk-xxx
# EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# Required for auto_memory, auto_resource, and auto_dream.
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EOF
```
Basic file operations, BM25 search, wikilink traversal, and reading proactive topics can run without LLM credentials.
> [!NOTE]
> To enable embedding-based semantic retrieval, uncomment `components.as_embedding` and
> `components.embedding_store` in [`reme/config/default.yaml`](reme/config/default.yaml), then change
> `components.file_store.default.embedding_store` from `""` to `default`. See the
> [memory search guide](docs/en/memory_search.md) for details.
The static build requires Node.js 22.13 or newer and makes Studio available from the source tree.
### Start the Service
@ -120,10 +101,10 @@ reme start service.port=8181
# reme start workspace_dir=/tmp/reme-demo service.port=8181
```
After startup, check the service status. If you use a custom port, replace `2333` in the URL below with that port.
```bash
reme version
reme health_check
reme help
curl -s http://127.0.0.1:2333/version -H 'Content-Type: application/json' -d '{}'
```
@ -161,94 +142,51 @@ ReMe stores agent memory as readable Markdown.
Related: [[digest/wiki/memory-as-file.md]]
```
## 📁 Memory System
### ReMe Studio (Optional)
> Memory as File, File as Memory.
The `core` installation includes Studio. After starting ReMe, open <http://127.0.0.1:2333/> to browse, edit, and search
the workspace. To add Studio to a base installation, use `pip install "reme-ai[web]"`. See the
[ReMe Studio guide](https://reme.agentscope.io/?doc=studio-en) for source builds, configuration, and development.
ReMe treats **memory as files**, progressively processing raw conversations and external resources from `session/` and
`resource/` into `daily/`, then consolidating them into reusable long-term memory nodes under `digest/`.
### Optional Model Configuration
### Directory Structure
Configure environment variables when you want LLM-powered memory evolution or embedding retrieval. Embeddings are
disabled by default, so the default setup does not start an embedding model or require an embedding API key.
```text
<workspace_dir>/
├── metadata/ # Persistent system state such as indexes, graphs, and catalogs
├── session/ # Raw conversations and agent sessions
│ ├── dialog/
│ │ └── <session_id>.jsonl
│ ├── agentscope/
│ └── claude_code/
├── resource/ # External raw materials
│ └── YYYY-MM-DD/
│ └── <resource>.<ext>
├── daily/ # Lightly processed memory: daily facts, conversation summaries, resource readings
│ ├── YYYY-MM-DD.md
│ └── YYYY-MM-DD/
│ ├── <session_event>.md
│ ├── <resource_stem>.md
│ └── interests.yaml
└── digest/ # Long-term memory: personal facts, procedural experience, knowledge nodes
├── personal/
│ └── {topic/event}.md
├── procedure/
│ └── {topic/event}.md
└── wiki/
└── {topic/event}.md
```bash
cat > .env <<'EOF'
# Optional: used only after embedding components are explicitly enabled in the config.
# EMBEDDING_API_KEY=sk-xxx
# EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# Required for auto_memory, auto_resource, and auto_dream.
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EOF
```
<p align="center">
<img src="docs/figure/reme-overview.svg" alt="ReMe file-based memory system overview" width="92%">
</p>
Basic file operations, BM25 search, wikilink traversal, and reading proactive topics can run without LLM credentials.
## 🧭 Memory Design Philosophy
> [!NOTE]
> To enable embedding-based semantic retrieval, uncomment `components.as_embedding` and
> `components.embedding_store` in [`reme/config/default.yaml`](reme/config/default.yaml), then change
> `components.file_store.default.embedding_store` from `""` to `default`. See the
> [memory search guide](docs/en/memory_search.md) for details.
> Capture raw dialogs and resources, refine them into long-term preferences, reusable experience, and valuable
> knowledge,
> while keeping the result editable by humans and agents.
## 🤝 Use ReMe with Your Agent
### Automatic Memory Flow
ReMe can run as a local memory service accessed through the CLI, HTTP API, or MCP server, or it can be embedded in the
host process through its Python API. Host integrations can add memory guidance, recall, and capture to the agent
lifecycle according to the capabilities of each runtime.
ReMe follows a capture → index → consolidate → recall loop. Conversations and resources first become daily memory cards;
background jobs keep files searchable; `auto_dream` distills stable knowledge into `digest/`; agents recall memory
through search, wikilinks, or proactive topics.
| Capability | Entry point | What it does | Output |
|---------------------------------------------|-------------------------------------------------|-------------------------------------------------------------------------------------------------|---------------------------------------------------------|
| [`auto_memory`](docs/en/auto_memory.md) | Agent hook or `reme auto_memory` | Distills useful conversation facts while preserving the raw session. | `session/dialog/*.jsonl`, `daily/<date>/<session>.md` |
| [`auto_resource`](docs/en/auto_resource.md) | Resource watcher or `reme auto_resource` | Turns files under `resource/<date>/` into source-linked daily cards. | `daily/<date>/<resource-card>.md` |
| [`auto_index`](docs/en/memory_search.md) | Background watcher or `reme reindex` | Maintains chunks, the BM25 index, the wikilink graph, and the optional embedding index. | Searchable `daily/`, `digest/`, and `resource/` content |
| [`auto_dream`](docs/en/auto_dream.md) | `dream_cron` or `reme auto_dream` | Consolidates changed daily cards into long-term personal, procedure, and wiki memory. | `digest/**`, `daily/<date>/interests.yaml` |
| [`proactive`](docs/en/proactive.md) | `reme proactive` before an agent decides to act | Reads topics generated by `auto_dream`; the host agent decides whether and how to mention them. | Structured topics from `daily/<date>/interests.yaml` |
<table>
<tr>
<td align="center" width="50%">
<img src="docs/figure/memory-as-file.svg" alt="Memory as File" width="92%">
</td>
<td align="center" width="50%">
<img src="docs/figure/auto-memory-resource.svg" alt="Auto Memory and Resource" width="92%">
</td>
</tr>
<tr>
<td align="center" width="50%">
<img src="docs/figure/auto-dream-and-proactive.svg" alt="Auto Dream and Proactive" width="92%">
</td>
<td align="center" width="50%">
<img src="docs/figure/auto-index-and-memory-search.svg" alt="Auto Index and Memory Search" width="92%">
</td>
</tr>
</table>
## 🤝 Agent-friendly Integration
ReMe runs as a local memory service and offers multiple integration paths: CLI, HTTP API, MCP server, and SDK. Different
agents can choose the path that fits their runtime while sharing the same local memory workspace.
| Agents | Recommended path | What works out of the box |
|------------------------------------------------------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| **QwenPaw** | Embed ReMe via the Python SDK. | Reuse the app's own lifecycle and model config while keeping memory local and file-based. |
| **Claude Code** | Start ReMe as an MCP service and install [plugins/reme](plugins/reme). | MCP recall tools, a `reme-memory` skill, and a Stop hook that records sessions automatically. |
| **Other CLI-capable agents (OpenClaw/Hermes/Codex)** | Copy or install [skills/reme_memory/SKILL.md](skills/reme_memory/SKILL.md). | Search/read/write memory and call `auto_memory`, `auto_dream`, and `proactive` via the CLI. |
| Agent | Recommended path | Available after integration |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| **DeepSeek Harness** | Install [`@agentscope-ai/reme`](typescript/README.md#deepseek-harness) with `dsh plugin --profile web add @agentscope-ai/reme`. | Long-term memory guidance, the `reme_search` tool, and automatic capture of completed main-agent turns. |
| **OpenClaw** | Install [`@agentscope-ai/reme`](typescript/README.md#openclaw) with `openclaw plugins install @agentscope-ai/reme`. | Native memory tools, recall before user-triggered runs, and automatic turn capture. |
| **QwenPaw** | Embed ReMe in-process through its Python API. | Reuse the host lifecycle and model config while keeping memory local and file-based. |
| **Claude Code** | Start the streamable HTTP MCP service and install [the ReMe plugin](integrations/claude_code/reme). | MCP recall tools, the `reme-memory` skill, and a Stop hook that records sessions automatically. |
| **Hermes** | Start the HTTP service and install [the ReMe provider](integrations/hermes_agent). | Recall before model calls and asynchronous `auto_memory` after each completed turn. |
| **Codex and other CLI agents** | Install or copy the [ReMe Memory skill](skills/reme_memory/SKILL.md). | Search, read, and write memory through the CLI; automatic capture requires host lifecycle integration. |
<p align="center"><b>Integration demos</b></p>
@ -278,39 +216,166 @@ agents can choose the path that fits their runtime while sharing the same local
</tr>
</table>
## 🛠️ ReMe Operations
## 🧠 How ReMe Works
ReMe operates the workspace through a unified job interface exposed by the CLI. Agents usually only need retrieval,
reading, writing, editing, and automatic memory commands. Lower-level indexing, frontmatter, and file operation commands
are mainly for maintenance, debugging, or advanced integration. Run `reme help` for the full job list.
> Memory as File, File as Memory.
| Command | Purpose |
|-------------------------------------------|----------------------------------------------------------------------------------------|
| `reme start` | Start the local ReMe service. |
| `reme version` / `reme health_check` | Check package and component status. |
| `reme status` | Show stateful data-component memory estimates and process RSS. |
| [`reme search`](docs/en/memory_search.md) | Retrieve memory with BM25 and wikilinks by default, plus vectors when enabled. |
| `reme read` / `reme write` / `reme edit` | Inspect and maintain Markdown memory files. |
| `reme auto_memory` | Turn conversation messages into daily memory cards. Requires LLM credentials. |
| `reme auto_resource` | Interpret files under `resource/` into daily resource cards. Requires LLM credentials. |
| `reme auto_dream` / `reme proactive` | Consolidate daily memory into long-term digest and surface topics worth attention. |
| `reme reindex` | Rebuild search and wikilink indexes from existing files. |
ReMe treats **memory as files**, progressively processing filtered conversation source records and external resources
from `session/` and `resource/` into `daily/`, then `digest/`. The default workspace is `.reme/` under the current
directory; `workspace_dir=...` selects a different user-owned location.
## 🤝 Community and Support
### Workspace Layout
- **Issues and requests**: Check [Open Issues](https://github.com/agentscope-ai/ReMe/issues) first. If there is no
related discussion, open a new issue with background, expected behavior, and impact scope.
- **Code contributions**: Before making changes, read
the [contribution guide](https://docs.agentscope.io/reme/stable/en/contributing). Source,
schemas, and tests are the authoritative architecture and extension guide.
- **Documentation contributions**: Submit user-facing documentation changes to the
[unified documentation repository](https://github.com/agentscope-ai/docs) under `reme/<version>/{en,zh}/`.
```text
<workspace_dir>/
├── metadata/ # Rebuildable indexes, graphs, catalogs, and caches
├── session/ # Conversation source records and agent sessions
│ ├── dialog/
│ │ └── <session_id>.jsonl # Source messages saved by auto_memory
│ └── claude_code/
│ └── <session_id>.jsonl # ReMe copy used by auto_memory_cc
├── mem_session/ # Generated agent-wrapper sessions/config, not user memory
│ ├── agentscope/
│ ├── claude_config/
│ └── codex/
├── resource/ # External raw materials
│ ├── <resource>.<ext> # Root-level files enter today's daily layer
│ └── YYYY-MM-DD/
│ └── <resource>.<ext>
├── daily/ # Lightly processed memory: daily facts, conversation summaries, resource readings
│ ├── YYYY-MM-DD.md
│ └── YYYY-MM-DD/
│ ├── <generated_name>.md # Topic-named conversation or resource card
│ └── interests.yaml
└── digest/ # Long-term memory: personal facts, procedural experience, knowledge nodes
├── personal/
│ └── {topic/event}.md
├── procedure/
│ └── {topic/event}.md
└── wiki/
└── {topic/event}.md
```
<p align="center">
<img src="docs/figure/reme-overview.svg" alt="ReMe file-based memory system overview" width="92%">
</p>
### Memory Lifecycle
ReMe follows a capture → index → consolidate → recall loop. Workspace files remain the durable source of truth;
everything under `metadata/` is rebuildable.
| Capability | Entry point | What it does | Output |
| ------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [`auto_memory`](docs/en/auto_memory.md) | Agent hook or `reme auto_memory` | Distills useful conversation facts while preserving a filtered conversation source record. | `session/dialog/*.jsonl`, `daily/<date>/<generated-name>.md` |
| [`auto_resource`](docs/en/auto_resource.md) | Resource watcher or `reme auto_resource` | Turns files under `resource/` into source-linked, content-named daily cards. | `daily/<date>/<resource-card>.md` |
| [`auto_index`](docs/en/memory_search.md) | Background watcher or `reme reindex` | Live-indexes Markdown in `daily/` and `digest/`; a full rebuild also scans `resource/` and JSONL. | Searchable chunks, BM25, wikilink graph, and optional vectors |
| [`auto_dream`](docs/en/auto_dream.md) | `dream_cron` or `reme auto_dream` | By default, extracts up to five reusable units from changed files in the latest two-day window, then creates, corroborates, refines, or corrects digest nodes. | `digest/**`, `daily/<date>/interests.yaml` |
| [`proactive`](docs/en/proactive.md) | `reme proactive` before an agent decides to act | Reads topics generated by `auto_dream`; the host agent decides whether and how to mention them. | Structured topics from `daily/<date>/interests.yaml` |
<table>
<tr>
<td align="center" width="50%">
<img src="docs/figure/memory-as-file.svg" alt="Memory as File" width="92%">
</td>
<td align="center" width="50%">
<img src="docs/figure/auto-memory-resource.svg" alt="Auto Memory and Resource" width="92%">
</td>
</tr>
<tr>
<td align="center" width="50%">
<img src="docs/figure/auto-dream-and-proactive.svg" alt="Auto Dream and Proactive" width="92%">
</td>
<td align="center" width="50%">
<img src="docs/figure/auto-index-and-memory-search.svg" alt="Auto Index and Memory Search" width="92%">
</td>
</tr>
</table>
Search returns matching chunks with line ranges and bounded wikilink neighbors. Optional vector results are fused with
BM25 through reciprocal rank fusion (RRF).
> [!IMPORTANT]
>
> `proactive` only reads and exposes interest topics produced by Auto Dream. It does not independently browse the web,
> send notifications, or rewrite the knowledge base; the host agent decides whether and how to act on a topic.
## 📊 Benchmarks
ReMe evaluates multi-session and long-context memory with agentic search-and-read workflows. The figures below are the
published reference runs in this repository; model, prompt, dataset, and judging details are documented with each
benchmark.
| Benchmark | Setting | Sample size | Agentic score | Focus |
| --------------------------------------------------------------------------- | ------------ | -----------------------: | ------------: | ------------------------------------------------------------------ |
| **[LongMemEval cleaned-s](https://reme.agentscope.io/?doc=longmemeval-en)** | **Overall** | **500 questions** | **89.4%** | Cross-session retrieval, knowledge updates, and temporal reasoning |
| [BEAM](https://reme.agentscope.io/?doc=beam-en) | 100K context | 20 cases / 400 questions | 66.1% | Ten types of long-context memory tasks |
| [BEAM](https://reme.agentscope.io/?doc=beam-en) | 1M context | 35 cases / 700 questions | 65.0% | Ultra-long conversation settings |
ReMe also achieved a **0.580 PROC score across five user personas** in the repository's
[π-Bench evaluation](https://reme.agentscope.io/?doc=pibench-en), 2.4% above NanoBot under the same test-model configuration. PROC
measures proactive handling of hidden intent, clarification, cross-session preferences and conventions, task
dependencies, and underspecified requests.
## 🧩 Extensions and Plugins
Plugins are optional Python distributions that contribute Component, Step, or Job backends and configuration. They are
installed separately and enabled explicitly by configuration. Daily Paper and Auto Fin are independently packaged
plugins; see the source distributions and their documentation for [Daily Paper](plugins/daily_paper/README.md) and
[Auto Fin](plugins/auto-fin/README.md).
| Plugin | Capability |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| [Daily Paper](https://reme.agentscope.io/?doc=daily-paper-en) | Discover and rank papers, analyze PDFs with an agent, and generate file-native notes and a five-minute brief. |
| [Auto Fin](https://reme.agentscope.io/?doc=auto-fin-en) | Fetch topic-related CLS news, search ReMe history, and generate wikilink-backed Markdown reports. |
See [Plugin Management](docs/en/plugin_management.md) to install, inspect, validate, enable, and uninstall ReMe plugins.
## 📚 Documentation
These guides cover the main user workflows and the runtime contracts implemented by the current code.
| Guide | What you will learn |
| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [Quick Start](docs/en/quick_start.md) | Install ReMe, start the service, and run the first file and memory operations. |
| [Memory as File](docs/en/memory_as_file.md) | Understand workspace layers, frontmatter, wikilinks, chunks, and the file-as-source-of-truth model. |
| [Auto Memory](docs/en/auto_memory.md) | Preserve source conversations and distill reusable daily memory cards. |
| [Auto Resource](docs/en/auto_resource.md) | Import supported text resources and turn them into source-linked daily cards. |
| [Auto Dream](docs/en/auto_dream.md) and [Auto Link](docs/en/auto_link.md) | Consolidate daily notes into evolving digest nodes and readable wikilink relationships. |
| [Memory Search](docs/en/memory_search.md) | Use BM25, optional vectors, RRF fusion, line-range recall, and progressive link expansion. |
| [Proactive](docs/en/proactive.md) | Read interest topics safely and integrate them into a host agent's decision flow. |
| [Application Scenarios](docs/en/reme_scene.md) | Follow concrete financial research, coding-memory, and personal knowledge-base examples. |
| [Framework](docs/en/framework.md) | Understand Application, Job, Step, Component, service, configuration, and lifecycle boundaries. |
| [TypeScript integrations](typescript/README.md) | Configure the shared client and native DeepSeek Harness and OpenClaw adapters. |
| [ReMe Blog](https://agentscope-ai.github.io/ReMe/?doc=en-reme-blog) | Read the product story, design rationale, examples, and benchmark summary. |
## 🛠️ Common Commands
Run `reme help` for the full job list. Common workspace and maintenance commands are:
| Command | Purpose |
| ----------------------------------------- | --------------------------------------------------------------------------------- |
| `reme status` | Show stateful data-component memory estimates and process RSS. |
| [`reme search`](docs/en/memory_search.md) | Retrieve memory with BM25 and wikilinks by default, plus vectors when enabled. |
| `reme read` / `reme write` / `reme edit` | Inspect and maintain Markdown memory files. |
| `reme traverse` / `reme graph_snapshot` | Explore wikilink neighborhoods or the category-rooted digest graph. |
| `reme chat` | Stream a read-only, workspace-aware agent conversation. Requires LLM credentials. |
| `reme reindex` | Rebuild search and wikilink indexes from existing files. |
## 🤝 Community and Contributing
- **Issues, requests, and help**: Check [Open Issues](https://github.com/agentscope-ai/ReMe/issues) first. If there is no
related discussion, open one with the background, expected behavior, and impact scope.
- **Code contributions**: Before making changes, read the repository's
[contribution guide](docs/en/contributing.md). Source, schemas, and tests are the authoritative architecture and
extension guide.
- **Documentation contributions**: Update the canonical files under `docs/en/`, `docs/zh/`, or the relevant package
directory in this repository. The documentation site is generated from these files.
- **Commit convention**: Conventional Commits are recommended, for example `feat(search): add link expansion option` or
`docs(zh): update quick start`.
- **Pre-submit checks**: Before submitting a PR, try to run `pre-commit run --all-files` and `pytest`. If tests that
depend on LLMs, embeddings, or external services cannot run, explain that in the PR.
- **Get help**: Use [GitHub Issues](https://github.com/agentscope-ai/ReMe/issues) for bugs and feature requests. Project
documentation is available at [https://docs.agentscope.io/](https://docs.agentscope.io/reme/stable/en/).
- **Documentation**: Visit [reme.agentscope.io](https://reme.agentscope.io).
### Contributors

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="docs/figure/reme_logo.png" alt="ReMe Logo" width="50%">
<img src="https://raw.githubusercontent.com/agentscope-ai/ReMe/main/docs/figure/reme_logo.png" alt="ReMe Logo" width="50%">
</p>
<p align="center">
@ -8,6 +8,7 @@
<a href="https://pepy.tech/project/reme-ai/"><img src="https://img.shields.io/pypi/dm/reme-ai" alt="PyPI Downloads"></a>
<a href="https://github.com/agentscope-ai/ReMe"><img src="https://img.shields.io/github/commit-activity/m/agentscope-ai/ReMe?style=flat-square" alt="GitHub commit activity"></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-black" alt="License"></a>
<a href="https://reme.agentscope.io"><img src="https://img.shields.io/badge/docs-ReMe-blue" alt="文档"></a>
<a href="./README.md"><img src="https://img.shields.io/badge/English-Click-yellow" alt="English"></a>
<a href="./README_ZH.md"><img src="https://img.shields.io/badge/简体中文-点击查看-orange" alt="简体中文"></a>
<a href="https://github.com/agentscope-ai/ReMe"><img src="https://img.shields.io/github/stars/agentscope-ai/ReMe?style=social" alt="GitHub Stars"></a>
@ -19,38 +20,43 @@
</p>
<p align="center">
<strong>一个将对话和资料转化为可读、可编辑、可检索 Markdown 记忆的 Agent 记忆层</strong><br>
<strong>面向 AI Agent 的 local-first 自进化个人知识库</strong><br>
</p>
> 历史版本:[0.3.x](https://github.com/agentscope-ai/ReMe/tree/reme_v3) ·
> [0.2.x](https://github.com/agentscope-ai/ReMe/tree/v0.2.0.6) ·
> [MemoryScope](https://github.com/agentscope-ai/ReMe/tree/memoryscope_branch)
🧠 ReMe 是一个面向 **AI 智能体** 的 local-first 记忆层。它把对话和资料沉淀为文件化长期记忆,并持续完成索引、链接和整理,让后续
Agent 能够可靠召回。
## ✨ 为什么选择 ReMe
## ✨ 核心创新
🧠 ReMe 将对话和资料持续沉淀为可读、可编辑、可检索、相互链接的 Markdown 记忆。QwenPaw、DeepSeek Harness 等 Agent
可以共享同一个 workspace共同检索、维护和演化知识而持久文件始终由用户掌控。
- **Memory as File**:以带 frontmatter 和 wikilink 的 Markdown 作为记忆节点,让用户和 Agent 都能直接读写。
- **自进化知识库**:通过 Auto Memory、Auto Resource 和 Auto Dream把对话与资料逐步加工为长期记忆并自动建立 wikilink 关系。
- **渐进式混合搜索**:融合 wikilink、BM25 和 embedding支持从关键词匹配到语义召回、关系扩展的混合检索。
- **Agent 友好集成**:通过 SKILL.md + CLI 接入,方便不同 Agent 读写、维护与复用记忆。
- **Memory as File, File as Memory**ReMe 使用带 frontmatter 和 wikilink 的普通 Markdown 保存持久记忆。用户和 Agent
都可以使用熟悉的工具查看、编辑、移动、同步和备份;索引及生成的元数据均可重建。
- **自进化知识库**ReMe 将对话和资料逐步加工为 daily note 与长期知识,在保留来源的同时,持续提炼事实、偏好、
流程经验及其关系。
- **精准召回所需上下文。** ReMe 结合 BM25、可选 embedding 和 wikilink 展开,召回带行号的相关片段及其关系,无需把整个知识库塞入
Agent 上下文。
- **一个 workspace可供不同 Agent 共同使用。** 个人助理、coding agent 和其他 Agent runtime 可以通过原生集成、SKILL.md、CLI、
HTTP、MCP 或 Python API 共享同一个本地记忆空间。
<p align="center">
<img src="docs/figure/design-philosophy.svg" alt="ReMe 设计理念" width="92%">
</p>
## 🔭 适用场景
- **Personal assistants**:为 [QwenPaw](https://github.com/agentscope-ai/QwenPaw)、
[OpenClaw](https://github.com/openclaw/openclaw)、[Hermes](https://github.com/nousresearch/hermes-agent)
等个人助理提供用户可编辑的长期记忆层。
- **Coding agents**:在接入 [Claude Code](plugins/reme) 等 coding agent 时,跨会话保留代码风格、项目背景、仓库决策和流程经验。
- **LLM Wiki**:把对话、笔记和资料转化为可检索、可追溯、可链接的 Markdown 知识库,由用户和 Agent 共同维护。
- **Self-evolving agents**:帮助 Agent 从经验中学习,把成功路径、失败尝试、可复用流程和阶段性反思沉淀为记忆。
## 📰 新闻
## 📰 最新动态
- [2026.08] - 发布 [`@agentscope-ai/reme`](https://www.npmjs.com/package/@agentscope-ai/reme),提供统一 TypeScript HTTP
client以及 DeepSeek Harness 和 OpenClaw 的原生 ReMe 记忆集成。
- [2026.08] - 发布 [ReMe 博客](https://agentscope-ai.github.io/ReMe/?doc=zh-reme-blog),系统介绍本地优先的记忆架构、自进化工作流、混合检索、
主动发现与评测结果。
- [2026.08] - 基于 ReMe 的智能体工具使用
[经验驱动增强方法](https://reme.agentscope.io/?doc=toolmemory-zh)已发布,见
[arXiv:2608.03403](https://arxiv.org/abs/2608.03403)。
- [2026.07] - 新增可选插件:[每日论文](https://reme.agentscope.io/?doc=daily-paper-zh)用于论文发现与解析,
[Auto Fin](https://reme.agentscope.io/?doc=auto-fin-zh)用于研究最近 24 小时的主题相关财联社新闻,通过本地记忆搜索回顾历史材料并构建
wikilink。
- [2026.07] -
我们的论文 [Remember Me, Refine Me: A Dynamic Procedural Memory Framework for Experience-Driven Agent Evolution](https://aclanthology.org/2026.findings-acl.829/)
已被 Findings of ACL 2026 接收。
@ -72,33 +78,14 @@ pip install "reme-ai[core]"
```bash
git clone https://github.com/agentscope-ai/ReMe.git
cd ReMe
pip install -e ".[core]"
pip install -e reme_studio -e ".[core]"
cd reme_studio
npm ci
npm run build:static
cd ..
```
### 环境变量
如果需要 LLM 驱动的记忆演化或 embedding 检索可以配置环境变量。embedding 默认关闭,因此默认配置不会启动
embedding 模型,也不需要 embedding API key。
```bash
cat > .env <<'EOF'
# 可选:仅在配置中显式启用 embedding 组件后使用。
# EMBEDDING_API_KEY=sk-xxx
# EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# 必须auto_memory、auto_resource 和 auto_dream 需要 LLM。
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EOF
```
基础文件读写、BM25 检索、wikilink 遍历和 proactive topics 读取可以先不配置 LLM 凭证。
> [!NOTE]
> 如需启用基于 embedding 的语义检索,请取消 [`reme/config/default.yaml`](reme/config/default.yaml) 中
> `components.as_embedding``components.embedding_store` 的注释,并将
> `components.file_store.default.embedding_store``""` 改为 `default`。完整说明见
> [记忆检索文档](docs/zh/memory_search.md)。
静态构建要求 Node.js 22.13 或更高版本,并让源码安装可以直接使用 Studio。
### 启动服务
@ -113,10 +100,10 @@ reme start service.port=8181
# reme start workspace_dir=/tmp/reme-demo service.port=8181
```
启动后可以检查服务状态;如果使用了自定义端口,请将下面 URL 中的 `2333` 替换为对应端口。
```bash
reme version
reme health_check
reme help
curl -s http://127.0.0.1:2333/version -H 'Content-Type: application/json' -d '{}'
```
@ -154,91 +141,50 @@ ReMe 会把 Agent 记忆保存为可读的 Markdown。
相关链接:[[digest/wiki/memory-as-file.md]]
```
## 📁 记忆系统
### ReMe Studio可选
> Memory as File, File as Memory.
上面的 `core` 安装已包含 Studio。启动 ReMe 后,打开 <http://127.0.0.1:2333/> 即可浏览、编辑和搜索 workspace。
如需为基础安装单独添加 Studio可使用 `pip install "reme-ai[web]"`。源码构建、配置和开发说明见
[ReMe Studio 指南](https://reme.agentscope.io/?doc=studio-zh)。
ReMe 将**记忆视为文件**,让原始对话和外部资料从 `session/``resource/` 渐进加工到 `daily/`,再沉淀为 `digest/`
中可长期复用的知识节点。
### 可选模型配置
### 目录结构
如果需要 LLM 驱动的记忆演化或 embedding 检索可以配置环境变量。embedding 默认关闭,因此默认配置不会启动 embedding 模型,也不需要
embedding API key。
```text
<workspace_dir>/
├── metadata/ # 系统索引、图谱、catalog 等持久状态
├── session/ # 原始对话和 Agent session
│ ├── dialog/
│ │ └── <session_id>.jsonl
│ ├── agentscope/
│ └── claude_code/
├── resource/ # 外部原始材料
│ └── YYYY-MM-DD/
│ └── <resource>.<ext>
├── daily/ # 浅加工记忆:当天事实、对话摘要、资源解读
│ ├── YYYY-MM-DD.md
│ └── YYYY-MM-DD/
│ ├── <session_event>.md
│ ├── <resource_stem>.md
│ └── interests.yaml
└── digest/ # 长期记忆:个人事实、流程经验、知识节点
├── personal/
│ └── {topic/event}.md
├── procedure/
│ └── {topic/event}.md
└── wiki/
└── {topic/event}.md
```bash
cat > .env <<'EOF'
# 可选:仅在配置中显式启用 embedding 组件后使用。
# EMBEDDING_API_KEY=sk-xxx
# EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# 必须auto_memory、auto_resource 和 auto_dream 需要 LLM。
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EOF
```
<p align="center">
<img src="docs/figure/reme-overview.svg" alt="ReMe 文件化记忆系统总览" width="92%">
</p>
基础文件读写、BM25 检索、wikilink 遍历和 proactive topics 读取可以先不配置 LLM 凭证。
## 🧭 记忆设计理念
> [!NOTE]
> 如需启用基于 embedding 的语义检索,请取消 [`reme/config/default.yaml`](reme/config/default.yaml) 中
> `components.as_embedding``components.embedding_store` 的注释,并将
> `components.file_store.default.embedding_store``""` 改为 `default`。完整说明见
> [记忆检索文档](docs/zh/memory_search.md)。
> 捕获原始对话和资料,将其整理为长期偏好、可复用经验和有价值的知识,并让结果始终能被用户和 Agent 直接编辑。
## 🤝 将 ReMe 接入你的 Agent
### 自动记忆流程
ReMe 既可以作为本地记忆服务,通过 CLI、HTTP API 或 MCP server 接入,也可以通过 Python API 嵌入宿主进程。宿主集成可根据不同
runtime 的能力,将记忆指引、召回和捕获接入 Agent 生命周期。
ReMe 遵循 capture → index → consolidate → recall 的循环。对话和资料先变成 daily 记忆卡片;后台任务保持文件可检索;
`auto_dream` 将稳定知识沉淀到 `digest/`Agent 再通过搜索、wikilink 或 proactive topics 召回记忆。
| 能力 | 入口 | 作用 | 输出 |
|---------------------------------------------|----------------------------------|----------------------------------------------------|------------------------------------------------------|
| [`auto_memory`](docs/zh/auto_memory.md) | Agent hook 或 `reme auto_memory` | 提炼有长期价值的对话事实,同时保留原始 session。 | `session/dialog/*.jsonl``daily/<date>/<session>.md` |
| [`auto_resource`](docs/zh/auto_resource.md) | 资源监听或 `reme auto_resource` | 将 `resource/<date>/` 下的文件转为带来源链接的 daily 卡片。 | `daily/<date>/<resource-card>.md` |
| [`auto_index`](docs/zh/memory_search.md) | 后台监听或 `reme reindex` | 维护 chunks、BM25 索引、wikilink 图谱及可选的 embedding 索引。 | 可检索的 `daily/``digest/``resource/` 内容 |
| [`auto_dream`](docs/zh/auto_dream.md) | `dream_cron``reme auto_dream` | 将变化的 daily 卡片整理为长期 personal、procedure 和 wiki 记忆。 | `digest/**``daily/<date>/interests.yaml` |
| [`proactive`](docs/zh/proactive.md) | Agent 决定主动行动前调用 `reme proactive` | 读取 `auto_dream` 生成的 topics是否以及如何提醒用户由宿主 Agent 决定。 | 来自 `daily/<date>/interests.yaml` 的结构化 topics |
<table>
<tr>
<td align="center" width="50%">
<img src="docs/figure/memory-as-file.svg" alt="Memory as File" width="92%">
</td>
<td align="center" width="50%">
<img src="docs/figure/auto-memory-resource.svg" alt="Auto Memory and Resource" width="92%">
</td>
</tr>
<tr>
<td align="center" width="50%">
<img src="docs/figure/auto-dream-and-proactive.svg" alt="Auto Dream and Proactive" width="92%">
</td>
<td align="center" width="50%">
<img src="docs/figure/auto-index-and-memory-search.svg" alt="Auto Index and Memory Search" width="92%">
</td>
</tr>
</table>
## 🤝 Agent-friendly Integration
ReMe 作为本地记忆服务运行,并提供 CLI、HTTP API、MCP server 和 SDK 等多种接入方式。不同 Agent 可以选择适合自身 runtime
的路径,同时共享同一个本地 memory workspace。
| Agent | 推荐接入方式 | 开箱可用能力 |
|------------------------------------------------------|-------------------------------------------------------------------|-----------------------------------------------------------------|
| **QwenPaw** | 通过 Python SDK 嵌入 ReMe。 | 复用应用自身生命周期和模型配置,同时保持 memory 本地、文件化。 |
| **Claude Code** | 以 MCP service 启动 ReMe并安装 [plugins/reme](plugins/reme)。 | MCP recall tools、`reme-memory` skill以及自动记录会话的 Stop hook。 |
| **Other CLI-capable agents (OpenClaw/Hermes/Codex)** | 复制或安装 [skills/reme_memory/SKILL.md](skills/reme_memory/SKILL.md)。 | 通过 CLI 搜索/读取/写入记忆,并调用 `auto_memory``auto_dream``proactive`。 |
| Agent | 推荐接入方式 | 接入后能力 |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| **DeepSeek Harness** | 使用 `dsh plugin --profile web add @agentscope-ai/reme` 安装 [`@agentscope-ai/reme`](typescript/README_ZH.md#deepseek-harness)。 | 长期记忆指引、`reme_search` 工具,以及自动捕获已完成的主 Agent 对话。 |
| **OpenClaw** | 使用 `openclaw plugins install @agentscope-ai/reme` 安装 [`@agentscope-ai/reme`](typescript/README_ZH.md#openclaw)。 | 原生记忆工具、用户触发运行前召回和自动对话捕获。 |
| **QwenPaw** | 通过 Python API 在进程内嵌入 ReMe。 | 复用宿主生命周期和模型配置,同时保持记忆本地、文件化。 |
| **Claude Code** | 启动 streamable HTTP MCP service并安装 [ReMe 插件](integrations/claude_code/reme)。 | MCP 召回工具、`reme-memory` skill以及自动记录会话的 Stop hook。 |
| **Hermes** | 启动 HTTP service并安装 [ReMe provider](integrations/hermes_agent)。 | 模型调用前召回,每轮对话完成后异步执行 `auto_memory`。 |
| **Codex 及其他 CLI Agent** | 安装或复制 [ReMe Memory skill](skills/reme_memory/SKILL.md)。 | 通过 CLI 搜索、读取和写入记忆;自动捕获需要显式接入宿主生命周期。 |
<p align="center"><b>集成演示</b></p>
@ -268,36 +214,155 @@ ReMe 作为本地记忆服务运行,并提供 CLI、HTTP API、MCP server 和
</tr>
</table>
## 🛠️ ReMe Operations
## 🧠 ReMe 如何工作
ReMe 通过 CLI 暴露的统一 job interface 操作 workspace。Agent 通常只需要使用检索、读取、写入、编辑和自动记忆相关命令;更底层的索引、
frontmatter 和文件操作接口主要用于维护、调试或高级集成。完整 job 列表可以运行 `reme help` 查看。
> Memory as File, File as Memory.
| 命令 | 作用 |
|-------------------------------------------|---------------------------------------------|
| `reme start` | 启动本地 ReMe 服务。 |
| `reme version` / `reme health_check` | 检查包版本和组件状态。 |
| `reme status` | 查看有状态数据组件的内存估算及进程 RSS。 |
ReMe 将 **记忆视为文件**,让过滤后的对话来源记录和外部资料从 `session/``resource/` 渐进加工到 `daily/`,再沉淀为
`digest/`。默认 workspace 是当前目录下的 `.reme/`;可通过 `workspace_dir=...` 选择其他由用户控制的位置。
### Workspace 结构
```text
<workspace_dir>/
├── metadata/ # 可重建的索引、图谱、catalog 和缓存
├── session/ # 对话来源记录和 Agent session
│ ├── dialog/
│ │ └── <session_id>.jsonl # auto_memory 保存的来源消息
│ └── claude_code/
│ └── <session_id>.jsonl # auto_memory_cc 使用的 ReMe 副本
├── mem_session/ # Agent wrapper 生成的 session/配置,不是用户记忆
│ ├── agentscope/
│ ├── claude_config/
│ └── codex/
├── resource/ # 外部原始材料
│ ├── <resource>.<ext> # 根目录文件进入当天 daily 层
│ └── YYYY-MM-DD/
│ └── <resource>.<ext>
├── daily/ # 浅加工记忆:当天事实、对话摘要、资源解读
│ ├── YYYY-MM-DD.md
│ └── YYYY-MM-DD/
│ ├── <generated_name>.md # 按主题命名的对话或资源卡片
│ └── interests.yaml
└── digest/ # 长期记忆:个人事实、流程经验、知识节点
├── personal/
│ └── {topic/event}.md
├── procedure/
│ └── {topic/event}.md
└── wiki/
└── {topic/event}.md
```
<p align="center">
<img src="docs/figure/reme-overview.svg" alt="ReMe 文件化记忆系统总览" width="92%">
</p>
### 记忆生命周期
ReMe 遵循 capture → index → consolidate → recall 的循环。workspace 文件是持久化的事实来源,`metadata/` 中的内容均可重建。
| 能力 | 入口 | 作用 | 输出 |
| ------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| [`auto_memory`](docs/zh/auto_memory.md) | Agent hook 或 `reme auto_memory` | 提炼有长期价值的对话事实,同时保留过滤后的对话来源记录。 | `session/dialog/*.jsonl``daily/<date>/<generated-name>.md` |
| [`auto_resource`](docs/zh/auto_resource.md) | 资源监听或 `reme auto_resource` | 将 `resource/` 下的文件转为带来源链接、按内容命名的 daily 卡片。 | `daily/<date>/<resource-card>.md` |
| [`auto_index`](docs/zh/memory_search.md) | 后台监听或 `reme reindex` | 实时索引 `daily/``digest/` 中的 Markdown全量重建还会扫描 `resource/` 和 JSONL。 | 可检索的 chunks、BM25、wikilink 图谱和可选向量 |
| [`auto_dream`](docs/zh/auto_dream.md) | `dream_cron``reme auto_dream` | 默认从最近两天内变化的文件中最多提取 5 个可复用 unit再创建、印证、补充或修正 digest 节点。 | `digest/**``daily/<date>/interests.yaml` |
| [`proactive`](docs/zh/proactive.md) | Agent 决定主动行动前调用 `reme proactive` | 读取 `auto_dream` 生成的 topics是否以及如何提醒用户由宿主 Agent 决定。 | 来自 `daily/<date>/interests.yaml` 的结构化 topics |
<table>
<tr>
<td align="center" width="50%">
<img src="docs/figure/memory-as-file.svg" alt="Memory as File" width="92%">
</td>
<td align="center" width="50%">
<img src="docs/figure/auto-memory-resource.svg" alt="Auto Memory and Resource" width="92%">
</td>
</tr>
<tr>
<td align="center" width="50%">
<img src="docs/figure/auto-dream-and-proactive.svg" alt="Auto Dream and Proactive" width="92%">
</td>
<td align="center" width="50%">
<img src="docs/figure/auto-index-and-memory-search.svg" alt="Auto Index and Memory Search" width="92%">
</td>
</tr>
</table>
搜索返回带行号范围的相关 chunks 和数量受限的 wikilink 邻居;可选向量结果通过 RRF 与 BM25 融合。
> [!IMPORTANT]
>
> `proactive` 只读取并暴露 Auto Dream 生成的兴趣主题,不会自行联网、发送通知或改写知识库;是否以及如何使用主题,由宿主 Agent
> 决定。
## 📊 评测结果
ReMe 通过 Agent 多轮搜索与读取的方式评测多会话和超长上下文中的记忆能力。下表为仓库中已公开的参考实验结果模型、prompt、数据集和评判细节见各评测文档。
| 基准 | 设置 | 样本量 | Agentic 得分 | 主要检验内容 |
| --------------------------------------------------------------------------- | ----------- | ----------------: | -----------: | ------------------------------ |
| **[LongMemEval cleaned-s](https://reme.agentscope.io/?doc=longmemeval-zh)** | **整体** | **500 题** | **89.4%** | 跨会话检索、知识更新与时间推理 |
| [BEAM](https://reme.agentscope.io/?doc=beam-zh) | 100K 上下文 | 20 cases / 400 题 | 66.1% | 十类长上下文记忆任务 |
| [BEAM](https://reme.agentscope.io/?doc=beam-zh) | 1M 上下文 | 35 cases / 700 题 | 65.0% | 超长对话设置 |
在仓库的 [π-Bench 评测](https://reme.agentscope.io/?doc=pibench-zh)中ReMe Agent 在 5 种用户角色上的平均 **PROC 得分为 0.580**
,比相同测试模型配置的 NanoBot 高 2.4%。PROC 用于评估隐藏意图完成、针对性澄清、跨会话偏好和规范复用、跨任务依赖推断以及欠规格请求推进等主动性能力。
## 🧩 扩展与插件
插件是可选的独立 Python distribution可以贡献 Component、Step、Job backend 和配置,并通过配置显式启用。每日论文与 Auto Fin
均已独立打包,源码 distribution 及说明分别见[每日论文](plugins/daily_paper/README_ZH.md)和
[Auto Fin](plugins/auto-fin/README_ZH.md)。
| 插件 | 能力 |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [每日论文](https://reme.agentscope.io/?doc=daily-paper-zh) | 发现并排序论文,使用 Agent 解读 PDF生成文件化论文笔记和五分钟简报。 |
| [Auto Fin](https://reme.agentscope.io/?doc=auto-fin-zh) | 拉取主题相关财联社新闻,搜索 ReMe 历史材料并生成带 wikilink 的 Markdown 报告。 |
安装、查看、校验、启用和卸载 ReMe 插件的方法见[插件管理](docs/zh/plugin_management.md)。
## 📚 文档
下列文档覆盖主要使用流程,并以当前代码的运行时契约为准。
| 文档 | 主要内容 |
| ------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| [快速开始](docs/zh/quick_start.md) | 安装 ReMe、启动服务并执行首次文件和记忆操作。 |
| [Memory as File](docs/zh/memory_as_file.md) | 理解 workspace 分层、frontmatter、wikilink、chunk 和文件事实来源模型。 |
| [Auto Memory](docs/zh/auto_memory.md) | 保留过滤后的对话来源记录,并提炼可复用的 daily 记忆卡片。 |
| [Auto Resource](docs/zh/auto_resource.md) | 导入支持的文本资料,转换为可追溯来源的 daily 卡片。 |
| [Auto Dream](docs/zh/auto_dream.md) 与 [Auto Link](docs/zh/auto_link.md) | 将 daily 记忆整理为持续演化的 digest 节点和可读 wikilink 关系。 |
| [记忆检索](docs/zh/memory_search.md) | 使用 BM25、可选向量、RRF 融合、行号范围召回和渐进式链接扩展。 |
| [Proactive](docs/zh/proactive.md) | 安全读取兴趣主题,并将其接入宿主 Agent 的决策流程。 |
| [应用场景](docs/zh/reme_scene.md) | 查看金融研究、研发记忆和个人知识库的完整使用示例。 |
| [框架说明](docs/zh/framework.md) | 理解 Application、Job、Step、Component、service、配置和生命周期边界。 |
| [TypeScript 集成](typescript/README_ZH.md) | 配置统一 client以及 DeepSeek Harness 和 OpenClaw 原生适配器。 |
| [ReMe 博客](https://agentscope-ai.github.io/ReMe/?doc=zh-reme-blog) | 了解完整产品故事、设计动机、使用示例和评测摘要。 |
## 🛠️ 常用命令
运行 `reme help` 可查看完整 job 列表。常用 workspace 与维护命令如下:
| 命令 | 作用 |
| ----------------------------------------- | ------------------------------------------------------------- |
| `reme status` | 查看有状态数据组件的内存估算及进程 RSS。 |
| [`reme search`](docs/zh/memory_search.md) | 默认使用 BM25 和 wikilink 检索,启用后增加向量检索。 |
| `reme read` / `reme write` / `reme edit` | 检查和维护 Markdown 记忆文件。 |
| `reme auto_memory` | 将对话 messages 转为 daily 记忆卡片;需要 LLM 凭证。 |
| `reme auto_resource` | 将 `resource/` 下的文件解读为 daily 资料卡片;需要 LLM 凭证。 |
| `reme auto_dream` / `reme proactive` | 将 daily 记忆整理为长期 digest并暴露值得关注的主题。 |
| `reme reindex` | 基于已有文件重建检索和 wikilink 索引。 |
| `reme read` / `reme write` / `reme edit` | 检查和维护 Markdown 记忆文件。 |
| `reme traverse` / `reme graph_snapshot` | 浏览 wikilink 邻域或按类别组织的 digest 图。 |
| `reme chat` | 与可感知 workspace 的只读 Agent 进行流式对话;需要 LLM 凭证。 |
| `reme reindex` | 基于已有文件重建检索和 wikilink 索引。 |
## 🤝 社区与支持
## 🤝 社区与贡献
- **问题反馈与需求**:请先查看 [Open Issues](https://github.com/agentscope-ai/ReMe/issues);如无相关讨论,可新建 Issue
- **问题反馈、需求与帮助**:请先查看 [Open Issues](https://github.com/agentscope-ai/ReMe/issues);如无相关讨论,可新建 Issue
说明背景、目标行为和影响范围。
- **代码贡献**:改动前建议阅读 [贡献指南](https://docs.agentscope.io/reme/stable/zh/contributing)。架构与扩展方式以源码、schema
和测试为准。
- **文档贡献**:用户可见文档请提交到[统一文档仓库](https://github.com/agentscope-ai/docs)的 `reme/<version>/{en,zh}/` 目录。
- **代码贡献**:改动前建议阅读仓库内的[贡献指南](docs/zh/contributing.md)。架构与扩展方式以源码、schema 和测试为准。
- **文档贡献**:请直接更新本仓库 `docs/en/``docs/zh/` 或对应 package 目录中的规范源文件;文档站点会从这些文件生成。
- **提交规范**:建议使用 Conventional Commits例如 `feat(search): add link expansion option`
`docs(zh): update quick start`
- **提交前检查**:提交 PR 前请尽量运行 `pre-commit run --all-files``pytest`;如有依赖 LLM、embedding 或外部服务的测试无法运行,请在
PR 中说明。
- **获取帮助**:如需反馈 Bug 或功能请求,请使用 [GitHub Issues](https://github.com/agentscope-ai/ReMe/issues);项目文档见
[https://docs.agentscope.io/](https://docs.agentscope.io/reme/stable/zh/)。
- **项目文档**:访问 [reme.agentscope.io](https://reme.agentscope.io)。
### 贡献者

124
benchmark/beam/README.md Normal file
View file

@ -0,0 +1,124 @@
[中文版 / Chinese version](./README_ZH.md)
# BEAM Benchmark
BEAM is a benchmark for **memory capability over long-context chat cases**. Each
case contains a very long chat history split into batches; ReMe converts each
batch into a session, ingests them in chronological order, then answers probing
questions via an agentic (ReAct) mode. Answers are scored with BEAM's
rubric-based `answer_judge` job, which produces both a graded score and a binary
verdict, and per-type averages are reported.
BEAM ships dataset variants by chat size — `100K` / `500K` / `1M` / `10M` — so
memory systems can be stressed at different context lengths. Question types
include abstention, contradiction resolution, event ordering, information
extraction, instruction following, knowledge update, multi-session reasoning,
preference following, summarization, and temporal reasoning.
> For the shared setup (dependencies, credentials, log conventions) see the
> [top-level benchmark README](../README.md).
## 1. Get the Dataset
BEAM is a public repository, cloned into `benchmark/beam/dataset/`:
```bash
mkdir -p benchmark/beam/dataset
cd benchmark/beam/dataset
git clone https://github.com/mohammadtavakoli78/BEAM.git
```
After cloning, `benchmark/beam/dataset/BEAM/` should contain `chats/`, `src/`,
`topics/` and other subdirectories.
## 2. Run
From the repository root:
```bash
python benchmark/beam/run.py
python benchmark/beam/run.py --config benchmark/beam/config.yaml
python benchmark/beam/run.py -q # quiet
python benchmark/beam/run.py --eval_only # reuse existing workspaces, query + judge only
```
## 3. Pipeline
1. For each case, load `chat.json` and convert each batch into a ReMe session.
2. Ingest sessions in chronological order into an isolated workspace, then `digest_update`.
3. Answer each probing question via agentic (ReAct) mode.
4. Score answers with BEAM's rubric-based `answer_judge` job and print per-type averages.
## 4. Key config — `benchmark/beam/config.yaml`
| Key | Meaning |
| --- | --- |
| `dataset.beam_root` | BEAM dataset root (`benchmark/beam/dataset/BEAM`). |
| `dataset.chat_size` | Variant to run: `100K` / `500K` / `1M` / `10M`. |
| `dataset.case_ids` | Specific cases (e.g. `["1","2"]`); empty = all cases. |
| `dataset.start_index` / `num_items` | Case pagination (`num_items` `0` = all). |
| `dataset.workspace_root` | Per-case workspace root (`benchmark/beam/workspaces/beam`). |
| `evaluation.num_workers` | `0` = auto, `1` = sequential, `>1` = parallel. |
| `reme.config` | ReMe config used (`beam.yaml`). |
| `output.dir` | Results directory (`benchmark/beam/results`). |
## 5. Outputs
Results are JSON files written to `output.dir` as
`results_<chat_size>_<timestamp>.json`, with a per-type score summary also
printed to the console. Logging conventions are shared across benchmarks — see
the [top-level README](../README.md#outputs--logs).
## 6. Reference Results
> The results below use the longmemeval-version prompt.
### 100K
agentscope==2.0.4.post1, conda reme env, 20 workers, eval-only (reusing prebuilt memory)
(2026-08-05, 20 cases / 400 Qs, total 46.0 min)
| Type | Agentic | Binary | input tok/q | output tok/q | total tok/q | tool calls/q |
|---|---|---|---|---|---|---|
| abstention | 0.550 | 0.550 | 96,031 | 1,070 | 97,101 | 4.58 |
| contradiction_resolution | 0.438 | 0.412 | 32,263 | 872 | 33,135 | 2.48 |
| event_ordering | 0.501 | 0.423 | 140,195 | 5,163 | 145,358 | 4.70 |
| information_extraction | 0.873 | 0.832 | 50,245 | 883 | 51,128 | 3.15 |
| instruction_following | 0.750 | 0.725 | 37,986 | 848 | 38,834 | 2.67 |
| knowledge_update | 0.688 | 0.675 | 31,198 | 651 | 31,849 | 2.27 |
| multi_session_reasoning | 0.626 | 0.584 | 85,038 | 4,563 | 89,601 | 4.28 |
| preference_following | 0.925 | 0.912 | 34,281 | 989 | 35,270 | 2.50 |
| summarization | 0.623 | 0.461 | 89,657 | 2,056 | 91,713 | 4.12 |
| temporal_reasoning | 0.637 | 0.625 | 34,563 | 1,049 | 35,612 | 2.52 |
| **OVERALL** | **0.661** | **0.620** | **63,146** | **1,814** | **64,960** | **3.33** |
Memory Construction average token consumption (default agent, full build over 20 cases):
| Agent | input tok/case | output tok/case | total tok/case |
|---|---|---|---|
| default | 2,172,316 | 136,697 | 2,309,013 |
### 1M
agentscope==2.0.4.post1, conda reme env, 20 workers, full memory build
(2026-08-05, 35 cases / 700 Qs, total 459.2 min)
| Type | Agentic | Binary | input tok/q | output tok/q | total tok/q | tool calls/q |
|---|---|---|---|---|---|---|
| abstention | 0.429 | 0.429 | 118,707 | 1,178 | 119,886 | 4.20 |
| contradiction_resolution | 0.391 | 0.364 | 49,787 | 810 | 50,597 | 2.50 |
| event_ordering | 0.558 | 0.456 | 201,514 | 3,889 | 205,403 | 4.79 |
| information_extraction | 0.809 | 0.772 | 78,950 | 894 | 79,844 | 3.00 |
| instruction_following | 0.852 | 0.832 | 55,757 | 924 | 56,681 | 2.81 |
| knowledge_update | 0.779 | 0.771 | 45,981 | 665 | 46,646 | 2.37 |
| multi_session_reasoning | 0.658 | 0.612 | 138,133 | 2,873 | 141,006 | 4.40 |
| preference_following | 0.798 | 0.777 | 51,796 | 920 | 52,716 | 2.53 |
| summarization | 0.693 | 0.537 | 158,794 | 2,905 | 161,700 | 4.44 |
| temporal_reasoning | 0.536 | 0.536 | 100,176 | 3,148 | 103,324 | 3.90 |
| **OVERALL** | **0.650** | **0.609** | **99,959** | **1,821** | **101,780** | **3.49** |
Memory Construction average token consumption (default agent, full build over 35 cases):
| Agent | input tok/case | output tok/case | total tok/case |
|---|---|---|---|
| default | 31,943,817 | 1,417,061 | 33,360,878 |

119
benchmark/beam/README_ZH.md Normal file
View file

@ -0,0 +1,119 @@
# BEAM 评测
[English version](./README.md)
BEAM 是一个面向**长上下文对话场景**的记忆能力评测基准。每个 case 包含一段被切分为多个
batch 的超长对话ReMe 将每个 batch 转换为一个会话,按时间顺序摄入后,以 agenticReAct
模式回答探测问题。答案由 BEAM 基于 rubric 的 `answer_judge` 任务打分,同时给出分级分数与二元
判定,并输出各类型平均分。
BEAM 按对话规模提供多种数据变体 —— `100K` / `500K` / `1M` / `10M`,可在不同上下文长度下
压测记忆系统。题型包括 abstention拒答、contradiction resolution矛盾消解、event
ordering事件排序、information extraction信息抽取、instruction following指令遵循
knowledge update知识更新、multi-session reasoning多会话推理、preference following
偏好遵循、summarization摘要与 temporal reasoning时间推理
> 公共设置(依赖、凭据、日志约定)见[总评测说明](../README_ZH.md)。
## 1. 获取数据集
BEAM 是公开仓库clone 到 `benchmark/beam/dataset/` 下:
```bash
mkdir -p benchmark/beam/dataset
cd benchmark/beam/dataset
git clone https://github.com/mohammadtavakoli78/BEAM.git
```
clone 完成后,`benchmark/beam/dataset/BEAM/` 目录下应包含 `chats/``src/``topics/` 等子目录。
## 2. 运行
在仓库根目录执行:
```bash
python benchmark/beam/run.py
python benchmark/beam/run.py --config benchmark/beam/config.yaml
python benchmark/beam/run.py -q # 安静模式
python benchmark/beam/run.py --eval_only # 复用已有工作区,仅执行查询 + 评判
```
## 3. 流程
1. 为每个 case 加载 `chat.json`,将每个 batch 转换为一个 ReMe 会话。
2. 按时间顺序将会话摄入独立工作区,随后执行 `digest_update`
3. 以 agenticReAct模式回答每个探测问题。
4. 通过 BEAM 基于 rubric 的 `answer_judge` 任务打分,并输出各类型平均分。
## 4. 关键配置 —— `benchmark/beam/config.yaml`
| 配置项 | 含义 |
| --- | --- |
| `dataset.beam_root` | BEAM 数据集根目录(`benchmark/beam/dataset/BEAM`)。 |
| `dataset.chat_size` | 运行的变体:`100K` / `500K` / `1M` / `10M`。 |
| `dataset.case_ids` | 指定 case`["1","2"]`),空表示全部。 |
| `dataset.start_index` / `num_items` | case 分页(`num_items``0` 表示全部)。 |
| `dataset.workspace_root` | case 工作区根目录(`benchmark/beam/workspaces/beam`)。 |
| `evaluation.num_workers` | `0` = 自动,`1` = 串行,`>1` = 并行。 |
| `reme.config` | 使用的 ReMe 配置(`beam.yaml`)。 |
| `output.dir` | 结果目录(`benchmark/beam/results`)。 |
## 5. 输出
结果以 JSON 文件写入 `output.dir`,文件名为 `results_<chat_size>_<timestamp>.json`
同时控制台会打印含各类型分数的汇总。日志约定在各基准间通用,见
[总说明](../README_ZH.md#输出与日志)。
## 6. 参考结果
> 以下结果使用 longmemeval 版本的 prompt。
### 100K
agentscope==2.0.4.post1conda reme 环境20 并发eval-only复用已构建 memory
2026-08-0520 cases / 400 Qs总耗时 46.0 min
| 题型 | Agentic | Binary | input tok/q | output tok/q | total tok/q | tool calls/q |
|---|---|---|---|---|---|---|
| abstention | 0.550 | 0.550 | 96,031 | 1,070 | 97,101 | 4.58 |
| contradiction_resolution | 0.438 | 0.412 | 32,263 | 872 | 33,135 | 2.48 |
| event_ordering | 0.501 | 0.423 | 140,195 | 5,163 | 145,358 | 4.70 |
| information_extraction | 0.873 | 0.832 | 50,245 | 883 | 51,128 | 3.15 |
| instruction_following | 0.750 | 0.725 | 37,986 | 848 | 38,834 | 2.67 |
| knowledge_update | 0.688 | 0.675 | 31,198 | 651 | 31,849 | 2.27 |
| multi_session_reasoning | 0.626 | 0.584 | 85,038 | 4,563 | 89,601 | 4.28 |
| preference_following | 0.925 | 0.912 | 34,281 | 989 | 35,270 | 2.50 |
| summarization | 0.623 | 0.461 | 89,657 | 2,056 | 91,713 | 4.12 |
| temporal_reasoning | 0.637 | 0.625 | 34,563 | 1,049 | 35,612 | 2.52 |
| **OVERALL** | **0.661** | **0.620** | **63,146** | **1,814** | **64,960** | **3.33** |
Memory Construction 平均 token 消耗default agent20 cases 全量构建):
| Agent | input tok/case | output tok/case | total tok/case |
|---|---|---|---|
| default | 2,172,316 | 136,697 | 2,309,013 |
### 1M
agentscope==2.0.4.post1conda reme 环境20 并发,全量构建 memory
2026-08-0535 cases / 700 Qs总耗时 459.2 min
| 题型 | Agentic | Binary | input tok/q | output tok/q | total tok/q | tool calls/q |
|---|---|---|---|---|---|---|
| abstention | 0.429 | 0.429 | 118,707 | 1,178 | 119,886 | 4.20 |
| contradiction_resolution | 0.391 | 0.364 | 49,787 | 810 | 50,597 | 2.50 |
| event_ordering | 0.558 | 0.456 | 201,514 | 3,889 | 205,403 | 4.79 |
| information_extraction | 0.809 | 0.772 | 78,950 | 894 | 79,844 | 3.00 |
| instruction_following | 0.852 | 0.832 | 55,757 | 924 | 56,681 | 2.81 |
| knowledge_update | 0.779 | 0.771 | 45,981 | 665 | 46,646 | 2.37 |
| multi_session_reasoning | 0.658 | 0.612 | 138,133 | 2,873 | 141,006 | 4.40 |
| preference_following | 0.798 | 0.777 | 51,796 | 920 | 52,716 | 2.53 |
| summarization | 0.693 | 0.537 | 158,794 | 2,905 | 161,700 | 4.44 |
| temporal_reasoning | 0.536 | 0.536 | 100,176 | 3,148 | 103,324 | 3.90 |
| **OVERALL** | **0.650** | **0.609** | **99,959** | **1,821** | **101,780** | **3.49** |
Memory Construction 平均 token 消耗default agent35 cases 全量构建):
| Agent | input tok/case | output tok/case | total tok/case |
|---|---|---|---|
| default | 31,943,817 | 1,417,061 | 33,360,878 |

View file

@ -0,0 +1,24 @@
# BEAM evaluation configuration
# This file controls what/how to evaluate.
dataset:
beam_root: "benchmark/beam/dataset/BEAM" # BEAM dataset root
chat_size: "1M" # 100K | 500K | 1M | 10M (dataset variant)
case_ids: [] # empty = all cases; or ["1", "2", "3"]
start_index: 0 # first case index (for pagination)
num_items: 0 # 0 = all cases; >0 = limit
workspace_root: "benchmark/beam/workspaces/beam" # workspace root for case workspaces
evaluation:
num_workers: 20 # 0 = auto; 1 = sequential; >1 = parallel (per-case)
compress_session: false # true = compress session chunks in search_v2 (query-aware); false = no compression
reme:
config: "beam.yaml" # reme config (in reme/config/)
output:
dir: "benchmark/beam/results"
log_dir: "logs" # log directory (relative to project root)
log_prefix: "beam" # benchmark name used in log filenames
log_to_console: true
log_to_file: true

76
benchmark/beam/kill.sh Normal file
View file

@ -0,0 +1,76 @@
#!/bin/bash
# 杀死指定进程及其所有子进程
# Usage: bash kill.sh <PID>
if [ -z "$1" ]; then
echo "Usage: bash kill.sh <PID>"
echo " 杀死指定进程及其所有子进程"
exit 1
fi
PID=$1
# 检查进程是否存在
if ! kill -0 "$PID" 2>/dev/null; then
echo "进程 $PID 不存在"
exit 1
fi
# 递归收集所有子进程(包括子进程的子进程)
collect_children() {
local parent=$1
local children
children=$(ps -o pid= --ppid "$parent" 2>/dev/null | tr -d ' ')
for child in $children; do
collect_children "$child"
done
echo "$parent"
}
# 收集进程树(子进程在前,父进程在后,保证先杀子再杀父)
PROCESS_TREE=$(collect_children "$PID")
TOTAL=$(echo "$PROCESS_TREE" | wc -l | tr -d ' ')
echo "进程树(共 $TOTAL 个进程):"
while read -r p; do
cmd=$(ps -o args= -p "$p" 2>/dev/null | head -c 80)
printf " PID=%-8s %s\n" "$p" "$cmd"
done <<< "$PROCESS_TREE"
# 先 SIGTERM 优雅终止
echo ""
echo "发送 SIGTERM..."
while read -r p; do
kill "$p" 2>/dev/null
done <<< "$PROCESS_TREE"
# 等待最多 5 秒
for i in $(seq 1 5); do
alive=false
while read -r p; do
if kill -0 "$p" 2>/dev/null; then
alive=true
fi
done <<< "$PROCESS_TREE"
if [ "$alive" = false ]; then
break
fi
sleep 1
done
# 检查是否还有残留,强制 SIGKILL
remaining=false
while read -r p; do
if kill -0 "$p" 2>/dev/null; then
remaining=true
fi
done <<< "$PROCESS_TREE"
if [ "$remaining" = true ]; then
echo "部分进程未响应,发送 SIGKILL..."
while read -r p; do
kill -9 "$p" 2>/dev/null
done <<< "$PROCESS_TREE"
fi
echo "已终止进程树(根 PID=$PID,共 $TOTAL 个进程)"

891
benchmark/beam/run.py Normal file
View file

@ -0,0 +1,891 @@
"""BEAM evaluation runner for ReMe.
Evaluates ReMe's memory capability using the BEAM dataset.
Each case gets an isolated workspace; chat.json batches are ingested as
sessions in chronological order; finally probing questions are answered
via an agentic (ReAct) approach, then
judged by BEAM's rubric-based LLM-as-judge.
Usage:
python benchmark/beam/run.py
python benchmark/beam/run.py --config benchmark/beam/config.yaml
python benchmark/beam/run.py -q # quiet: only eval-level logs
python benchmark/beam/run.py --log-level WARNING # reduce eval runner logs
python benchmark/beam/run.py --reme-log-level WARNING # reduce reme internal logs
python benchmark/beam/run.py --eval_only # query+judge only, reuse existing workspace
"""
import json
import logging
import os
import re
import shutil
import time
import threading
from datetime import datetime
from pathlib import Path
import yaml
from dotenv import load_dotenv
# Load .env from project root
_PROJECT_ROOT = Path(__file__).parent.parent.parent
load_dotenv(_PROJECT_ROOT / ".env")
# Workspace root — read from config.yaml (dataset.workspace_root)
_WORKSPACE_ROOT_DEFAULT = "benchmark/beam/workspaces/beam"
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
_DEFAULT_LOG_FORMAT = "%(asctime)s | %(levelname)s | %(message)s"
logging.basicConfig(level=logging.INFO, format=_DEFAULT_LOG_FORMAT)
logger = logging.getLogger("beam")
# Noisy library loggers silenced by default
_NOISY_LOGGERS = [
"httpx",
"httpcore",
"openai",
"uvicorn",
"multipart",
"asyncio",
"watchfiles",
"filelock",
]
def setup_logging(
log_level: str,
reme_log_level: str,
log_dir: str | None = None,
):
"""Configure logging for the eval runner and reme internals.
Args:
log_level: Level for the eval runner logger (DEBUG/INFO/WARNING/ERROR).
reme_log_level: Level for reme's internal loguru logger.
log_dir: Per-run log directory (absolute path). None = no file logging.
"""
numeric = getattr(logging, log_level.upper(), logging.INFO)
# Eval runner logger
logging.getLogger().setLevel(numeric)
logger.setLevel(numeric)
# Suppress noisy library loggers when above DEBUG
if numeric > logging.DEBUG:
for name in _NOISY_LOGGERS:
lib_logger = logging.getLogger(name)
lib_logger.setLevel(max(numeric, logging.WARNING))
# Add file handler for eval runner if log_dir is specified
if log_dir:
os.makedirs(log_dir, exist_ok=True)
log_filepath = os.path.join(log_dir, "runner.log")
file_handler = logging.FileHandler(log_filepath, encoding="utf-8")
file_handler.setLevel(numeric)
file_handler.setFormatter(logging.Formatter(_DEFAULT_LOG_FORMAT))
logging.getLogger().addHandler(file_handler)
logger.info(f"Eval runner log file: {log_filepath}")
# Reme internal logger (loguru) — will be applied per-worker via _configure_worker
os.environ["REME_LOG_LEVEL"] = reme_log_level.upper()
if log_dir:
os.environ["REME_LOG_DIR"] = log_dir
def _configure_worker(
log_level: str,
reme_log_level: str,
log_dir: str | None = None,
):
"""Set up logging inside a multiprocessing worker process.
Must be called at the top of each worker because child processes inherit
parent state but loguru sinks are NOT shared across fork/spawn.
"""
numeric = getattr(logging, log_level.upper(), logging.INFO)
logging.basicConfig(level=numeric, format=_DEFAULT_LOG_FORMAT, force=True)
logging.getLogger("beam").setLevel(numeric)
if numeric > logging.DEBUG:
for name in _NOISY_LOGGERS:
logging.getLogger(name).setLevel(max(numeric, logging.WARNING))
# Add file handler for eval runner in worker process
if log_dir:
os.makedirs(log_dir, exist_ok=True)
pid = os.getpid()
log_filepath = os.path.join(log_dir, f"worker-{pid}.log")
file_handler = logging.FileHandler(log_filepath, encoding="utf-8")
file_handler.setLevel(numeric)
file_handler.setFormatter(logging.Formatter(_DEFAULT_LOG_FORMAT))
logging.getLogger().addHandler(file_handler)
# Re-initialize loguru for reme internals at the desired level
from reme.utils import get_logger
reme_log_dir = log_dir or "logs"
get_logger(log_dir=reme_log_dir, level=reme_log_level.upper(), force_init=True)
# ---------------------------------------------------------------------------
# Config loading
# ---------------------------------------------------------------------------
def load_eval_config(config_path: str | None = None) -> dict:
"""Load evaluation config yaml with env-var expansion."""
if config_path is None:
config_path = str(Path(__file__).parent / "config.yaml")
with open(config_path, encoding="utf-8") as f:
raw = f.read()
# Expand ${VAR} and ${VAR:-default}
def _expand(m):
expr = m.group(1)
if ":-" in expr:
key, default = expr.split(":-", 1)
return os.environ.get(key, default)
return os.environ.get(expr, "")
raw = re.sub(r"\$\{([^}]+)\}", _expand, raw)
return yaml.safe_load(raw)
# ---------------------------------------------------------------------------
# BEAM data loading
# ---------------------------------------------------------------------------
def parse_beam_time_anchor(time_str: str) -> datetime:
"""Parse BEAM time_anchor format: 'March-15-2024' -> datetime."""
for fmt in ("%B-%d-%Y", "%b-%d-%Y"):
try:
return datetime.strptime(time_str, fmt)
except ValueError:
continue
raise ValueError(f"Cannot parse time_anchor: {time_str!r}")
def load_beam_chat(chat_path: Path, chat_size: str, case_id: str) -> list[dict]:
"""Load BEAM chat.json and convert to ReMe session format.
Each batch becomes one session with all its turns flattened.
Each turn resolves its own time_anchor independently; turns without
an explicit time_anchor inherit from the most recent preceding turn.
Returns list of sessions, each with:
- session_id: str
- date: str (YYYY-MM-DD) derived from the *first* turn's time
- messages: list[dict] with name, role, content, created_at
"""
with open(chat_path, encoding="utf-8") as f:
batches = json.load(f)
sessions = []
for batch in batches:
batch_num = batch["batch_number"]
# Resolve batch-level fallback (used when no turn has a time_anchor)
batch_anchor = batch.get("time_anchor")
if not batch_anchor:
batch_anchor = "January-1-2024"
# Flatten all turns, resolving time_anchor per turn
messages = []
prev_dt = None # carries forward from previous turn
first_dt = None # for session-level date
for turn in batch["turns"]:
# Find this turn's own time_anchor from its messages
turn_anchor = None
for msg in turn:
if msg.get("time_anchor"):
turn_anchor = msg["time_anchor"]
break
if turn_anchor:
dt = parse_beam_time_anchor(turn_anchor)
elif prev_dt is not None:
dt = prev_dt # inherit from previous turn
else:
dt = parse_beam_time_anchor(batch_anchor)
if first_dt is None:
first_dt = dt
prev_dt = dt
for msg in turn:
role = msg["role"]
messages.append(
{
"name": role,
"role": role,
"content": msg["content"],
"created_at": dt.strftime("%Y-%m-%dT%H:%M:%S"),
},
)
sessions.append(
{
"session_id": f"beam_{chat_size}_{case_id}_batch{batch_num}",
"date": first_dt.strftime("%Y-%m-%d"),
"messages": messages,
},
)
return sessions
def get_available_cases(beam_root: Path, chat_size: str) -> list[str]:
"""Return sorted list of case IDs for a given chat size."""
chats_dir = beam_root / "chats" / chat_size
if not chats_dir.exists():
return []
return sorted(
[d.name for d in chats_dir.iterdir() if d.is_dir()],
key=int,
)
# ---------------------------------------------------------------------------
# Answer generation
# ---------------------------------------------------------------------------
async def answer_question_agentic(app, question: str, compress_session: bool = False) -> tuple[str, dict]:
"""Answer a probing question using ReMe's agentic_answer job.
Returns (answer, metadata)
"""
from reme.utils.evaluation_interface import track_agent_token_usage, track_job_counts
with (
track_job_counts(["search"], app.context) as tool_counts,
track_agent_token_usage(
["bench"],
app.context,
) as token_usages,
):
query_resp = await app.run_job(
"agentic_answer",
query=question,
compress_session=compress_session,
)
answer = (query_resp.answer or "").strip()
return answer, {
"mode": "agentic",
"tool_counts": tool_counts,
"token_usage": token_usages["bench"],
}
# ---------------------------------------------------------------------------
# BEAM rubric-based LLM-as-Judge
# ---------------------------------------------------------------------------
async def judge_answer(
app,
question: str,
llm_response: str,
rubric: list[str],
question_type: str = "",
) -> dict:
"""Judge an answer via the answer_judge job (beam_rubric_judge_step)."""
judge_resp = await app.run_job(
"answer_judge",
llm_response=llm_response,
rubric=rubric,
probing_question=question,
question_type=question_type,
)
result = {
"llm_judge_score": (judge_resp.metadata or {}).get("llm_judge_score", 0.0),
"llm_judge_responses": (judge_resp.metadata or {}).get("llm_judge_responses", []),
}
# Include event_ordering extra metrics if present
eo = (judge_resp.metadata or {}).get("event_ordering")
if eo:
result["event_ordering"] = eo
return result
# ---------------------------------------------------------------------------
# Main evaluation pipeline
# ---------------------------------------------------------------------------
async def evaluate_case(eval_config: dict, case_id: str, eval_only: bool = False) -> dict:
"""Evaluate a single BEAM case end-to-end.
Args:
eval_config: The evaluation configuration dict.
case_id: The case directory name (e.g. "1").
eval_only: If True, skip ingestion and only run query+judge
using the existing workspace.
Returns:
A results dict with all questions, answers, and judgments.
"""
from reme import Application
from reme.config import resolve_app_config
dataset_cfg = eval_config["dataset"]
chat_size = dataset_cfg["chat_size"]
compress_session = bool(eval_config["evaluation"].get("compress_session", False))
beam_root = _PROJECT_ROOT / dataset_cfg.get("beam_root", "benchmark/beam/dataset/BEAM")
chat_path = beam_root / "chats" / chat_size / case_id / "chat.json"
probing_questions_path = beam_root / "chats" / chat_size / case_id / "probing_questions" / "probing_questions.json"
if not chat_path.exists():
raise FileNotFoundError(f"Chat file not found: {chat_path}")
if not probing_questions_path.exists():
raise FileNotFoundError(f"Probing questions not found: {probing_questions_path}")
logger.info(
"[Case %s] size=%s%s",
case_id,
chat_size,
" [eval_only]" if eval_only else "",
)
# Workspace setup
workspace_root = _PROJECT_ROOT / dataset_cfg.get("workspace_root", _WORKSPACE_ROOT_DEFAULT)
case_dir = workspace_root / f"{chat_size}_{case_id}"
workspace_dir = str(case_dir / ".reme")
if eval_only:
if not case_dir.exists() or not Path(workspace_dir).exists():
raise FileNotFoundError(
f"[Case {case_id}] eval_only: workspace not found at {case_dir}. "
f"Run without --eval_only first to build the workspace.",
)
else:
if case_dir.exists():
shutil.rmtree(case_dir)
logger.info(f"[Case {case_id}] Cleaned existing workspace: {case_dir}")
else:
logger.info(f"[Case {case_id}] Workspace not found, creating: {case_dir}")
case_dir.mkdir(parents=True, exist_ok=True)
# Pre-initialize ReMe's loguru logger with the correct log_dir
output_cfg = eval_config.get("output", {})
if output_cfg.get("log_to_file", False):
reme_log_dir = os.environ.get("REME_LOG_DIR")
if reme_log_dir:
from reme.utils import get_logger
get_logger(
log_dir=reme_log_dir,
level=os.environ.get("REME_LOG_LEVEL", "INFO"),
log_to_console=output_cfg.get("log_to_console", True),
log_to_file=True,
force_init=True,
)
cfg = resolve_app_config(
config=eval_config["reme"]["config"],
workspace_dir=workspace_dir,
log_to_console=output_cfg.get("log_to_console", True),
log_to_file=output_cfg.get("log_to_file", False),
enable_logo=False,
)
app = Application(**cfg)
await app.start()
from reme.utils.evaluation_interface import check_agent_token_usage # noqa: E402
_MEM_AGENT_NAMES = ("default", "bench")
sessions_ingested = 0
memory_token_usage: dict[str, dict[str, int | None]] = {}
try:
if not eval_only:
# ── Phase 1: Ingest sessions (with token tracking) ─────────
sessions = load_beam_chat(chat_path, chat_size, case_id)
logger.info(f"[Case {case_id}] Loaded {len(sessions)} sessions from chat.json")
# Snapshot token counters before memory construction
mem_token_start = {name: check_agent_token_usage(name, app.context) for name in _MEM_AGENT_NAMES}
for i, session in enumerate(sessions):
logger.info(
f"[Case {case_id}] Ingesting session {i+1}/{len(sessions)}: "
f"id={session['session_id']} date={session['date']} "
f"msgs={len(session['messages'])}",
)
resp = await app.run_job(
"auto_memory",
messages=session["messages"],
session_id=session["session_id"],
date=session["date"],
)
if not resp.success:
logger.warning(f"[Case {case_id}] auto_memory failed: {resp.answer}")
else:
logger.info(
f"[Case {case_id}] auto_memory success: " f"{resp.answer[:100] if resp.answer else ''}",
)
await app.run_job("index_update")
sessions_ingested += 1
# Final digest update
logger.info(f"[Case {case_id}] Running digest_update...")
await app.run_job("digest_update")
logger.info(f"[Case {case_id}] Ingestion complete.")
# Compute memory construction token deltas
for name in _MEM_AGENT_NAMES:
end_usage = check_agent_token_usage(name, app.context)
delta: dict[str, int | None] = {}
for metric in _TOKEN_USAGE_METRICS:
current = end_usage[metric]
start = mem_token_start[name][metric]
delta[metric] = None if current is None else current - (start or 0)
memory_token_usage[name] = delta
logger.info(f"[Case {case_id}] Memory construction token usage: {memory_token_usage}")
# ── Phase 2: Answer + Judge probing questions ───────────────
with open(probing_questions_path, encoding="utf-8") as f:
probing_questions = json.load(f)
total_questions = sum(len(v) for v in probing_questions.values())
logger.info(f"[Case {case_id}] Total probing questions: {total_questions}")
all_question_results = []
q_idx = 0
for q_type in probing_questions:
logger.info(
f"[Case {case_id}] Question type: {q_type} " f"({len(probing_questions[q_type])} questions)",
)
for i, q in enumerate(probing_questions[q_type]):
q_idx += 1
question = q["question"]
rubric = q.get("rubric", [])
logger.info(
f"[Case {case_id}] [{q_idx}/{total_questions}] " f"{q_type} Q{i+1}: {question[:100]}...",
)
q_result = {
"question_type": q_type,
"question_index": i,
"question": question,
"rubric": rubric,
}
# Agentic answer
try:
agentic_answer, agentic_meta = await answer_question_agentic(
app,
question,
compress_session=compress_session,
)
except Exception as e:
logger.error(f"[Case {case_id}] Agentic answer failed: {e}")
agentic_answer = f"(error: {e})"
agentic_meta = {"error": str(e)}
if not agentic_answer:
agentic_answer = "(no answer generated)"
logger.info(f"[Case {case_id}] Agentic answer: {agentic_answer[:200]}...")
logger.info(
f"[Case {case_id}] Agentic tool calls: {agentic_meta.get('tool_counts', {})}",
)
logger.info(f"[Case {case_id}] Bench token usage: {agentic_meta.get('token_usage', {})}")
# Judge agentic answer
logger.info(f"[Case {case_id}] Judging agentic ({q_type})...")
agentic_judgment = await judge_answer(
app,
question,
agentic_answer,
rubric,
question_type=q_type,
)
logger.info(
f"[Case {case_id}] Agentic score: " f"{agentic_judgment['llm_judge_score']:.3f}",
)
q_result["agentic_response"] = agentic_answer
q_result["agentic_judgment"] = agentic_judgment
q_result["agentic_metadata"] = agentic_meta
all_question_results.append(q_result)
finally:
await app.close()
return {
"case_id": case_id,
"chat_size": chat_size,
"sessions_ingested": sessions_ingested,
"total_questions": len(all_question_results),
"questions": all_question_results,
"memory_token_usage": memory_token_usage,
}
# ---------------------------------------------------------------------------
# Worker: runs a single case in its own process with its own event loop
# ---------------------------------------------------------------------------
def _evaluate_case_worker(task_input: tuple) -> dict:
"""Worker function for multiprocessing. Each process gets its own event loop."""
eval_config, case_id, log_level, reme_log_level, eval_only, log_dir = task_input
import asyncio # pylint: disable=import-outside-toplevel
_configure_worker(log_level, reme_log_level, log_dir=log_dir)
# Suppress httpx GC noise
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
return asyncio.run(evaluate_case(eval_config, case_id, eval_only=eval_only))
def _indexed_worker(indexed_input: tuple) -> tuple:
"""Module-level wrapper for imap_unordered with index tracking."""
idx, task_input = indexed_input
return idx, _evaluate_case_worker(task_input)
def _resolve_num_workers(configured: int) -> int:
"""Resolve num_workers: 0=auto (cpu_count-2, min 1), 1=sequential, >1=parallel."""
if configured == 0:
return max(1, (os.cpu_count() or 4) - 2)
return max(1, configured)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main( # pylint: disable=too-many-statements
config_path: str | None = None,
log_level: str = "INFO",
reme_log_level: str = "INFO",
eval_only: bool = False,
):
"""Run the BEAM evaluation pipeline.
Args:
config_path: Path to the YAML config file.
log_level: Log level for the eval runner.
reme_log_level: Log level for reme internal logs.
eval_only: If True, skip ingestion and only run query+judge using
existing workspaces.
"""
from multiprocessing import Pool # pylint: disable=import-outside-toplevel
# Load config BEFORE logging setup so log_dir is available
eval_config = load_eval_config(config_path)
# Resolve per-run log directory from config
output_cfg = eval_config.get("output", {})
log_dir_abs = None
if output_cfg.get("log_to_file", False):
log_dir_raw = output_cfg.get("log_dir", "logs")
log_prefix = output_cfg.get("log_prefix", "beam")
run_ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_dir_abs = str(_PROJECT_ROOT / log_dir_raw / f"{log_prefix}_{run_ts}")
setup_logging(log_level, reme_log_level, log_dir=log_dir_abs)
dataset_cfg = eval_config["dataset"]
chat_size = dataset_cfg["chat_size"]
beam_root = _PROJECT_ROOT / dataset_cfg.get("beam_root", "benchmark/beam/dataset/BEAM")
# Determine which cases to run
case_ids = dataset_cfg.get("case_ids") or []
if not case_ids:
case_ids = get_available_cases(beam_root, chat_size)
# Pagination
start = dataset_cfg.get("start_index", 0)
num_items = dataset_cfg.get("num_items", 0)
if num_items > 0:
case_ids = case_ids[start : start + num_items]
elif start > 0:
case_ids = case_ids[start:]
if not case_ids:
logger.error(f"No cases found for chat_size={chat_size}")
return
logger.info(
"Evaluating %d case(s) for chat_size=%s: %s%s",
len(case_ids),
chat_size,
case_ids,
" [eval_only: query+judge only]" if eval_only else "",
)
# Resolve parallelism
num_workers = _resolve_num_workers(eval_config["evaluation"].get("num_workers", 1))
logger.info(f"Using {num_workers} worker(s)")
# Create output directory
output_dir = _PROJECT_ROOT / output_cfg.get("dir", "benchmark/beam/results")
output_dir.mkdir(parents=True, exist_ok=True)
# Create workspace root directory
workspace_root = _PROJECT_ROOT / dataset_cfg.get("workspace_root", _WORKSPACE_ROOT_DEFAULT)
workspace_root.mkdir(parents=True, exist_ok=True)
# Pre-check: verify all workspaces exist in eval_only mode
if eval_only:
missing_cases = []
for case_id in case_ids:
case_dir = workspace_root / f"{chat_size}_{case_id}"
if not case_dir.exists() or not (case_dir / ".reme").exists():
missing_cases.append(case_id)
if missing_cases:
preview = missing_cases[:10]
suffix = "..." if len(missing_cases) > 10 else ""
raise FileNotFoundError(
f"eval_only: {len(missing_cases)} workspace(s) not found under {workspace_root}. "
f"Missing cases: {preview}{suffix}. "
f"Run without --eval_only first to build the workspaces.",
)
# Build task args
task_args = [(eval_config, case_id, log_level, reme_log_level, eval_only, log_dir_abs) for case_id in case_ids]
# Progress tracking
total_items = len(task_args)
completed_count = [0]
start_time = time.time()
progress_lock = threading.Lock()
def _print_progress(prefix: str = "PROGRESS"):
elapsed = time.time() - start_time
elapsed_min = elapsed / 60
done = completed_count[0]
pct = 100.0 * done / total_items if total_items else 0
eta_str = "N/A"
if done > 0:
eta_sec = elapsed / done * (total_items - done)
eta_str = f"{eta_sec/60:.1f}min"
print(
f"[{prefix}] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | "
f"{done}/{total_items} ({pct:.1f}%) completed | "
f"elapsed={elapsed_min:.1f}min | ETA={eta_str}",
flush=True,
)
def _progress_timer():
"""Background thread: print progress every 10 minutes."""
while not _timer_stop.is_set():
_timer_stop.wait(600)
if not _timer_stop.is_set():
with progress_lock:
_print_progress()
_timer_stop = threading.Event()
timer_thread = threading.Thread(target=_progress_timer, daemon=True)
timer_thread.start()
# Run evaluation
if num_workers == 1:
results = []
for task_input in task_args:
result = _evaluate_case_worker(task_input)
results.append(result)
with progress_lock:
completed_count[0] += 1
else:
results = [None] * total_items
indexed_args = list(enumerate(task_args))
with Pool(processes=num_workers) as pool:
for idx, result in pool.imap_unordered(_indexed_worker, indexed_args):
results[idx] = result
with progress_lock:
completed_count[0] += 1
# Stop progress timer
_timer_stop.set()
timer_thread.join(timeout=2)
# Save results
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = output_dir / f"results_{chat_size}_{timestamp}.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
logger.info(f"Results saved to {output_file}")
# Final progress
_print_progress("FINAL")
# Print concise summary
print("\n" + "=" * 70)
print(f" BEAM EVALUATION RESULTS | size={chat_size} cases={len(results)}")
print("=" * 70)
# Per-type stats (agentic only)
type_scores: dict[str, list[float]] = {}
type_binary_scores: dict[str, list[float]] = {}
all_scores: list[float] = []
all_binary_scores: list[float] = []
all_tool_call_totals: list[int] = []
all_token_usages: list[dict[str, int | None]] = []
all_memory_token_usages: list[dict[str, dict[str, int | None]]] = []
for case_result in results:
if "error" in case_result:
continue
mem_usage = case_result.get("memory_token_usage", {})
if mem_usage:
all_memory_token_usages.append(mem_usage)
for q in case_result.get("questions", []):
judgment = q.get("agentic_judgment", {})
score = judgment.get("llm_judge_score", 0.0)
# Binary: convert each rubric item score to 0/1, then average
judge_responses = judgment.get("llm_judge_responses", [])
if judge_responses:
binary_scores_per_item = [1.0 if r.get("score", 0) >= 1.0 else 0.0 for r in judge_responses]
binary_score = sum(binary_scores_per_item) / len(binary_scores_per_item)
else:
binary_score = 1.0 if score > 0.99 else 0.0
qtype = q["question_type"]
if qtype not in type_scores:
type_scores[qtype] = []
type_binary_scores[qtype] = []
type_scores[qtype].append(score)
type_binary_scores[qtype].append(binary_score)
all_scores.append(score)
all_binary_scores.append(binary_score)
metadata = q.get("agentic_metadata", {})
all_tool_call_totals.append(sum(metadata.get("tool_counts", {}).values()))
all_token_usages.append(metadata.get("token_usage", {}))
# Memory construction token usage summary
if all_memory_token_usages:
print("\n ── Memory Construction Token Usage ──")
for agent_name in ("default", "bench"):
for metric in _TOKEN_USAGE_METRICS:
values = [
usage[agent_name][metric]
for usage in all_memory_token_usages
if usage.get(agent_name, {}).get(metric) is not None
]
if values:
total = sum(values)
mean, std = _mean_and_std(values)
print(
f" {agent_name}/{metric}: total={total} mean={mean:.2f} std={std:.2f} ({len(values)} cases)",
)
else:
print(f" {agent_name}/{metric}: unavailable")
print()
print("\n ── AGENTIC ──")
if all_scores:
for qtype in sorted(type_scores.keys()):
scores = type_scores[qtype]
avg = sum(scores) / len(scores) if scores else 0
bin_scores = type_binary_scores[qtype]
bin_avg = sum(bin_scores) / len(bin_scores) if bin_scores else 0
print(f" {qtype:<40s}: {avg:.3f} binary={bin_avg:.3f} ({len(scores)} Qs)")
overall = sum(all_scores) / len(all_scores) if all_scores else 0
binary_overall = sum(all_binary_scores) / len(all_binary_scores) if all_binary_scores else 0
print(f" {'-'*38}")
print(f" {'OVERALL':<40s}: {overall:.3f} binary={binary_overall:.3f} ({len(all_scores)} Qs)")
tool_call_mean, tool_call_std = _mean_and_std(all_tool_call_totals)
print(f" Tool calls/query: mean={tool_call_mean:.2f} std={tool_call_std:.2f}")
print(" Bench reported tokens/query:")
for metric in _TOKEN_USAGE_METRICS:
values = [usage[metric] for usage in all_token_usages if usage.get(metric) is not None]
if values:
mean, std = _mean_and_std(values)
print(f" {metric}: mean={mean:.2f} std={std:.2f}")
else:
print(f" {metric}: unavailable")
else:
print(" (no results)")
# Per-case summary
print("\n ── Per-Case Summary ──")
for case_result in results:
case_id = case_result["case_id"]
if "error" in case_result:
print(f" Case {case_id}: ERROR — {case_result['error']}")
continue
n_qs = case_result.get("total_questions", 0)
n_sessions = case_result.get("sessions_ingested", 0)
mem_usage = case_result.get("memory_token_usage", {})
parts = [f"Case {case_id}: {n_sessions} sessions, {n_qs} questions"]
# Append memory construction total tokens if available
for agent_name in ("default", "bench"):
agent_usage = mem_usage.get(agent_name, {})
total = agent_usage.get("total_tokens")
if total is not None:
parts.append(f"mem_{agent_name}_tokens={total}")
questions = case_result.get("questions", [])
scores = [q.get("agentic_judgment", {}).get("llm_judge_score", 0.0) for q in questions]
if scores:
avg = sum(scores) / len(scores)
# Binary: 0/1 per rubric item, average per question, then across questions
bin_scores = []
for q in questions:
judge_responses = q.get("agentic_judgment", {}).get("llm_judge_responses", [])
if judge_responses:
item_bins = [1.0 if r.get("score", 0) >= 1.0 else 0.0 for r in judge_responses]
bin_scores.append(sum(item_bins) / len(item_bins))
else:
s = q.get("agentic_judgment", {}).get("llm_judge_score", 0.0)
bin_scores.append(1.0 if s > 0.99 else 0.0)
bin_avg = sum(bin_scores) / len(bin_scores)
parts.append(f"agentic={avg:.3f} binary={bin_avg:.3f}")
print(f" {' | '.join(parts)}")
print("=" * 70)
total_elapsed = time.time() - start_time
print(f"\n Total time: {total_elapsed/60:.1f} min")
print("\n" + "=" * 70)
print(" [DONE] BEAM EVALUATION COMPLETED SUCCESSFULLY")
print("=" * 70 + "\n")
_TOKEN_USAGE_METRICS = (
"input_tokens",
"output_tokens",
"total_tokens",
)
def _mean_and_std(values: list[int]) -> tuple[float, float]:
"""Return population mean and standard deviation for one per-question metric."""
if not values:
return 0.0, 0.0
mean = sum(values) / len(values)
return mean, (sum((value - mean) ** 2 for value in values) / len(values)) ** 0.5
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="BEAM evaluation runner")
parser.add_argument("--config", type=str, default=None, help="Path to config.yaml")
parser.add_argument(
"--log-level",
type=str,
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
help="Log level for the eval runner (default: INFO)",
)
parser.add_argument(
"--reme-log-level",
type=str,
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
help="Log level for reme internal logs — loguru (default: INFO)",
)
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Shortcut for --log-level WARNING --reme-log-level WARNING",
)
parser.add_argument(
"--eval_only",
action="store_true",
help="Skip ingestion. Reuse existing workspaces and only run query+judge.",
)
args = parser.parse_args()
if args.quiet:
args.log_level = "WARNING"
args.reme_log_level = "WARNING"
main(args.config, args.log_level, args.reme_log_level, eval_only=args.eval_only)

View file

@ -0,0 +1,96 @@
[中文版 / Chinese version](./README_ZH.md)
# LongMemEval Benchmark
LongMemEval is a benchmark for **long-term memory over multi-session chat
histories**. Each item provides a chronologically ordered set of chat sessions
between a user and an assistant, followed by a probing question whose answer is
only recoverable by reasoning over the user-owned memory. ReMe ingests the
sessions into an isolated per-item workspace, answers the question via an
agentic (ReAct) mode, and scores the answer with an LLM-as-judge.
Question types include single-session (user / assistant / preference),
multi-session reasoning, knowledge update, and temporal reasoning.
> For the shared setup (dependencies, credentials, log conventions) see the
> [top-level benchmark README](../README.md).
## 1. Get the Dataset
ReMe uses only the **cleaned-S** split, hosted on HuggingFace:
[agentscope-ai/ReMe_longmemeval_clean_s_v2](https://huggingface.co/datasets/agentscope-ai/ReMe_longmemeval_clean_s_v2).
The download script fetches it via the hf-mirror.com mirror; to use a different
mirror, modify `BASE_URL` in [`download.py`](./download.py).
```bash
cd benchmark/longmemeval
python download.py # saves dataset/longmemeval_s_reme_cleaned.json; skips if already present
```
Ground truth is embedded in the data file.
## 2. Run
From the repository root:
```bash
python benchmark/longmemeval/run.py
python benchmark/longmemeval/run.py --config benchmark/longmemeval/config.yaml
python benchmark/longmemeval/run.py -q # quiet: only eval-level logs
python benchmark/longmemeval/run.py --log-level WARNING # reduce eval runner logs
python benchmark/longmemeval/run.py --reme-log-level WARNING # reduce reme internal logs
python benchmark/longmemeval/run.py --eval_only # reuse existing workspaces, query + judge only
```
## 3. Pipeline
1. Load the dataset (ground truth is embedded in the data file).
2. For each item, create an isolated workspace and ingest sessions in chronological order.
3. Trigger `auto_dream` when consecutive sessions cross the configured hour (default 23:00).
4. Answer each question via agentic (ReAct) mode.
5. Judge the answer (binary yes/no) with the `answer_judge` job and print per-type accuracy.
## 4. Key config — `benchmark/longmemeval/config.yaml`
| Key | Meaning |
| --- | --- |
| `dataset.path` | Dataset file to evaluate (e.g. `longmemeval_s_reme_cleaned.json`); ground truth is included. |
| `dataset.start_index` / `num_items` | Slice of items to evaluate. |
| `dataset.question_types` | Filter by question type; empty = all. |
| `dataset.workspace_root` | Per-item workspace root (`benchmark/longmemeval/workspaces/longmemeval-s`). |
| `evaluation.num_workers` | `0` = auto (cpu-2), `1` = sequential, `>1` = parallel. |
| `evaluation.filter_future_sessions` | Only ingest sessions with timestamp ≤ `question_date`. |
| `reme.config` | ReMe config used (`lme.yaml`). |
| `reme.dream_trigger_hour` / `dream_scan_days` / `dream_max_units` | Dream triggering behavior. |
| `output.dir` | Results directory (`benchmark/longmemeval/results`). |
## 5. Outputs
Results are JSON files written to `output.dir` as `results_<timestamp>.json`,
with a per-type accuracy summary also printed to the console. Logging
conventions are shared across benchmarks — see the
[top-level README](../README.md#outputs--logs).
## 6. Reference Results
### cleaned-s
**Basic settings**
1. Modified auto-memory prompt, auto-dream disabled.
2. All sessions in reme-memory are strictly earlier than the question time.
**Results**
agentscope==2.0.4.post1, conda reme env, 32 workers, eval-only (reusing prebuilt memory)
(2026-08-06, 500 items, total 10.0 min)
| Type | Agentic | input tok/q | output tok/q | total tok/q | tool calls/q |
|---|---|---|---|---|---|
| knowledge-update | 0.910 | 31,581 | 589 | 32,169 | 2.90 |
| multi-session | 0.842 | 52,837 | 1,474 | 54,311 | 4.21 |
| single-session-assistant | 1.000 | 15,596 | 279 | 15,875 | 1.89 |
| single-session-preference | 0.633 | 36,802 | 818 | 37,620 | 3.60 |
| single-session-user | 0.986 | 27,433 | 359 | 27,792 | 2.60 |
| temporal-reasoning | 0.902 | 62,674 | 985 | 63,659 | 4.97 |
| **OVERALL** | **0.894** | **43,448** | **876** | **44,324** | **3.69** |

View file

@ -0,0 +1,90 @@
# LongMemEval 评测
[English version](./README.md)
LongMemEval 是一个面向**多轮多会话历史的长期记忆能力**的评测基准。每个条目提供一组按时间
顺序排列的用户与助手之间的会话以及一个只能通过推理用户自有记忆才能回答的探测问题。ReMe
将会话摄入按条目隔离的工作区,以 agenticReAct模式回答问题最后由 LLM-as-judge 打分。
题型包括单会话user / assistant / preference、多会话推理、知识更新与时间推理等。
> 公共设置(依赖、凭据、日志约定)见[总评测说明](../README_ZH.md)。
## 1. 获取数据集
ReMe 仅使用 **cleaned-S** 版本,数据托管在 HuggingFace
[agentscope-ai/ReMe_longmemeval_clean_s_v2](https://huggingface.co/datasets/agentscope-ai/ReMe_longmemeval_clean_s_v2)。
下载脚本经 hf-mirror.com 镜像源获取,如需更换源请修改 [`download.py`](./download.py) 中的
`BASE_URL`
```bash
cd benchmark/longmemeval
python download.py # 保存为 dataset/longmemeval_s_reme_cleaned.json已存在则自动跳过
```
ground truth 已内嵌在数据文件中。
## 2. 运行
在仓库根目录执行:
```bash
python benchmark/longmemeval/run.py
python benchmark/longmemeval/run.py --config benchmark/longmemeval/config.yaml
python benchmark/longmemeval/run.py -q # 安静模式:仅评测级日志
python benchmark/longmemeval/run.py --log-level WARNING # 降低评测 runner 日志
python benchmark/longmemeval/run.py --reme-log-level WARNING # 降低 reme 内部日志
python benchmark/longmemeval/run.py --eval_only # 复用已有工作区,仅执行查询 + 评判
```
## 3. 流程
1. 加载数据集ground truth 已内嵌在数据文件中)。
2. 为每个条目创建独立工作区,按时间顺序摄入会话。
3. 当相邻会话跨越配置的时刻(默认 23:00时触发 `auto_dream`
4. 以 agenticReAct模式回答每个问题。
5. 通过 `answer_judge` 任务对答案做二元yes/no评判并输出各类型准确率。
## 4. 关键配置 —— `benchmark/longmemeval/config.yaml`
| 配置项 | 含义 |
| --- | --- |
| `dataset.path` | 待评测的数据集文件(如 `longmemeval_s_reme_cleaned.json`),已包含 ground truth。 |
| `dataset.start_index` / `num_items` | 评测条目的切片范围。 |
| `dataset.question_types` | 按问题类型过滤,空表示全部。 |
| `dataset.workspace_root` | 条目工作区根目录(`benchmark/longmemeval/workspaces/longmemeval-s`)。 |
| `evaluation.num_workers` | `0` = 自动cpu-2`1` = 串行,`>1` = 并行。 |
| `evaluation.filter_future_sessions` | 仅摄入时间戳 ≤ `question_date` 的会话。 |
| `reme.config` | 使用的 ReMe 配置(`lme.yaml`)。 |
| `reme.dream_trigger_hour` / `dream_scan_days` / `dream_max_units` | dream 触发行为。 |
| `output.dir` | 结果目录(`benchmark/longmemeval/results`)。 |
## 5. 输出
结果以 JSON 文件写入 `output.dir`,文件名为 `results_<timestamp>.json`
同时控制台会打印含各类型准确率的汇总。日志约定在各基准间通用,见
[总说明](../README_ZH.md#输出与日志)。
## 6. 参考结果
### cleaned-s
**基础设置**
1. 使用修改后的 auto-memory prompt关闭 auto-dream 机制
2. reme-memory 中的全部 session 的时间一定早于 question 的时间
**结果**
agentscope==2.0.4.post1, conda reme env, 32 workers, eval-only复用预构建记忆
2026-08-06500 题,总计 10.0 min
| 类型 | Agentic | input tok/q | output tok/q | total tok/q | tool calls/q |
|---|---|---|---|---|---|
| knowledge-update | 0.910 | 31,581 | 589 | 32,169 | 2.90 |
| multi-session | 0.842 | 52,837 | 1,474 | 54,311 | 4.21 |
| single-session-assistant | 1.000 | 15,596 | 279 | 15,875 | 1.89 |
| single-session-preference | 0.633 | 36,802 | 818 | 37,620 | 3.60 |
| single-session-user | 0.986 | 27,433 | 359 | 27,792 | 2.60 |
| temporal-reasoning | 0.902 | 62,674 | 985 | 63,659 | 4.97 |
| **OVERALL** | **0.894** | **43,448** | **876** | **44,324** | **3.69** |

View file

@ -1,141 +0,0 @@
#!/usr/bin/env python3
"""Remove generated LongMemEval files while keeping source inputs.
For each ``datasets/longmemeval/<idx>`` workspace, this keeps only:
- query.json
- answer.json
- session/
All other files or directories in the sample root are considered generated
artifacts and can be removed. AppleDouble files whose names start with ``._``
are also removed recursively, including under ``session/``. The script is
dry-run by default; pass ``--apply`` to actually delete. To delete only specific
root-level generated files, pass one or more ``--filename`` values.
Examples:
python benchmark/longmemeval/clean_sample_outputs.py
python benchmark/longmemeval/clean_sample_outputs.py --apply
python benchmark/longmemeval/clean_sample_outputs.py --start 36 --end 79 --apply
python benchmark/longmemeval/clean_sample_outputs.py --filename check_golden.json --apply
python benchmark/longmemeval/clean_sample_outputs.py --filename session_review.json --apply
"""
import argparse
import shutil
import time
from collections.abc import Iterator
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
DATA = REPO / "datasets" / "longmemeval"
KEEP = {"query.json", "answer.json", "session"}
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--start", type=int, default=0, help="first numeric sample id to clean, inclusive (default 0)")
p.add_argument("--end", type=int, default=499, help="last numeric sample id to clean, inclusive (default 499)")
p.add_argument("--limit", type=int, default=0, help="only clean the first N selected samples (0 = all)")
p.add_argument("--progress-every", type=int, default=25, help="print progress every N samples when applying")
p.add_argument(
"--filename",
action="append",
default=[],
help="delete only this root-level file or directory name; can be passed multiple times",
)
p.add_argument("--apply", action="store_true", help="actually delete files; default is dry-run")
return p.parse_args()
def sample_ids() -> list[str]:
"""List all numeric sample IDs."""
ids = [p.name for p in DATA.iterdir() if p.is_dir() and p.name.isdigit()]
return sorted(ids, key=int)
def delete_path(path: Path) -> None:
"""Delete a file, symlink, or directory."""
if path.is_dir() and not path.is_symlink():
shutil.rmtree(path)
else:
path.unlink()
def iter_sample_targets(sample_dir: Path, filenames: set[str] | None = None) -> Iterator[Path]:
"""Yield generated artifacts for one sample.
Root-level generated directories are yielded as a whole, so there is no
need to recurse into them. AppleDouble files are only searched inside the
kept ``session/`` directory.
"""
if filenames:
for name in sorted(filenames):
path = sample_dir / name
if path.exists():
yield path
return
for path in sorted(sample_dir.iterdir(), key=lambda p: p.name):
if path.name not in KEEP:
yield path
session_dir = sample_dir / "session"
if session_dir.is_dir():
yield from session_dir.rglob("._*")
def main() -> int:
"""Main entry point."""
args = parse_args()
if args.end < args.start:
raise ValueError(f"--end ({args.end}) must be >= --start ({args.start})")
filenames = {name.strip() for name in args.filename if name.strip()}
invalid_filenames = [name for name in filenames if Path(name).name != name]
if invalid_filenames:
raise ValueError(f"--filename only accepts root-level names, got: {invalid_filenames}")
ids = [idx for idx in sample_ids() if args.start <= int(idx) <= args.end]
if args.limit:
ids = ids[: args.limit]
total_targets = 0
deleted = 0
started_at = time.time()
for ordinal, idx in enumerate(ids, start=1):
sample_dir = DATA / idx
sample_started_at = time.time()
targets = list(iter_sample_targets(sample_dir, filenames=filenames))
total_targets += len(targets)
print(f"[sample {ordinal}/{len(ids)}] {idx} targets={len(targets)}", flush=True)
for path in targets:
if args.apply:
target_started_at = time.time()
print(f"[delete] {path}", flush=True)
delete_path(path)
deleted += 1
print(f"[deleted] {path} elapsed={time.time() - target_started_at:.1f}s", flush=True)
else:
print(f"[would-delete] {path}")
if args.apply and args.progress_every > 0 and (int(idx) + 1) % args.progress_every == 0:
elapsed = time.time() - started_at
print(
f"[progress] processed={ordinal}/{len(ids)} through={idx} " f"deleted={deleted} elapsed={elapsed:.1f}s",
flush=True,
)
print(f"[sample-done] {idx} elapsed={time.time() - sample_started_at:.1f}s", flush=True)
mode = "DELETE" if args.apply else "DRY-RUN"
print(
f"{mode} LongMemEval generated artifacts: samples={len(ids)} "
f"targets={total_targets} deleted={deleted if args.apply else 0} range={args.start}..{args.end}",
flush=True,
)
if not args.apply:
print("No files deleted. Re-run with --apply to delete these paths.", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,33 @@
# LongMemEval evaluation configuration
# This file controls what/how to evaluate.
dataset:
path: "benchmark/longmemeval/dataset/longmemeval_s_reme_cleaned.json"
start_index: 0 # first item index
num_items: 500 # how many items to evaluate (starting from start_index)
max_sessions: 0 # 0 = all sessions; >0 = limit sessions per item for testing
question_types: [] # filter by question_type; empty list = no filtering (all types)
workspace_root: "benchmark/longmemeval/workspaces/longmemeval-s" # workspace root for item workspaces
evaluation:
# LLM-as-judge uses the 'judge' as_llm component defined in lme.yaml
# Model and credentials are configured there (reading from .env)
# Judgment is always binary (yes/no) — defined in lme/llm_judge.yaml
num_workers: 32 # 0 = auto (cpu_count - 2, min 1); 1 = sequential; >1 = parallel
filter_future_sessions: true # true = only ingest sessions with timestamp <= question_date
compress_session: false # true = compress session chunks in search_v2 (query-aware); false = no compression
reme:
config: "lme.yaml" # reme config to use (in reme/config/)
# Dream trigger: when gap between consecutive sessions crosses this hour (23:00)
dream_trigger_hour: 23
# Dream scan_days for each trigger
dream_scan_days: 2
dream_max_units: 5
output:
dir: "benchmark/longmemeval/results"
log_dir: "logs" # log directory (relative to project root)
log_prefix: "longmemeval" # benchmark name used in log filenames
log_to_console: true
log_to_file: true

View file

@ -0,0 +1,67 @@
"""Download the LongMemEval cleaned-S dataset used by ReMe.
Source: https://huggingface.co/datasets/agentscope-ai/ReMe_longmemeval_clean_s_v2
(downloaded via the hf-mirror.com mirror for reliability).
The file ``longmemeval_s_reme_cleaned.json`` is saved under ``dataset/`` next to this
script using the same name as on the remote (``benchmark/longmemeval/config.yaml``
points to it).
Usage:
python download.py # download cleaned-S (skip if it already exists)
"""
import os
import sys
import urllib.request
BASE_URL = "https://hf-mirror.com/datasets/agentscope-ai/ReMe_longmemeval_clean_s_v2/resolve/main"
TARGET_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "dataset")
# Files to download (saved with the same name as on the remote).
FILES = [
"longmemeval_s_reme_cleaned.json",
]
def download_file(filename: str):
"""Download a single file from the mirror to the target directory."""
url = f"{BASE_URL}/{filename}"
dest = os.path.join(TARGET_DIR, filename)
if os.path.exists(dest):
size = os.path.getsize(dest)
print(f" [skip] {filename} already exists ({size / 1024 / 1024:.1f} MB)")
return
print(f" [downloading] {filename} ...")
try:
urllib.request.urlretrieve(url, dest, reporthook=_progress)
size = os.path.getsize(dest)
print(f"\n [done] {filename} ({size / 1024 / 1024:.1f} MB)")
except Exception as e:
print(f"\n [error] {filename}: {e}")
if os.path.exists(dest):
os.remove(dest)
sys.exit(1)
def _progress(block_num, block_size, total_size):
downloaded = block_num * block_size
if total_size > 0:
pct = min(100, downloaded * 100 / total_size)
mb = downloaded / 1024 / 1024
total_mb = total_size / 1024 / 1024
sys.stdout.write(f"\r {mb:.1f}/{total_mb:.1f} MB ({pct:.1f}%)")
else:
mb = downloaded / 1024 / 1024
sys.stdout.write(f"\r {mb:.1f} MB downloaded")
sys.stdout.flush()
if __name__ == "__main__":
os.makedirs(TARGET_DIR, exist_ok=True)
print(f"Downloading LongMemEval cleaned-S dataset to: {TARGET_DIR}\n")
for fname in FILES:
download_file(fname)
print("\nAll files downloaded successfully!")

View file

@ -0,0 +1,76 @@
#!/bin/bash
# 杀死指定进程及其所有子进程
# Usage: bash kill.sh <PID>
if [ -z "$1" ]; then
echo "Usage: bash kill.sh <PID>"
echo " 杀死指定进程及其所有子进程"
exit 1
fi
PID=$1
# 检查进程是否存在
if ! kill -0 "$PID" 2>/dev/null; then
echo "进程 $PID 不存在"
exit 1
fi
# 递归收集所有子进程(包括子进程的子进程)
collect_children() {
local parent=$1
local children
children=$(ps -o pid= --ppid "$parent" 2>/dev/null | tr -d ' ')
for child in $children; do
collect_children "$child"
done
echo "$parent"
}
# 收集进程树(子进程在前,父进程在后,保证先杀子再杀父)
PROCESS_TREE=$(collect_children "$PID")
TOTAL=$(echo "$PROCESS_TREE" | wc -l | tr -d ' ')
echo "进程树(共 $TOTAL 个进程):"
while read -r p; do
cmd=$(ps -o args= -p "$p" 2>/dev/null | head -c 80)
printf " PID=%-8s %s\n" "$p" "$cmd"
done <<< "$PROCESS_TREE"
# 先 SIGTERM 优雅终止
echo ""
echo "发送 SIGTERM..."
while read -r p; do
kill "$p" 2>/dev/null
done <<< "$PROCESS_TREE"
# 等待最多 5 秒
for i in $(seq 1 5); do
alive=false
while read -r p; do
if kill -0 "$p" 2>/dev/null; then
alive=true
fi
done <<< "$PROCESS_TREE"
if [ "$alive" = false ]; then
break
fi
sleep 1
done
# 检查是否还有残留,强制 SIGKILL
remaining=false
while read -r p; do
if kill -0 "$p" 2>/dev/null; then
remaining=true
fi
done <<< "$PROCESS_TREE"
if [ "$remaining" = true ]; then
echo "部分进程未响应,发送 SIGKILL..."
while read -r p; do
kill -9 "$p" 2>/dev/null
done <<< "$PROCESS_TREE"
fi
echo "已终止进程树(根 PID=$PID,共 $TOTAL 个进程)"

View file

@ -0,0 +1,816 @@
"""LongMemEval evaluation runner for ReMe.
Evaluates ReMe's long-term memory capability using the LongMemEval dataset.
Each item gets an isolated workspace; sessions are ingested in chronological order;
dream is triggered when sessions cross midnight (23:00); finally questions are
answered via an agentic (ReAct) approach and judged by an LLM.
Usage:
python benchmark/longmemeval/run.py
python benchmark/longmemeval/run.py --config benchmark/longmemeval/config.yaml
python benchmark/longmemeval/run.py -q # quiet: only eval-level logs
python benchmark/longmemeval/run.py --log-level WARNING # reduce eval runner logs
python benchmark/longmemeval/run.py --reme-log-level WARNING # reduce reme internal logs
python benchmark/longmemeval/run.py --eval_only # query+judge only, reuse existing workspace
"""
import json
import logging
import os
import re
import shutil
import time
import threading
from datetime import datetime
from pathlib import Path
import yaml
from dotenv import load_dotenv
# Load .env from project root
_PROJECT_ROOT = Path(__file__).parent.parent.parent
load_dotenv(_PROJECT_ROOT / ".env")
# Workspace root for evaluation items — read from config.yaml (dataset.workspace_root)
_WORKSPACE_ROOT_DEFAULT = "benchmark/longmemeval/workspaces/longmemeval-s"
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
_DEFAULT_LOG_FORMAT = "%(asctime)s | %(levelname)s | %(message)s"
logging.basicConfig(level=logging.INFO, format=_DEFAULT_LOG_FORMAT)
logger = logging.getLogger("longmemeval")
# Noisy library loggers silenced by default
_NOISY_LOGGERS = [
"httpx",
"httpcore",
"openai",
"uvicorn",
"multipart",
"asyncio",
"watchfiles",
"filelock",
]
def setup_logging(
log_level: str,
reme_log_level: str,
log_dir: str | None = None,
):
"""Configure logging for the eval runner and reme internals.
Args:
log_level: Level for the eval runner logger (DEBUG/INFO/WARNING/ERROR).
reme_log_level: Level for reme's internal loguru logger.
log_dir: Per-run log directory (absolute path). None = no file logging.
"""
numeric = getattr(logging, log_level.upper(), logging.INFO)
# Eval runner logger
logging.getLogger().setLevel(numeric)
logger.setLevel(numeric)
# Suppress noisy library loggers when above DEBUG
if numeric > logging.DEBUG:
for name in _NOISY_LOGGERS:
lib_logger = logging.getLogger(name)
lib_logger.setLevel(max(numeric, logging.WARNING))
# Add file handler for eval runner if log_dir is specified
if log_dir:
os.makedirs(log_dir, exist_ok=True)
log_filepath = os.path.join(log_dir, "runner.log")
file_handler = logging.FileHandler(log_filepath, encoding="utf-8")
file_handler.setLevel(numeric)
file_handler.setFormatter(logging.Formatter(_DEFAULT_LOG_FORMAT))
logging.getLogger().addHandler(file_handler)
logger.info(f"Eval runner log file: {log_filepath}")
# Reme internal logger (loguru) — will be applied per-worker via _configure_worker
os.environ["REME_LOG_LEVEL"] = reme_log_level.upper()
if log_dir:
os.environ["REME_LOG_DIR"] = log_dir
def _configure_worker(
log_level: str,
reme_log_level: str,
log_dir: str | None = None,
):
"""Set up logging inside a multiprocessing worker process.
Must be called at the top of each worker because child processes inherit
parent state but loguru sinks are NOT shared across fork/spawn.
"""
numeric = getattr(logging, log_level.upper(), logging.INFO)
logging.basicConfig(level=numeric, format=_DEFAULT_LOG_FORMAT, force=True)
logging.getLogger("longmemeval").setLevel(numeric)
if numeric > logging.DEBUG:
for name in _NOISY_LOGGERS:
logging.getLogger(name).setLevel(max(numeric, logging.WARNING))
# Add file handler for eval runner in worker process
if log_dir:
os.makedirs(log_dir, exist_ok=True)
pid = os.getpid()
log_filepath = os.path.join(log_dir, f"worker-{pid}.log")
file_handler = logging.FileHandler(log_filepath, encoding="utf-8")
file_handler.setLevel(numeric)
file_handler.setFormatter(logging.Formatter(_DEFAULT_LOG_FORMAT))
logging.getLogger().addHandler(file_handler)
# Re-initialize loguru for reme internals at the desired level
from reme.utils import get_logger
reme_log_dir = log_dir or "logs"
get_logger(log_dir=reme_log_dir, level=reme_log_level.upper(), force_init=True)
# ---------------------------------------------------------------------------
# Config loading
# ---------------------------------------------------------------------------
def load_eval_config(config_path: str | None = None) -> dict:
"""Load evaluation config yaml with env-var expansion."""
if config_path is None:
config_path = str(Path(__file__).parent / "config.yaml")
with open(config_path, encoding="utf-8") as f:
raw = f.read()
# Expand ${VAR} and ${VAR:-default}
def _expand(m):
expr = m.group(1)
if ":-" in expr:
key, default = expr.split(":-", 1)
return os.environ.get(key, default)
return os.environ.get(expr, "")
raw = re.sub(r"\$\{([^}]+)\}", _expand, raw)
return yaml.safe_load(raw)
# ---------------------------------------------------------------------------
# Date utilities
# ---------------------------------------------------------------------------
def parse_haystack_date(date_str: str) -> datetime:
"""Parse LongMemEval date format: '2023/05/20 (Sat) 02:21' -> datetime."""
m = re.match(r"(\d{4}/\d{2}/\d{2})\s+\(\w+\)\s+(\d{2}:\d{2})", date_str)
if not m:
raise ValueError(f"Cannot parse haystack date: {date_str!r}")
return datetime.strptime(f"{m.group(1)} {m.group(2)}", "%Y/%m/%d %H:%M")
def to_iso(dt: datetime) -> str:
"""Convert datetime to ISO-8601 string precise to seconds."""
return dt.strftime("%Y-%m-%dT%H:%M:%S")
def should_trigger_dream(prev_dt: datetime, curr_dt: datetime, _trigger_hour: int = 23) -> bool:
"""Check if the time gap between two sessions crosses trigger_hour (e.g. 23:00)."""
if prev_dt.date() == curr_dt.date():
return False
# There's at least one midnight crossing; check if trigger_hour is between them
# Simple heuristic: if dates differ, dream should run for the previous day
return True
def sessions_sorted_by_time(item: dict) -> list[tuple[int, datetime, str, list[dict]]]:
"""Return (original_index, parsed_datetime, session_id, messages) sorted by time."""
entries = []
for i, (date_str, sid, msgs) in enumerate(
zip(item["haystack_dates"], item["haystack_session_ids"], item["haystack_sessions"]),
):
dt = parse_haystack_date(date_str)
entries.append((i, dt, sid, msgs))
# Sort by time (ascending)
entries.sort(key=lambda x: x[1])
return entries
# ---------------------------------------------------------------------------
# Message formatting
# ---------------------------------------------------------------------------
def format_messages_for_reme(messages: list[dict], session_dt: datetime) -> list[dict]:
"""Convert LongMemEval messages to ReMe auto_memory format.
Adds: name, created_at (ISO seconds). All messages in a session share the
same created_at (the session timestamp).
"""
formatted = []
for msg in messages:
role = msg["role"]
formatted.append(
{
"name": role,
"role": role,
"content": msg["content"],
"created_at": to_iso(session_dt),
},
)
return formatted
# ---------------------------------------------------------------------------
# LLM-as-Judge (delegated to answer_judge_step via app.run_job)
# ---------------------------------------------------------------------------
async def judge_response_via_job(
app,
question: str,
ground_truth: str,
response: str,
question_type: str,
) -> dict:
"""Use the answer_judge_step to evaluate a response against the golden answer."""
judge_resp = await app.run_job(
"answer_judge",
query=question,
agent_answer=response,
golden_answer=ground_truth,
question_type=question_type,
)
verdict = (judge_resp.answer or "").strip().lower()
raw_answer = (judge_resp.metadata or {}).get("raw_answer_judgement", "")
return {
"verdict": verdict,
"reason": raw_answer if verdict not in ("yes", "no") else "",
"metric": "binary",
"question_type": question_type,
}
# ---------------------------------------------------------------------------
# Main evaluation pipeline
# ---------------------------------------------------------------------------
async def evaluate_item(item: dict, eval_config: dict, item_index: int, eval_only: bool = False) -> dict:
"""Evaluate a single LongMemEval item end-to-end.
Args:
item: The dataset item containing question, answer, sessions, etc.
eval_config: The evaluation configuration dict.
item_index: The index of this item in the dataset.
eval_only: If True, skip ingestion (phases 1-3) and only run query+judge
using the existing workspace. Useful for re-evaluating different query
configurations without re-ingesting sessions.
"""
from reme import Application
from reme.config import resolve_app_config
from reme.utils.evaluation_interface import track_agent_token_usage, track_job_counts
reme_cfg = eval_config["reme"]
dream_trigger_hour = reme_cfg.get("dream_trigger_hour", 23)
dream_scan_days = reme_cfg.get("dream_scan_days", 2)
dream_max_units = reme_cfg.get("dream_max_units", 5)
# Sort sessions by time
sorted_sessions = sessions_sorted_by_time(item)
# Filter out sessions that occur after question_date (if enabled)
filter_future = eval_config["evaluation"].get("filter_future_sessions", True)
if filter_future and item.get("question_date"):
question_dt = parse_haystack_date(item["question_date"])
total_before_filter = len(sorted_sessions)
sorted_sessions = [(i, dt, sid, msgs) for i, dt, sid, msgs in sorted_sessions if dt <= question_dt]
if len(sorted_sessions) < total_before_filter:
logger.info(
f"[Item {item_index}] Filtered sessions: {total_before_filter} -> {len(sorted_sessions)} "
f"(removed {total_before_filter - len(sorted_sessions)} future sessions "
f"after question_date={item['question_date']})",
)
logger.info(
"[Item %s] question_id=%s type=%s sessions=%d%s",
item_index,
item["question_id"],
item["question_type"],
len(sorted_sessions),
" [eval_only]" if eval_only else "",
)
# Use fixed workspace directory (clean it for fresh evaluation)
workspace_root = _PROJECT_ROOT / eval_config["dataset"].get("workspace_root", _WORKSPACE_ROOT_DEFAULT)
item_dir = workspace_root / f"item_{item_index}"
workspace_dir = str(item_dir / ".reme")
if eval_only:
if not item_dir.exists() or not Path(workspace_dir).exists():
raise FileNotFoundError(
f"[Item {item_index}] eval_only: workspace not found at {item_dir}. "
f"Run without --eval_only first to build the workspace.",
)
else:
if item_dir.exists():
shutil.rmtree(item_dir)
logger.info(f"[Item {item_index}] Cleaned existing workspace: {item_dir}")
else:
logger.info(f"[Item {item_index}] Workspace not found, creating: {item_dir}")
item_dir.mkdir(parents=True, exist_ok=True)
# Pre-initialize ReMe's loguru logger with the correct log_dir
# (singleton — Application.__init__ will reuse this instance)
output_cfg = eval_config.get("output", {})
if output_cfg.get("log_to_file", False):
reme_log_dir = os.environ.get("REME_LOG_DIR")
if reme_log_dir:
from reme.utils import get_logger
get_logger(
log_dir=reme_log_dir,
level=os.environ.get("REME_LOG_LEVEL", "INFO"),
log_to_console=output_cfg.get("log_to_console", True),
log_to_file=True,
force_init=True,
)
cfg = resolve_app_config(
config=reme_cfg["config"],
workspace_dir=workspace_dir,
log_to_console=output_cfg.get("log_to_console", True),
log_to_file=output_cfg.get("log_to_file", False),
enable_logo=False,
)
app = Application(**cfg)
await app.start()
try:
dream_dates_triggered = set()
dream_available = True # Set to False if auto_dream job is not found
if not eval_only:
# ── Phase 1: Ingest sessions ──────────────────────────────
prev_dt = None
for idx, (_, session_dt, session_id, messages) in enumerate(sorted_sessions):
# Check if dream should be triggered before this session
if (
dream_available
and prev_dt is not None
and should_trigger_dream(prev_dt, session_dt, dream_trigger_hour)
):
dream_date = prev_dt.strftime("%Y-%m-%d")
if dream_date not in dream_dates_triggered:
logger.info(f"[Item {item_index}] Triggering dream for date={dream_date}")
try:
dream_resp = await app.run_job(
"auto_dream",
date=dream_date,
scan_days=dream_scan_days,
max_units=dream_max_units,
)
logger.info(
f"[Item {item_index}] Dream done: success={dream_resp.success} "
f"answer={dream_resp.answer[:100] if dream_resp.answer else ''}",
)
except Exception as e:
if "not found" in str(e).lower():
dream_available = False
logger.warning(f"[Item {item_index}] auto_dream job not found, skipping all dreams")
else:
logger.warning(f"[Item {item_index}] Dream failed for {dream_date}: {e}")
dream_dates_triggered.add(dream_date)
# Index update after dream to pick up new digest nodes
await app.run_job("index_update")
# Format and ingest the session
formatted_msgs = format_messages_for_reme(messages, session_dt)
date_str = session_dt.strftime("%Y-%m-%d")
logger.info(
f"[Item {item_index}] Ingesting session {idx+1}/{len(sorted_sessions)} "
f"id={session_id} date={date_str} msgs={len(formatted_msgs)}",
)
resp = await app.run_job(
"auto_memory",
messages=formatted_msgs,
session_id=session_id,
date=date_str,
)
if not resp.success:
logger.warning(
f"[Item {item_index}] auto_memory failed for session {session_id}: {resp.answer}",
)
# Manual index update after each session
await app.run_job("index_update")
prev_dt = session_dt
# ── Phase 2: Final dream for the last day ─────────────────
if dream_available and prev_dt is not None:
last_dream_date = prev_dt.strftime("%Y-%m-%d")
if last_dream_date not in dream_dates_triggered:
logger.info(f"[Item {item_index}] Final dream for date={last_dream_date}")
try:
await app.run_job(
"auto_dream",
date=last_dream_date,
scan_days=dream_scan_days,
max_units=dream_max_units,
)
except Exception as e:
if "not found" in str(e).lower():
dream_available = False
logger.warning(f"[Item {item_index}] auto_dream job not found, skipping all dreams")
else:
logger.warning(f"[Item {item_index}] Final dream failed: {e}")
dream_dates_triggered.add(last_dream_date)
# Index update after final dream
await app.run_job("index_update")
# ── Phase 3: Digest update ────────────────────────────────
await app.run_job("digest_update")
# ── Phase 4: Ask question via agentic_answer job (ReAct agent) ──
question = item["question"]
compress_session = bool(eval_config["evaluation"].get("compress_session", False))
question_date_raw = item.get("question_date", "")
question_dt = parse_haystack_date(question_date_raw) if question_date_raw else None
query_time = to_iso(question_dt) if question_dt else ""
logger.info(
f"[Item {item_index}] Asking (agentic): {question[:80]}... query_time={query_time}",
)
with (
track_job_counts(["search"], app.context) as tool_counts,
track_agent_token_usage(
["bench"],
app.context,
) as token_usages,
):
query_resp = await app.run_job(
"agentic_answer",
query=question,
query_time=query_time,
compress_session=compress_session,
)
agentic_tool_counts = tool_counts
agentic_token_usage = token_usages["bench"]
agentic_response = (query_resp.answer or "").strip()
if not agentic_response:
agentic_response = "(no answer generated)"
logger.info(f"[Item {item_index}] Agentic response: {agentic_response[:200]}...")
logger.info(f"[Item {item_index}] Agentic tool calls: {agentic_tool_counts}")
logger.info(f"[Item {item_index}] Bench token usage: {agentic_token_usage}")
# ── Phase 5: Judge agentic response (via answer_judge_step) ──────────
logger.info(f"[Item {item_index}] Judging agentic (binary, type={item['question_type']})...")
agentic_judgment = await judge_response_via_job(
app=app,
question=question,
ground_truth=item["answer"],
response=agentic_response,
question_type=item["question_type"],
)
logger.info(f"[Item {item_index}] agentic binary result: {agentic_judgment}")
finally:
await app.close()
return {
"question_id": item["question_id"],
"question_type": item["question_type"],
"question": question,
"ground_truth": item["answer"],
"agentic_response": agentic_response,
"agentic_judgment": agentic_judgment,
"agentic_tool_counts": agentic_tool_counts,
"agentic_token_usage": agentic_token_usage,
"sessions_ingested": len(sorted_sessions),
"dreams_triggered": len(dream_dates_triggered),
}
# ---------------------------------------------------------------------------
# Worker: runs a single item in its own process with its own event loop
# ---------------------------------------------------------------------------
def _evaluate_item_worker(task_input: tuple) -> dict:
"""Worker function for multiprocessing. Each process gets its own event loop."""
item, eval_config, item_index, log_level, reme_log_level, eval_only, log_dir = task_input
import asyncio # pylint: disable=import-outside-toplevel
_configure_worker(log_level, reme_log_level, log_dir=log_dir)
# Permanently suppress "Task exception was never retrieved" /
# "Event loop is closed" noise from httpx AsyncClient GC cleanup.
# These fire AFTER asyncio.run() closes the loop, during Python's
# garbage collection of httpx connection-pool tasks — harmless.
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
return asyncio.run(evaluate_item(item, eval_config, item_index, eval_only=eval_only))
def _indexed_worker(indexed_input: tuple) -> tuple:
"""Module-level wrapper for imap_unordered with index tracking."""
idx, task_input = indexed_input
return idx, _evaluate_item_worker(task_input)
def _resolve_num_workers(configured: int) -> int:
"""Resolve num_workers: 0=auto (cpu_count-2, min 1), 1=sequential, >1=parallel."""
if configured == 0:
return max(1, (os.cpu_count() or 4) - 2)
return max(1, configured)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main(
config_path: str | None = None,
log_level: str = "INFO",
reme_log_level: str = "INFO",
eval_only: bool = False,
):
"""Run the LongMemEval evaluation pipeline.
Args:
config_path: Path to the YAML config file.
log_level: Log level for the eval runner.
reme_log_level: Log level for reme internal logs.
eval_only: If True, skip ingestion and only run query+judge using
existing workspaces.
"""
from multiprocessing import Pool # pylint: disable=import-outside-toplevel
# Load config BEFORE logging setup so log_dir is available
eval_config = load_eval_config(config_path)
# Resolve per-run log directory from config
output_cfg = eval_config.get("output", {})
log_dir_abs = None
if output_cfg.get("log_to_file", False):
log_dir_raw = output_cfg.get("log_dir", "logs")
log_prefix = output_cfg.get("log_prefix", "longmemeval")
run_ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_dir_abs = str(_PROJECT_ROOT / log_dir_raw / f"{log_prefix}_{run_ts}")
setup_logging(log_level, reme_log_level, log_dir=log_dir_abs)
dataset_cfg = eval_config["dataset"]
# Load dataset
dataset_path = _PROJECT_ROOT / dataset_cfg["path"]
logger.info(f"Loading dataset from {dataset_path}")
with open(dataset_path, encoding="utf-8") as f:
data = json.load(f)
start = dataset_cfg.get("start_index", 0)
num_items = dataset_cfg.get("num_items", 0)
if num_items > 0:
raw_items = data[start : start + num_items]
else:
raw_items = data[start:]
# Build item list
items_with_idx = [(start + i, item) for i, item in enumerate(raw_items)]
# Filter by question_type if specified
question_types = dataset_cfg.get("question_types") or []
if question_types:
before_filter = len(items_with_idx)
items_with_idx = [(idx, item) for idx, item in items_with_idx if item.get("question_type") in question_types]
logger.info(
f"Filtered by question_types={question_types}: {before_filter} -> {len(items_with_idx)} items",
)
# Filter by question_id if specified
question_ids = dataset_cfg.get("question_ids") or []
if question_ids:
qid_set = set(question_ids)
before_filter = len(items_with_idx)
items_with_idx = [(idx, item) for idx, item in items_with_idx if item.get("question_id") in qid_set]
logger.info(
f"Filtered by question_ids ({len(qid_set)} ids): {before_filter} -> {len(items_with_idx)} items",
)
logger.info(
"Evaluating %d item(s) starting from index %d%s",
len(items_with_idx),
start,
" [eval_only: query+judge only]" if eval_only else "",
)
# Resolve parallelism
num_workers = _resolve_num_workers(eval_config["evaluation"].get("num_workers", 1))
logger.info(f"Using {num_workers} worker(s)")
# Create output directory
output_dir = _PROJECT_ROOT / output_cfg.get("dir", "benchmark/longmemeval/results")
output_dir.mkdir(parents=True, exist_ok=True)
# Create workspace root directory
workspace_root = _PROJECT_ROOT / dataset_cfg.get("workspace_root", _WORKSPACE_ROOT_DEFAULT)
workspace_root.mkdir(parents=True, exist_ok=True)
# Pre-check: verify all workspaces exist in eval_only mode
if eval_only:
missing_items = []
for orig_idx, _ in items_with_idx:
item_dir = workspace_root / f"item_{orig_idx}"
if not item_dir.exists() or not (item_dir / ".reme").exists():
missing_items.append(orig_idx)
if missing_items:
preview = missing_items[:10]
suffix = "..." if len(missing_items) > 10 else ""
raise FileNotFoundError(
f"eval_only: {len(missing_items)} workspace(s) not found under {workspace_root}. "
f"Missing item indices: {preview}{suffix}. "
f"Run without --eval_only first to build the workspaces.",
)
# Build task args — include log levels, eval_only flag, and log paths (use original index for workspace lookup)
task_args = [
(item, eval_config, orig_idx, log_level, reme_log_level, eval_only, log_dir_abs)
for orig_idx, item in items_with_idx
]
# Progress tracking (force print regardless of log level, every 10 minutes)
total_items = len(task_args)
completed_count = [0] # use list for mutability in closure
start_time = time.time()
progress_lock = threading.Lock()
def _print_progress(prefix: str = "PROGRESS"):
elapsed = time.time() - start_time
elapsed_min = elapsed / 60
done = completed_count[0]
pct = 100.0 * done / total_items if total_items else 0
eta_str = "N/A"
if done > 0:
eta_sec = elapsed / done * (total_items - done)
eta_str = f"{eta_sec/60:.1f}min"
print(
f"[{prefix}] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | "
f"{done}/{total_items} ({pct:.1f}%) completed | "
f"elapsed={elapsed_min:.1f}min | ETA={eta_str}",
flush=True,
)
def _progress_timer():
"""Background thread: print progress every 10 minutes."""
while not _timer_stop.is_set():
_timer_stop.wait(600) # 10 minutes
if not _timer_stop.is_set():
with progress_lock:
_print_progress()
_timer_stop = threading.Event()
timer_thread = threading.Thread(target=_progress_timer, daemon=True)
timer_thread.start()
# Run evaluation
if num_workers == 1:
# Sequential mode
results = []
for task_input in task_args:
result = _evaluate_item_worker(task_input)
results.append(result)
with progress_lock:
completed_count[0] += 1
else:
# Parallel mode — use imap_unordered for progress tracking
results = [None] * total_items
indexed_args = list(enumerate(task_args))
with Pool(processes=num_workers) as pool:
for idx, result in pool.imap_unordered(_indexed_worker, indexed_args):
results[idx] = result
with progress_lock:
completed_count[0] += 1
# Stop progress timer
_timer_stop.set()
timer_thread.join(timeout=2)
# Save results
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = output_dir / f"results_{timestamp}.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
logger.info(f"Results saved to {output_file}")
# Final progress
_print_progress("FINAL")
_print_summary(results, start_time)
# ---------------------------------------------------------------------------
# Summary printing
# ---------------------------------------------------------------------------
def _print_summary(results: list[dict], start_time: float) -> None:
"""Print per-item verdicts and per-type accuracy."""
print("\n" + "=" * 60)
print("EVALUATION RESULTS")
print("=" * 60)
def _accumulate(judgment_key):
correct = 0
stats: dict = {} # {question_type: {correct: int, total: int}}
for r in results:
qtype = r["question_type"]
verdict = r.get(judgment_key, {}).get("verdict", "N/A")
if qtype not in stats:
stats[qtype] = {"correct": 0, "total": 0}
stats[qtype]["total"] += 1
if verdict == "yes":
correct += 1
stats[qtype]["correct"] += 1
return correct, stats
agentic_correct, agentic_type_stats = _accumulate("agentic_judgment")
total = len(results)
# Per-item verdict rows
for r in results:
a_verdict = r.get("agentic_judgment", {}).get("verdict", "N/A")
print(f" [{r['question_id']}] type={r['question_type']} agentic={a_verdict}")
print("\n" + "-" * 60)
print(f" Items: {total}")
# Agentic stats
print("\n ── Agentic (ReAct) ──")
print(f" Overall accuracy: {agentic_correct}/{total} ({100*agentic_correct/total:.1f}%)")
tool_call_totals = [sum(r.get("agentic_tool_counts", {}).values()) for r in results]
tool_call_mean, tool_call_std = _mean_and_std(tool_call_totals)
print(f" Tool calls/query: mean={tool_call_mean:.2f} std={tool_call_std:.2f}")
token_usages = [r.get("agentic_token_usage", {}) for r in results]
print(" Bench reported tokens/query:")
for metric in _TOKEN_USAGE_METRICS:
values = [usage[metric] for usage in token_usages if usage.get(metric) is not None]
if values:
mean, std = _mean_and_std(values)
print(f" {metric}: mean={mean:.2f} std={std:.2f}")
else:
print(f" {metric}: unavailable")
print(" Per-type accuracy:")
for qtype, stats in sorted(agentic_type_stats.items()):
acc = 100 * stats["correct"] / stats["total"] if stats["total"] else 0
print(f" {qtype}: {stats['correct']}/{stats['total']} ({acc:.1f}%)")
print("=" * 60)
total_elapsed = time.time() - start_time
print(f"\n Total time: {total_elapsed/60:.1f} min")
print("\n" + "=" * 60)
print(" [DONE] EVALUATION COMPLETED SUCCESSFULLY")
print("=" * 60 + "\n")
_TOKEN_USAGE_METRICS = (
"input_tokens",
"output_tokens",
"total_tokens",
)
def _mean_and_std(values: list[int]) -> tuple[float, float]:
"""Return population mean and standard deviation for one per-query metric."""
if not values:
return 0.0, 0.0
mean = sum(values) / len(values)
return mean, (sum((value - mean) ** 2 for value in values) / len(values)) ** 0.5
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="LongMemEval evaluation runner")
parser.add_argument("--config", type=str, default=None, help="Path to config.yaml")
parser.add_argument(
"--log-level",
type=str,
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
help="Log level for the eval runner (default: INFO)",
)
parser.add_argument(
"--reme-log-level",
type=str,
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
help="Log level for reme internal logs — loguru (default: INFO)",
)
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Shortcut for --log-level WARNING --reme-log-level WARNING",
)
parser.add_argument(
"--eval_only",
action="store_true",
help="Skip ingestion (phases 1-3). Reuse existing workspaces and only run query+judge.",
)
args = parser.parse_args()
if args.quiet:
args.log_level = "WARNING"
args.reme_log_level = "WARNING"
main(args.config, args.log_level, args.reme_log_level, eval_only=args.eval_only)

View file

@ -1,343 +0,0 @@
#!/usr/bin/env python3
"""Drive the LongMemEval memory pipeline across all samples.
For every workspace under ``datasets/longmemeval/<idx>`` this launches one or more
``reme start config=jinli_lme job=<job>`` runs with ``LME_WORKSPACE_DIR`` pointed
at that sample. The pipeline jobs, in order, are:
1. auto_memory distil every raw session into a daily note (``daily/*.md``)
2. update_index clear the store and rebuild the index over ``daily/*.md``
3. agentic_answer read ``query.json`` and answer it, writing ``mem_answer.json``
4. llm_judge judge ``mem_answer.json`` against ``answer.json``
Pick one with ``--job``, or ``--job all`` to run the full pipeline *serially per sample*.
Runs are capped at ``--concurrency`` (default 1 for ``--job auto_memory``, otherwise
3) samples at once and each launch is staggered by ``--stagger`` seconds so they
do not all hit the LLM API at once.
By default every selected job is rerun for every sample each job's own clear
step (configured in jinli_lme.yaml) wipes stale output first, so a run is always
a clean rebuild. Pass ``--resume`` to instead skip samples whose output already
exists (``daily/`` for auto_memory, ``metadata/embedding_store/`` for
update_index, ``mem_answer.json`` for agentic_answer, ``mem_answer.json`` with
``llm_judge.judgement`` for llm_judge) and continue an interrupted batch. Each
sample's stdout/stderr goes to ``logs/agentic_answer/<job>/<idx>.log``.
After an agentic_answer run finishes, the driver aggregates every sample's query,
golden answer, predicted answer, LLM judgement and a best-effort tool-call trail
into one big JSON at ``logs/agentic_answer/aggregate.json``.
Examples:
python benchmark/longmemeval/run_agentic_answer.py # agentic_answer, all 500, conc 3
python benchmark/longmemeval/run_agentic_answer.py --job all # full pipeline serially per sample
python benchmark/longmemeval/run_agentic_answer.py --job auto_memory # just step 1
python benchmark/longmemeval/run_agentic_answer.py --job llm_judge # just judge existing answers
python benchmark/longmemeval/run_agentic_answer.py --limit 5 --dry-run # list what would run
python benchmark/longmemeval/run_agentic_answer.py --start 187 # samples 187..499
python benchmark/longmemeval/run_agentic_answer.py --start 187 --end 499 # samples 187..499
python benchmark/longmemeval/run_agentic_answer.py --job all --resume # continue an interrupted batch
"""
import argparse
import asyncio
import json
import os
import re
import time
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
DATA = REPO / "datasets" / "longmemeval"
LOGDIR = REPO / "logs" / "agentic_answer"
AGGREGATE = LOGDIR / "aggregate.json"
# Pipeline jobs in execution order.
JOB_ORDER = ["auto_memory", "update_index", "agentic_answer", "llm_judge"]
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument(
"--job",
choices=[*JOB_ORDER, "all"],
default="agentic_answer",
help="which job to run per sample; 'all' runs the full pipeline serially (default: agentic_answer)",
)
p.add_argument("--concurrency", type=int, default=1, help="max samples running at once (default 3)")
p.add_argument("--stagger", type=float, default=1.0, help="seconds between consecutive launches (default 1)")
p.add_argument("--start", type=int, default=0, help="first numeric sample id to process, inclusive (default 0)")
p.add_argument(
"--end",
type=int,
default=0,
help="last numeric sample id to process, inclusive (0 = no upper bound)",
)
p.add_argument("--limit", type=int, default=0, help="only process the first N samples (0 = all)")
p.add_argument(
"--resume",
action="store_true",
help="skip a sample when the job's output already exists (resume an interrupted run); "
"by default every selected job is rerun so the config's clear step rebuilds cleanly",
)
p.add_argument("--dry-run", action="store_true", help="list what would run, launch nothing")
p.add_argument("--no-aggregate", action="store_true", help="skip writing aggregate.json after answer/judge jobs")
return p.parse_args()
def selected_jobs(job: str) -> list[str]:
"""Expand the --job choice into an ordered list of jobs."""
return list(JOB_ORDER) if job == "all" else [job]
def sample_ids() -> list[str]:
"""List all sample IDs (numeric workspace dirs), numerically sorted."""
ids = [p.name for p in DATA.iterdir() if p.is_dir() and p.name.isdigit()]
return sorted(ids, key=int)
def job_done(idx: str, job: str) -> bool:
"""Return True when ``job``'s expected output already exists for sample ``idx``."""
ws = DATA / idx
if job == "auto_memory":
daily = ws / "daily"
return daily.is_dir() and any(daily.rglob("*.md"))
if job == "update_index":
store = ws / "metadata" / "embedding_store"
return store.is_dir() and any(store.iterdir())
if job == "agentic_answer":
return (ws / "mem_answer.json").exists()
if job == "llm_judge":
judge = _load_json(ws / "mem_answer.json").get("llm_judge")
return isinstance(judge, dict) and bool(str(judge.get("judgement") or "").strip())
raise ValueError(f"unknown job: {job}")
async def run_job(idx: str, job: str, counters: dict) -> bool:
"""Run a single job for a single sample. Returns True on success."""
log = LOGDIR / job / f"{idx}.log"
log.parent.mkdir(parents=True, exist_ok=True)
env = dict(os.environ, LME_WORKSPACE_DIR=f"datasets/longmemeval/{idx}")
started = time.strftime("%H:%M:%S")
print(f"[start {started}] {idx}/{job}", flush=True)
with log.open("w", encoding="utf-8") as f:
proc = await asyncio.create_subprocess_exec(
"reme",
"start",
"config=jinli_lme",
f"job={job}",
cwd=str(REPO),
env=env,
stdout=f,
stderr=asyncio.subprocess.STDOUT,
)
rc = await proc.wait()
ok = rc == 0 and job_done(idx, job)
counters["done" if ok else "fail"] += 1
tag = "done" if ok else "fail"
print(f"[{tag}] {idx}/{job} rc={rc} ({counters['done']} done / {counters['fail']} fail)", flush=True)
return ok
async def run_one(idx: str, jobs: list[str], sem: asyncio.Semaphore, resume: bool, counters: dict) -> None:
"""Run the selected jobs for one sample, serially.
By default every selected job is rerun (the job's own clear step wipes stale
output first). With ``resume`` a job is skipped when its output already
exists, so an interrupted batch can continue without redoing finished work.
"""
async with sem:
for job in jobs:
if resume and job_done(idx, job):
counters["skip"] += 1
print(f"[skip] {idx}/{job} (output exists)", flush=True)
continue
ok = await run_job(idx, job, counters)
if not ok:
# Later jobs depend on earlier ones; don't waste a run on a broken workspace.
print(f"[abort] {idx}: {job} failed, skipping remaining jobs", flush=True)
break
# --------------------------------------------------------------------------- #
# Aggregation of agentic_answer results into one big JSON.
# --------------------------------------------------------------------------- #
# Match ``session_id=abc123`` headers and ``"...session_id": "abc123"`` fields in
# tool-result text, so we can list which sessions each search actually surfaced.
_SID_RE = re.compile(r'session_id["\s:=]+"?([A-Za-z0-9_\-]+)')
def _load_json(path: Path) -> dict:
"""Load a JSON object, returning {} on any error."""
try:
with path.open(encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except (OSError, json.JSONDecodeError):
return {}
def parse_tool_calls(idx: str, session_id: str) -> list[dict]:
"""Best-effort: parse the agent trajectory into an ordered tool-call summary.
Reads ``mem_session/agentscope/<session_id>.jsonl`` the trajectory the
agentic_answer run dumped and pairs every ``tool_call`` (name + parsed
args) with the ``session_id`` hits found in its ``tool_result``. Returns an
empty list if the file is missing or unreadable (never raises).
"""
if not session_id:
return []
path = DATA / idx / "mem_session" / "agentscope" / f"{session_id}.jsonl"
if not path.exists():
return []
calls: dict[str, dict] = {}
order: list[str] = []
try:
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except json.JSONDecodeError:
continue
for c in msg.get("content") or []:
if not isinstance(c, dict):
continue
cid = c.get("id")
if c.get("type") == "tool_call" and cid:
try:
args = json.loads(c.get("input") or "{}")
except (json.JSONDecodeError, TypeError):
args = c.get("input")
calls[cid] = {"name": c.get("name"), "args": args, "hit_session_ids": []}
order.append(cid)
elif c.get("type") == "tool_result" and cid in calls:
text = ""
for o in c.get("output") or []:
if isinstance(o, dict) and isinstance(o.get("text"), str):
text += o["text"]
hits = list(dict.fromkeys(_SID_RE.findall(text)))
calls[cid]["hit_session_ids"] = hits
except OSError:
return []
return [{"iter": i + 1, **calls[cid]} for i, cid in enumerate(order)]
def build_record(idx: str) -> dict:
"""Assemble one sample's aggregate record from its on-disk artifacts."""
ws = DATA / idx
query = _load_json(ws / "query.json")
golden = _load_json(ws / "answer.json")
mem = _load_json(ws / "mem_answer.json")
pred = str(mem.get("answer") or "").strip()
session_id = str(mem.get("session_id") or "")
llm_judge = mem.get("llm_judge") if isinstance(mem.get("llm_judge"), dict) else {}
tool_calls = parse_tool_calls(idx, session_id) if mem else []
if not mem:
status = "missing"
elif not pred:
status = "empty"
elif "not provided" in pred.lower():
status = "not_provided"
else:
status = "answered"
return {
"idx": idx,
"question_id": query.get("question_id"),
"question_type": query.get("question_type"),
"question": query.get("question"),
"question_date": query.get("question_date"),
"golden_answer": golden.get("answer"),
"golden_answer_session_ids": golden.get("answer_session_ids"),
"pred_answer": pred,
"session_id": session_id,
"status": status,
"llm_judge": llm_judge.get("judgement"),
"llm_judge_raw": llm_judge.get("raw_judgement"),
"num_tool_calls": len(tool_calls),
"tool_calls": tool_calls,
}
def write_aggregate(ids: list[str]) -> None:
"""Aggregate every sample's agentic_answer artifacts into one big JSON."""
records = [build_record(idx) for idx in ids]
finished = [r for r in records if r["status"] != "missing"]
by_status: dict[str, int] = {}
by_llm_judge: dict[str, int] = {}
for r in records:
by_status[r["status"]] = by_status.get(r["status"], 0) + 1
judgement = r.get("llm_judge") or "missing"
by_llm_judge[judgement] = by_llm_judge.get(judgement, 0) + 1
payload = {
"generated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
"total": len(records),
"finished": len(finished),
"by_status": by_status,
"by_llm_judge": by_llm_judge,
"samples": records,
}
AGGREGATE.parent.mkdir(parents=True, exist_ok=True)
AGGREGATE.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[aggregate] wrote {len(records)} samples ({len(finished)} finished) -> {AGGREGATE}", flush=True)
async def main() -> int:
"""Run the driver."""
args = parse_args()
LOGDIR.mkdir(parents=True, exist_ok=True)
jobs = selected_jobs(args.job)
ids = sample_ids()
if args.end and args.end < args.start:
raise ValueError(f"--end ({args.end}) must be >= --start ({args.start})")
ids = [i for i in ids if int(i) >= args.start and (not args.end or int(i) <= args.end)]
if args.limit:
ids = ids[: args.limit]
# Without --resume every job reruns; with --resume, jobs whose output exists are skipped.
def todo_jobs(i: str) -> list[str]:
return [j for j in jobs if not (args.resume and job_done(i, j))]
pending = [i for i in ids if todo_jobs(i)]
print(
f"jobs={jobs} resume={args.resume} samples total={len(ids)} pending={len(pending)} "
f"concurrency={args.concurrency} stagger={args.stagger}s",
flush=True,
)
if args.dry_run:
for i in pending:
print(f"[would-run] {i}: {todo_jobs(i)}")
return 0
sem = asyncio.Semaphore(args.concurrency)
counters = {"done": 0, "fail": 0, "skip": 0}
tasks: list[asyncio.Task] = []
for n, idx in enumerate(ids):
if n and args.stagger > 0:
await asyncio.sleep(args.stagger) # stagger each launch relative to the previous
tasks.append(asyncio.create_task(run_one(idx, jobs, sem, args.resume, counters)))
await asyncio.gather(*tasks, return_exceptions=True)
print(
f"ALL FINISHED done={counters['done']} fail={counters['fail']} skip={counters['skip']}",
flush=True,
)
if any(j in jobs for j in ("agentic_answer", "llm_judge")) and not args.no_aggregate:
write_aggregate(ids)
return 0 if counters["fail"] == 0 else 1
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))

View file

@ -1,469 +0,0 @@
#!/usr/bin/env python3
"""Review every LongMemEval golden answer with the configured Claude Code job.
Every numeric ``datasets/longmemeval/<idx>`` workspace is processed sequentially.
The reference JSONL files are merged by ``question_id`` and supplied only when
they contain an alternative answer for that sample:
reme start config=jinli_lme job=final_answer_review
The job returns a plain four-field JSON object with ``reason``,
``golden_answer_correct``, ``answer``, and ``is_session_time_wrong``. After
every new success, this driver atomically rewrites the complete accumulated
output JSONL so an interrupted run can safely resume.
Examples:
python benchmark/longmemeval/run_final_answer_review.py
python benchmark/longmemeval/run_final_answer_review.py --exclude-reference-question-ids
python benchmark/longmemeval/run_final_answer_review.py --only-reference-question-ids --rerun-selected
python benchmark/longmemeval/run_final_answer_review.py --concurrency 2 --submit-interval-seconds 6
python benchmark/longmemeval/run_final_answer_review.py --question-id e47becba
python benchmark/longmemeval/run_final_answer_review.py --reference path/to/results.jsonl
python benchmark/longmemeval/run_final_answer_review.py --limit 3
python benchmark/longmemeval/run_final_answer_review.py --no-resume
python benchmark/longmemeval/run_final_answer_review.py --dry-run
"""
import argparse
import concurrent.futures
import json
import os
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any
REPO = Path(__file__).resolve().parents[2]
DATA = REPO / "datasets" / "longmemeval"
DEFAULT_REFERENCES = (
REPO / "benchmark" / "longmemeval" / "golden_check_list_false.jsonl",
REPO / "benchmark" / "longmemeval" / "merge_confirm_jinli_false.jsonl",
)
DEFAULT_OUTPUT = REPO / "benchmark" / "longmemeval" / "final_answer_review.jsonl"
DEFAULT_LOG_DIR = REPO / "logs" / "final_answer_review"
REFERENCE_PATHS_ENV = "LME_FINAL_ANSWER_REFERENCE_PATHS"
MAX_CONCURRENCY = 3
MIN_SUBMIT_INTERVAL_SECONDS = 5.0
DEFAULT_SUBMIT_INTERVAL_SECONDS = 5.1
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--question-id",
dest="question_ids",
action="append",
help="process only this dataset question ID; repeat for multiple IDs (default: all)",
)
reference_selection = parser.add_mutually_exclusive_group()
reference_selection.add_argument(
"--exclude-reference-question-ids",
action="store_true",
help="skip question IDs found in the selected reference-answer JSONL files",
)
reference_selection.add_argument(
"--only-reference-question-ids",
action="store_true",
help="process only question IDs found in the selected reference-answer JSONL files",
)
parser.add_argument(
"--reference",
dest="references",
action="append",
type=Path,
help="reference-answer JSONL; repeat for multiple files (default: built-in disputed results)",
)
parser.add_argument(
"--output",
type=Path,
default=DEFAULT_OUTPUT,
help=f"output JSONL (default: {DEFAULT_OUTPUT})",
)
parser.add_argument(
"--log-dir",
type=Path,
default=DEFAULT_LOG_DIR,
help="directory for per-question logs",
)
parser.add_argument(
"--concurrency",
type=int,
default=MAX_CONCURRENCY,
help=f"maximum concurrent jobs, from 1 to {MAX_CONCURRENCY} (default: {MAX_CONCURRENCY})",
)
parser.add_argument(
"--submit-interval-seconds",
type=float,
default=DEFAULT_SUBMIT_INTERVAL_SECONDS,
help=f"minimum time between job submissions; must be > {MIN_SUBMIT_INTERVAL_SECONDS:g} "
f"(default: {DEFAULT_SUBMIT_INTERVAL_SECONDS:g})",
)
parser.add_argument(
"--limit",
type=int,
default=0,
help="process only the first N pending questions (0 = all)",
)
resume_mode = parser.add_mutually_exclusive_group()
resume_mode.add_argument(
"--no-resume",
action="store_true",
help="ignore existing output and rerun every selected question",
)
resume_mode.add_argument(
"--rerun-selected",
action="store_true",
help="rerun every selected question while preserving existing results until replacements finish",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="show the selected cases without invoking ReMe",
)
return parser.parse_args()
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
"""Read a JSONL file and reject malformed or non-object rows."""
rows: list[dict[str, Any]] = []
try:
with path.open(encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
if not line.strip():
continue
try:
row = json.loads(line)
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid JSON at {path}:{line_number}") from exc
if not isinstance(row, dict):
raise ValueError(f"Expected a JSON object at {path}:{line_number}")
rows.append(row)
except OSError as exc:
raise FileNotFoundError(f"Cannot read JSONL file: {path}") from exc
return rows
def merge_references(paths: list[Path]) -> dict[str, list[dict[str, Any]]]:
"""Merge reference rows by question ID, preserving file and row order."""
merged: dict[str, list[dict[str, Any]]] = {}
seen_sources: set[tuple[str, str]] = set()
for path in paths:
for row in _read_jsonl(path):
question_id = str(row.get("question_id") or "").strip()
if not question_id:
raise ValueError(f"Reference row in {path} has no question_id")
source_key = (question_id, str(path.resolve()))
if source_key in seen_sources:
raise ValueError(f"Duplicate question_id={question_id!r} within {path}")
seen_sources.add(source_key)
merged.setdefault(question_id, []).append({"source": path.name, **row})
if not merged:
raise ValueError("No reference answers found")
return merged
def workspace_map() -> dict[str, Path]:
"""Map every dataset question ID to its numeric sample workspace."""
mapping: dict[str, Path] = {}
for workspace in sorted(
(path for path in DATA.iterdir() if path.is_dir() and path.name.isdigit()),
key=lambda p: int(p.name),
):
query_path = workspace / "query.json"
if not query_path.is_file():
continue
try:
with query_path.open(encoding="utf-8") as file:
query = json.load(file)
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(f"Cannot parse {query_path}") from exc
if not isinstance(query, dict):
raise ValueError(f"Expected a JSON object in {query_path}")
question_id = str(query.get("question_id") or "").strip()
if not question_id:
raise ValueError(f"Missing question_id in {query_path}")
if question_id in mapping:
raise ValueError(
f"Duplicate dataset question_id={question_id!r}: {mapping[question_id]} and {workspace}",
)
mapping[question_id] = workspace
return mapping
def select_question_ids(
mapping: dict[str, Path],
requested: list[str] | None,
excluded: set[str] | None = None,
) -> list[str]:
"""Return all dataset IDs or validate an explicitly requested subset."""
excluded = excluded or set()
if not requested:
return [question_id for question_id in mapping if question_id not in excluded]
selected: list[str] = []
seen: set[str] = set()
for raw_question_id in requested:
question_id = raw_question_id.strip()
if not question_id:
raise ValueError("--question-id must not be empty")
if question_id in seen:
raise ValueError(f"Duplicate --question-id: {question_id}")
if question_id not in mapping:
raise ValueError(f"No dataset workspace for question ID: {question_id}")
if question_id not in excluded:
selected.append(question_id)
seen.add(question_id)
return selected
def _validate_result(value: Any, *, source: str) -> dict[str, Any]:
"""Validate the final four-field answer contract."""
expected_keys = {"reason", "golden_answer_correct", "answer", "is_session_time_wrong"}
if not isinstance(value, dict) or set(value) != expected_keys:
raise ValueError(
f"{source} must contain exactly 'reason', 'golden_answer_correct', 'answer', "
"and 'is_session_time_wrong'",
)
if not isinstance(value["reason"], str) or not value["reason"].strip():
raise ValueError(f"{source} has an invalid reason")
if not isinstance(value["golden_answer_correct"], bool):
raise ValueError(f"{source} has an invalid golden_answer_correct")
if not isinstance(value["answer"], str):
raise ValueError(f"{source} has an invalid answer")
answer = value["answer"].strip()
if value["golden_answer_correct"] and answer:
raise ValueError(f"{source} answer must be empty when golden_answer_correct is true")
if not value["golden_answer_correct"] and not answer:
raise ValueError(f"{source} answer must be non-empty when golden_answer_correct is false")
if not isinstance(value["is_session_time_wrong"], bool):
raise ValueError(f"{source} has an invalid is_session_time_wrong")
return {
"reason": value["reason"].strip(),
"golden_answer_correct": value["golden_answer_correct"],
"answer": answer,
"is_session_time_wrong": False,
}
def load_existing(path: Path) -> dict[str, dict[str, Any]]:
"""Load resumable output, rejecting duplicate or malformed rows."""
if not path.exists():
return {}
results: dict[str, dict[str, Any]] = {}
for row in _read_jsonl(path):
question_id = str(row.get("question_id") or "").strip()
if not question_id:
raise ValueError(f"Existing output row in {path} has no question_id")
if question_id in results:
raise ValueError(
f"Duplicate question_id={question_id!r} in existing output {path}",
)
results[question_id] = _validate_result(
{key: value for key, value in row.items() if key != "question_id"},
source=f"existing result for {question_id}",
)
return results
def atomic_write_results(
path: Path,
order: list[str],
results: dict[str, dict[str, Any]],
) -> None:
"""Atomically rewrite all accumulated rows in stable merged-input order."""
path.parent.mkdir(parents=True, exist_ok=True)
temp_path: Path | None = None
try:
with tempfile.NamedTemporaryFile(
"w",
encoding="utf-8",
dir=path.parent,
prefix=f".{path.name}.",
delete=False,
) as file:
temp_path = Path(file.name)
for question_id in order:
if question_id not in results:
continue
row = {"question_id": question_id, **results[question_id]}
file.write(
json.dumps(row, ensure_ascii=False, separators=(",", ":")) + "\n",
)
file.flush()
os.fsync(file.fileno())
os.replace(temp_path, path)
finally:
if temp_path is not None and temp_path.exists():
temp_path.unlink()
def run_one(
question_id: str,
workspace: Path,
log_dir: Path,
reference_paths: list[Path],
) -> dict[str, Any]:
"""Run the configured one-shot job and validate its stdout JSON."""
env = dict(os.environ, LME_WORKSPACE_DIR=str(workspace.relative_to(REPO)))
env[REFERENCE_PATHS_ENV] = json.dumps(
[str(path.resolve()) for path in reference_paths],
ensure_ascii=False,
)
completed = subprocess.run(
[
sys.executable,
"-c",
"from reme.reme import main; main()",
"start",
"config=jinli_lme",
"job=final_answer_review",
],
cwd=REPO,
env=env,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
log_dir.mkdir(parents=True, exist_ok=True)
log_path = log_dir / f"{question_id}.log"
log_text = (
f"workspace={workspace}\nreturncode={completed.returncode}\n\n"
f"[stdout]\n{completed.stdout}\n[stderr]\n{completed.stderr}"
)
log_path.write_text(
log_text,
encoding="utf-8",
)
if completed.returncode != 0:
raise RuntimeError(
f"Job failed for {question_id} with rc={completed.returncode}; see {log_path}",
)
try:
value = json.loads(completed.stdout.strip())
except json.JSONDecodeError as exc:
raise ValueError(
f"Job stdout is not JSON for {question_id}; see {log_path}",
) from exc
return _validate_result(value, source=f"job result for {question_id}")
def main() -> int:
"""Review and checkpoint the selected dataset cases sequentially."""
args = parse_args()
if args.limit < 0:
raise ValueError("--limit must be >= 0")
if not 1 <= args.concurrency <= MAX_CONCURRENCY:
raise ValueError(f"--concurrency must be between 1 and {MAX_CONCURRENCY}")
if args.submit_interval_seconds <= MIN_SUBMIT_INTERVAL_SECONDS:
raise ValueError(
f"--submit-interval-seconds must be > {MIN_SUBMIT_INTERVAL_SECONDS:g}",
)
reference_paths = [path.resolve() for path in (args.references or DEFAULT_REFERENCES)]
mapping = workspace_map()
references = merge_references(reference_paths)
missing = [question_id for question_id in references if question_id not in mapping]
if missing:
raise ValueError(f"No dataset workspace for question IDs: {', '.join(missing)}")
full_order = list(mapping)
excluded = set(references) if args.exclude_reference_question_ids else set()
order = select_question_ids(mapping, args.question_ids, excluded)
if args.only_reference_question_ids:
order = [question_id for question_id in order if question_id in references]
results = {} if args.no_resume else load_existing(args.output.resolve())
pending = (
list(order) if args.rerun_selected else [question_id for question_id in order if question_id not in results]
)
if args.limit:
pending = pending[: args.limit]
no_reference = sum(question_id not in references for question_id in order)
one_reference = sum(len(references.get(question_id, [])) == 1 for question_id in order)
multiple_references = sum(len(references.get(question_id, [])) > 1 for question_id in order)
print(
f"total={len(order)} no_reference={no_reference} one_reference={one_reference} "
f"multiple_references={multiple_references} "
f"excluded={len(excluded)} "
f"only_reference_questions={args.only_reference_question_ids} "
f"concurrency={args.concurrency} submit_interval={args.submit_interval_seconds:g}s "
f"existing={len(results)} pending={len(pending)} output={args.output.resolve()}",
flush=True,
)
if args.dry_run:
for question_id in pending:
print(
f"[would-run] question_id={question_id} workspace={mapping[question_id].name} "
f"references={len(references.get(question_id, []))}",
)
return 0
executor = concurrent.futures.ThreadPoolExecutor(max_workers=args.concurrency)
active: dict[concurrent.futures.Future[dict[str, Any]], tuple[int, str]] = {}
next_position = 0
saved_count = 0
next_submit_at = 0.0
try:
while next_position < len(pending) or active:
can_submit = next_position < len(pending) and len(active) < args.concurrency
if can_submit and time.monotonic() >= next_submit_at:
question_id = pending[next_position]
position = next_position + 1
workspace = mapping[question_id]
print(
f"[submit {position}/{len(pending)}] question_id={question_id} "
f"workspace={workspace.name} references={len(references.get(question_id, []))}",
flush=True,
)
future = executor.submit(
run_one,
question_id,
workspace,
args.log_dir.resolve(),
reference_paths,
)
active[future] = (position, question_id)
next_position += 1
next_submit_at = time.monotonic() + args.submit_interval_seconds
continue
if not active:
time.sleep(max(0.0, next_submit_at - time.monotonic()))
continue
timeout = None
if can_submit:
timeout = max(0.0, next_submit_at - time.monotonic())
done, _ = concurrent.futures.wait(
active,
timeout=timeout,
return_when=concurrent.futures.FIRST_COMPLETED,
)
for future in done:
position, question_id = active.pop(future)
results[question_id] = future.result()
atomic_write_results(args.output.resolve(), full_order, results)
saved_count += 1
print(
f"[saved {saved_count}/{len(pending)}] submitted_position={position} " f"question_id={question_id}",
flush=True,
)
finally:
executor.shutdown(wait=True, cancel_futures=True)
print(
f"ALL FINISHED total_saved={sum(question_id in results for question_id in order)}",
flush=True,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,216 +0,0 @@
#!/usr/bin/env python3
"""Run LongMemEval ``golden_check`` concurrently across samples.
For every workspace under ``datasets/longmemeval/<idx>`` in the selected numeric
range, this launches:
reme start config=jinli_lme job=golden_check
with ``LME_WORKSPACE_DIR`` pointed at that sample. Multiple samples can run at
once, capped by ``--concurrency``. The ``golden_check`` job itself waits for
``session_review.json`` when configured with ``wait_for_paths_step`` in
``jinli_lme.yaml``. Each sample's stdout/stderr goes to
``logs/golden_check/<idx>.log``.
By default the script processes samples 0..499 inclusive and reruns every sample
in that range. Pass ``--resume`` to skip samples whose ``check_golden.json``
already exists.
Examples:
python benchmark/longmemeval/run_golden_check.py
python benchmark/longmemeval/run_golden_check.py --start 187 --end 499
python benchmark/longmemeval/run_golden_check.py --concurrency 8 --stagger 1
python benchmark/longmemeval/run_golden_check.py --progress-interval 10
python benchmark/longmemeval/run_golden_check.py --resume
python benchmark/longmemeval/run_golden_check.py --limit 5 --dry-run
"""
import argparse
import asyncio
import json
import os
import time
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
DATA = REPO / "datasets" / "longmemeval"
LOGDIR = REPO / "logs" / "golden_check"
OUTPUT_FILENAME = "check_golden.json"
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--start", type=int, default=0, help="first numeric sample id to process, inclusive (default 0)")
p.add_argument("--end", type=int, default=499, help="last numeric sample id to process, inclusive (default 499)")
p.add_argument("--limit", type=int, default=0, help="only process the first N selected samples (0 = all)")
p.add_argument("--concurrency", type=int, default=3, help="max samples running at once (default 3)")
p.add_argument("--stagger", type=float, default=1.0, help="seconds between consecutive launches (default 1)")
p.add_argument(
"--progress-interval",
type=float,
default=30.0,
help="seconds between progress reports while running (0 = disabled, default 30)",
)
p.add_argument(
"--resume",
action="store_true",
help=f"skip samples whose {OUTPUT_FILENAME} already exists",
)
p.add_argument("--dry-run", action="store_true", help="list what would run, launch nothing")
return p.parse_args()
def sample_ids() -> list[str]:
"""List all sample IDs (numeric workspace dirs), numerically sorted."""
ids = [p.name for p in DATA.iterdir() if p.is_dir() and p.name.isdigit()]
return sorted(ids, key=int)
def output_is_current(idx: str) -> bool:
"""Return True when the sample already has a current-schema golden-check artifact."""
path = DATA / idx / OUTPUT_FILENAME
try:
with path.open(encoding="utf-8") as f:
data = json.load(f)
except (OSError, json.JSONDecodeError):
return False
verdict = data.get("verdict") if isinstance(data, dict) else None
if not isinstance(verdict, dict):
return False
return isinstance(verdict.get("golden_answer_correct"), bool) and isinstance(
verdict.get("answer_session_ids_correct"),
bool,
)
def print_progress(counters: dict, active: set[str], selected_total: int, started_at: float) -> None:
"""Print a one-line progress snapshot."""
finished = counters["done"] + counters["fail"] + counters["skip"]
running = len(active)
outstanding = max(selected_total - finished - running, 0)
elapsed = time.monotonic() - started_at
print(
f"[progress] selected={selected_total} done={counters['done']} fail={counters['fail']} "
f"skip={counters['skip']} running={running} outstanding={outstanding} "
f"elapsed={elapsed:.0f}s",
flush=True,
)
async def progress_reporter(
counters: dict,
active: set[str],
selected_total: int,
started_at: float,
interval: float,
stop: asyncio.Event,
) -> None:
"""Periodically report progress until ``stop`` is set."""
if interval <= 0:
return
while not stop.is_set():
try:
await asyncio.wait_for(stop.wait(), timeout=interval)
except asyncio.TimeoutError:
print_progress(counters, active, selected_total, started_at)
async def run_one(idx: str, sem: asyncio.Semaphore, resume: bool, counters: dict, active: set[str]) -> None:
"""Run ``golden_check`` for one sample."""
if resume and output_is_current(idx):
counters["skip"] += 1
print(f"[skip] {idx} ({OUTPUT_FILENAME} exists)", flush=True)
return
async with sem:
active.add(idx)
log = LOGDIR / f"{idx}.log"
log.parent.mkdir(parents=True, exist_ok=True)
env = dict(os.environ, LME_WORKSPACE_DIR=f"datasets/longmemeval/{idx}")
started = time.strftime("%H:%M:%S")
print(f"[start {started}] {idx}", flush=True)
try:
with log.open("w", encoding="utf-8") as f:
proc = await asyncio.create_subprocess_exec(
"reme",
"start",
"config=jinli_lme",
"job=golden_check",
cwd=str(REPO),
env=env,
stdout=f,
stderr=asyncio.subprocess.STDOUT,
)
rc = await proc.wait()
ok = rc == 0 and output_is_current(idx)
counters["done" if ok else "fail"] += 1
tag = "done" if ok else "fail"
print(
f"[{tag}] {idx} rc={rc} log={log} ({counters['done']} done / {counters['fail']} fail)",
flush=True,
)
finally:
active.discard(idx)
async def main() -> int:
"""Run the concurrent driver."""
args = parse_args()
if args.end < args.start:
raise ValueError(f"--end ({args.end}) must be >= --start ({args.start})")
if args.concurrency < 1:
raise ValueError("--concurrency must be >= 1")
if args.progress_interval < 0:
raise ValueError("--progress-interval must be >= 0")
LOGDIR.mkdir(parents=True, exist_ok=True)
ids = [i for i in sample_ids() if args.start <= int(i) <= args.end]
if args.limit:
ids = ids[: args.limit]
pending = [i for i in ids if not (args.resume and output_is_current(i))]
print(
f"job=golden_check samples total={len(ids)} pending={len(pending)} "
f"range={args.start}..{args.end} resume={args.resume} "
f"concurrency={args.concurrency} stagger={args.stagger}s",
flush=True,
)
if args.dry_run:
for idx in pending:
print(f"[would-run] {idx}")
return 0
sem = asyncio.Semaphore(args.concurrency)
counters = {"done": 0, "fail": 0, "skip": 0}
active: set[str] = set()
started_at = time.monotonic()
stop_progress = asyncio.Event()
progress_task = asyncio.create_task(
progress_reporter(counters, active, len(ids), started_at, args.progress_interval, stop_progress),
)
tasks: list[asyncio.Task] = []
try:
for n, idx in enumerate(ids):
if n and args.stagger > 0:
await asyncio.sleep(args.stagger)
tasks.append(asyncio.create_task(run_one(idx, sem, args.resume, counters, active)))
await asyncio.gather(*tasks)
finally:
stop_progress.set()
await progress_task
print_progress(counters, active, len(ids), started_at)
print(
f"ALL FINISHED done={counters['done']} fail={counters['fail']} skip={counters['skip']}",
flush=True,
)
return 0 if counters["fail"] == 0 else 1
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))

View file

@ -1,203 +0,0 @@
#!/usr/bin/env python3
"""Run LongMemEval ``session_review`` concurrently across samples.
For every workspace under ``datasets/longmemeval/<idx>`` in the selected numeric
range, this launches:
reme start config=jinli_lme job=session_review
with ``LME_WORKSPACE_DIR`` pointed at that sample. Multiple samples can run at
once, capped by ``--concurrency``. By default this runner launches one sample at
a time; request submission is throttled inside each ``session_review`` process.
Each sample's stdout/stderr goes to ``logs/session_review/<idx>.log``.
By default the script processes samples 0..499 inclusive and reruns every sample
in that range. Pass ``--resume`` to skip samples whose ``session_review.json``
already exists.
Examples:
python benchmark/longmemeval/run_session_review.py
python benchmark/longmemeval/run_session_review.py --start 187 --end 499
python benchmark/longmemeval/run_session_review.py --concurrency 2
python benchmark/longmemeval/run_session_review.py --resume
python benchmark/longmemeval/run_session_review.py --limit 5 --dry-run
"""
import argparse
import asyncio
import json
import os
import time
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
DATA = REPO / "datasets" / "longmemeval"
LOGDIR = REPO / "logs" / "session_review"
OUTPUT_FILENAME = "session_review.json"
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--start", type=int, default=0, help="first numeric sample id to process, inclusive (default 0)")
p.add_argument("--end", type=int, default=499, help="last numeric sample id to process, inclusive (default 499)")
p.add_argument("--limit", type=int, default=0, help="only process the first N selected samples (0 = all)")
p.add_argument("--concurrency", type=int, default=1, help="max samples running at once (default 1)")
p.add_argument("--stagger", type=float, default=1.0, help="seconds between worker launches (default 1)")
p.add_argument(
"--resume",
action="store_true",
help=f"skip samples whose {OUTPUT_FILENAME} already exists",
)
p.add_argument("--dry-run", action="store_true", help="list what would run, launch nothing")
p.add_argument("--stop-on-fail", action="store_true", help="stop immediately after the first failed sample")
return p.parse_args()
def sample_ids() -> list[str]:
"""List all sample IDs (numeric workspace dirs), numerically sorted."""
ids = [p.name for p in DATA.iterdir() if p.is_dir() and p.name.isdigit()]
return sorted(ids, key=int)
def output_exists(idx: str) -> bool:
"""Return True when the sample already has a session review artifact."""
return (DATA / idx / OUTPUT_FILENAME).exists()
def output_is_healthy(idx: str) -> bool:
"""Return True when ``session_review.json`` exists and has no failed reviews."""
path = DATA / idx / OUTPUT_FILENAME
if not path.exists():
return False
try:
with path.open(encoding="utf-8") as f:
data = json.load(f)
except (OSError, json.JSONDecodeError):
return False
review = data.get("review") if isinstance(data, dict) else None
if not isinstance(review, dict):
return False
raw = review.get("num_failed_reviews")
if isinstance(raw, int):
return raw == 0
failed_reviews = review.get("failed_reviews")
return not failed_reviews
async def run_one(idx: str, active: set[str]) -> bool:
"""Run ``session_review`` for one sample. Returns True on success."""
log = LOGDIR / f"{idx}.log"
log.parent.mkdir(parents=True, exist_ok=True)
env = dict(os.environ, LME_WORKSPACE_DIR=f"datasets/longmemeval/{idx}")
started = time.strftime("%H:%M:%S")
print(f"[start {started}] {idx}", flush=True)
active.add(idx)
try:
with log.open("w", encoding="utf-8") as f:
proc = await asyncio.create_subprocess_exec(
"reme",
"start",
"config=jinli_lme",
"job=session_review",
cwd=str(REPO),
env=env,
stdout=f,
stderr=asyncio.subprocess.STDOUT,
)
rc = await proc.wait()
finally:
active.discard(idx)
ok = rc == 0 and output_exists(idx)
tag = "done" if ok else "fail"
print(f"[{tag}] {idx} rc={rc} log={log}", flush=True)
return ok
async def worker(
name: int,
queue: asyncio.Queue[str],
args: argparse.Namespace,
counters: dict[str, int],
active: set[str],
stop: asyncio.Event,
) -> None:
"""Run samples from ``queue`` until exhausted or fail-fast is triggered."""
if name and args.stagger > 0:
await asyncio.sleep(args.stagger * name)
while not stop.is_set():
try:
idx = queue.get_nowait()
except asyncio.QueueEmpty:
return
try:
if args.resume and output_is_healthy(idx):
counters["skip"] += 1
print(f"[skip] {idx} (healthy {OUTPUT_FILENAME} exists)", flush=True)
continue
if await run_one(idx, active):
counters["done"] += 1
else:
counters["fail"] += 1
if args.stop_on_fail:
stop.set()
finally:
queue.task_done()
async def main() -> int:
"""Run the concurrent driver."""
args = parse_args()
if args.end < args.start:
raise ValueError(f"--end ({args.end}) must be >= --start ({args.start})")
if args.concurrency < 1:
raise ValueError("--concurrency must be >= 1")
if args.stagger < 0:
raise ValueError("--stagger must be >= 0")
LOGDIR.mkdir(parents=True, exist_ok=True)
ids = [i for i in sample_ids() if args.start <= int(i) <= args.end]
if args.limit:
ids = ids[: args.limit]
pending = [i for i in ids if not (args.resume and output_exists(i))]
print(
f"job=session_review samples total={len(ids)} pending={len(pending)} "
f"range={args.start}..{args.end} resume={args.resume} "
f"concurrency={args.concurrency} stagger={args.stagger}s",
flush=True,
)
if args.dry_run:
for idx in pending:
print(f"[would-run] {idx}")
return 0
counters: dict[str, int] = {"done": 0, "fail": 0, "skip": 0}
active: set[str] = set()
stop = asyncio.Event()
queue: asyncio.Queue[str] = asyncio.Queue()
for idx in ids:
queue.put_nowait(idx)
workers = [
asyncio.create_task(worker(n, queue, args, counters, active, stop))
for n in range(min(args.concurrency, len(ids)))
]
await asyncio.gather(*workers)
print(
f"ALL FINISHED done={counters['done']} fail={counters['fail']} skip={counters['skip']}",
flush=True,
)
return 0 if counters["fail"] == 0 else 1
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))

View file

@ -1,202 +0,0 @@
#!/usr/bin/env python3
"""Summarise the ``agentic_answer`` results across all LongMemEval samples.
Reports progress (how many of the 500 samples produced ``mem_answer.json``) and a
breakdown of answer *status*:
- answered a non-empty answer that is not "not provided";
- not_provided the agent gave up ("not provided");
- empty ``mem_answer.json`` exists but the answer is blank;
- missing no ``mem_answer.json`` yet.
Everything is broken down by ``question_type``. This script does NOT judge answer
correctness (there is no grader for ``mem_answer`` yet) it only tracks progress
and collects predicted-vs-golden pairs. Tool-call statistics are read from the
aggregate written by ``run_agentic_answer.py`` when it is present.
Examples:
python benchmark/longmemeval/stats_agentic_answer.py
python benchmark/longmemeval/stats_agentic_answer.py --list-run-failed
python benchmark/longmemeval/stats_agentic_answer.py --list-unanswered
python benchmark/longmemeval/stats_agentic_answer.py --json
"""
import argparse
import json
from collections import defaultdict
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
DATA = REPO / "datasets" / "longmemeval"
LOGBASE = REPO / "logs" / "agentic_answer"
AGGREGATE = LOGBASE / "aggregate.json"
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--list-unanswered", action="store_true", help="list samples answered 'not provided' or empty")
p.add_argument("--list-run-failed", action="store_true", help="list launched samples with no readable output")
p.add_argument("--json", action="store_true", help="emit the summary as JSON")
return p.parse_args()
def sample_ids() -> list[str]:
"""List all sample IDs (numeric workspace dirs), numerically sorted."""
ids = [p.name for p in DATA.iterdir() if p.is_dir() and p.name.isdigit()]
return sorted(ids, key=int)
def pct(num: int, den: int) -> str:
"""Format a percentage."""
return f"{(100.0 * num / den):.1f}%" if den else "n/a"
def logged_sample_ids() -> list[str]:
"""List sample IDs that have an agentic_answer launch log."""
logdir = LOGBASE / "agentic_answer"
if not logdir.exists():
return []
ids = [p.stem for p in logdir.glob("*.log") if p.stem.isdigit()]
return sorted(ids, key=int)
def answer_status(pred: str, has_file: bool) -> str:
"""Classify an answer into answered / not_provided / empty / missing."""
if not has_file:
return "missing"
if not pred:
return "empty"
if "not provided" in pred.lower():
return "not_provided"
return "answered"
def load_tool_calls() -> dict[str, int]:
"""Map idx -> num_tool_calls from the aggregate, if it exists."""
if not AGGREGATE.exists():
return {}
try:
with AGGREGATE.open(encoding="utf-8") as f:
data = json.load(f)
except (OSError, json.JSONDecodeError):
return {}
return {s["idx"]: s.get("num_tool_calls", 0) for s in data.get("samples", []) if "idx" in s}
def main() -> int:
"""Main entry point."""
args = parse_args()
ids = sample_ids()
total = len(ids)
tool_calls = load_tool_calls()
rows, unreadable = [], []
finished_ids = set()
for idx in ids:
query_path = DATA / idx / "query.json"
mem_path = DATA / idx / "mem_answer.json"
qtype = "(unknown)"
try:
with query_path.open(encoding="utf-8") as f:
qtype = json.load(f).get("question_type") or "(unknown)"
except (OSError, json.JSONDecodeError):
pass
has_file = mem_path.exists()
pred = ""
if has_file:
try:
with mem_path.open(encoding="utf-8") as f:
pred = str(json.load(f).get("answer") or "").strip()
finished_ids.add(idx)
except (OSError, json.JSONDecodeError):
unreadable.append(idx)
has_file = False
rows.append({"idx": idx, "type": qtype, "status": answer_status(pred, has_file)})
finished = [r for r in rows if r["status"] != "missing"]
n = len(finished)
launched = logged_sample_ids()
run_failed = [idx for idx in launched if idx not in finished_ids]
# Overall status tallies.
status_counts: dict[str, int] = defaultdict(int)
for r in rows:
status_counts[r["status"]] += 1
answered = status_counts["answered"]
unanswered = [r["idx"] for r in rows if r["status"] in ("not_provided", "empty")]
calls_vals = [tool_calls[i] for i in finished_ids if i in tool_calls]
avg_calls = sum(calls_vals) / len(calls_vals) if calls_vals else 0.0
# Per question_type breakdown.
by_type: dict[str, dict[str, int]] = defaultdict(lambda: {"n": 0, "answered": 0})
for r in finished:
by_type[r["type"]]["n"] += 1
by_type[r["type"]]["answered"] += 1 if r["status"] == "answered" else 0
if args.json:
print(
json.dumps(
{
"total": total,
"finished": n,
"pending": total - n - len(unreadable),
"unreadable": unreadable,
"launched": len(launched),
"run_failed": run_failed,
"status_counts": dict(status_counts),
"answered_rate": round(answered / n, 4) if n else None,
"avg_tool_calls": round(avg_calls, 2) if calls_vals else None,
"by_type": {
t: {**c, "answered_rate": round(c["answered"] / c["n"], 4)} for t, c in by_type.items()
},
"unanswered": unanswered,
"aggregate": str(AGGREGATE) if AGGREGATE.exists() else None,
},
ensure_ascii=False,
indent=2,
),
)
return 0
print("=" * 60)
print("LongMemEval agentic_answer 统计")
print("=" * 60)
print(f"样例总数 : {total}")
print(f"已完成 (有产出) : {n} ({pct(n, total)})")
print(f"未完成 : {total - n - len(unreadable)}")
if unreadable:
print(f"损坏/无法解析 : {len(unreadable)} {unreadable}")
print(f"已启动过 (有 log) : {len(launched)}")
print(f"运行失败/无可读产出 : {len(run_failed)}")
print("-" * 60)
print(f"已作答 (非 not provided): {answered} ({pct(answered, n)} of finished)")
print(f" 其中 not provided : {status_counts['not_provided']}")
print(f" 其中 空答案 : {status_counts['empty']}")
if calls_vals:
print(f"平均工具调用次数 : {avg_calls:.1f} (来自 {AGGREGATE.name})")
else:
print("平均工具调用次数 : n/a (先跑 run_agentic_answer.py 生成 aggregate.json)")
print("-" * 60)
print("按 question_type:")
print(f" {'type':<24} {'n':>4} {'已作答率':>12}")
for t in sorted(by_type):
c = by_type[t]
print(f" {t:<24} {c['n']:>4} {pct(c['answered'], c['n']):>12}")
if args.list_unanswered:
print("-" * 60)
print(f"not provided / 空答案的样例 ({len(unanswered)}): {unanswered}")
if args.list_run_failed:
print("-" * 60)
print(f"运行失败/无可读 mem_answer.json 的样例 ({len(run_failed)}): {run_failed}")
for idx in run_failed:
print(f" {idx}: {LOGBASE / 'agentic_answer' / f'{idx}.log'}")
print("=" * 60)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,344 +0,0 @@
#!/usr/bin/env python3
"""Summarise the ``check_golden.json`` verdicts across all LongMemEval samples.
Reports progress (how many of the 500 samples have finished) and accuracy:
- golden answer accuracy = share of finished samples whose golden answer the
auditor judged correct (``verdict.golden_answer_correct``);
- answer_session_ids accuracy = share whose claimed answer sessions the auditor
judged exactly correct (``verdict.answer_session_ids_correct``).
Everything is also broken down by ``question_type``. Use ``--list-bad`` to print
the samples whose golden answer was judged NOT correct.
Examples:
python benchmark/longmemeval/stats_golden_check.py
python benchmark/longmemeval/stats_golden_check.py --list-bad
python benchmark/longmemeval/stats_golden_check.py --list-run-failed
python benchmark/longmemeval/stats_golden_check.py --json
"""
import argparse
import json
from collections import defaultdict
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
DATA = REPO / "datasets" / "longmemeval"
LOGDIR = REPO / "logs" / "golden_check"
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--list-bad", action="store_true", help="list samples whose golden answer is NOT correct")
p.add_argument(
"--list-bad-sessions",
action="store_true",
help="list samples whose answer_session_ids is NOT correct",
)
p.add_argument(
"--list-run-failed",
action="store_true",
help="list launched samples that did not produce readable output",
)
p.add_argument("--json", action="store_true", help="emit the summary as JSON")
return p.parse_args()
def sample_ids() -> list[str]:
"""List all sample IDs."""
ids = [p.name for p in DATA.iterdir() if p.is_dir() and p.name.isdigit()]
return sorted(ids, key=int)
def pct(num: int, den: int) -> str:
"""Format a percentage."""
return f"{(100.0 * num / den):.1f}%" if den else "n/a"
def logged_sample_ids() -> list[str]:
"""List all sample IDs that have been launched but not finished."""
if not LOGDIR.exists():
return []
ids = [p.stem for p in LOGDIR.glob("*.log") if p.stem.isdigit()]
return sorted(ids, key=int)
def load_json(path: Path) -> dict:
"""Load a JSON object, returning {} on any error."""
try:
with path.open(encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except (OSError, json.JSONDecodeError):
return {}
def question_type_for(idx: str, data: dict) -> str:
"""Return question_type from the output, session review, or query.json."""
question_type = str(data.get("question_type") or "").strip()
if question_type:
return question_type
review_path_raw = str(data.get("session_review_path") or "").strip()
review_path = Path(review_path_raw) if review_path_raw else DATA / idx / "session_review.json"
if not review_path.is_absolute():
review_path = REPO / review_path
review = load_json(review_path)
review_question_type = str((review.get("query") or {}).get("question_type") or "").strip()
if review_question_type:
return review_question_type
query = load_json(DATA / idx / "query.json")
return str(query.get("question_type") or "(unknown)").strip() or "(unknown)"
def question_id_for(idx: str, data: dict) -> str:
"""Return question_id from the output, session review, or query.json."""
question_id = str(data.get("question_id") or "").strip()
if question_id:
return question_id
review_path_raw = str(data.get("session_review_path") or "").strip()
review_path = Path(review_path_raw) if review_path_raw else DATA / idx / "session_review.json"
if not review_path.is_absolute():
review_path = REPO / review_path
review = load_json(review_path)
review_question_id = str((review.get("query") or {}).get("question_id") or "").strip()
if review_question_id:
return review_question_id
query = load_json(DATA / idx / "query.json")
return str(query.get("question_id") or "").strip()
def sample_label(data: dict) -> str:
"""Format sample id as idx(question_id) when question_id is available."""
idx = str(data.get("_idx") or "")
qid = str(data.get("_question_id") or "").strip()
return f"{idx}({qid})" if qid else idx
def related_session_ids(data: dict) -> list[str]:
"""Return the best available session ids for a bad verdict record."""
verdict = data.get("verdict") if isinstance(data, dict) else None
if isinstance(verdict, dict):
true_ids = verdict.get("true_answer_session_ids")
if isinstance(true_ids, list):
ids = [str(session_id) for session_id in true_ids if str(session_id).strip()]
if ids:
return ids
summaries = data.get("session_summaries")
if isinstance(summaries, list):
return [
str(summary.get("session_id"))
for summary in summaries
if isinstance(summary, dict) and str(summary.get("session_id") or "").strip()
]
return []
def grouped_records(records: list[dict]) -> dict[str, list[dict]]:
"""Group records by question_type for human-readable list output."""
grouped: dict[str, list[dict]] = defaultdict(list)
for data in records:
question_type = str(data.get("_question_type") or "(unknown)")
grouped[question_type].append(
{
"index": str(data.get("_idx") or ""),
"question_id": str(data.get("_question_id") or ""),
"session_id": related_session_ids(data),
},
)
return dict(sorted(grouped.items()))
def verdict_bool(verdict: dict, new_key: str, old_key: str) -> bool:
"""Read a verdict boolean, accepting the old field name for compatibility."""
if verdict.get(new_key) is True:
return True
if verdict.get(new_key) is False:
return False
return verdict.get(old_key) is True
def has_current_verdict(data: dict) -> bool:
"""Return True when ``check_golden.json`` uses the current golden_check schema."""
verdict = data.get("verdict") if isinstance(data, dict) else None
if not isinstance(verdict, dict):
return False
return isinstance(verdict.get("golden_answer_correct"), bool) and isinstance(
verdict.get("answer_session_ids_correct"),
bool,
)
def write_golden_check_list(done: list[dict], output_path: Path) -> None:
"""Write all readable check_golden records as JSONL."""
with output_path.open("w", encoding="utf-8") as f:
for data in done:
f.write(json.dumps(data, ensure_ascii=False))
f.write("\n")
def main() -> int:
"""Main entry point."""
args = parse_args()
ids = sample_ids()
total = len(ids)
done, unreadable, stale = [], [], []
finished_ids = set()
for idx in ids:
path = DATA / idx / "check_golden.json"
if not path.exists():
continue
try:
with path.open(encoding="utf-8") as f:
data = json.load(f)
if not has_current_verdict(data):
stale.append(idx)
continue
data["_idx"] = idx
data["_question_type"] = question_type_for(idx, data)
data["_question_id"] = question_id_for(idx, data)
done.append(data)
finished_ids.add(idx)
except (OSError, json.JSONDecodeError):
unreadable.append(idx)
n = len(done)
output_path = Path.cwd() / "golden_check_list.jsonl"
write_golden_check_list(done, output_path)
launched = logged_sample_ids()
run_failed = [idx for idx in launched if idx not in finished_ids]
# Overall tallies.
golden_ok = sum(
1 for d in done if verdict_bool(d.get("verdict", {}), "golden_answer_correct", "golden_answer_reasonable")
)
sess_ok = sum(
1
for d in done
if verdict_bool(d.get("verdict", {}), "answer_session_ids_correct", "answer_session_ids_reasonable")
)
both_ok = sum(
1
for d in done
if verdict_bool(d.get("verdict", {}), "golden_answer_correct", "golden_answer_reasonable")
and verdict_bool(d.get("verdict", {}), "answer_session_ids_correct", "answer_session_ids_reasonable")
)
# Per question_type breakdown.
by_type: dict[str, dict[str, int]] = defaultdict(lambda: {"n": 0, "golden_ok": 0, "sess_ok": 0, "both_ok": 0})
for d in done:
v = d.get("verdict", {})
golden_is_ok = verdict_bool(v, "golden_answer_correct", "golden_answer_reasonable")
sess_is_ok = verdict_bool(v, "answer_session_ids_correct", "answer_session_ids_reasonable")
t = d.get("_question_type") or "(unknown)"
by_type[t]["n"] += 1
by_type[t]["golden_ok"] += 1 if golden_is_ok else 0
by_type[t]["sess_ok"] += 1 if sess_is_ok else 0
by_type[t]["both_ok"] += 1 if golden_is_ok and sess_is_ok else 0
bad_golden_records = [
d for d in done if not verdict_bool(d.get("verdict", {}), "golden_answer_correct", "golden_answer_reasonable")
]
bad_session_records = [
d
for d in done
if not verdict_bool(d.get("verdict", {}), "answer_session_ids_correct", "answer_session_ids_reasonable")
]
bad_golden = [d["_idx"] for d in bad_golden_records]
bad_sessions = [d["_idx"] for d in bad_session_records]
if args.json:
print(
json.dumps(
{
"total": total,
"finished": n,
"pending": total - n - len(unreadable),
"unreadable": unreadable,
"stale": stale,
"launched": len(launched),
"run_failed": run_failed,
"golden_answer_accuracy": round(golden_ok / n, 4) if n else None,
"answer_session_ids_accuracy": round(sess_ok / n, 4) if n else None,
"both_correct_rate": round(both_ok / n, 4) if n else None,
"golden_ok": golden_ok,
"sess_ok": sess_ok,
"both_ok": both_ok,
"by_type": {
t: {
**c,
"golden_bad": c["n"] - c["golden_ok"],
"session_bad": c["n"] - c["sess_ok"],
"both_bad": c["n"] - c["both_ok"],
"golden_acc": round(c["golden_ok"] / c["n"], 4),
"session_acc": round(c["sess_ok"] / c["n"], 4),
"both_acc": round(c["both_ok"] / c["n"], 4),
}
for t, c in by_type.items()
},
"bad_golden": bad_golden,
"bad_sessions": bad_sessions,
"golden_check_list": str(output_path),
},
ensure_ascii=False,
indent=2,
),
)
return 0
print("=" * 60)
print("LongMemEval golden_check 统计")
print("=" * 60)
print(f"样例总数 : {total}")
print(f"已完成 (有产出) : {n} ({pct(n, total)})")
print(f"未完成 : {total - n - len(unreadable)}")
if unreadable:
print(f"损坏/无法解析 : {len(unreadable)} {unreadable}")
if stale:
print(f"旧格式待重跑 : {len(stale)} {stale}")
print(f"已合并 JSONL : {output_path}")
print(f"已启动过 (有 log) : {len(launched)}")
print(f"运行失败/无可读产出 : {len(run_failed)}")
print("-" * 60)
print(f"golden answer 正确率 : {pct(golden_ok, n)} ({golden_ok}/{n})")
print(f"answer_session 正确率: {pct(sess_ok, n)} ({sess_ok}/{n})")
print(f"两者都正确 : {pct(both_ok, n)} ({both_ok}/{n})")
print("-" * 60)
print("按 question_type:")
print(
f" {'type':<24} {'n':>4} {'golden正确率':>14} {'golden错误':>10} "
f"{'session正确率':>14} {'session错误':>11} {'都正确':>10} {'都正确错误':>12}",
)
for t in sorted(by_type):
c = by_type[t]
print(
f" {t:<24} {c['n']:>4} {pct(c['golden_ok'], c['n']):>14} {c['n'] - c['golden_ok']:>10} "
f"{pct(c['sess_ok'], c['n']):>14} {c['n'] - c['sess_ok']:>11} "
f"{pct(c['both_ok'], c['n']):>10} {c['n'] - c['both_ok']:>12}",
)
if args.list_bad:
print("-" * 60)
print(f"golden answer 判为不正确的样例 ({len(bad_golden_records)}):")
print(json.dumps(grouped_records(bad_golden_records), ensure_ascii=False))
if args.list_bad_sessions:
print("-" * 60)
print(f"answer_session_ids 判为不正确的样例 ({len(bad_session_records)}):")
print(json.dumps(grouped_records(bad_session_records), ensure_ascii=False))
if args.list_run_failed:
print("-" * 60)
print(f"运行失败/无可读 check_golden.json 的样例 ({len(run_failed)}): {run_failed}")
for idx in run_failed:
print(f" {idx}: {LOGDIR / f'{idx}.log'}")
print("=" * 60)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,248 +0,0 @@
#!/usr/bin/env python3
"""Summarise LongMemEval ``session_review.json`` artifacts.
This script is for upstream health checks before running ``golden_check``.
Samples with retryable per-session failures should be rerun as a whole; samples
with non-retryable fallback reviews are reported separately.
Examples:
python benchmark/longmemeval/stats_session_review.py
python benchmark/longmemeval/stats_session_review.py --list-failed
python benchmark/longmemeval/stats_session_review.py --list-fallback
python benchmark/longmemeval/stats_session_review.py --json
"""
import argparse
import json
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
DATA = REPO / "datasets" / "longmemeval"
LOGDIR = REPO / "logs" / "session_review"
OUTPUT_FILENAME = "session_review.json"
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--list-failed", action="store_true", help="list samples with retryable failed per-session reviews")
p.add_argument("--list-fallback", action="store_true", help="list non-retryable fallback reviews")
p.add_argument("--list-missing", action="store_true", help="list samples missing session_review.json")
p.add_argument("--list-run-failed", action="store_true", help="list launched samples without a healthy output")
p.add_argument("--json", action="store_true", help="emit the summary as JSON")
return p.parse_args()
def sample_ids() -> list[str]:
"""List all numeric sample IDs."""
ids = [p.name for p in DATA.iterdir() if p.is_dir() and p.name.isdigit()]
return sorted(ids, key=int)
def pct(num: int, den: int) -> str:
"""Format a percentage."""
return f"{(100.0 * num / den):.1f}%" if den else "n/a"
def load_json(path: Path) -> dict:
"""Load a JSON object, returning {} on any error."""
try:
with path.open(encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except (OSError, json.JSONDecodeError):
return {}
def logged_sample_ids() -> list[str]:
"""List sample IDs that have a session_review runner log."""
if not LOGDIR.exists():
return []
ids = [p.stem for p in LOGDIR.glob("*.log") if p.stem.isdigit()]
return sorted(ids, key=int)
def review_block(data: dict) -> dict:
"""Return the review block when present."""
review = data.get("review") if isinstance(data, dict) else None
return review if isinstance(review, dict) else {}
def failure_details(data: dict) -> list[dict]:
"""Return retryable failed_reviews when present."""
failed_reviews = review_block(data).get("failed_reviews")
if not isinstance(failed_reviews, list):
return []
return [item for item in failed_reviews if isinstance(item, dict) and not item.get("fallback")]
def fallback_details(data: dict) -> list[dict]:
"""Return non-retryable fallback review details when present."""
review = review_block(data)
fallback_reviews = review.get("fallback_reviews")
if isinstance(fallback_reviews, list):
return [item for item in fallback_reviews if isinstance(item, dict)]
failed_reviews = review.get("failed_reviews")
if isinstance(failed_reviews, list):
return [item for item in failed_reviews if isinstance(item, dict) and item.get("fallback")]
return []
def failure_count(data: dict) -> int:
"""Return retryable failed review count."""
review = review_block(data)
raw = review.get("num_failed_reviews")
raw_fallback = review.get("num_fallback_reviews")
if isinstance(raw, int) and isinstance(raw_fallback, int):
return max(0, raw - raw_fallback)
return len(failure_details(data))
def fallback_count(data: dict) -> int:
"""Return non-retryable fallback review count."""
review = review_block(data)
raw = review.get("num_fallback_reviews")
if isinstance(raw, int):
return raw
return len(fallback_details(data))
def question_id(data: dict) -> str:
"""Return query.question_id when present."""
query = data.get("query") if isinstance(data, dict) else None
if not isinstance(query, dict):
return ""
return str(query.get("question_id") or "").strip()
def main() -> int:
"""Main entry point."""
args = parse_args()
ids = sample_ids()
total = len(ids)
healthy, failed, fallback, missing, unreadable = [], [], [], [], []
total_failed_sessions = 0
total_fallback_sessions = 0
failed_details_by_id: dict[str, list[dict]] = {}
fallback_details_by_id: dict[str, list[dict]] = {}
question_id_by_id: dict[str, str] = {}
for idx in ids:
path = DATA / idx / OUTPUT_FILENAME
if not path.exists():
missing.append(idx)
continue
data = load_json(path)
if not data:
unreadable.append(idx)
continue
question_id_by_id[idx] = question_id(data)
n_failed = failure_count(data)
n_fallback = fallback_count(data)
if n_failed:
failed.append(idx)
total_failed_sessions += n_failed
failed_details_by_id[idx] = failure_details(data)
if n_fallback:
fallback.append(idx)
total_fallback_sessions += n_fallback
fallback_details_by_id[idx] = fallback_details(data)
if not n_failed:
healthy.append(idx)
launched = logged_sample_ids()
healthy_set = set(healthy)
run_failed = [idx for idx in launched if idx not in healthy_set]
if args.json:
print(
json.dumps(
{
"total": total,
"healthy": len(healthy),
"failed_samples": failed,
"failed_sample_count": len(failed),
"failed_session_count": total_failed_sessions,
"fallback_samples": fallback,
"fallback_sample_count": len(fallback),
"fallback_session_count": total_fallback_sessions,
"missing": missing,
"unreadable": unreadable,
"launched": len(launched),
"run_failed_or_unhealthy": run_failed,
"failed_details": failed_details_by_id,
"fallback_details": fallback_details_by_id,
},
ensure_ascii=False,
indent=2,
),
)
return 0
print("=" * 60)
print("LongMemEval session_review 统计")
print("=" * 60)
print(f"样例总数 : {total}")
print(f"可继续产出 : {len(healthy)} ({pct(len(healthy), total)})")
print(f"有可重试失败 : {len(failed)}")
print(f"可重试失败 session : {total_failed_sessions}")
print(f"有不可重试 fallback : {len(fallback)}")
print(f"fallback session : {total_fallback_sessions}")
print(f"缺少 session_review : {len(missing)}")
print(f"损坏/无法解析 : {len(unreadable)}")
print(f"已启动过 (有 log) : {len(launched)}")
print(f"运行失败/非健康产出 : {len(run_failed)}")
print("-" * 60)
print("有可重试 failed_reviews 的样例需要整体重跑:")
if failed:
print(" ".join(failed))
print("重跑命令示例:")
print(f"python benchmark/longmemeval/run_session_review.py --start {failed[0]} --end {failed[0]}")
else:
print("(none)")
if fallback:
print("-" * 60)
print("不可重试 fallback 的样例不用重跑:")
for idx in fallback:
details = fallback_details_by_id.get(idx) or []
session_ids = [str(item.get("session_id") or "(unknown)") for item in details]
qid = question_id_by_id.get(idx)
sample_label = f"{idx}({qid})" if qid else idx
print(f"{sample_label}: {' '.join(session_ids) if session_ids else '(unknown)'}")
if args.list_failed and failed:
print("-" * 60)
for idx in failed:
details = failed_details_by_id.get(idx) or []
print(f"{idx}: {DATA / idx / OUTPUT_FILENAME} failed_sessions={len(details)}")
for item in details:
session_id = item.get("session_id", "(unknown)")
error = str(item.get("error") or "").replace("\n", " ")
print(f" - {session_id}: {error}")
if args.list_fallback and fallback:
print("-" * 60)
for idx in fallback:
details = fallback_details_by_id.get(idx) or []
print(f"{idx}: {DATA / idx / OUTPUT_FILENAME} fallback_sessions={len(details)}")
for item in details:
session_id = item.get("session_id", "(unknown)")
reason = str(item.get("fallback_reason") or "fallback")
error = str(item.get("error") or "").replace("\n", " ")
raw_saved = "yes" if item.get("raw_session") else "no"
print(f" - {session_id}: reason={reason} raw_session_saved={raw_saved} error={error}")
if args.list_missing and missing:
print("-" * 60)
print(f"缺少 session_review.json 的样例 ({len(missing)}): {missing}")
if args.list_run_failed and run_failed:
print("-" * 60)
print(f"运行失败/非健康产出的样例 ({len(run_failed)}): {run_failed}")
for idx in run_failed:
print(f" {idx}: {LOGDIR / f'{idx}.log'}")
print("=" * 60)
return 0
if __name__ == "__main__":
raise SystemExit(main())

14
benchmark/pibench/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# 含真实 API key绝不入库
env.sh
# 运行时产物(含对话内容,勿入库)
logs/
outputs/
reme_workspace/
nanobot_workspace/
# 数据符号链接(指向外部 π-Bench 仓库)
data
__pycache__/
*.pyc

327
benchmark/pibench/README.md Normal file
View file

@ -0,0 +1,327 @@
[中文版 / Chinese version](./README_ZH.md)
# π-Bench Evaluation Suite
A glue layer that connects the **ReMe agent (with persistent memory)** to
**π-Bench** (Proactive Personal Assistant Benchmark). This directory contains
only the minimal code and configuration needed for the integration: the
π-Bench framework (`src/`), evaluation data (`data/`), the AppWorld tool
environment, and ReMe itself are all **external third-party dependencies**,
referenced in place via symlink and environment variables and never bundled
with this suite.
- π-Bench: https://github.com/Simplified-Reasoning/Pi-Bench (arXiv: 2605.14678)
- ReMe: the root of the ReMe repository this suite lives in (recommended
location: `ReMe/benchmark/pibench/`)
## 1. Architecture
```
π-Bench runner (src.main --mode run)
│ user_agent (simulated-user LLM) walks data/{persona}/episode.yaml
│ task by task, chatting with the agent over multiple turns and judging
│ hidden intents (PROC) during the run phase
test server (π-Bench scripts/test_server.py, HTTP long-polling)
▲ /send │ /poll
│ ▼
bridge_reme.py ──────────────► ReMe Application (embedded as a library)
│ ├─ agent_wrapper: agent under test (AgentScope)
│ ├─ jobs: search / auto_memory / daily_write
│ └─ workspace: reme_workspace/{persona}/
│ (isolated persistent memory per persona)
└──── MCP ────► AppWorld MCP ────► AppWorld APIs (tool/app environment)
π-Bench runner (src.main --mode eval)
judger (judge LLM) reads the traces and scores each checklist item (COMP)
```
Key points:
- The bridge runs on **ReMe's own venv python** and uses ReMe as a library
(`resolve_app_config` + `Application`); **no ReMe source modification** is
required.
- Every incoming user message automatically triggers a ReMe memory `search`
and injects the matched memories (tuning knobs in §8); on task end (reset)
the session is distilled into daily notes by `auto_memory`.
- Tool calls executed by the agent (AppWorld MCP + ReMe job tools) are
captured per turn into the trace as `tool_steps`, so π-Bench
`tools_evaluation_path` scripts can score tool behavior (§7).
- π-Bench's `data/`, `src/` and AppWorld are not part of this suite; install
π-Bench first (§3.1).
## 2. Directory layout
```
pibench/
├── README.md / README_ZH.md # this document (English / Chinese)
├── env.sh.example # environment template (copy to env.sh, fill TODOs)
├── bridge_reme.py # ReMe ↔ test server bridge (memory inject/save,
│ # profile injection, tool-trace capture)
├── run_persona.sh # full pipeline for ONE persona (5 services + run + eval)
├── run_all.sh # batch over 5 personas (fresh/resume, default parallel=2)
├── resume.py # checkpoint resume: completion detection + surgical
│ # cleanup of interrupted tasks' residual memory
├── fix_trace_logs.py # run outputs → ~/.nanobot/trace_logs conversion,
│ # merging tool sidecars into turn files (pre-eval)
├── .gitignore # excludes env.sh and all runtime artifacts
└── config/
├── models/reme.yaml # runner model config (model_id=reme)
└── bench/evaluation/trace_history.yaml # trace render policy (shipped with
# the suite; passed via --history-config-path)
```
Generated at runtime (all git-ignored): `data` (symlink), `logs/`, `outputs/`,
`reme_workspace/`, `nanobot_workspace/`.
## 3. Prerequisites (third-party, install first)
### 3.1 π-Bench repository (with AppWorld)
```bash
git clone https://github.com/Simplified-Reasoning/Pi-Bench.git <pi-bench-dir>
cd <pi-bench-dir>
python3.11 -m venv .venv # scripts expect exactly this venv name
source .venv/bin/activate
pip install -e . # pibench runner (src.main)
bash scripts/setup_appworld.sh # install AppWorld and download its data (large)
```
Post-install sanity checks:
```bash
ls data/ # should contain researcher marketer pharmacist law_trainee Financier
.venv/bin/python -c "import src" && echo OK
.venv/bin/appworld --help >/dev/null && echo OK
```
### 3.2 ReMe repository
```bash
cd <reme-dir> # ReMe repository root (contains the reme/ package)
python3.11 -m venv .venv # scripts expect exactly this venv name
source .venv/bin/activate
pip install -e . # or ReMe's own install flow; `import reme` must work
```
Sanity check: `.venv/bin/python -c "import reme; print('ok')"`
## 4. Install this suite (step by step)
1. **Place the suite** (recommended inside the ReMe repo so `REME_DIR` is
inferred automatically):
```bash
cp -r pibench <reme-dir>/benchmark/pibench
cd <reme-dir>/benchmark/pibench
```
If placed elsewhere, set `REME_DIR` explicitly in env.sh later.
2. **Create the environment file and fill in the custom parameters**:
```bash
cp env.sh.example env.sh
```
Open `env.sh`; required items (marked TODO):
| Variable | Description |
|---|---|
| `PI_BENCH_ROOT` | π-Bench repo root (contains `src/` `data/` `.venv` `third_party/appworld`) |
| `USER_API_KEY` | API key of the simulated-user LLM (run phase, hidden-intent judging) |
| `JUDGER_API_KEY` | API key of the judger LLM (eval phase, checklist scoring) |
| `BRAVE_SEARCH_API_KEY` | optional; for the agent's web_search tool, `dummy` when unused |
Optional tuning: `REME_MODEL_NAME` (base model of the agent under test),
`REME_DIR`, `REME_LLM_BASE_URL` (default: DashScope OpenAI-compatible
endpoint).
3. **Link the evaluation data** (referenced in place, never copied):
```bash
ln -s "$PI_BENCH_ROOT/data" data
```
4. **(Optional) adjust model config** `config/models/reme.yaml`:
- `user_agent.model` / `judger.model`: model names for the simulated user
and the judger (literal values; π-Bench only expands `${ENV}` in
base_url/api_key).
- `run.turn_timeout`, `max_tool_iterations`, etc. as needed.
5. **Smoke check** (does not start the evaluation):
```bash
bash -n run_all.sh && bash -n run_persona.sh
source env.sh && "$REME_DIR/.venv/bin/python" -c "import reme; print('reme ok')"
```
## 5. Run the evaluation
> ⚠️ For long runs use `screen`, **not nohup** (nohup loses the permission
> context in sandboxed/restricted environments and breaks child processes).
```bash
# Full official run: wipe ALL personas' memory/outputs/traces first (default
# fresh mode, parallel=2)
mkdir -p logs # on a fresh deployment logs/ does not exist yet
screen -dmS pibench_suite bash -c "cd $(pwd) && bash run_all.sh > logs/run_all_master.log 2>&1"
# Checkpoint continuation (after an interruption; no wipe, completed tasks skipped)
bash run_all.sh --resume
# Other usages
bash run_all.sh --parallel 1 # sequential
bash run_all.sh --resume --skip-eval # run phase only
bash run_persona.sh researcher # single persona (default --resume semantics)
bash run_persona.sh researcher --fresh
```
Time reference: 5 personas × 20 tasks, parallel=2, fresh full run ≈ 1214 hours.
`run_all.sh` exits non-zero when any persona fails, so upstream automation
cannot mistake a partially failed suite run for a success.
## 6. Port allocation (parallel personas never collide)
| persona | AppWorld API | AppWorld MCP | Test Server | ReMe internal service |
|-------------|------|-------|------|-------|
| marketer | 9001 | 10001 | 9998 | 18766 |
| law_trainee | 9002 | 10002 | 9997 | 18767 |
| pharmacist | 9003 | 10003 | 9996 | 18768 |
| researcher | 9004 | 10004 | 9995 | 18765 |
| Financier | 9005 | 10005 | 9994 | 18769 |
## 7. Outputs and scores
- **Results**: `outputs/reme/{persona}/{task}/eval/results/*_result.json`
- `overall_average_score`: checklist completeness (COMP; the judger scores
each criterion YES/NO, weighted across dependency groups)
- `overall_proactiveness_average_score`: proactiveness (PROC; the
user_agent judges hidden-intent coverage during the run phase; each task
file also carries the global average)
- **Traces**: `~/.nanobot/trace_logs/reme/{persona}/{task}/...` (the scoring
input of the eval phase)
- **Logs**: `logs/` (`suite_<persona>.log` per persona; `bridge_*`,
`runner_run/eval_*`, `appworld_*`, `test_server_*` per service)
- **Memory store**: `reme_workspace/{persona}/` (daily/digest notes, raw
session dialogs, BM25 index, etc.; persistent across runs, wiped only in
fresh mode)
Score summary:
```bash
grep -h "overall_average_score\|overall_proactiveness" \
outputs/reme/*/*/eval/results/*_result.json | head
```
### Tool-trace capture (tools_evaluation support)
Some tasks define `objectives.tools_evaluation_path`: Python scripts that
score tool behavior (e.g. "the temporary Todoist board was created and
removed"). They need the executed tool calls in the trace. The pipeline:
1. During `reply()`, the bridge reads the persisted AgentScope session state
after each turn and extracts the new `tool_call` / `tool_result` blocks
(tool name, arguments, result).
2. Records are appended to
`outputs/reme/{persona}/{task}/history/{ts}-tools.jsonl`, tagged with the
turn number; AgentScope MCP names (`mcp__AppWorld__<tool>`) are normalized
to the π-Bench convention (`mcp_appworld_<tool>`).
3. `fix_trace_logs.py` pairs each `{ts}-messages.jsonl` run with the
temporally closest tools sidecar and merges the records into the generated
`turn_N.json` files under the `tool_steps` key — one of the two
tool-history formats understood by π-Bench's `collect_tool_history()`.
4. The eval phase then feeds `tool_steps` to both the tools_evaluation
scripts and the rendered `<tool_trace_extracts>` seen by the judger.
## 8. Memory mechanism (core design of this suite)
- **Persona isolation**: each persona has its own workspace
(`reme_workspace/{persona}/`); the bridge takes an exclusive
`.bridge.lock` on it at startup, so two bridges can never share one memory
store, and one persona's memory search can never reach another's memories.
- **Writes**: on task end (runner sends reset), the session is distilled by
the `auto_memory` job into daily notes and indexed by the background
watcher (BM25). Saves are non-blocking background tasks; the first message
of a new session waits for in-flight writes before searching.
- **Reads**: on every incoming user message the bridge runs one `search` and
injects matched memories (`[Relevant memories from previous sessions]`
prefix); without matches the message passes through unchanged. Retrieval
tuning (bridge CLI flags, adjustable in run_persona.sh):
- `--search-limit 3`: at most 3 memory chunks injected per message;
- `--search-min-score 2.0`: weak BM25 hits are filtered out;
- `tool_context_id` rotates per task: chunks already injected within the
same task are not re-injected (ReMe's seen-chunk dedup, 24h TTL); normal
recall resumes after task boundaries.
- **No self-leakage**: the in-progress session is not in the store yet
(saves happen on reset), so a task can never retrieve its own unfinished
content.
- The agent also holds `search`/`daily_write` tools and can retrieve/record
proactively.
- **System prompt**: `bridge_reme.py:build_system_prompt()` embeds the
HIDDEN-NEEDS protocol (proactiveness-oriented) and injects the persona
profile from `data/{persona}/profile.yaml` into every turn's system prompt.
## 9. Checkpoint resume and memory-cleanup semantics
- **Completion detection** (resume.py): scans
`outputs/reme/{persona}/**/history/*-log.jsonl` and
`outputs/reme/{persona}/run/*-log.jsonl` for
`Task finished task_id=X status=Y`. The status with the **newest event
timestamp** wins per task (record `timestamp`, falling back to
`timestamp_iso`, then to the timestamp embedded in the log file name) —
file category and read order alone can never override a newer record, so an
old run-level SUCCESS cannot mask a newer per-task ERROR. `SUCCESS /
MAX_TURNS / TIMEOUT` count as completed; `ERROR` and never-started tasks
are re-run (passed to the runner as repeated `--task-id` flags in episode
order).
- **Answer-leak prevention**: an interrupted task may already have been
distilled into daily notes during graceful shutdown; re-running it with
that memory injected would inflate scores. Before resuming,
`resume.py cleanup` therefore removes residual memory **only for tasks
about to be re-run** (daily/digest notes, session/dialog, mem_session;
matched via `session_id = pibench_{task}_*`). Completed tasks' memories are
never touched. Daily index files are refreshed **only for the dates that
lost notes**, by full workspace-relative wikilink path — and when the ReMe
package is importable, the refresh reuses ReMe's own daily-index rebuild
logic (`refresh_day_index`), so same-named notes on other dates are never
modified.
- **fresh vs resume are mutually exclusive**: a full memory wipe belongs to
fresh mode only (`run_all.sh` default, executed before any service starts);
resume never wipes.
## 10. Customization entry points
| Goal | Location |
|---|---|
| Base model of the agent under test | `REME_MODEL_NAME` in `env.sh` |
| user_agent / judger models | `config/models/reme.yaml` |
| Agent system prompt | `bridge_reme.py` `build_system_prompt()` |
| Memory retrieval limit/threshold | `--search-limit/--search-min-score` on the bridge command in `run_persona.sh` |
| ReMe internal parameters | **Do not modify ReMe source**; write a dedicated config modeled on `reme/config/beam.yaml` and override via `resolve_app_config(config=...)` (see bridge `_init_reme_app`) |
| Turn timeout / tool iteration cap | `config/models/reme.yaml` `run.turn_timeout`, `model.max_tool_iterations` |
## 11. Troubleshooting
- **Port already in use**: the scripts auto-kill residual processes on the
four port groups above; if another suite (e.g. a different π-Bench
experiment) holds them, stop it first or change the port table in
run_persona.sh.
- **Bridge exits immediately with workspace locked**: another bridge already
holds the same workspace; make sure each persona uses its own
`--workspace-dir` (the scripts allocate one per persona).
- **Runner reports `${USER_API_KEY} ... empty`**: env.sh is unfilled or not
sourced; run_persona.sh sources env.sh automatically — when running the
runner manually, `source env.sh` first.
- **`Cannot import 'reme'`**: the bridge must run with
`${REME_DIR}/.venv/bin/python` (run_persona.sh already does); otherwise
check that `REME_DIR` points at the ReMe repository root.
- **AppWorld fails to start**: run `bash scripts/setup_appworld.sh` in the
π-Bench repo first (downloads data); inspect
`logs/appworld_*_<persona>.log`.
- **trace_history.yaml not found**: the runner needs
`config/bench/evaluation/trace_history.yaml`; this suite ships the file and
passes it explicitly via `--history-config-path`, and run_persona.sh fails
fast with a clear error if it is missing. Always launch run_persona.sh /
run_all.sh from the suite directory.
## 12. Privacy and security
- The suite code and config templates contain **no real API keys, user names
or absolute paths**; real keys live only in your local `env.sh`
(git-ignored).
- `logs/`, `outputs/`, `reme_workspace/` and `nanobot_workspace/` contain
full conversations and model outputs; never commit or share them.
- The `data` symlink points at the official π-Bench evaluation data; respect
its data license terms.

View file

@ -0,0 +1,284 @@
# π-Bench 评测说明
[English version](./README.md)
**ReMe agent带持久记忆** 接入 **π-Bench**Proactive Personal Assistant
Benchmark的胶水层评测套件。只含对接所需的最小代码与配置π-Bench 框架
`src/`)、评测数据(`data/`、AppWorld 工具环境、ReMe 本体均为**外部第三方
依赖**,通过符号链接与环境变量原位引用,不随本套件分发。
- π-Bench: https://github.com/Simplified-Reasoning/Pi-Bench arXiv: 2605.14678
- ReMe: 你所在 ReMe 仓库的根目录(本套件推荐放在 `ReMe/benchmark/pibench/`
## 1. 架构总览
```
π-Bench runner (src.main --mode run)
│ user_agent模拟用户 LLM按 data/{persona}/episode.yaml 顺序
│ 逐任务、多轮地与 agent 对话,并在 run 阶段判定隐藏意图(PROC)
test server (π-Bench scripts/test_server.py, HTTP 长轮询)
▲ /send │ /poll
│ ▼
bridge_reme.py ──────────────► ReMe Application以库方式内嵌启动
│ ├─ agent_wrapper: 被测 agentAgentScope
│ ├─ jobs: search / auto_memory / daily_write
│ └─ workspace: reme_workspace/{persona}/
│ (每 persona 独立持久记忆库,互不可见)
└──── MCP ────► AppWorld MCP ────► AppWorld API工具/应用环境)
π-Bench runner (src.main --mode eval)
judger裁判 LLM读取 trace按 checklist 逐条 YES/NO 打分(COMP)
```
要点:
- bridge 用 **ReMe 自己的 venv python** 运行,把 ReMe 当库用(`resolve_app_config`
+ `Application`**ReMe 源码零改动**。
- 每条用户消息都会自动触发一次 ReMe memory `search` 并把命中记忆注入当前消息
(参数见 §8任务结束reset时会话被 `auto_memory` 提炼为 daily 笔记落盘。
- agent 执行的每一轮工具调用AppWorld MCP + ReMe job 工具)都会被采集并以
`tool_steps` 形式写入 trace供 π-Bench 的 `tools_evaluation_path` 脚本
对工具行为评分§7
- π-Bench 的 `data/``src/`、AppWorld 均不属于本套件,需先装好 π-Bench§3.1)。
## 2. 目录结构
```
pibench/
├── README.md / README_ZH.md # 本文档(英文 / 中文)
├── env.sh.example # 环境配置模板(复制为 env.sh 后填写 TODO 项)
├── bridge_reme.py # ReMe ↔ test server 桥接(记忆注入/保存、
│ # profile 注入、工具调用轨迹采集)
├── run_persona.sh # 单 persona 全流程5 个服务 + run + eval
├── run_all.sh # 5 个 persona 批跑fresh/resume默认 2 并行)
├── resume.py # 断点续跑:完成判定 + 中断任务残留记忆的外科清理
├── fix_trace_logs.py # run 输出 → ~/.nanobot/trace_logs 转换,
│ # 并把工具轨迹合并进 turn 文件eval 前置)
├── .gitignore # 排除 env.sh 与全部运行产物
└── config/
├── models/reme.yaml # runner 模型配置model_id=reme
└── bench/evaluation/trace_history.yaml # trace 渲染策略(随套件提供,
# 经 --history-config-path 显式传入)
```
运行时自动生成(均被 .gitignore 排除):`data`(符号链接)、`logs/`
`outputs/``reme_workspace/``nanobot_workspace/`
## 3. 前置依赖(第三方,先装好)
### 3.1 π-Bench 仓库(含 AppWorld
```bash
git clone https://github.com/Simplified-Reasoning/Pi-Bench.git <pi-bench-dir>
cd <pi-bench-dir>
python3.11 -m venv .venv # 脚本约定使用 .venv 这个目录名
source .venv/bin/activate
pip install -e . # pibench runnersrc.main
bash scripts/setup_appworld.sh # 安装 AppWorld 并下载其数据(体积较大,需网络)
```
装完自检:
```bash
ls data/ # 应含 researcher marketer pharmacist law_trainee Financier
.venv/bin/python -c "import src" && echo OK
.venv/bin/appworld --help >/dev/null && echo OK
```
### 3.2 ReMe 仓库
```bash
cd <reme-dir> # ReMe 仓库根目录(含 reme/ 包)
python3.11 -m venv .venv # 脚本约定使用 .venv 这个目录名
source .venv/bin/activate
pip install -e . # 或按 ReMe 自身安装方式,保证 `import reme` 可用
```
自检:`.venv/bin/python -c "import reme; print('ok')"`
## 4. 安装本套件(逐步)
1. **放置套件**(推荐放进 ReMe 仓库,`REME_DIR` 可自动推断):
```bash
cp -r pibench <reme-dir>/benchmark/pibench
cd <reme-dir>/benchmark/pibench
```
若放在其他位置,稍后在 env.sh 中显式设置 `REME_DIR`
2. **创建环境文件并填写自定义参数**
```bash
cp env.sh.example env.sh
```
打开 `env.sh`,必填项(标 TODO 的):
| 变量 | 说明 |
|---|---|
| `PI_BENCH_ROOT` | π-Bench 仓库根目录(含 `src/` `data/` `.venv` `third_party/appworld` |
| `USER_API_KEY` | 模拟用户 LLM 的 API keyrun 阶段判定隐藏意图) |
| `JUDGER_API_KEY` | 裁判 LLM 的 API keyeval 阶段 checklist 打分) |
| `BRAVE_SEARCH_API_KEY` | 可选agent 的 web_search 工具用,不用填 `dummy` |
可选调整:`REME_MODEL_NAME`(被测 agent 基模)、`REME_DIR`
`REME_LLM_BASE_URL`(默认 DashScope OpenAI 兼容端点)。
3. **链接评测数据**(π-Bench 数据原位引用,不复制):
```bash
ln -s "$PI_BENCH_ROOT/data" data
```
4. **(可选)调整模型配置** `config/models/reme.yaml`
- `user_agent.model` / `judger.model`:模拟用户与裁判的模型名(字面量,
π-Bench 仅对 base_url/api_key 做 `${ENV}` 展开)。
- `run.turn_timeout``max_tool_iterations` 等按需。
5. **冒烟自检**(不启动评测):
```bash
bash -n run_all.sh && bash -n run_persona.sh
source env.sh && "$REME_DIR/.venv/bin/python" -c "import reme; print('reme ok')"
```
## 5. 运行评测
> ⚠️ 长时间运行请放进 `screen`**不要用 nohup**nohup 在沙箱/受限环境下
> 会丢失权限上下文导致子进程异常)。
```bash
# 完整正式评测:先清空全部 persona 的记忆/输出/trace再从头跑默认 fresh2 并行)
mkdir -p logs # 全新部署时 logs/ 尚不存在,先建再重定向
screen -dmS pibench_suite bash -c "cd $(pwd) && bash run_all.sh > logs/run_all_master.log 2>&1"
# 断点续跑(中断后继续;不清记忆,跳过已完成任务)
bash run_all.sh --resume
# 其他用法
bash run_all.sh --parallel 1 # 串行
bash run_all.sh --resume --skip-eval # 只跑 run 阶段
bash run_persona.sh researcher # 单 persona默认 --resume 语义)
bash run_persona.sh researcher --fresh
```
耗时参考5 persona × 20 任务、2 并行fresh 全量约 1214 小时。
任一 persona 失败时 `run_all.sh` 以非零状态退出,上层自动化不会把部分失败
的评测误判为成功。
## 6. 端口分配(多 persona 并行互不冲突)
| persona | AppWorld API | AppWorld MCP | Test Server | ReMe 内部服务 |
|-------------|------|-------|------|-------|
| marketer | 9001 | 10001 | 9998 | 18766 |
| law_trainee | 9002 | 10002 | 9997 | 18767 |
| pharmacist | 9003 | 10003 | 9996 | 18768 |
| researcher | 9004 | 10004 | 9995 | 18765 |
| Financier | 9005 | 10005 | 9994 | 18769 |
## 7. 输出与分数
- **结果**`outputs/reme/{persona}/{task}/eval/results/*_result.json`
- `overall_average_score`checklist 完整度COMPjudger 逐条 YES/NO 按依赖组加权)
- `overall_proactiveness_average_score`主动性PROCrun 阶段 user_agent
判定隐藏意图覆盖率;每个任务文件同时携带全局均值)
- **trace**`~/.nanobot/trace_logs/reme/{persona}/{task}/...`eval 的判分输入)
- **日志**`logs/``suite_<persona>.log` 为每 persona 总日志,`bridge_*`
`runner_run/eval_*``appworld_*``test_server_*` 分服务)
- **记忆库**`reme_workspace/{persona}/`daily/digest 笔记、session 原始对话、
BM25 索引等跨运行持久fresh 才清空)
查看汇总:
```bash
grep -h "overall_average_score\|overall_proactiveness" \
outputs/reme/*/*/eval/results/*_result.json | head
```
### 工具轨迹采集tools_evaluation 支持)
部分任务定义了 `objectives.tools_evaluation_path`:用 Python 脚本对工具行为
打分(例如"临时 Todoist 看板已创建并被删除")。这些脚本需要 trace 里有真实
的工具调用记录。采集链路:
1. 每轮 `reply()` 之后bridge 读取 AgentScope 落盘的会话状态,提取本轮新增
`tool_call` / `tool_result` 块(工具名、参数、结果)。
2. 记录按 turn 编号追加写入
`outputs/reme/{persona}/{task}/history/{ts}-tools.jsonl`AgentScope 的
MCP 工具名(`mcp__AppWorld__<tool>`)会规范化为 π-Bench 约定
`mcp_appworld_<tool>`)。
3. `fix_trace_logs.py` 将每个 `{ts}-messages.jsonl` 运行与时间上最接近的
tools 旁路文件配对,把记录合并进生成的 `turn_N.json``tool_steps`
字段——这是 π-Bench `collect_tool_history()` 支持的两种工具轨迹格式之一。
4. eval 阶段 `tool_steps` 既提供给 tools_evaluation 脚本,也会被渲染为
judger 可见的 `<tool_trace_extracts>`
## 8. 记忆机制(本套件的核心设计)
- **persona 隔离**:每个 persona 独立 workspace`reme_workspace/{persona}/`
bridge 启动时对 workspace 加 `.bridge.lock` 排他锁,两个 bridge 不可能共用
同一记忆库;一个 persona 的 memory search 永远接触不到其他 persona 的记忆。
- **写入**任务结束runner 发送 reset会话经 `auto_memory` job 提炼为
daily 笔记落盘,后台 watcher 建 BM25 索引。保存为非阻塞后台任务,
新会话首条消息会先等待在途写入完成再检索。
- **读取**bridge 每收到一条用户消息自动 `search` 一次并注入命中记忆
`[Relevant memories from previous sessions]` 前缀),无命中则原样透传。
检索参数bridge 命令行,可在 run_persona.sh 中调整):
- `--search-limit 3`:每条消息最多注入 3 个记忆块;
- `--search-min-score 2.0`:过滤弱 BM25 命中;
- `tool_context_id` 按任务轮换:同一任务内已注入的记忆块不重复注入
ReMe 自带 seen-chunk 去重24h TTL任务边界后恢复正常召回。
- **无自泄漏**进行中的会话尚未入库save 发生在 reset任务不会检索到
自己未完成的内容。
- agent 同时持有 `search`/`daily_write` 工具,可主动检索/记录。
- **system prompt**`bridge_reme.py:build_system_prompt()` 内置
HIDDEN-NEEDS 协议(面向 proactiveness并把 `data/{persona}/profile.yaml`
的 persona profile 注入每轮 system prompt。
## 9. 断点续跑与记忆清理语义
- **完成判定**resume.py扫描 `outputs/reme/{persona}/**/history/*-log.jsonl`
`outputs/reme/{persona}/run/*-log.jsonl` 中的
`Task finished task_id=X status=Y`。每个任务以**事件时间最新**的记录为准
(优先取记录的 `timestamp`,回退 `timestamp_iso`,再回退日志文件名中的
时间戳)——文件类别与读取顺序本身不能覆盖更新的记录,因此旧的 run 级
SUCCESS 不会掩盖更新的 per-task ERROR。`SUCCESS/MAX_TURNS/TIMEOUT` 记为
完成,`ERROR`/未开始的任务重跑(按 episode 顺序以 `--task-id` 传给 runner
- **防答案泄漏**:被中断的任务可能已在优雅退出时提炼成 daily 笔记,直接重跑会
把答案注入、抬高分数。因此 resume 启动前 `resume.py cleanup` **只删除待重跑
任务**的残留记忆daily/digest 笔记、session/dialog、mem_session
`session_id = pibench_{task}_*` 匹配已完成任务的记忆一律不动。daily
索引**只刷新实际发生删除的日期**,按完整的 workspace 相对 wikilink 路径
匹配;当 ReMe 包可导入时,刷新直接复用 ReMe 自带的 daily 索引重建逻辑
`refresh_day_index`),不会误改其他日期下的同名笔记条目。
- **fresh vs resume 互斥**:全量清记忆只属于 fresh 模式(`run_all.sh` 默认,
在任何服务启动前执行resume 永不清全量。
## 10. 自定义与调优入口
| 目标 | 位置 |
|---|---|
| 被测 agent 基模 | `env.sh``REME_MODEL_NAME` |
| user_agent / judger 模型 | `config/models/reme.yaml` |
| agent system prompt | `bridge_reme.py` `build_system_prompt()` |
| 记忆检索条数/阈值 | `run_persona.sh` bridge 启动命令的 `--search-limit/--search-min-score` |
| ReMe 内部参数 | **不要改 ReMe 源码**;仿照 `reme/config/beam.yaml` 写专有配置,经 `resolve_app_config(config=...)` 覆盖(见 bridge `_init_reme_app` |
| 轮超时/工具迭代上限 | `config/models/reme.yaml` `run.turn_timeout``model.max_tool_iterations` |
## 11. 故障排查
- **端口被占用**:脚本会自动 kill 上述 4 组端口上的残留进程;若与其他套件
(如别的 π-Bench 实验)冲突,请先停掉对方或改 run_persona.sh 的端口表。
- **bridge 启动即退出,提示 workspace locked**:另一个 bridge 正占用同一
workspace确认每个 persona 用各自的 `--workspace-dir`(脚本已按 persona 分配)。
- **runner 报 `${USER_API_KEY} ... empty`**env.sh 未填写或未生效;
run_persona.sh 会自动 source env.sh手动运行 runner 时请先 `source env.sh`
- **`Cannot import 'reme'`**bridge 必须用 `${REME_DIR}/.venv/bin/python` 运行
run_persona.sh 已如此),或检查 `REME_DIR` 是否指向 ReMe 仓库根目录。
- **AppWorld 启动失败**:先在 π-Bench 仓库执行 `bash scripts/setup_appworld.sh`
下载数据;查看 `logs/appworld_*_<persona>.log`
- **trace_history.yaml 找不到**runner 需要
`config/bench/evaluation/trace_history.yaml`;本套件已随附该文件并通过
`--history-config-path` 显式传入run_persona.sh 启动前会做存在性检查,
缺失时立即报出清晰错误。请始终从套件目录启动 run_persona.sh / run_all.sh。
## 12. 隐私与安全
- 套件代码与配置模板中**不含任何真实 API key、用户名或绝对路径**
真实 key 只存在于你本地的 `env.sh`(已被 .gitignore 排除)。
- `logs/``outputs/``reme_workspace/``nanobot_workspace/` 含完整对话内容
与模型输出,请勿提交仓库或外传。
- `data` 符号链接指向 π-Bench 官方评测数据,请遵守其数据许可条款。

1039
benchmark/pibench/bridge_reme.py Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
version: 1
format:
root_tag: trace
turn_tag: turn
message_tag: message
file_tag: file
tool_call_tag_prefix: tool_call
tool_result_tag_prefix: tool_result
text_policy:
default:
truncate_chars: 1200
mask_newlines: false
field_overrides:
files_read:
truncate_chars: 40000
assistant_content:
truncate_chars: 40000
tool_result_content:
truncate_chars: 40000
fields:
turn:
include_session_key: false
files:
enabled: true
messages:
enabled: true
include_message_role_attr: true
include_message_index_attr: false
include_system: false
include_user: true
include_assistant_thinking_content: false
include_assistant_thinking_reasoning: false
include_assistant_content: true
include_assistant_reasoning: false
include_assistant_tool_calls: false
require_matching_tool_call: true
tool_calls:
include_tool_call_id: false
tools:
web_fetch:
enabled: true
include_tool_call_keys: [url]
include_tool_result: false
web_search:
enabled: true
include_tool_call_keys: [query]
include_tool_result: false

View file

@ -0,0 +1,40 @@
# ReMe model configuration for Pi-Bench
# Uses ReMe's AgentScope agent with Dashscope as the LLM backend
model:
model: reme
base_url: "http://localhost:8088"
api_key: "dummy"
provider: custom
max_tokens: 16384
max_tool_iterations: 120
memory_window: 100
user_agent:
model: qwen3.8-max
base_url: "${USER_BASE_URL}"
api_key: "${USER_API_KEY}"
temperature: 0.0
request_timeout: 360.0
judger:
model: qwen3.8-max
base_url: "${JUDGER_BASE_URL}"
api_key: "${JUDGER_API_KEY}"
temperature: 0.0
request_timeout: 360.0
tools:
brave_search_api_key: "${BRAVE_SEARCH_API_KEY}"
web_search_max_results: 10
nanobot:
trace_logs_dir: "~/.nanobot/trace_logs"
workspace_dir: "~/.nanobot/workspace"
copy_task_assets_to_workspace: true
run:
output_dir: outputs
log_level: INFO
user_mode: llm
turn_timeout: 2400.0

View file

@ -0,0 +1,57 @@
#!/bin/bash
# ═══════════════════════════════════════════════════════════════════════
# pibench evaluation suite - environment configuration template
# Usage: cp env.sh.example env.sh, then fill in the TODO items below.
# ⚠️ env.sh contains real API keys; never commit or share it
# (already excluded via .gitignore).
# ═══════════════════════════════════════════════════════════════════════
SUITE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ─── TODO: π-Bench repository root ────────────────────────────────────
# Must contain src/, data/, scripts/test_server.py, third_party/appworld
# and .venv (see README setup).
export PI_BENCH_ROOT=""
# ─── ReMe repository ──────────────────────────────────────────────────
# Defaults to two levels above this directory (the layout this suite uses
# when placed at ReMe/benchmark/pibench); point it at the actual ReMe
# repository root if the suite lives elsewhere.
export REME_DIR="${REME_DIR:-$(cd "${SUITE_DIR}/../.." && pwd)}"
# ─── Base model of the agent under test (LLM used by the ReMe agent) ──
export REME_MODEL_NAME="${REME_MODEL_NAME:-qwen3.6-plus}"
# ─── LLM service endpoint (default: DashScope OpenAI-compatible; any
# OpenAI-compatible endpoint works) ────────────────────────────────
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
export REME_LLM_BASE_URL="${REME_LLM_BASE_URL:-${DASHSCOPE_BASE_URL}}"
# ─── TODO: API keys ───────────────────────────────────────────────────
# USER_API_KEY : drives the simulated user LLM (run phase; judges whether
# hidden intents are satisfied and asks follow-ups)
# JUDGER_API_KEY: drives the judger LLM (eval phase; scores the checklist)
# The two may be identical; one strong model is recommended for both.
export USER_BASE_URL="${DASHSCOPE_BASE_URL}"
export USER_API_KEY="TODO-fill-in-user-agent-api-key"
export JUDGER_BASE_URL="${DASHSCOPE_BASE_URL}"
export JUDGER_API_KEY="TODO-fill-in-judger-api-key"
# The ReMe agent's key reuses USER_API_KEY by default (no need to repeat
# it when both use the same service and key).
export REME_LLM_API_KEY="${REME_LLM_API_KEY:-${USER_API_KEY}}"
# Brave Search (optional; used by the agent's web_search tool - use
# "dummy" when not needed).
export BRAVE_SEARCH_API_KEY="TODO-optional-brave-search-key-or-dummy"
# ─── Persistent memory workspaces (one subdirectory per persona,
# created automatically) ───────────────────────────────────────────
export REME_WORKSPACE_ROOT="${REME_WORKSPACE_ROOT:-${SUITE_DIR}/reme_workspace}"
# ─── Variables consumed by ReMe's default.yaml model config expansion;
# do not remove ────────────────────────────────────────────────────
export LLM_MODEL_NAME="${REME_MODEL_NAME}"
export LLM_BASE_URL="${REME_LLM_BASE_URL}"
export LLM_API_KEY="${REME_LLM_API_KEY}"

View file

@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""Convert reme_eval run outputs into eval-compatible trace logs.
outputs/{model_id}/{user_id}/{task_id}/history/{ts}-messages.jsonl
-> ~/.nanobot/trace_logs/{model_id}/{user_id}/{task_id}/{ts}/turn_N.json
The bridge additionally writes {ts}-tools.jsonl sidecar files next to the
message histories: one JSON object per executed tool call with fields
{turn, name, arguments, result}. Each messages run is paired with the
temporally closest sidecar, and the records are merged into the generated
turn files under the "tool_steps" key, which is one of the tool-history
formats π-Bench's collect_tool_history() understands. Without this step,
tools_evaluation scripts would see no tool evidence at all.
Usage: python fix_trace_logs.py [user_id ...] (no args = all users)
"""
import json
import re
import sys
from datetime import datetime
from pathlib import Path
SUITE_DIR = Path(__file__).resolve().parent
OUTPUTS_DIR = SUITE_DIR / "outputs"
TRACE_LOGS_DIR = Path.home() / ".nanobot" / "trace_logs"
MESSAGES_FILE_RE = re.compile(r"^(\d{8}_\d{6})-messages\.jsonl$")
TOOLS_FILE_RE = re.compile(r"^(\d{8}_\d{6})-tools\.jsonl$")
TIME_FORMAT = "%Y%m%d_%H%M%S"
# A tool sidecar belongs to the messages run that started at most this many
# seconds earlier (the bridge stamps the sidecar when the task's first user
# message arrives, shortly after the runner opened the messages file).
MAX_PAIR_DELTA_SECONDS = 6 * 3600
def _to_epoch(timestamp: str) -> float:
"""Parse a YYYYMMDD_HHMMSS timestamp into epoch seconds."""
try:
return datetime.strptime(timestamp, TIME_FORMAT).timestamp()
except ValueError:
return 0.0
def load_tool_records(tools_file: Path) -> dict:
"""Group sidecar tool records by turn number."""
by_turn: dict = {}
try:
with open(tools_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
if not isinstance(record, dict) or not record.get("name"):
continue
turn = int(record.get("turn") or 0)
by_turn.setdefault(turn, []).append(
{
"name": record["name"],
"arguments": record.get("arguments", {}),
"result": record.get("result", ""),
},
)
except OSError as exc:
print(f" WARNING: cannot read tool sidecar {tools_file}: {exc}")
return by_turn
def pair_tool_sidecars(message_runs: list, tool_runs: list) -> dict:
"""Pair each messages run with the temporally closest unused tool sidecar.
Fresh runs produce exactly one messages file and one sidecar per task;
re-runs append matching pairs, so sorted greedy nearest-timestamp
matching is stable. Sidecars farther away than MAX_PAIR_DELTA_SECONDS
(e.g. leftovers of a crashed bridge) stay unpaired.
"""
pairing: dict = {}
unused = list(tool_runs)
for msg_ts, _ in message_runs:
best_delta = None
best_item = None
for tool_ts, tool_path in unused:
delta = abs(_to_epoch(tool_ts) - _to_epoch(msg_ts))
if best_delta is None or delta < best_delta:
best_delta = delta
best_item = (tool_ts, tool_path)
if best_delta is not None and best_item is not None and best_delta <= MAX_PAIR_DELTA_SECONDS:
pairing[msg_ts] = best_item[1]
unused.remove(best_item)
return pairing
def build_turns(messages: list) -> list:
"""Split the flat message list into per-turn [user, assistant] groups."""
turns = []
i = 0
while i < len(messages):
turn_msgs = []
if messages[i]["role"] == "user":
turn_msgs.append({"role": "user", "content": messages[i]["message"]})
i += 1
if i < len(messages) and messages[i]["role"] == "assistant":
turn_msgs.append({"role": "assistant", "content": messages[i]["message"]})
i += 1
if not turn_msgs:
i += 1 # defensive: never spin on unexpected roles
continue
turns.append(turn_msgs)
return turns
def convert_task(model_id: str, user_id: str, task_dir: Path) -> None:
"""Convert one task's history dir into trace turn files with tool_steps."""
history_dir = task_dir / "history"
if not history_dir.is_dir():
return
message_runs = []
tool_runs = []
for msg_file in history_dir.glob("*-messages.jsonl"):
match = MESSAGES_FILE_RE.match(msg_file.name)
if match:
message_runs.append((match.group(1), msg_file))
for tools_file in history_dir.glob("*-tools.jsonl"):
match = TOOLS_FILE_RE.match(tools_file.name)
if match:
tool_runs.append((match.group(1), tools_file))
if not message_runs:
return
message_runs.sort(key=lambda item: item[0])
tool_runs.sort(key=lambda item: item[0])
pairing = pair_tool_sidecars(message_runs, tool_runs)
print(f"\n{model_id}/{user_id}/{task_dir.name}")
for timestamp, msg_file in message_runs:
trace_dir = TRACE_LOGS_DIR / model_id / user_id / task_dir.name / timestamp
trace_dir.mkdir(parents=True, exist_ok=True)
messages = []
with open(msg_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
msg = json.loads(line)
if msg.get("role") == "user" and msg.get("message") == "/new":
continue
messages.append(msg)
tools_file = pairing.get(timestamp)
tools_by_turn = load_tool_records(tools_file) if tools_file else {}
if tools_file is not None:
print(f" {timestamp}: paired tool sidecar {tools_file.name}")
turns = build_turns(messages)
for turn_idx, turn_msgs in enumerate(turns, start=1):
turn_data = {"messages": turn_msgs}
tool_steps = tools_by_turn.get(turn_idx)
if tool_steps:
turn_data["tool_steps"] = tool_steps
turn_file = trace_dir / f"turn_{turn_idx}.json"
with open(turn_file, "w", encoding="utf-8") as f:
json.dump(turn_data, f, indent=2, ensure_ascii=False)
tool_total = sum(len(steps) for steps in tools_by_turn.values())
print(f" {timestamp}: {len(turns)} turns, {tool_total} tool step(s) -> {trace_dir}")
def convert_outputs(user_filter=None):
"""Convert message history JSONL files into per-turn trace JSON files."""
if not OUTPUTS_DIR.exists():
print(f"outputs dir not found: {OUTPUTS_DIR}")
return
for model_dir in sorted(OUTPUTS_DIR.iterdir()):
if not model_dir.is_dir():
continue
model_id = model_dir.name
for user_dir in sorted(model_dir.iterdir()):
if not user_dir.is_dir():
continue
user_id = user_dir.name
if user_filter and user_id not in user_filter:
continue
for task_dir in sorted(user_dir.iterdir()):
if task_dir.is_dir():
convert_task(model_id, user_id, task_dir)
if __name__ == "__main__":
convert_outputs(set(sys.argv[1:]) or None)
print("\ndone")

332
benchmark/pibench/resume.py Executable file
View file

@ -0,0 +1,332 @@
#!/usr/bin/env python3
"""Checkpoint-resume support for the reme_eval suite.
Completion source of truth:
- outputs/reme/<persona>/<task_id>/history/*-log.jsonl (per-task logs,
flushed incrementally, survive mid-run kills)
- outputs/reme/<persona>/run/*-log.jsonl (run-level logs,
may be truncated if the process was killed before flush)
lines: "Task finished task_id=<id> status=<STATUS>"
A task counts as COMPLETED when its latest terminal status is one of
SUCCESS / MAX_TURNS / TIMEOUT. ERROR or never-started tasks stay pending.
"Latest" is decided by EVENT TIME, not by file category or read order:
each record's "timestamp" (epoch seconds, or "timestamp_iso" as fallback)
is compared across per-task and run-level logs alike, with the timestamp
embedded in the log file name as a last-resort fallback. This keeps an
old run-level SUCCESS from overriding a newer per-task ERROR when the
re-run died before the new run-level log captured the task.
Commands:
remaining <persona> [--json]
Print task_ids still to run, in data/<persona>/episode.yaml order
(one per line; --json prints {"completed": [...], "remaining": [...]}).
cleanup <persona> [--dry-run]
Surgically remove residual memory artifacts of tasks that are about
to be RE-RUN (i.e. pending tasks that left partial state because a
previous run was interrupted). This prevents answer leakage: an
interrupted task's conversation may already have been distilled into
daily notes during graceful shutdown, and re-running the task with
that memory injected would inflate scores.
Removed artifacts (only for pending tasks with residual state):
- daily/<date>/<note>.md whose frontmatter session_id matches
pibench_<task_id>_*, plus a refresh of ONLY the daily index of
the affected date(s) (daily/<date>.md), matched by the full
workspace-relative note path, never by bare file name
- digest notes with matching session_id
- session/dialog/pibench_<task_id>_*.jsonl
- mem_session/**.jsonl files containing pibench_<task_id>_
When the ReMe package is importable, the daily index refresh reuses
ReMe's own rebuild logic (reme.steps.file_io._daily_index.
refresh_day_index); otherwise index lines are dropped by exact
wikilink path match. Either way, indexes of other dates are never
touched. The ReMe watcher (init_changes_step) detects the deleted
daily notes on next bridge startup and removes them from the BM25
index itself.
Completed tasks' memories are NEVER touched by this command.
Design note (resume vs memory-wipe conflict):
A full memory wipe is a suite-level action of fresh mode (run_all.sh
without --resume) and happens before any service starts. Resume mode
never wipes; it only performs the surgical cleanup above. The two modes
are mutually exclusive, so a resumed run can never lose the cross-session
memory accumulated by completed tasks.
"""
import asyncio
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
import yaml
try: # Reuse ReMe's daily-index rebuild when running inside the ReMe venv.
from reme.steps.file_io._daily_index import refresh_day_index
except ImportError: # pragma: no cover - depends on runtime venv
refresh_day_index = None
SUITE_DIR = Path(__file__).resolve().parent
DATA_DIR = Path(os.environ.get("REME_EVAL_DATA_DIR", SUITE_DIR / "data")).resolve()
OUTPUTS_DIR = Path(os.environ.get("REME_EVAL_OUTPUTS_DIR", SUITE_DIR / "outputs")) / "reme"
WORKSPACE_ROOT = Path(
os.environ.get("REME_WORKSPACE_ROOT", SUITE_DIR / "reme_workspace"),
).resolve()
COMPLETED_STATUSES = {"SUCCESS", "MAX_TURNS", "TIMEOUT"}
TASK_FINISHED_RE = re.compile(r"Task finished task_id=(\S+) status=(\S+)")
SESSION_ID_RE = re.compile(r"^session_id:\s*(\S+)", re.MULTILINE)
NOTE_COUNT_RE = re.compile(r"(description:\s*)\d+(\s*note\(s\) today)")
LOG_FILE_TS_RE = re.compile(r"^(\d{8}_\d{6})-log\.jsonl$")
TIME_FORMAT = "%Y%m%d_%H%M%S"
def log(msg: str) -> None:
"""Print a status message to stderr."""
print(msg, file=sys.stderr)
def episode_task_order(persona: str) -> list[str]:
"""Return the ordered task ids from the persona's episode.yaml."""
episode_path = DATA_DIR / persona / "episode.yaml"
with open(episode_path, "r", encoding="utf-8") as f:
episode = yaml.safe_load(f)
return [task["task_id"] for task in episode.get("tasks", [])]
def _event_time(record: dict, file_ts: str) -> float:
"""Best-effort event time (epoch seconds) of one log record.
Prefers the record's own timestamp fields; falls back to the timestamp
embedded in the log file name so that even stripped records keep a
meaningful order. Returns 0.0 when nothing is parseable.
"""
timestamp = record.get("timestamp")
if isinstance(timestamp, (int, float)) and not isinstance(timestamp, bool):
return float(timestamp)
iso = record.get("timestamp_iso")
if isinstance(iso, str):
try:
return datetime.fromisoformat(iso).timestamp()
except ValueError:
pass
if file_ts:
try:
return datetime.strptime(file_ts, TIME_FORMAT).timestamp()
except ValueError:
pass
return 0.0
def latest_task_statuses(persona: str) -> dict[str, str]:
"""Scan per-task and run-level logs; the newest EVENT TIME wins per task.
Every "Task finished" record across both log categories is keyed by
(event_time, file timestamp, file order, line number); the record with
the highest key decides the task's status. File category and read order
alone can never override a newer record from the other category.
"""
persona_dir = OUTPUTS_DIR / persona
if not persona_dir.is_dir():
return {}
log_files = sorted(persona_dir.glob("*/history/*-log.jsonl"))
log_files += sorted(persona_dir.glob("run/*-log.jsonl"))
best: dict[str, tuple[tuple, str]] = {}
for file_order, log_file in enumerate(log_files):
ts_match = LOG_FILE_TS_RE.match(log_file.name)
file_ts = ts_match.group(1) if ts_match else ""
try:
with open(log_file, "r", encoding="utf-8") as f:
for line_no, line in enumerate(f):
if "Task finished" not in line:
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
match = TASK_FINISHED_RE.search(str(record.get("message", "")))
if not match:
continue
task_id, status = match.group(1), match.group(2)
sort_key = (_event_time(record, file_ts), file_ts, file_order, line_no)
current = best.get(task_id)
if current is None or sort_key > current[0]:
best[task_id] = (sort_key, status)
except OSError:
continue
return {task_id: status for task_id, (_, status) in best.items()}
def split_tasks(persona: str) -> tuple[list[str], list[str]]:
"""Split the episode task order into completed and remaining tasks."""
order = episode_task_order(persona)
statuses = latest_task_statuses(persona)
completed = [t for t in order if statuses.get(t) in COMPLETED_STATUSES]
remaining = [t for t in order if t not in set(completed)]
return completed, remaining
def _daily_note_session_id(note_path: Path) -> str:
try:
text = note_path.read_text(encoding="utf-8")
except OSError:
return ""
match = SESSION_ID_RE.search(text)
return match.group(1) if match else ""
class _WorkspaceFileStoreShim:
"""Structural stand-in for ReMe's file store; only workspace_path is read."""
def __init__(self, workspace_path: Path):
self.workspace_path = workspace_path
def _refresh_daily_indexes(
workspace: Path,
removed_by_date: dict[str, set[str]],
removed: list[str],
) -> None:
"""Rebuild the daily index of each affected date via ReMe's own logic."""
for date in sorted(removed_by_date):
result = asyncio.run(
refresh_day_index(_WorkspaceFileStoreShim(workspace), date, "daily"),
)
if result.get("error"):
log(f"[resume] WARNING: daily index refresh failed for {date}: {result['error']}")
continue
removed.append(f"daily/{date}.md (refreshed, {len(removed_by_date[date])} note(s) removed)")
def _strip_index_lines(
workspace: Path,
removed_by_date: dict[str, set[str]],
removed: list[str],
dry_run: bool,
) -> None:
"""Fallback index edit: drop lines that reference removed notes by full
workspace-relative wikilink path, and fix the note count. Only the index
files of affected dates are touched."""
for date in sorted(removed_by_date):
index_path = workspace / "daily" / f"{date}.md"
if not index_path.is_file():
continue
wikilinks = [f"[[{rel_path}]]" for rel_path in sorted(removed_by_date[date])]
lines = index_path.read_text(encoding="utf-8").splitlines()
kept = [line for line in lines if not any(link in line for link in wikilinks)]
if len(kept) == len(lines):
continue
note_count = sum(1 for line in kept if line.startswith("- [[daily/"))
kept = [NOTE_COUNT_RE.sub(rf"\g<1>{note_count}\2", line) for line in kept]
removed.append(f"{index_path.relative_to(workspace)} (rewritten)")
if not dry_run:
index_path.write_text("\n".join(kept) + "\n", encoding="utf-8")
def cleanup_partial_memory(persona: str, remaining: list[str], dry_run: bool = False) -> list[str]:
"""Remove partial memory artifacts of remaining tasks so they can be re-run cleanly."""
workspace = WORKSPACE_ROOT / persona
removed: list[str] = []
if not workspace.is_dir() or not remaining:
return removed
prefixes = tuple(f"pibench_{task_id}_" for task_id in remaining)
def act(path: Path, label: str) -> None:
removed.append(label)
if not dry_run:
path.unlink()
# 1) daily / digest notes distilled from interrupted sessions. For daily
# notes, remember the full workspace-relative path grouped by date so only
# the affected daily indexes are refreshed below.
removed_by_date: dict[str, set[str]] = {}
for section in ("daily", "digest"):
section_root = workspace / section
if not section_root.is_dir():
continue
for note_path in section_root.rglob("*.md"):
if note_path.parent == section_root:
continue # index files handled below
session_id = _daily_note_session_id(note_path)
if session_id.startswith(prefixes):
rel_path = note_path.relative_to(workspace).as_posix()
act(note_path, rel_path)
if section == "daily":
removed_by_date.setdefault(note_path.parent.name, set()).add(rel_path)
# 2) daily index files: refresh only the dates that lost notes, matching
# notes by their full wikilink path instead of their bare file name.
if removed_by_date:
if dry_run:
for date in sorted(removed_by_date):
removed.append(f"daily/{date}.md (would refresh index)")
elif refresh_day_index is not None:
_refresh_daily_indexes(workspace, removed_by_date, removed)
else:
_strip_index_lines(workspace, removed_by_date, removed, dry_run)
# 3) raw dialog logs of interrupted sessions
dialog_dir = workspace / "session" / "dialog"
if dialog_dir.is_dir():
for task_id in remaining:
for dialog_path in dialog_dir.glob(f"pibench_{task_id}_*.jsonl"):
act(dialog_path, str(dialog_path.relative_to(workspace)))
# 4) agent-scope session states that contain interrupted-task sessions
mem_session_dir = workspace / "mem_session"
if mem_session_dir.is_dir():
for session_path in mem_session_dir.rglob("*.jsonl"):
try:
content = session_path.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
if any(prefix in content for prefix in prefixes):
act(session_path, str(session_path.relative_to(workspace)))
return removed
def main() -> int:
"""CLI entrypoint: run 'remaining' or 'cleanup' action for a persona."""
args = sys.argv[1:]
if len(args) < 2 or args[0] not in {"remaining", "cleanup"}:
print(__doc__, file=sys.stderr)
return 2
command, persona = args[0], args[1]
completed, remaining = split_tasks(persona)
if command == "remaining":
if "--json" in args:
print(json.dumps({"completed": completed, "remaining": remaining}))
else:
for task_id in remaining:
print(task_id)
log(
f"[resume] {persona}: completed={len(completed)} "
f"({', '.join(completed) if completed else '-'}) remaining={len(remaining)}",
)
return 0
dry_run = "--dry-run" in args
removed = cleanup_partial_memory(persona, remaining, dry_run=dry_run)
if removed:
verb = "would remove" if dry_run else "removed"
log(f"[resume] {persona}: {verb} {len(removed)} partial-memory artifact(s):")
for item in removed:
log(f" - {item}")
else:
log(f"[resume] {persona}: no partial-memory artifacts to clean")
return 0
if __name__ == "__main__":
sys.exit(main())

119
benchmark/pibench/run_all.sh Executable file
View file

@ -0,0 +1,119 @@
#!/bin/bash
# Run all 5 personas with the ReMe agent, PARALLEL at a time (default 2).
# Each persona's tasks follow data/{persona}/episode.yaml order.
#
# Usage:
# bash run_all.sh # FRESH official run: wipes ALL personas'
# # ReMe memory/outputs/trace logs first,
# # then runs everything from scratch.
# bash run_all.sh --resume # Checkpoint continuation: no wipe; every
# # persona skips already-completed tasks.
# bash run_all.sh --parallel 1 # sequential (original behavior)
# bash run_all.sh --skip-eval # run phase only
#
# Memory-wipe vs resume conflict resolution:
# The full ReMe memory wipe happens ONLY here, ONLY in fresh mode (the
# default), and ONLY before any service/bridge starts. --resume never
# wipes; run_persona.sh then additionally performs a surgical cleanup of
# residual memory belonging to interrupted (to-be-re-run) tasks, so a
# resumed run keeps all completed-task memory but never inherits a partial
# task's own answer. The two modes are mutually exclusive.
set -uo pipefail
SUITE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PERSONAS=(researcher marketer law_trainee pharmacist Financier)
TRACE_ROOT="${HOME}/.nanobot/trace_logs"
PARALLEL=2
MODE="fresh"
PASS_ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
--parallel)
PARALLEL="${2:-}"; shift 2 || true
case "$PARALLEL" in (""|*[!0-9]*) echo "--parallel needs a positive integer"; exit 2 ;; esac
[ "$PARALLEL" -lt 1 ] && PARALLEL=1
[ "$PARALLEL" -gt ${#PERSONAS[@]} ] && PARALLEL=${#PERSONAS[@]}
;;
--resume)
if [ "$MODE" = "fresh_set" ]; then echo "--fresh and --resume are mutually exclusive"; exit 2; fi
MODE="resume"; shift ;;
--fresh)
if [ "$MODE" = "resume" ]; then echo "--fresh and --resume are mutually exclusive"; exit 2; fi
MODE="fresh_set"; shift ;;
--skip-eval) PASS_ARGS+=(--skip-eval); shift ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
[ "$MODE" = "fresh_set" ] && MODE="fresh"
START_TS=$(date +%Y%m%d_%H%M%S)
SUMMARY_LOG="${SUITE_DIR}/logs/run_all_${START_TS}.summary"
mkdir -p "${SUITE_DIR}/logs"
echo "############################################################"
echo "# reme_eval suite | mode=${MODE} parallel=${PARALLEL} | ${START_TS}"
echo "############################################################"
# ─── Fresh mode: suite-level wipe BEFORE anything starts ──────────────
if [ "$MODE" = "fresh" ]; then
echo "[fresh] wiping ALL personas' memory workspaces, outputs and trace logs..."
for persona in "${PERSONAS[@]}"; do
rm -rf "${SUITE_DIR}/reme_workspace/${persona}"
rm -rf "${SUITE_DIR}/outputs/reme/${persona}"
rm -rf "${TRACE_ROOT}/reme/${persona}"
rm -rf "${SUITE_DIR}/nanobot_workspace/${persona}"
done
echo "[fresh] wipe done."
else
echo "[resume] no memory wipe; personas resume after their last completed task."
fi
# ─── Run personas in batches of PARALLEL ──────────────────────────────
STATUS_LIST=()
ANY_FAILED=0
OVERALL_START=$(date +%s)
TOTAL=${#PERSONAS[@]}
for ((i = 0; i < TOTAL; i += PARALLEL)); do
BATCH=("${PERSONAS[@]:i:PARALLEL}")
BATCH_PIDS=()
BATCH_NAMES=()
echo ""
echo "============================================================"
echo "# BATCH $(( i / PARALLEL + 1 )): ${BATCH[*]} started $(date '+%F %T')"
echo "============================================================"
for persona in "${BATCH[@]}"; do
bash "${SUITE_DIR}/run_persona.sh" "${persona}" --resume ${PASS_ARGS[@]+"${PASS_ARGS[@]}"} \
> "${SUITE_DIR}/logs/suite_${persona}.log" 2>&1 &
BATCH_PIDS+=($!)
BATCH_NAMES+=("$persona")
done
for j in $(seq 0 $(( ${#BATCH[@]} - 1 ))); do
pid=${BATCH_PIDS[$j]}
persona=${BATCH_NAMES[$j]}
if wait "$pid"; then
STATUS_LIST+=("${persona}: OK")
else
rc=$?
ANY_FAILED=1
STATUS_LIST+=("${persona}: FAILED rc=${rc}")
echo "[run_all] ${persona} FAILED (rc=${rc}); see logs/suite_${persona}.log"
fi
done
done
total=$(( $(date +%s) - OVERALL_START ))
echo ""
echo "================ FINAL SUMMARY (${total}s total) ================" | tee -a "${SUMMARY_LOG}"
for line in "${STATUS_LIST[@]}"; do
echo " ${line}" | tee -a "${SUMMARY_LOG}"
done
echo "Summary: ${SUMMARY_LOG}"
if [ "${ANY_FAILED}" -ne 0 ]; then
FAILED_COUNT=$(printf '%s\n' "${STATUS_LIST[@]}" | grep -c "FAILED")
echo "[run_all] ${FAILED_COUNT} persona(s) FAILED; suite run is marked as failed." | tee -a "${SUMMARY_LOG}"
exit 1
fi
exit 0

301
benchmark/pibench/run_persona.sh Executable file
View file

@ -0,0 +1,301 @@
#!/bin/bash
# Run the full pi-bench evaluation for ONE persona with the ReMe agent.
# Tasks follow data/{persona}/episode.yaml order (runner-native).
#
# Usage: bash run_persona.sh <persona> [--fresh|--resume] [--skip-eval]
#
# Modes (default: --resume):
# --resume Checkpoint continuation. Never wipes memory. Tasks already
# finished (SUCCESS/MAX_TURNS/TIMEOUT in the task history logs)
# are skipped via repeated --task-id flags. Before starting, any
# residual memory of tasks that are about to be RE-RUN (partial
# sessions from an interrupted run) is surgically removed by
# resume.py cleanup, so re-runs don't inherit leaked answers.
# --fresh Wipes THIS persona's ReMe memory, outputs and trace logs first,
# then runs all tasks from scratch.
# The two flags are mutually exclusive. A full multi-persona memory wipe is a
# suite-level action of `run_all.sh` (fresh mode), never done here implicitly.
set -uo pipefail
SUITE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TRACE_ROOT="${HOME}/.nanobot/trace_logs"
# ─── External dependencies (pi-bench / ReMe are NOT bundled; see README) ──
if [ ! -f "${SUITE_DIR}/env.sh" ]; then
echo "env.sh not found. Run: cp env.sh.example env.sh (then fill in the TODO items)"
exit 1
fi
source "${SUITE_DIR}/env.sh"
PIBENCH_DIR="${PI_BENCH_ROOT:-}"
if [ -z "${PIBENCH_DIR}" ] || [ ! -f "${PIBENCH_DIR}/src/main.py" ]; then
echo "PI_BENCH_ROOT is unset or invalid (src/main.py not found). Set it in env.sh."
exit 1
fi
if [ ! -x "${PIBENCH_DIR}/.venv/bin/python" ] || [ ! -x "${PIBENCH_DIR}/.venv/bin/appworld" ]; then
echo "pi-bench venv incomplete: ${PIBENCH_DIR}/.venv must provide python + appworld (see README setup)."
exit 1
fi
if [ ! -x "${REME_DIR}/.venv/bin/python" ]; then
echo "ReMe venv not found: ${REME_DIR}/.venv/bin/python (check REME_DIR in env.sh)"
exit 1
fi
if [ ! -e "${SUITE_DIR}/data" ]; then
echo 'Benchmark data not linked. Run: ln -s "$PI_BENCH_ROOT/data" data'
exit 1
fi
# ─── Pre-flight: files the runner needs before any service starts ─────
MODEL_CONFIG="${SUITE_DIR}/config/models/reme.yaml"
HISTORY_CONFIG="${SUITE_DIR}/config/bench/evaluation/trace_history.yaml"
if [ ! -f "${MODEL_CONFIG}" ]; then
echo "Model config not found: ${MODEL_CONFIG} (see README directory layout)."
exit 1
fi
if [ ! -f "${HISTORY_CONFIG}" ]; then
echo "Trace history config not found: ${HISTORY_CONFIG}"
echo "pi-bench requires config/bench/evaluation/trace_history.yaml; see README."
exit 1
fi
APPWORLD_DIR="${PIBENCH_DIR}/third_party/appworld"
PI_PYTHON="${PIBENCH_DIR}/.venv/bin/python"
APPWORLD_BIN="${PIBENCH_DIR}/.venv/bin/appworld"
# resume.py runs on the ReMe venv so it can reuse ReMe's daily-index rebuild.
REME_PYTHON="${REME_DIR}/.venv/bin/python"
PERSONA="${1:-}"
if [ -z "$PERSONA" ]; then
echo "Usage: $0 <persona> [--fresh|--resume] [--skip-eval]"
exit 1
fi
shift
MODE="resume"
SKIP_EVAL=false
while [[ $# -gt 0 ]]; do
case $1 in
--fresh)
if [ "$MODE" = "resume_set" ]; then echo "--fresh and --resume are mutually exclusive"; exit 2; fi
MODE="fresh"; shift ;;
--resume)
if [ "$MODE" = "fresh" ]; then echo "--fresh and --resume are mutually exclusive"; exit 2; fi
MODE="resume_set"; shift ;;
--skip-eval) SKIP_EVAL=true; shift ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
[ "$MODE" = "resume_set" ] && MODE="resume"
# ─── Per-persona ports (pi-bench AGENTS.md convention) ────────────────
# REME_PORT: ReMe's internal HTTP service; must be unique per concurrent bridge.
case "$PERSONA" in
marketer) API_PORT=9001; MCP_PORT=10001; TEST_PORT=9998; REME_PORT=18766 ;;
law_trainee) API_PORT=9002; MCP_PORT=10002; TEST_PORT=9997; REME_PORT=18767 ;;
pharmacist) API_PORT=9003; MCP_PORT=10003; TEST_PORT=9996; REME_PORT=18768 ;;
researcher) API_PORT=9004; MCP_PORT=10004; TEST_PORT=9995; REME_PORT=18765 ;;
Financier) API_PORT=9005; MCP_PORT=10005; TEST_PORT=9994; REME_PORT=18769 ;;
*) echo "Unknown persona: $PERSONA"; exit 1 ;;
esac
API_URL="http://127.0.0.1:${API_PORT}"
MCP_URL="http://127.0.0.1:${MCP_PORT}/mcp"
TEST_URL="http://127.0.0.1:${TEST_PORT}"
LOG_DIR="${SUITE_DIR}/logs"
mkdir -p "${LOG_DIR}"
# ─── Environment (env.sh already sourced at the top) ──────────────────
WORKSPACE_DIR="${REME_WORKSPACE_ROOT}/${PERSONA}"
NANOBOT_WORKSPACE_DIR="${SUITE_DIR}/nanobot_workspace/${PERSONA}"
mkdir -p "${WORKSPACE_DIR}" "${NANOBOT_WORKSPACE_DIR}"
echo "========================================="
echo "ReMe x Pi-Bench | persona=${PERSONA} | mode=${MODE}"
echo " api=${API_PORT} mcp=${MCP_PORT} test=${TEST_PORT} reme=${REME_PORT}"
echo " model=${REME_MODEL_NAME}"
echo " memory workspace=${WORKSPACE_DIR} (persistent)"
echo "========================================="
# ─── Fresh mode: wipe this persona's state ────────────────────────────
if [ "$MODE" = "fresh" ]; then
echo "[fresh] wiping persona state: memory workspace, outputs, trace logs"
rm -rf "${WORKSPACE_DIR}"
rm -rf "${SUITE_DIR}/outputs/reme/${PERSONA}"
rm -rf "${TRACE_ROOT}/reme/${PERSONA}"
rm -rf "${NANOBOT_WORKSPACE_DIR}"
mkdir -p "${WORKSPACE_DIR}" "${NANOBOT_WORKSPACE_DIR}"
fi
# ─── Resume: determine remaining tasks + clean partial memories ───────
TASK_ARGS=()
RUN_PHASE_NEEDED=true
if [ "$MODE" = "resume" ]; then
REMAINING_JSON="$("${REME_PYTHON}" "${SUITE_DIR}/resume.py" remaining "${PERSONA}" --json)"
if [ -z "$REMAINING_JSON" ]; then
echo "Failed to compute remaining tasks"; exit 1
fi
echo "[resume] ${REMAINING_JSON}"
REMAINING_TASKS=()
while IFS= read -r tid_line; do
[ -n "$tid_line" ] && REMAINING_TASKS+=("$tid_line")
done < <("${REME_PYTHON}" "${SUITE_DIR}/resume.py" remaining "${PERSONA}" 2>/dev/null)
if [ ${#REMAINING_TASKS[@]} -eq 0 ]; then
RUN_PHASE_NEEDED=false
echo "[resume] all tasks already completed; skipping run phase"
else
# Remove residual memory of interrupted (to-be-re-run) tasks so
# re-runs don't get their own partial answers injected.
"${REME_PYTHON}" "${SUITE_DIR}/resume.py" cleanup "${PERSONA}"
for tid in "${REMAINING_TASKS[@]}"; do
TASK_ARGS+=(--task-id "$tid")
done
echo "[resume] running ${#REMAINING_TASKS[@]} remaining task(s): ${REMAINING_TASKS[*]}"
fi
fi
# ─── Port cleanup from previous runs ──────────────────────────────────
for port in ${API_PORT} ${MCP_PORT} ${TEST_PORT} ${REME_PORT}; do
pids=$(lsof -ti :${port} 2>/dev/null || true)
if [ -n "$pids" ]; then
echo "Killing stale processes on port ${port}: ${pids}"
kill -9 $pids 2>/dev/null || true
fi
done
sleep 2
PIDS=()
cleanup() {
echo "[${PERSONA}] cleaning up services..."
for pid in "${PIDS[@]:-}"; do
kill "$pid" 2>/dev/null || true
done
wait 2>/dev/null || true
}
trap cleanup EXIT INT TERM
wait_for_service() {
local url="$1" name="$2" port="$3" timeout="${4:-180}"
echo -n " waiting for ${name}..."
local start=$(date +%s)
while true; do
if curl -sf --max-time 5 "${url}" > /dev/null 2>&1; then
echo " ready"; return 0
fi
if [ -n "$port" ] && lsof -ti :${port} > /dev/null 2>&1; then
local elapsed=$(( $(date +%s) - start ))
if [ "$elapsed" -ge 10 ]; then echo " ready (port)"; return 0; fi
fi
if [ $(( $(date +%s) - start )) -ge "$timeout" ]; then
echo " TIMEOUT"; return 1
fi
sleep 2
done
}
# ─── [1/5] AppWorld API ────────────────────────────────────────────────
echo "[1/5] AppWorld API (:${API_PORT})"
(cd "${APPWORLD_DIR}" && exec "${APPWORLD_BIN}" serve apis --root . \
--port ${API_PORT}) > "${LOG_DIR}/appworld_api_${PERSONA}.log" 2>&1 &
PIDS+=($!)
if ! wait_for_service "${API_URL}/docs" "AppWorld API" "${API_PORT}" 180; then
tail -20 "${LOG_DIR}/appworld_api_${PERSONA}.log"; exit 1
fi
# ─── [2/5] AppWorld MCP ────────────────────────────────────────────────
echo "[2/5] AppWorld MCP (:${MCP_PORT})"
TOOLS_CONFIG="${SUITE_DIR}/data/${PERSONA}/tools.yaml"
(cd "${APPWORLD_DIR}" && exec "${APPWORLD_BIN}" serve mcp http --root . \
--remote-apis-url "${API_URL}" --port ${MCP_PORT} \
--tools-config-file "${TOOLS_CONFIG}") > "${LOG_DIR}/appworld_mcp_${PERSONA}.log" 2>&1 &
PIDS+=($!)
if ! wait_for_service "${MCP_URL}" "AppWorld MCP" "${MCP_PORT}" 180; then
tail -20 "${LOG_DIR}/appworld_mcp_${PERSONA}.log"; exit 1
fi
# ─── [3/5] Test Server ─────────────────────────────────────────────────
echo "[3/5] Test Server (:${TEST_PORT})"
PORT=${TEST_PORT} "${PI_PYTHON}" "${PIBENCH_DIR}/scripts/test_server.py" \
> "${LOG_DIR}/test_server_${PERSONA}.log" 2>&1 &
PIDS+=($!)
if ! wait_for_service "${TEST_URL}/sent?after=-1" "Test Server" "${TEST_PORT}" 30; then
tail -20 "${LOG_DIR}/test_server_${PERSONA}.log"; exit 1
fi
# ─── [4/5] ReMe Bridge (ReMe venv) ─────────────────────────────────────
echo "[4/5] ReMe Bridge (reme service port ${REME_PORT})"
"${REME_DIR}/.venv/bin/python" "${SUITE_DIR}/bridge_reme.py" \
--test-server-url "${TEST_URL}" \
--appworld-mcp-url "${MCP_URL}" \
--reme-dir "${REME_DIR}" \
--data-root "${SUITE_DIR}/data" \
--user-id "${PERSONA}" \
--workspace-dir "${WORKSPACE_DIR}" \
--reme-port "${REME_PORT}" \
--model-name "${REME_MODEL_NAME}" \
--model-base-url "${REME_LLM_BASE_URL}" \
--model-api-key "${REME_LLM_API_KEY}" \
> "${LOG_DIR}/bridge_${PERSONA}.log" 2>&1 &
BRIDGE_PID=$!
PIDS+=(${BRIDGE_PID})
sleep 5
if ! kill -0 "${BRIDGE_PID}" 2>/dev/null; then
echo "Bridge failed to start:"; tail -30 "${LOG_DIR}/bridge_${PERSONA}.log"; exit 1
fi
for i in $(seq 1 12); do
if grep -q "Bridge started:" "${LOG_DIR}/bridge_${PERSONA}.log" 2>/dev/null; then
echo " bridge initialized"; break
fi
sleep 5
done
grep -q "Bridge started:" "${LOG_DIR}/bridge_${PERSONA}.log" 2>/dev/null || {
echo "WARNING: bridge may not be ready:"; tail -20 "${LOG_DIR}/bridge_${PERSONA}.log"; }
# ─── [5/5] Runner (run phase) ──────────────────────────────────────────
if [ "$RUN_PHASE_NEEDED" = true ]; then
echo "[5/5] Runner: run phase (episode order from data/${PERSONA}/episode.yaml)"
cd "${SUITE_DIR}"
BENCH_TEST_SERVER_URL="${TEST_URL}" PYTHONPATH="${PIBENCH_DIR}" \
"${PI_PYTHON}" -m src.main \
--model-config "${MODEL_CONFIG}" \
--history-config-path "${HISTORY_CONFIG}" \
--mode run --user-id "${PERSONA}" \
--workspace-dir "${NANOBOT_WORKSPACE_DIR}" \
${TASK_ARGS[@]+"${TASK_ARGS[@]}"} \
2>&1 | tee "${LOG_DIR}/runner_run_${PERSONA}.log"
RUN_EXIT=${PIPESTATUS[0]}
if [ ${RUN_EXIT} -ne 0 ]; then
echo "Run phase failed (exit ${RUN_EXIT}). Logs: ${LOG_DIR}/"
exit ${RUN_EXIT}
fi
else
echo "[5/5] Runner: run phase skipped (all tasks completed)"
fi
if [ "$SKIP_EVAL" = true ]; then
echo "Skipping eval (--skip-eval)"
exit 0
fi
# ─── Trace conversion + eval phase (always over all available traces) ──
echo "Converting trace logs..."
"${PI_PYTHON}" "${SUITE_DIR}/fix_trace_logs.py" "${PERSONA}"
echo "Runner: eval phase"
cd "${SUITE_DIR}"
BENCH_TEST_SERVER_URL="${TEST_URL}" PYTHONPATH="${PIBENCH_DIR}" \
"${PI_PYTHON}" -m src.main \
--model-config "${MODEL_CONFIG}" \
--history-config-path "${HISTORY_CONFIG}" \
--mode eval --user-id "${PERSONA}" \
--workspace-dir "${NANOBOT_WORKSPACE_DIR}" \
2>&1 | tee "${LOG_DIR}/runner_eval_${PERSONA}.log"
EVAL_EXIT=${PIPESTATUS[0]}
echo ""
echo "========================================="
echo "persona=${PERSONA} finished (eval exit=${EVAL_EXIT})"
echo " results : ${SUITE_DIR}/outputs/reme/${PERSONA}/"
echo " memory : ${WORKSPACE_DIR}/"
echo " logs : ${LOG_DIR}/"
echo "========================================="
exit ${EVAL_EXIT}

View file

@ -0,0 +1,98 @@
## Towards Robust Tool Use in Agents via Experience-Driven Adaptive Guidance
**Language**: English (default) / [中文](./README_ZH.md)
> Paper: [arXiv:2608.03403](https://arxiv.org/abs/2608.03403)
> Code: [https://github.com/WangCan1178/ExpG](https://github.com/WangCan1178/ExpG)
<p align="center">
<img src="gitcha.png" alt="ExpG challenges and overview" width="85%">
</p>
### Overview
This folder archives **ExpG**, a tool-use enhancement built on [Agentscope ReMe](https://github.com/agentscope-ai/ReMe). ExpG mines, distills, and reuses experience from historical tool calls to provide **capability boundaries** and **best-practice guidance**, which helps agents:
- Select and invoke tools more robustly under dynamic or noisy environments;
- Let smaller models with guidance outperform larger, memoryless baselines;
- Improve consistently across tool selection, tool calling, and response generation.
**How ReMe is used:** Start the Tool Memory service; historical tool calls are written and evaluated via `add_tool_call_result`, distilled into tool-level guidance via `summary_tool_memory`, then retrieved and injected into later reasoning via `retrieve_tool_memory`. ReMe provides the vector store and service APIs; the acquisition / distillation / reuse strategy is implemented by ExpG. Full implementation and experiments are in [WangCan1178/ExpG](https://github.com/WangCan1178/ExpG).
---
### ExpG Mechanism
ExpG treats tool invocations as learnable experience and runs a three-stage pipeline:
1. **Experience Acquisition**
- Analyze invocation quality from historical trajectories (success/failure, cost, latency, etc.);
- Build structured experience units per tool, recording context, parameter patterns, and outcomes.
2. **Experience Distillation**
- Filter noisy or unhelpful experiences and keep representative patterns;
- Aggregate by equivalence classes to cover common and rare failure modes;
- Summarize with an LLM into generalizable textual guidance.
3. **Experience Reuse**
- Retrieve relevant experience / guidance for future tasks;
- Inject guidance into tool selection, argument generation, and response synthesis;
- Improve stability under dynamic environments and imperfect feedback.
---
### Main Results
Performance comparison (%) across MetaTool, API-Bank, and BFCL-V3. **Bold** indicates the best results within each model.
| Model | Method | MetaTool Pass@1 | MetaTool Avg@3 | MetaTool Pass@3 | API-Bank Pass@1 | API-Bank Avg@3 | API-Bank Pass@3 | BFCL-V3 Pass@1 | BFCL-V3 Avg@3 | BFCL-V3 Pass@3 | Total Pass@1 | Total Avg@3 | Total Pass@3 |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| GPT-5 nano | No Method | 72.62 | 72.76 | 78.49 | 82.96 | 83.46 | 86.97 | 53.80 | 53.00 | 60.95 | 70.82 | 70.62 | 76.63 |
| GPT-5 nano | Few-shot | 74.12 | 75.11 | 82.32 | 83.71 | 83.96 | **87.22** | 56.18 | 55.24 | 61.39 | 72.36 | 72.65 | 79.28 |
| GPT-5 nano | DRAFT | 73.94 | 73.04 | 78.97 | 84.21 | 83.46 | **87.22** | 57.27 | 57.27 | 62.26 | 72.52 | 71.58 | 77.23 |
| GPT-5 nano | Mem0 | 74.96 | 76.13 | 82.92 | 84.96 | 85.21 | **87.22** | 60.95 | 61.61 | 65.08 | 73.98 | 74.67 | 80.35 |
| GPT-5 nano | **ExpG** | **81.67** | **82.07** | **84.60** | **86.72** | **86.55** | **87.22** | **64.43** | **63.99** | **66.38** | **79.32** | **79.22** | **81.69** |
| DeepSeek-V3 | No Method | 83.10 | 82.94 | 84.66 | 84.71 | 84.38 | 85.46 | 58.79 | 59.65 | 65.94 | 78.92 | 78.66 | 81.37 |
| DeepSeek-V3 | Few-shot | 82.74 | 83.90 | 86.28 | 85.21 | 84.63 | 86.22 | 60.52 | 60.30 | 67.90 | 79.08 | 79.45 | 82.92 |
| DeepSeek-V3 | DRAFT | 80.23 | 80.79 | 82.44 | 84.96 | 85.63 | 86.47 | 62.26 | 61.61 | 68.55 | 77.70 | 77.80 | 80.54 |
| DeepSeek-V3 | Mem0 | 83.88 | 84.56 | 86.40 | 85.46 | 85.55 | 86.47 | 65.08 | 65.15 | 68.33 | 80.70 | 80.91 | 83.12 |
| DeepSeek-V3 | **ExpG** | **85.26** | **85.38** | **86.52** | **87.72** | **87.39** | **87.97** | **69.41** | **69.92** | **72.02** | **82.76** | **82.61** | **84.11** |
| Qwen3-8B | No Method | 76.51 | 76.97 | 77.71 | 83.96 | 83.88 | 84.21 | 58.79 | 58.28 | 60.30 | 74.46 | 74.41 | 75.56 |
| Qwen3-8B | Few-shot | 79.93 | 79.83 | 82.92 | 83.71 | 82.62 | 84.96 | 60.09 | 59.29 | 61.39 | 76.91 | 76.27 | 79.32 |
| Qwen3-8B | DRAFT | 78.19 | 77.33 | 77.89 | 85.71 | 84.96 | 85.46 | 60.74 | 60.30 | 62.91 | 76.20 | 75.18 | 76.35 |
| Qwen3-8B | Mem0 | 75.07 | 75.47 | 82.38 | 86.22 | 86.05 | 86.47 | 63.34 | 64.93 | 66.16 | 74.69 | 74.98 | 80.07 |
| Qwen3-8B | **ExpG** | **83.52** | **84.88** | **85.08** | **86.47** | **87.89** | **87.97** | **67.46** | **66.96** | **68.33** | **81.06** | **81.82** | **82.48** |
| Qwen3-32B | No Method | 80.05 | 79.43 | 80.17 | 84.71 | 84.88 | 85.21 | 65.15 | 65.08 | 66.16 | 78.05 | 77.55 | 78.41 |
| Qwen3-32B | **ExpG** | **84.68** | **85.02** | **86.28** | **86.97** | **87.30** | **87.72** | **70.72** | **71.01** | **73.32** | **82.48** | **82.56** | **84.14** |
| Qwen3-235B | No Method | 78.25 | 79.23 | 80.29 | 85.46 | 85.46 | 85.71 | 71.37 | 71.15 | 73.54 | 78.13 | 78.49 | 79.91 |
| Qwen3-235B | **ExpG** | **86.34** | **86.70** | **86.94** | **87.47** | **86.97** | **88.22** | **79.61** | **78.52** | **80.04** | **85.29** | **84.98** | **85.69** |
---
### Reference Code
| Path | Role |
| --- | --- |
| [`tool_memory.py`](./tool_memory.py) | HTTP client for official ReMe Tool Memory APIs (`add_tool_call_result` / `summary_tool_memory` / `retrieve_tool_memory`) |
| [`parse_tool_call_result_prompt.yaml`](./parse_tool_call_result_prompt.yaml) | Prompt for multi-aspect evaluation of each tool call |
| [`summary_tool_memory_prompt.yaml`](./summary_tool_memory_prompt.yaml) | Prompt for summarizing tool call history into guidance |
| [`tool_memory_flows.yaml`](./tool_memory_flows.yaml) | Tool Memory flow / op config excerpt |
These are reference snippets. For the full runnable codebase, see [WangCan1178/ExpG](https://github.com/WangCan1178/ExpG).
---
### Citation
```bibtex
@misc{wang2026expg,
title = {Towards Robust Tool Use in Agents via Experience-Driven Adaptive Guidance},
author = {Can Wang and Haoran Chen and Li Yu and Ding Hao and Bohai Zhao and Zhaoyang Liu and Zhiying Tu},
year = {2026},
eprint = {2608.03403},
archivePrefix = {arXiv},
primaryClass = {cs.AI},
url = {https://arxiv.org/abs/2608.03403},
howpublished = {\url{https://github.com/WangCan1178/ExpG}}
}
```

View file

@ -0,0 +1,98 @@
## Towards Robust Tool Use in Agents via Experience-Driven Adaptive Guidance
**语言**:中文 / [English](./README.md)
> 论文:[arXiv:2608.03403](https://arxiv.org/abs/2608.03403)
> 代码:[https://github.com/WangCan1178/ExpG](https://github.com/WangCan1178/ExpG)
<p align="center">
<img src="gitcha.png" alt="ExpG 挑战与概览" width="85%">
</p>
### 简介
本目录归档基于 [Agentscope ReMe](https://github.com/agentscope-ai/ReMe) 的工具使用增强工作 **ExpG**:在 ReMe 记忆框架之上,从历史工具调用中挖掘、提炼并复用经验,为智能体提供工具的 **能力边界****最佳实践指导**,从而:
- 在动态或有噪环境下更鲁棒地选择和调用工具;
- 让较小模型在带有经验指导时超越更大、但无记忆的基线;
- 在工具选择、工具调用和响应生成等多个阶段带来一致收益。
**如何使用 ReMe** 启动 Tool Memory 服务后,历史工具调用经 `add_tool_call_result` 写入并评估,经 `summary_tool_memory` 蒸馏成工具级指导,再经 `retrieve_tool_memory` 取回并注入后续推理。向量存储与服务接口由 ReMe 提供,经验获取 / 蒸馏 / 复用策略由 ExpG 实现。完整实现与实验见 [WangCan1178/ExpG](https://github.com/WangCan1178/ExpG)。
---
### ExpG 机制概览
ExpG 将工具调用视为可学习经验,并通过三阶段流水线完成经验的获取、提炼与复用:
1. **经验获取Experience Acquisition**
- 从历史工具调用轨迹中分析调用质量(成功/失败、代价、时间等);
- 针对不同工具构建结构化的经验单元,记录调用上下文、参数模式和结果。
2. **经验蒸馏Experience Distillation**
- 过滤无效 / 噪声经验,保留具有代表性的调用模式;
- 基于“等价类”视角对经验进行聚合,覆盖常见模式与稀有失败模式;
- 使用 LLM 对经验进行总结形成可泛化的文本化指导guidance
3. **经验复用Experience Reuse**
- 在未来任务中,根据当前工具调用上下文检索相关经验 / 指导;
- 将经验引导融入到工具选择、参数生成和响应整理等环节;
- 使得代理在面对动态环境和不完美反馈时仍能保持稳定表现。
---
### 主实验结果
MetaTool、API-Bank、BFCL-V3 上的性能对比(%)。**加粗**为各模型组内最优。
| Model | Method | MetaTool Pass@1 | MetaTool Avg@3 | MetaTool Pass@3 | API-Bank Pass@1 | API-Bank Avg@3 | API-Bank Pass@3 | BFCL-V3 Pass@1 | BFCL-V3 Avg@3 | BFCL-V3 Pass@3 | Total Pass@1 | Total Avg@3 | Total Pass@3 |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| GPT-5 nano | No Method | 72.62 | 72.76 | 78.49 | 82.96 | 83.46 | 86.97 | 53.80 | 53.00 | 60.95 | 70.82 | 70.62 | 76.63 |
| GPT-5 nano | Few-shot | 74.12 | 75.11 | 82.32 | 83.71 | 83.96 | **87.22** | 56.18 | 55.24 | 61.39 | 72.36 | 72.65 | 79.28 |
| GPT-5 nano | DRAFT | 73.94 | 73.04 | 78.97 | 84.21 | 83.46 | **87.22** | 57.27 | 57.27 | 62.26 | 72.52 | 71.58 | 77.23 |
| GPT-5 nano | Mem0 | 74.96 | 76.13 | 82.92 | 84.96 | 85.21 | **87.22** | 60.95 | 61.61 | 65.08 | 73.98 | 74.67 | 80.35 |
| GPT-5 nano | **ExpG** | **81.67** | **82.07** | **84.60** | **86.72** | **86.55** | **87.22** | **64.43** | **63.99** | **66.38** | **79.32** | **79.22** | **81.69** |
| DeepSeek-V3 | No Method | 83.10 | 82.94 | 84.66 | 84.71 | 84.38 | 85.46 | 58.79 | 59.65 | 65.94 | 78.92 | 78.66 | 81.37 |
| DeepSeek-V3 | Few-shot | 82.74 | 83.90 | 86.28 | 85.21 | 84.63 | 86.22 | 60.52 | 60.30 | 67.90 | 79.08 | 79.45 | 82.92 |
| DeepSeek-V3 | DRAFT | 80.23 | 80.79 | 82.44 | 84.96 | 85.63 | 86.47 | 62.26 | 61.61 | 68.55 | 77.70 | 77.80 | 80.54 |
| DeepSeek-V3 | Mem0 | 83.88 | 84.56 | 86.40 | 85.46 | 85.55 | 86.47 | 65.08 | 65.15 | 68.33 | 80.70 | 80.91 | 83.12 |
| DeepSeek-V3 | **ExpG** | **85.26** | **85.38** | **86.52** | **87.72** | **87.39** | **87.97** | **69.41** | **69.92** | **72.02** | **82.76** | **82.61** | **84.11** |
| Qwen3-8B | No Method | 76.51 | 76.97 | 77.71 | 83.96 | 83.88 | 84.21 | 58.79 | 58.28 | 60.30 | 74.46 | 74.41 | 75.56 |
| Qwen3-8B | Few-shot | 79.93 | 79.83 | 82.92 | 83.71 | 82.62 | 84.96 | 60.09 | 59.29 | 61.39 | 76.91 | 76.27 | 79.32 |
| Qwen3-8B | DRAFT | 78.19 | 77.33 | 77.89 | 85.71 | 84.96 | 85.46 | 60.74 | 60.30 | 62.91 | 76.20 | 75.18 | 76.35 |
| Qwen3-8B | Mem0 | 75.07 | 75.47 | 82.38 | 86.22 | 86.05 | 86.47 | 63.34 | 64.93 | 66.16 | 74.69 | 74.98 | 80.07 |
| Qwen3-8B | **ExpG** | **83.52** | **84.88** | **85.08** | **86.47** | **87.89** | **87.97** | **67.46** | **66.96** | **68.33** | **81.06** | **81.82** | **82.48** |
| Qwen3-32B | No Method | 80.05 | 79.43 | 80.17 | 84.71 | 84.88 | 85.21 | 65.15 | 65.08 | 66.16 | 78.05 | 77.55 | 78.41 |
| Qwen3-32B | **ExpG** | **84.68** | **85.02** | **86.28** | **86.97** | **87.30** | **87.72** | **70.72** | **71.01** | **73.32** | **82.48** | **82.56** | **84.14** |
| Qwen3-235B | No Method | 78.25 | 79.23 | 80.29 | 85.46 | 85.46 | 85.71 | 71.37 | 71.15 | 73.54 | 78.13 | 78.49 | 79.91 |
| Qwen3-235B | **ExpG** | **86.34** | **86.70** | **86.94** | **87.47** | **86.97** | **88.22** | **79.61** | **78.52** | **80.04** | **85.29** | **84.98** | **85.69** |
---
### 参考代码
| 路径 | 作用 |
| --- | --- |
| [`tool_memory.py`](./tool_memory.py) | 官方风格 ReMe Tool Memory HTTP 客户端(`add_tool_call_result` / `summary_tool_memory` / `retrieve_tool_memory` |
| [`parse_tool_call_result_prompt.yaml`](./parse_tool_call_result_prompt.yaml) | 单次工具调用多维评估用的 prompt |
| [`summary_tool_memory_prompt.yaml`](./summary_tool_memory_prompt.yaml) | 将工具调用历史总结为 guidance 的 prompt |
| [`tool_memory_flows.yaml`](./tool_memory_flows.yaml) | Tool Memory 相关的 flow / op 配置摘录 |
以上为参考片段。完整可运行代码见 [WangCan1178/ExpG](https://github.com/WangCan1178/ExpG)。
---
### 引用
```bibtex
@misc{wang2026expg,
title = {Towards Robust Tool Use in Agents via Experience-Driven Adaptive Guidance},
author = {Can Wang and Haoran Chen and Li Yu and Ding Hao and Bohai Zhao and Zhaoyang Liu and Zhiying Tu},
year = {2026},
eprint = {2608.03403},
archivePrefix = {arXiv},
primaryClass = {cs.AI},
url = {https://arxiv.org/abs/2608.03403},
howpublished = {\url{https://github.com/WangCan1178/ExpG}}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View file

@ -0,0 +1,49 @@
prompt: |
You are an expert in evaluating tool invocation process. The tool is invoked by an AI agent.
Tool invocation Information:
- Tool Name: {tool_name}
- Success Flag: {success_flag}
- Time Cost: {time_cost}s
- Token Cost: {token_cost} tokens
- Agent Context: {context}
- Input Parameters: {input_params}
- Tool Response: {response}
- Tool Schema: {schema}
Evaluation Method:
Start from a default score list of scores = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0].
For each item below that is satisfied, assign 1 point to the corresponding index.
The final scores should be a list of 10 integers, each being either 0 or 1.
1. Use Quality (total 2 points. If context is provided, use it as an aid when evaluating):
- Index 1: Should the tool be invoked now? Consider whether all necessary information for the tool's invocation is ready, and whether the tool execution environment is correct. If it is a multi-round conversation, also consider the dependency relationships of the tool chain.
- Index 2: If should, is the chosen tool appropriate?
2. Input Quality (total 4 points. When evaluating, consider both the context and the tool schema):
- Index 3: Are all required parameters provided?
- Index 4: Are the input parameters valid and supported by the tool?
- Index 5: Are the input parameters in the correct format for their respective fields?
- Index 6: Does the value (content) of input parameter correctly reflect and match the given context?
3. Response Quality (total 4 points):
- Index 7: Does the response provide meaningful and useful information? Or are there any error messages or information that can be used as guidance for agent invoking tool better?
- Index 8: Does the response match the tool's intended purpose/function?
- Index 9: Does the response value correct (content appropriate) given the input parameters?
- Index 10: Does the response help accomplish the task within the given context?
Important:
1. Sometimes there is not enough information in the context or schema to make a complete evaluation. In such cases, make your best judgment based on the available information.
2. Some tools (commonly system tools such as mkdir, touch, echo, etc.) modify the external environment. Since these results cannot be obtained, they return "None" as the response. At this point, all the scores in the quality of the response should be obtained and should not be seen as a problem for the tool.
3. Evaluation independently from the success flag. The success_flag indicates whether the tool executed without technical errors. The evaluation should evaluate the quality of the tool invocation. A tool can execute successfully (Success Flag=1) but still produce low-quality or irrelevant responses, leading to a low evaluation score.
4. Sometimes an agent will execute multiple steps and invoke multiple tools to complete a task, but you only need to evaluate the use of one tool for one of the steps, not whether the final task is completed or not.
Answer Format:
Please provide your answer in the following JSON format:
```json
{
"scores": [0,0,0,0,0,0,0,0,0,0],
"explanation": "A brief evaluation (2-3 sentences) explaining the quality of the tool invocation, based on your evaluation. Low-quality aspects need to be reified, especially the causes of tool invocation errors."
}
```

View file

@ -0,0 +1,32 @@
prompt: |
You are an expert in analyzing tool usage patterns and generating practical usage guidance for agents.
Tool Information:
- Tool Name: {tool_name}
- Tool Schema: {tool_schema}
Recent Tool Invocation Experiences:
{experiences}
Important:
1. Assume the tool (tool schema) can't be changed, your task is to guide agent to use it better.
2. Your answer must be based on the information given, don't make it up. If not enough data, state "Not enough data to determine Core Function/Success Patterns/Common Issues/Best Practices."
3. Your answer will be used to guide the use of the tool in the future, so do not include content related to recent tool invocation experience such as "case #3" or "Call #2", but some values can be used as examples.
4. Pay attention to information not mentioned in the tool schema, such as the response upon successful tool invocation. It's also welcome to uncover insights, such as how tools can be used more effectively, and possible dependencies between tools. But if they aren't, don't make them up.
5. Finally, to avoid deriving incorrect guidance from individual invocation, check whether, if the agent follows the proposed guidance, it can perform better on all recent invocation histories. If not, revise the guidance until it can. Specifically:
- Don't write guidance in an absolute tone without a very deterministic message (meaning that all invocation histories are satisfied, otherwise it will result in failure).
- Sometimes there may be inconsistencies. Consider whether this is due to the context in which the tool is being used.
Your Task:
Based on the tool invocation history, generate a concise and logical tool usage guidance following this structure:
1. Core Function: What this tool does and when to use it.
2. Success Patterns: Parameter patterns and usage scenarios that work well.
3. Common Issues: Main pitfalls to avoid and why they fail.
4. Best Practices: 2-3 actionable recommendations.
Answer Format:
Provide a structured, concise guidance (max 200 words). Focus on actionable insights derived from actual usage data. Avoid generic advice and think step by step.
```txt
Your concise, data-driven tool usage guidance
```

View file

@ -0,0 +1,234 @@
"""Official-style ReMe Tool Memory HTTP helpers.
Aligned with ReMe Tool Memory HTTP APIs (see ReMe cookbook
``use_tool_memory_demo.py`` and docs under ``docs/tool_memory/``):
- ``add_tool_call_result``
- ``summary_tool_memory``
- ``retrieve_tool_memory``
Response memories are read from ``metadata.memory_list[].content``.
This module does not use ExpG-only fields such as ``no_persist``,
``source_task``, or ``add_to``.
"""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
import httpx
logger = logging.getLogger(__name__)
DEFAULT_BASE_URL = "http://localhost:8002"
class ToolMemoryFetcher:
"""HTTP client for ReMe Tool Memory endpoints."""
def __init__(
self,
workspace_id: str,
base_url: str = DEFAULT_BASE_URL,
timeout: float = 60.0,
) -> None:
self.workspace_id = workspace_id
self.base_url = base_url.rstrip("/")
self.timeout = timeout
def _url(self, endpoint: str) -> str:
return f"{self.base_url}/{endpoint.lstrip('/')}"
@staticmethod
def _join_tool_names(tool_names: List[str] | str) -> str:
if isinstance(tool_names, str):
return tool_names
return ",".join(tool_names)
@staticmethod
def _memory_list(payload: Dict[str, Any]) -> List[Dict[str, Any]]:
metadata = payload.get("metadata") or {}
if not isinstance(metadata, dict):
return []
memory_list = metadata.get("memory_list") or []
return memory_list if isinstance(memory_list, list) else []
@classmethod
def _content_by_tool(cls, payload: Dict[str, Any]) -> Dict[str, str]:
result: Dict[str, str] = {}
for memory in cls._memory_list(payload):
if not isinstance(memory, dict):
continue
tool_name = str(memory.get("when_to_use") or "").strip()
content = memory.get("content") or ""
if tool_name:
result[tool_name] = str(content)
return result
async def add_tool_call_result_async(
self,
tool_call_results: List[Dict[str, Any]],
) -> Dict[str, Any]:
"""Call ``add_tool_call_result``."""
async with httpx.AsyncClient() as client:
response = await client.post(
self._url("add_tool_call_result"),
json={
"workspace_id": self.workspace_id,
"tool_call_results": tool_call_results,
},
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
async def summary_tool_memory_async(
self,
tool_names: List[str] | str,
) -> Dict[str, Any]:
"""Call ``summary_tool_memory``."""
async with httpx.AsyncClient() as client:
response = await client.post(
self._url("summary_tool_memory"),
json={
"workspace_id": self.workspace_id,
"tool_names": self._join_tool_names(tool_names),
},
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
async def retrieve_tool_memory_async(
self,
tool_names: List[str] | str,
) -> Dict[str, Any]:
"""Call ``retrieve_tool_memory``."""
async with httpx.AsyncClient() as client:
response = await client.post(
self._url("retrieve_tool_memory"),
json={
"workspace_id": self.workspace_id,
"tool_names": self._join_tool_names(tool_names),
},
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
async def collect_memory_async(
self,
tool_names: List[str],
) -> Dict[str, str]:
"""Summarize then retrieve guidance for tools.
Returns:
Mapping from tool name to memory ``content`` string.
"""
if not tool_names:
return {}
names = self._join_tool_names(tool_names)
try:
summary = await self.summary_tool_memory_async(names)
if not summary.get("success"):
logger.warning("summary_tool_memory failed for %s", names)
except Exception as exc: # noqa: BLE001
logger.warning("summary_tool_memory error for %s: %s", names, exc)
try:
retrieved = await self.retrieve_tool_memory_async(names)
except Exception as exc: # noqa: BLE001
logger.warning("retrieve_tool_memory error for %s: %s", names, exc)
return {}
if not retrieved.get("success"):
logger.warning("retrieve_tool_memory failed for %s", names)
return {}
return self._content_by_tool(retrieved)
def add_tool_call_result(
self,
tool_call_results: List[Dict[str, Any]],
) -> Dict[str, Any]:
"""Sync wrapper for ``add_tool_call_result``."""
with httpx.Client() as client:
response = client.post(
self._url("add_tool_call_result"),
json={
"workspace_id": self.workspace_id,
"tool_call_results": tool_call_results,
},
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
def summary_tool_memory(self, tool_names: List[str] | str) -> Dict[str, Any]:
"""Sync wrapper for ``summary_tool_memory``."""
with httpx.Client() as client:
response = client.post(
self._url("summary_tool_memory"),
json={
"workspace_id": self.workspace_id,
"tool_names": self._join_tool_names(tool_names),
},
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
def retrieve_tool_memory(self, tool_names: List[str] | str) -> Dict[str, Any]:
"""Sync wrapper for ``retrieve_tool_memory``."""
with httpx.Client() as client:
response = client.post(
self._url("retrieve_tool_memory"),
json={
"workspace_id": self.workspace_id,
"tool_names": self._join_tool_names(tool_names),
},
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
def collect_memory(self, tool_names: List[str]) -> Dict[str, str]:
"""Sync wrapper for summarize + retrieve.
Prefer ``collect_memory_async`` inside an existing event loop.
"""
if not tool_names:
return {}
names = self._join_tool_names(tool_names)
try:
summary = self.summary_tool_memory(names)
if not summary.get("success"):
logger.warning("summary_tool_memory failed for %s", names)
except Exception as exc: # noqa: BLE001
logger.warning("summary_tool_memory error for %s: %s", names, exc)
try:
retrieved = self.retrieve_tool_memory(names)
except Exception as exc: # noqa: BLE001
logger.warning("retrieve_tool_memory error for %s: %s", names, exc)
return {}
if not retrieved.get("success"):
logger.warning("retrieve_tool_memory failed for %s", names)
return {}
return self._content_by_tool(retrieved)
def get_memory_content(
self,
tool_names: List[str] | str,
) -> Optional[str]:
"""Retrieve and join memory contents for the given tools."""
payload = self.retrieve_tool_memory(tool_names)
if not payload.get("success"):
return None
contents = [content for content in self._content_by_tool(payload).values() if content]
return "\n\n".join(contents) if contents else None

View file

@ -0,0 +1,45 @@
# Tool Memory flow / op config excerpt used by ExpG.
# Full runnable code: https://github.com/WangCan1178/ExpG
flow:
retrieve_tool_memory:
flow_content: retrieve_tool_memory_op
description: "Retrieves tool memories from the vector database based on tool names to provide tool usage patterns and best practices"
input_schema:
tool_names:
type: string
description: "Comma-separated tool names (e.g., 'tool_name1,tool_name2')"
required: true
add_tool_call_result:
flow_content: parse_tool_call_result_op >> update_vector_store_op
description: "Evaluates and adds tool call results to the tool memory database, creating new memory or updating existing memory for the specified tool"
input_schema:
tool_call_results:
type: array
description: "List of tool call result objects, each containing: tool_name, input, output, success, time_cost, token_cost, create_time"
required: true
summary_tool_memory:
flow_content: summary_tool_memory_op >> update_vector_store_op
description: "Analyzes tool call history and generates comprehensive usage patterns, best practices, and recommendations for the specified tools"
input_schema:
tool_names:
type: string
description: "Comma-separated tool names to summarize (e.g., 'tool_name1,tool_name2')"
required: true
op:
parse_tool_call_result_op:
backend: parse_tool_call_result_op
llm: default
params:
max_history_tool_call_cnt: 100
evaluation_sleep_interval: 1.0
summary_tool_memory_op:
backend: summary_tool_memory_op
llm: default
params:
data_from: '2025-09-10 10:56:58'
summary_sleep_interval: 1.0

View file

@ -1,23 +0,0 @@
# ReMe 仓库文档
本目录保存 ReMe 仓库 README 直接引用的中英文补充说明和图片资源,不作为文档站点的构建或部署来源。
面向用户发布的中英文文档位于 [agentscope-ai/docs](https://github.com/agentscope-ai/docs) 仓库,并由该仓库统一完成版本管理和 Mintlify 部署。
## 目录用途
```text
docs/
├── README.md 本目录的维护说明
├── doc.md 当前文档设计与维护边界
├── en/ README 引用的英文补充说明
├── zh/ README 引用的中文补充说明
└── figure/ ReMe README 使用的图片资源
```
## 维护原则
- `en/``zh/` 保持精简,服务 README 中需要进一步解释的功能与场景;修改路径时同步更新 README 链接。
- 具体实现以源码、schema、测试和运行时帮助为准避免维护重复且容易过期的开发手册。
- README 引用的图片保留在 `figure/`;发布文档需要图片时,在统一文档仓库的 `images/reme/` 中维护对应副本。
- 网页文档、导航、版本和部署在统一文档仓库中维护。

View file

@ -1,85 +0,0 @@
# ReMe 文档设计
本文定义 ReMe 文档的内容边界和维护方式。目标是让文档保持精简、稳定,并适合用户与 AI coding agent 快速理解。
## 两类文档,两种职责
| 位置 | 用途 | 是否部署 |
|---|---|---|
| `ReMe/docs/` | README 引用的中英文补充说明和图片 | 否 |
| `agentscope-ai/docs/reme/<version>/` | 面向用户的中英文产品文档 | 是 |
ReMe 仓库维护 `docs/en/``docs/zh/` 中供 README 直接引用的页面,但不把它们作为网页部署来源。网站内容、发布、版本选择、
导航和重定向都由统一文档仓库负责。
## 内容原则
### Concepts 只讲理念
Concepts 应解释 ReMe 为什么这样设计,而不是逐项描述组件和流水线实现。核心判断包括:
- **Memory as File**:记忆首先是用户拥有、可读写和可迁移的文件。
- **Memory from Experience**:长期记忆来自经验的提炼、修正和合并,而不是无限累积上下文。
- **Human-Agent Shared Memory**:用户和 Agent 共同读写同一份可见记忆。
- **Connected and Traceable**:长期结论可以通过链接回到来源和上下文。
算法、索引、Job、Step 和存储实现只有在帮助解释理念取舍时才进入 Concepts。
### Development 保持轻量
现代开发主要由 AI 直接阅读源码、schema 和测试完成。Development 只需要提供:
- 开发环境和最小验证命令;
- 代码目录入口;
- 兼容性与贡献要求;
- 哪些源码或 schema 是权威依据。
不为每个类、组件或扩展点编写重复的开发手册,也不维护 `generic_agent` 一类泛化教程。
### Reference 只记录稳定契约
Reference 记录 workspace、配置入口、CLI、HTTP、MCP 和文件格式的稳定语义。精确参数交给运行时帮助、Pydantic schema 和源码,避免文档复制一份容易失真的接口定义。
### Guides 只保留已验证路径
接入文档应对应真实、可验证的工作流。目前优先维护 Claude Code、QwenPaw以及 Skill、CLI、MCP、HTTP、Python 的选择说明。没有可验证实现的框架不提前创建占位页。
## 发布文档结构
ReMe 参考 AgentScope 的版本目录和导航方式:
```text
agentscope-ai/docs/
├── reme/
│ └── 0.4.0.6/
│ ├── en/
│ └── zh/
└── images/
└── reme/
```
每个语言版本保持三组导航:
1. **Get Started / 快速开始**Index、Overview、Quick Start、Concepts。
2. **Integrate / 接入**接入选择、Claude Code、QwenPaw。
3. **Reference / 查阅与参与**Reference、Support、Contributing。
ReMe 使用项目级别的 `/reme/latest/``/reme/stable/` 别名,不影响 AgentScope 自己的 `/latest/``/stable/`
## 变更应该写在哪里
| 变更类型 | ReMe 仓库 | 统一文档仓库 |
|---|---|---|
| 产品理念或长期设计判断 | 更新 `docs/doc.md` 或相关设计记录 | 必要时同步 Concepts |
| 用户可见的安装、配置或行为 | 源码、schema、测试影响 README 时同步 `docs/en/``docs/zh/` | 更新对应版本的用户文档 |
| 内部重构或组件调整 | 以代码和测试表达 | 稳定契约未变时无需更新 |
| README 图片 | 更新 `docs/figure/` | 发布页使用时同步到 `images/reme/` |
| 新版本发布 | 更新版本号和代码 | 新建版本目录、双语导航与 ReMe 别名 |
## 质量要求
- 每个用户流程必须能够在当前版本运行和验证。
- 文档不复制能够从代码可靠获得的细节。
- 删除过期内容优先于继续叠加补丁说明。
- 中英文页面保持信息等价,不要求逐句直译。
- 发布前在统一文档仓库运行 Mintlify 严格校验。

View file

@ -1,16 +1,17 @@
# Auto Dream
`auto_dream` is ReMe's long-term memory distillation flow from daily to digest. It scans daily inputs for a specified date,
processes only files that changed since the previous dream, extracts content worth retaining as memory units, integrates those
units into `digest/`, and generates the day's `interests.yaml` for proactive use.
`auto_dream` is ReMe's long-term memory distillation flow from daily to digest. By default it scans the target date and
the previous day, processes only files changed since the previous dream, extracts a small set of high-value memory units
across that window, integrates them into `digest/`, and writes the target day's `interests.yaml` for proactive use.
<p align="center">
<img src="../figure/auto-dream-and-proactive.svg" alt="ReMe Auto Dream and Proactive flow from daily to digest to proactive" width="92%">
</p>
Its daily inputs usually come from [Auto Memory](./auto_memory.md) and [Auto Resource](./auto_resource.md). For the file
semantics of `digest/`, `derived_from::`, and wikilinks, see [Memory as File](./memory_as_file.md). For the linking strategy
used during Integrate, see [Auto Link](./auto_link.md). To read `interests.yaml`, use [Proactive](./proactive.md).
semantics of `digest/`, Sources sections, and wikilinks, see [Memory as File](./memory_as_file.md). For the linking
strategy used during Integrate, see [Auto Link](./auto_link.md). To read `interests.yaml`,
use [Proactive](./proactive.md).
## Configuration
@ -26,6 +27,12 @@ auto_dream:
hint:
type: string
default: ""
scan_days:
type: integer
default: 2
max_units:
type: integer
default: 5
topic_count:
type: integer
default: 3
@ -36,6 +43,8 @@ auto_dream:
- backend: dream_extract_step
file_catalog: dream
topic_session_id: interests
scan_days: 2
max_units: 5
- backend: dream_integrate_step
- backend: dream_topics_step
topic_count: 3
@ -46,34 +55,39 @@ auto_dream:
Parameters:
| Parameter | Purpose |
|---|---|
| `date` | Date to process in `YYYY-MM-DD` format. When empty, use today in the application's timezone. |
| `hint` | Additional guidance from the caller for the Extract and Integrate stages. |
| `topic_count` | Maximum number of topics written to `interests.yaml`. Defaults to 3. |
| Parameter | Purpose |
|------------------------|---------------------------------------------------------------------------------------------------------|
| `date` | Date to process in `YYYY-MM-DD` format. When empty, use today in the application's timezone. |
| `hint` | Additional guidance from the caller for the Extract and Integrate stages. |
| `scan_days` | Recent-date window ending at `date`; defaults to 2 and has a minimum of 1. |
| `max_units` | Maximum reusable units extracted in one run; defaults to 5. |
| `topic_count` | Maximum number of topics written to `interests.yaml`. Defaults to 3. |
| `topic_diversity_days` | Number of past days of `interests.yaml` files considered when avoiding duplicate topics. Defaults to 7. |
## Inputs and Outputs
Inputs are daily Markdown files for the specified date:
Inputs are daily Markdown files from the most recent `scan_days` ending at the specified date. For example,
`date=2026-06-20` with `scan_days=2` scans:
```text
daily/<date>.md
daily/<date>/**/*.md
daily/2026-06-19.md
daily/2026-06-19/**/*.md
daily/2026-06-20.md
daily/2026-06-20/**/*.md
```
`daily/<date>/interests.yaml` is excluded from extraction input so topics from the previous run do not feed back into the
next extraction.
Every `daily/<date>/interests.yaml` in the scan window is excluded from extraction so previous proactive output cannot
feed back into the next run. Final topics are written only for the target date.
The main outputs are:
| Output | Description |
|---|---|
| `digest/procedure/*.md` | Methods, workflows, runbooks, and executable experience. |
| `digest/personal/*.md` | User-, team-, and project-related preferences, facts, and long-term context. |
| `digest/wiki/*.md` | General knowledge, concepts, observations, and decision precedents. |
| `daily/<date>/interests.yaml` | Topics worth proactive attention from the host agent that day. |
| `metadata/file_catalog/dream*` | Dream-specific catalog used to detect changes in daily inputs. |
| Output | Description |
|--------------------------------|------------------------------------------------------------------------------|
| `digest/procedure/*.md` | Methods, workflows, runbooks, and executable experience. |
| `digest/personal/*.md` | User-, team-, and project-related preferences, facts, and long-term context. |
| `digest/wiki/*.md` | General knowledge, concepts, observations, and decision precedents. |
| `daily/<date>/interests.yaml` | Topics worth proactive attention from the host agent that day. |
| `metadata/file_catalog/dream*` | Dream-specific catalog used to detect changes in daily inputs. |
## Four Stages
@ -81,43 +95,50 @@ The main outputs are:
`dream_extract_step` performs three tasks:
1. Refresh the day's index page at `daily/<date>.md`.
2. Scan `daily/<date>.md` and `daily/<date>/**/*.md` and compare their mtimes with `file_catalog: dream`.
3. Send only changed files to the LLM and globally extract two structured result types: `units` and `topics`.
1. Refresh each `daily/<date>.md` in the scan window.
2. Scan those day indexes and `daily/<date>/**/*.md`, comparing mtimes with `file_catalog: dream`.
3. Send all changed files together to the LLM and globally extract two structured result types: `units` and `topics`.
`units` are long-term memory units ready to be distilled into digest. Each has `name`, `bucket`, `summary`, and `paths`.
`bucket` may only be `procedure`, `personal`, or `wiki`; unknown values are routed to `wiki`.
A run returns at most `max_units`; extraction merges cross-file evidence for the same abstraction and drops passing
mentions, per-file summaries, and weak candidates without reusable value. `bucket` may only be `procedure`, `personal`,
or `wiki`; unknown values are routed to `wiki`.
`topics` are proactive-interest candidates for the day. They contain `title`, `reason`, `evidence`, `keywords`, and
`paths` and are filtered again in the Topics stage.
If there are no changed files, the flow ends early with success and skips later extraction work. If files changed but no LLM
is configured, Extract fails because extraction requires an LLM.
If there are no changed files, Extract succeeds with no units; Integrate then has no unit work, Topics preserves any
existing target-day topics, and Finish still performs its normal catalog summary. If files changed but no LLM is
configured, Extract fails because extraction requires an LLM.
### 2. Integrate
`dream_integrate_step` invokes an agent independently for each unit and integrates that unit into one digest node. It exposes
these tools to the agent:
`dream_integrate_step` invokes an agent independently for each unit and integrates that unit into one digest node. It
exposes these tools to the agent:
```text
node_search, read, frontmatter_read, write, edit, frontmatter_update
```
This stage carries the core responsibility of `auto_link`. It first uses `node_search` to recall similar or related nodes at
digest-node granularity, decides whether to create or update a node, and finally writes sources and related digest nodes as
wikilinks. See [Auto Link](./auto_link.md) for the recall, deduplication, and edge-writing rules.
This stage carries the core responsibility of `auto_link`. It first uses `node_search` to recall similar or related
nodes at digest-node granularity, decides whether to create or update a node, and finally writes sources and related
digest nodes as wikilinks. See [Auto Link](./auto_link.md) for the recall, deduplication, and edge-writing rules.
Extract is the gate for deciding whether material is worth remembering, so Integrate has no `SKIP` action: each admitted
unit must land in exactly one digest node. Creates and updates must retain provenance and weave related digest links
into contextual sentences; bare wikilinks and standalone relationship fields are not valid output.
There are four integration actions:
| Action | Meaning |
|---|---|
| `CREATE` | No equivalent abstraction exists; create a new digest node. |
| Action | Meaning |
|---------------|--------------------------------------------------------------------------------|
| `CREATE` | No equivalent abstraction exists; create a new digest node. |
| `CORROBORATE` | The same memory appeared again; append a source or strengthen the description. |
| `REFINE` | New material adds boundaries, steps, prerequisites, applicability, or detail. |
| `CORRECT` | New material corrects errors, omissions, or conflicts in the existing node. |
| `REFINE` | New material adds boundaries, steps, prerequisites, applicability, or detail. |
| `CORRECT` | New material corrects errors, omissions, or conflicts in the existing node. |
Successfully integrated units are recorded in `integrate_results`. Failed units enter `failed_units`, and their source paths
enter `failed_paths`. The Finish stage does not checkpoint failed paths, ensuring that they can be retried later.
Successfully integrated units are recorded in `integrate_results`. Failed units enter `failed_units`, and their source
paths enter `failed_paths`. The Finish stage does not checkpoint failed paths, ensuring that they can be retried later.
### 3. Topics
@ -127,7 +148,7 @@ It reads:
```text
daily/<date>/interests.yaml
daily/<previous-date>/interests.yaml
daily/<each of the previous topic_diversity_days dates>/interests.yaml
```
Existing topics from the same day are preserved, while similar topics from the previous `topic_diversity_days` days are
@ -156,7 +177,8 @@ topics:
`dream_finish_step` completes the run:
1. Write successfully processed changed paths to `file_catalog: dream`.
2. Also write `daily/<date>/interests.yaml` and `daily/<date>.md` to the catalog.
2. Also write the target `daily/<date>/interests.yaml` and every refreshed day-index page in the scan window to the
catalog.
3. Persist the dream catalog if there were upserts or deletions.
4. Return a summary containing counts for scanned, changed, integrated, topics, checkpoints, and related values.
@ -177,6 +199,12 @@ With caller guidance:
reme auto_dream date=2026-06-20 hint="Prioritize engineering decisions and long-term preferences"
```
Override the default scan window and unit cap:
```bash
reme auto_dream date=2026-06-20 scan_days=3 max_units=8
```
The same set of steps can also be placed in a `cron` Job, for example to run every morning:
```yaml
@ -195,15 +223,16 @@ jobs:
## Important Boundaries
`auto_dream` consumes only daily inputs and does not rewrite daily bodies. Daily preserves facts and the original situation;
digest is the abstracted long-term memory layer.
`auto_dream` consumes only daily inputs and does not rewrite daily bodies. Daily preserves facts and the original
situation; digest is the abstracted long-term memory layer.
`digest` is not a copy of the source text. Its body should preserve reusable abstractions, while details point back to sources
through `derived_from:: [[daily/<date>/...]]`. Links follow the workspace-relative wikilink semantics described in
`digest` is not a copy of the source text. Its body should preserve reusable abstractions, while a Sources section
points back with contextual sentences such as `The decision was recorded in [[daily/<date>/decision.md]].` Links follow
the workspace-relative wikilink semantics described in
[Memory as File](./memory_as_file.md).
`auto_dream` does not invent an overview from nothing. Only content that actually appears in daily input and is extracted as
a unit or topic can enter digest or `interests.yaml`.
`auto_dream` does not invent an overview from nothing. Only content that actually appears in daily input and is
extracted as a unit or topic can enter digest or `interests.yaml`.
The complete flow depends on an LLM for Extract and Integrate. Topics can perform local deduplication without an LLM, but that
does not mean the full dream flow can run offline.
The complete flow depends on an LLM for Extract and Integrate. Topics can perform local deduplication without an LLM,
but that does not mean the full dream flow can run offline.

View file

@ -1,11 +1,12 @@
# Auto Link
In the current implementation, `auto_link` is not a separately registered Job. It is a capability of the Integrate stage in
In the current implementation, `auto_link` is not a separately registered Job. It is a capability of the Integrate stage
in
`auto_dream`: when `dream_integrate_step` writes a memory unit to `digest/`, it also recalls digest nodes, makes a
deduplication decision, links sources, and weaves wikilinks to related nodes into the result.
For the complete dream flow, see [Auto Dream](./auto_dream.md). For general wikilink, frontmatter, and workspace-relative
path semantics, see [Memory as File](./memory_as_file.md). For question-answering retrieval, see
For the complete dream flow, see [Auto Dream](./auto_dream.md). For general wikilink, frontmatter, and
workspace-relative path semantics, see [Memory as File](./memory_as_file.md). For question-answering retrieval, see
[Memory Search](./memory_search.md).
## Where It Runs
@ -21,19 +22,19 @@ auto_dream:
- dream_finish_step
```
The Integrate stage processes each unit independently. A unit is written to exactly one target digest node, but that node may
link to multiple sources and multiple related digest nodes.
The Integrate stage processes each unit independently. A unit is written to exactly one target digest node, but that
node may link to multiple sources and multiple related digest nodes.
## Goals
`auto_link` addresses graph quality at write time:
| Problem | Handling |
|---|---|
| The same memory already exists | Recall and update the existing node instead of creating a duplicate. |
| New and existing material are related | Write workspace-relative wikilinks into the body. |
| A digest node is disconnected from its sources | Point back to daily/resource source material with `derived_from:: [[...]]`. |
| A node contains only isolated prose | Add links to related digest nodes on both CREATE and UPDATE. |
| Problem | Handling |
|------------------------------------------------|----------------------------------------------------------------------|
| The same memory already exists | Recall and update the existing node instead of creating a duplicate. |
| New and existing material are related | Write workspace-relative wikilinks into the body. |
| A digest node is disconnected from its sources | Add daily/resource links under a `## Sources` section. |
| A node contains only isolated prose | Add links to related digest nodes on both CREATE and UPDATE. |
## Toolchain
@ -48,77 +49,78 @@ edit
frontmatter_update
```
`node_search` is digest-only node retrieval designed for dream integration. It returns node-level signals such as the digest
node's `path` and the `name` and `description` from frontmatter. It does not expand the body and does not perform the link
expansion used by ordinary search.
`node_search` is digest-only node retrieval designed for dream integration. It returns node-level signals such as the
digest node's `path` and the `name` and `description` from frontmatter. It does not expand the body and does not perform
the link expansion used by ordinary search.
`read` and `frontmatter_read` are used only for candidates that may be relevant, avoiding expansion of every recalled result
into a large context.
`read` and `frontmatter_read` are used only for candidates that may be relevant, avoiding expansion of every recalled
result into a large context.
## Linking Flow
### 1. Recall candidate nodes
The agent first calls `node_search` with the unit's triggers, verbs, nouns, synonyms, and possible failure modes. Broad recall,
for example `limit=20-30`, is recommended by default because this step serves both deduplication and link discovery.
The agent first calls `node_search` with the unit's triggers, verbs, nouns, synonyms, and possible failure modes. Broad
recall, for example `limit=20-30`, is recommended by default because this step serves both deduplication and link
discovery.
Recalled results are internally classified into three groups:
| Classification | Meaning | Next action |
|---|---|---|
| `same_abstraction` | The trigger or underlying abstraction is the same, with substantial content overlap. | Use as the UPDATE target. |
| `related` | An adjacent process, prerequisite, failure mode, concept, preference, or upstream/downstream knowledge. | Write a body wikilink. |
| `unrelated` | Only superficially similar or unrelated. | Ignore. |
| Classification | Meaning | Next action |
|--------------------|---------------------------------------------------------------------------------------------------------|---------------------------|
| `same_abstraction` | The trigger or underlying abstraction is the same, with substantial content overlap. | Use as the UPDATE target. |
| `related` | An adjacent process, prerequisite, failure mode, concept, preference, or upstream/downstream knowledge. | Write a body wikilink. |
| `unrelated` | Only superficially similar or unrelated. | Ignore. |
### 2. Choose a write action
Every unit must select one action:
| Action | Linking semantics |
|---|---|
| `CREATE` | Write a new `digest/<bucket>/<slug>.md` and add source and related-node links to its body. |
| `CORROBORATE` | The same abstraction appeared again; append a new `derived_from:: [[...]]` and strengthen the description when needed. |
| `REFINE` | New material extends the existing node; insert the additional content in the appropriate section and preserve existing links. |
| `CORRECT` | New material corrects the existing node; use source links to identify the basis for the correction. |
| Action | Linking semantics |
|---------------|-------------------------------------------------------------------------------------------------------------------------------|
| `CREATE` | Write a new `digest/<bucket>/<slug>.md` and add source and related-node links to its body. |
| `CORROBORATE` | The same abstraction appeared again; append its source link and strengthen the description when needed. |
| `REFINE` | New material extends the existing node; insert the additional content in the appropriate section and preserve existing links. |
| `CORRECT` | New material corrects the existing node; use source links to identify the basis for the correction. |
An UPDATE should be additive whenever possible: do not delete existing wikilinks or `derived_from` entries. This prevents
later graph indexing and retrieval from losing edges.
An UPDATE should be additive whenever possible: do not delete existing wikilinks or source entries. This prevents later
graph indexing and retrieval from losing edges.
### 3. Write source edges
Source edges use Markdown wikilinks:
Source edges are ordinary wikilinks grouped under a Markdown heading:
```markdown
derived_from:: [[daily/2026-06-20/session.md]]
derived_from:: [[resource/2026-06-20/paper.md]]
## Sources
The decision was recorded in [[daily/2026-06-20/session.md]], while the supporting technical evidence comes from
[[resource/2026-06-20/paper.md]].
```
These edges represent the evidence behind a digest node. Plain-text descriptions do not count as source edges because only
wikilinks can be parsed reliably by the file graph. For the complete parsing rules, see
These edges represent the evidence behind a digest node. Plain-text descriptions do not count as source edges because
only wikilinks can be parsed reliably by the file graph. The surrounding sentence must explain what each source
supports; a bare wikilink line is not valid Integrate output. For the complete parsing rules, see
[Memory as File](./memory_as_file.md#wikilink).
### 4. Write relationships between digest nodes
Relationships between digest nodes also use complete workspace-relative paths:
Relationships between digest nodes use complete workspace-relative paths woven into natural prose:
```markdown
relates_to:: [[digest/wiki/hybrid-search.md]]
depends_on:: [[digest/procedure/rebuild-index.md]]
blocks_on:: [[digest/personal/team-review-preference.md]]
This design extends [[digest/wiki/hybrid-search.md]] and uses
[[digest/procedure/rebuild-index.md]]. Follow
[[digest/personal/team-review-preference.md]] during review.
```
Predicates are open-ended. Common forms include `relates_to::`, `depends_on::`, and `blocks_on::`. The predicate sits outside
the brackets, while the target path goes inside `[[...]]` and should include the `.md` suffix.
## Bucket Differences
`auto_link` adjusts the shape of its output according to the unit bucket:
| Bucket | Writing focus |
|---|---|
| `procedure` | Write a runbook with triggers, steps, inputs, and failure modes. Link prerequisites, substeps, and related preferences. |
| `personal` | Write user-, team-, or project-specific facts and preferences. Link related projects, habits, and decision context. |
| `wiki` | Write general knowledge, principles, observations, and decision precedents. Link concepts, methods, and adjacent knowledge. |
| Bucket | Writing focus |
|-------------|-----------------------------------------------------------------------------------------------------------------------------|
| `procedure` | Write a runbook with triggers, steps, inputs, and failure modes. Link prerequisites, substeps, and related preferences. |
| `personal` | Write user-, team-, or project-specific facts and preferences. Link related projects, habits, and decision context. |
| `wiki` | Write general knowledge, principles, observations, and decision precedents. Link concepts, methods, and adjacent knowledge. |
Regardless of bucket, preserve source edges and weave recalled related digest nodes into the body whenever possible.
@ -126,19 +128,19 @@ Regardless of bucket, preserve source edges and weave recalled related digest no
`auto_link` uses `node_search`, not the question-answering `search`.
| Capability | Purpose |
|---|---|
| `search` | External question answering; returns chunks and can expand upstream/downstream link context. |
| Capability | Purpose |
|---------------|-----------------------------------------------------------------------------------------------------------|
| `search` | External question answering; returns chunks and can expand upstream/downstream link context. |
| `node_search` | Dream integration; recalls only digest node-level summaries for deduplication and related-link decisions. |
This boundary matters. The Integrate stage needs to decide whether the same abstraction already exists and which nodes should
be linked; it should not load large numbers of body chunks into context. [Memory Search](./memory_search.md) handles
question-oriented chunk retrieval, RRF fusion, and link expansion.
This boundary matters. The Integrate stage needs to decide whether the same abstraction already exists and which nodes
should be linked; it should not load large numbers of body chunks into context. [Memory Search](./memory_search.md)
handles question-oriented chunk retrieval, RRF fusion, and link expansion.
## Failure and Retry
If integration of a unit fails, `dream_integrate_step` records `failed_units` and `failed_paths`.
`dream_finish_step` does not checkpoint those source paths, so the next `auto_dream` run processes them again.
This makes auto_link writes retryable: a failure does not mark the input as complete or silently discard digest edges that
should have been created.
This makes auto_link writes retryable: a failure does not mark the input as complete or silently discard digest edges
that should have been created.

View file

@ -1,8 +1,9 @@
# Auto Memory
Auto Memory is ReMe's entry point for conversational memory. Each conversation is first distilled into a daily memory card
identified by `session_id`, and the day's `YYYY-MM-DD.md` page then indexes all of those cards. It turns "we talked about it"
into "it was remembered" while preserving the original conversation as evidence.
Auto Memory is ReMe's entry point for conversational memory. Within a target date, it uses `session_id` to find or update at
most one daily memory card, whose filename is a concise topic or event name chosen by the Agent. The day's `YYYY-MM-DD.md`
page indexes those cards. It turns "we talked about it" into "it was remembered" while retaining a source conversation record
as evidence.
<p align="center">
<img src="../figure/auto-memory-resource.svg" alt="ReMe Auto Memory and Auto Resource writing daily memory cards" width="92%">
@ -13,9 +14,9 @@ For the general file semantics of `daily/`, `session/`, frontmatter, and wikilin
```text
Conversation
├─ step 1: daily/YYYY-MM-DD/<session_id>.md # one card per conversation
├─ step 2: daily/YYYY-MM-DD.md # daily index linking the cards
└─ source: session/dialog/<session_id>.jsonl # original conversation
├─ step 1: daily/YYYY-MM-DD/<generated_name>.md # one topic-named card per session
├─ step 2: daily/YYYY-MM-DD.md # daily index linking the cards
└─ source: session/dialog/<session_id>.jsonl # source conversation record
```
## What It Records
@ -39,29 +40,33 @@ workspace/
daily/
2026-06-20.md
2026-06-20/
session-a.md
session-b.md
login-refactor-decision.md
retrieval-regression.md
```
`daily/2026-06-20/session-a.md` and `daily/2026-06-20/session-b.md` are memory cards distilled from different
conversations. `daily/2026-06-20.md` is the index page for that day. Resource files enter the same daily memory layer; see
The two files under the date directory are topic-named cards distilled from different conversations.
`daily/2026-06-20.md` is the index page for that day. Resource files enter the same daily memory layer; see
[Auto Resource](./auto_resource.md).
When a call includes `session_id`, Auto Memory records that conversation separately under the given ID:
When a call includes `session_id`, Auto Memory uses it to find the corresponding card through frontmatter, while the Agent
chooses a readable filename through `name`:
```text
daily/2026-06-20/session-a.md
```yaml
name: login-refactor-decision
session_id: session-a
source_conversation: "[[session/dialog/session-a.jsonl]]"
```
This keeps different conversations separate. A requirements discussion, a debugging session, and a documentation update can
each have their own memory card. To see what happened on a particular day, start with `YYYY-MM-DD.md`. To inspect what was
distilled from one conversation, open the corresponding `<session_id>.md`.
This keeps different conversations separate without forcing opaque IDs into filenames. An update locates the existing note by
`session_id` or `source_conversation`; if the Agent supplies a better frontmatter `name`, the system can rename the note and
retarget inbound wikilinks. To see what happened on a day, start with `YYYY-MM-DD.md`.
## Preserving the Original Information
The distilled daily note is optimized for readability; the original conversation is retained for trust and verification.
The distilled daily note is optimized for readability; a filtered source conversation record is retained for trust and
verification.
While generating memory cards, Auto Memory also saves the raw sessions:
While generating memory cards, Auto Memory also saves the source messages:
```text
session/
@ -70,12 +75,12 @@ session/
session-b.jsonl
```
Each daily note points to its corresponding original conversation. When a memory needs verification, follow that link back to
the complete context in which it was created.
Each daily note points to its corresponding conversation record. Saved messages omit tool-result blocks and base64 data
blocks, preventing recalled memory and binary payloads from being mistaken for user-provided evidence later.
## Message Timestamps
Auto Memory preserves each message's `created_at` in both the prompt and the raw session JSONL. When importing historical
Auto Memory preserves each retained message's `created_at` in both the prompt and the source conversation JSONL. When importing historical
conversations or benchmark data, provide the actual occurrence time for every message so the model does not confuse event
time with execution time:
@ -92,8 +97,8 @@ For compatibility with common dataset schemas, `auto_memory` also checks `time_c
`timeCreated`, and `created_time` when `created_at` is absent. These fields may appear either at the top level of a message
or inside `metadata`.
When a call does not explicitly provide `date`, Auto Memory uses the date of the earliest valid `created_at` value in the
messages. If no message contains a valid timestamp, it falls back to the current date. Historical imports may also specify the
When a call does not explicitly provide `date`, Auto Memory uses the latest valid `created_at` date in the messages. If no
message contains a valid timestamp, it falls back to the current date. Historical imports may also specify the
target date directly:
```bash

View file

@ -1,8 +1,8 @@
# Auto Resource `Beta`
Auto Resource is ReMe's entry point for interpreting resources and is currently in **Beta**. Resource files first enter
`resource/` by date and are then interpreted into daily resource cards. Each card's filename comes from the LLM-generated
frontmatter `name`, and `source_resource` links the card back to its original file.
`resource/`, preferably under a date directory, and are then interpreted into daily resource cards. Each card's filename
comes from the LLM-generated frontmatter `name`, and `source_resource` links the card back to its original file.
<p align="center">
<img src="../figure/auto-memory-resource.svg" alt="ReMe Auto Memory and Auto Resource writing daily memory cards" width="92%">
@ -13,7 +13,7 @@ For the general file semantics of workspace layers, `resource/`, and `daily/`, s
[Auto Memory](./auto_memory.md).
```text
resource/YYYY-MM-DD/<resource_file>
resource/[YYYY-MM-DD/]<resource_file>
├─ step 1: daily/YYYY-MM-DD/<generated_name>.md # interpreted resource card
├─ step 2: source_resource points to the original resource
└─ step 3: daily/YYYY-MM-DD.md # daily index linking the cards
@ -21,8 +21,8 @@ resource/YYYY-MM-DD/<resource_file>
## What It Records
Auto Resource does more than copy file content. It extracts information that will make the resource easier to retrieve and
understand later:
Auto Resource does more than copy file content. It extracts information that will make the resource easier to retrieve
and understand later:
- Core content: what the resource is mainly about.
- Structure: its sections, tables, fields, and data organization.
@ -34,26 +34,28 @@ In short, it turns "a file was archived" into "the resource is usable."
## Original Resource Entry Point
Auto Resource uses `resource/` as the entry point for source material. Resources must be placed under a date, which determines
the day whose daily memory layer receives the interpreted card.
Auto Resource uses `resource/` as the entry point for source material. Date directories are recommended, and their date
determines which daily memory layer receives the interpreted card. A file directly under `resource/` is also supported
and uses today in the application timezone.
Example directory:
```text
workspace/
resource/
quick-note.txt # enters today's daily layer
2026-06-20/
market-report.md
meeting-notes.csv
```
The current Beta version is best suited to text-based resources such as `md`, `txt`, `json`, `jsonl`, `csv`, `yaml`,
and `html`.
The current Beta version is best suited to text-based resources such as `md`, `txt`, `json`, `jsonl`, `csv`, `yaml`, and
`html`.
## Resource Cards
Each resource file produces one daily resource card. The system initially uses the resource file's stem as a temporary path.
After the agent writes the card, the file is renamed according to its frontmatter `name`:
Each resource file produces one daily resource card. The system initially uses the resource file's stem as a temporary
path. After the agent writes the card, the file is renamed according to its frontmatter `name`:
```text
resource/2026-06-20/market-report.md
@ -67,14 +69,14 @@ The resource card links to the original file through frontmatter:
source_resource: "[[resource/2026-06-20/market-report.md]]"
```
When a resource changes, Auto Resource finds and updates the corresponding card through `source_resource`. When a resource is
deleted, its daily note is also removed. The older `daily/YYYY-MM-DD/<resource_stem>.md` naming convention remains supported
as a fallback.
When a resource changes, Auto Resource finds and updates the corresponding card through `source_resource`. When a
resource is deleted, its daily note is also removed. The older `daily/YYYY-MM-DD/<resource_stem>.md` naming convention
remains supported as a fallback.
## Daily Index
Resource cards enter the same daily memory layer as Auto Memory cards. The day's `YYYY-MM-DD.md` page acts as an index and
organizes those resource cards:
Resource cards enter the same daily memory layer as Auto Memory cards. The day's `YYYY-MM-DD.md` page acts as an index
and organizes those resource cards:
```text
daily/
@ -91,11 +93,13 @@ resource, open its corresponding resource card.
The interpreted daily note is optimized for readability; the original resource is retained for trust and verification.
Auto Resource does not move the original file. It remains under `resource/YYYY-MM-DD/`. Text resources can therefore enter
the daily memory flow while their source files stay in their original location.
Auto Resource does not move the original file. It remains at its original path under `resource/`. Text resources can
therefore enter the daily memory flow while their source files stay in their original location.
## What Happens Next
Auto Resource only creates resource interpretations in the daily layer. To distill long-term knowledge from resources into
`digest/`, use [Auto Dream](./auto_dream.md). To search original resources, daily cards, and digest nodes, use
[Memory Search](./memory_search.md).
Auto Resource only creates resource interpretations in the daily layer. To distill long-term knowledge from resources
into
`digest/`, use [Auto Dream](./auto_dream.md). The default live index covers daily cards and digest nodes. Run
`reme reindex`
when original resource files must also be directly searchable; see [Memory Search](./memory_search.md).

View file

@ -8,12 +8,12 @@ ReMe is open source and hosted on GitHub:
## How to Contribute
Thank you for your interest in ReMe. ReMe is a file-first, self-evolving memory system for agents. Contributions are welcome
through issue reports, documentation improvements, additional tests, bug fixes, and new capabilities.
Thank you for your interest in ReMe. ReMe is a file-first, self-evolving memory system for agents. Contributions are
welcome through issue reports, documentation improvements, additional tests, bug fixes, and new capabilities.
If this is your first time running ReMe locally, start with [Quick Start](./quick_start.md). If your change affects runtime
layers, Jobs, Steps, or components, read [ReMe Framework](./framework.md). If it affects workspace directories, frontmatter,
wikilinks, or chunking, read [Memory as File](./memory_as_file.md).
If this is your first time running ReMe locally, start with [Quick Start](./quick_start.md). If your change affects
runtime layers, Jobs, Steps, or components, read [ReMe Framework](./framework.md). If it affects workspace directories,
frontmatter, wikilinks, or chunking, read [Memory as File](./memory_as_file.md).
### 1. Before You Begin
@ -21,9 +21,10 @@ Before investing in an implementation:
- Check [Open Issues](https://github.com/agentscope-ai/ReMe/issues) for an existing issue or discussion.
- If a related issue is still open, comment that you would like to work on it to avoid duplicate effort.
- If no issue exists, create one describing the context, expected behavior, possible implementation, and scope of impact.
- For larger feature changes, align with maintainers on interfaces, configuration, compatibility, and test strategy before
submitting an implementation.
- If no issue exists, create one describing the context, expected behavior, possible implementation, and scope of
impact.
- For larger feature changes, align with maintainers on interfaces, configuration, compatibility, and test strategy
before submitting an implementation.
### 2. Local Development Environment
@ -38,7 +39,11 @@ The project requires Python 3.11 or later. A virtual environment is recommended:
```bash
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev,full]"
pip install -e reme_studio -e ".[dev,full]"
cd reme_studio
npm ci
npm run build:static
cd ..
pre-commit install
```
@ -53,8 +58,8 @@ CLI / Client -> Service -> Application -> Job -> Step -> Component / Workspace
In practice:
- Capabilities exposed to users or external systems should normally be orchestrated by a Job, then exposed by a Service as a
CLI-, HTTP-, or MCP-callable interface.
- Capabilities exposed to users or external systems should normally be orchestrated by a Job, then exposed by a Service
as a CLI-, HTTP-, or MCP-callable interface.
- Reusable infrastructure belongs in `reme/components/`, with dependencies declared through `BaseComponent.bind()`.
- Atomic business operations belong in `reme/steps/` and access the file store, agent wrapper, catalog, LLM, and other
components through `BaseStep.Ref`.
@ -65,31 +70,33 @@ In practice:
When adding a Step or Job, pay particular attention to these conventions:
- Register implementations with `@R.register("<backend_name>")`. Registration names should be stable, clear, and match the
configured `backend`.
- After adding a Step file, make sure its package `__init__.py` imports the module; otherwise, the registry will not load it.
- Register implementations with `@R.register("<backend_name>")`. Registration names should be stable, clear, and match
the configured `backend`.
- After adding a Step file, make sure its package `__init__.py` imports the module; otherwise, the registry will not
load it.
- A Step should perform one atomic business operation. Cross-step flows belong in Job configuration or a dedicated
orchestration Step.
- A Job composes Steps and selects normal, streaming, background, or scheduled execution. `enable_serve` controls whether it
is externally exposed.
- A Job composes Steps and selects normal, streaming, background, or scheduled execution. `enable_serve` controls
whether it is externally exposed.
- When a Step needs components, prefer `BaseStep.Ref`. Do not reconstruct global components inside a Step or bypass
`ApplicationContext`.
- File, index, graph, frontmatter, and wikilink behavior must preserve consistent workspace-relative path semantics.
- Add fast tests under `tests/unit/` for new capabilities. Put cross-component, LLM, embedding, or service behavior under
- Add fast tests under `tests/unit/` for new capabilities. Put cross-component, LLM, embedding, or service behavior
under
`tests/integration/` when appropriate.
### 4. Code and Documentation Changes
Choose the appropriate entry point for the type of change:
| Change type | Primary location | Guidance |
|---|---|---|
| Configuration or startup behavior | `reme/config/`, `reme/application.py`, `reme/reme.py` | Keep the default configuration runnable and avoid breaking existing CLI, HTTP, and MCP entry points. |
| Component capability | `reme/components/` | Reuse `BaseComponent`, the registry, and context objects. |
| Job or Step | `reme/components/job/`, `reme/steps/` | Follow the Job -> Step model in [ReMe Framework](./framework.md), keep request and response schemas clear, and add corresponding tests. |
| Data structure | `reme/schema/`, `reme/enumeration/` | Preserve serialization compatibility and existing frontmatter and wikilink semantics. |
| Utility | `reme/utils/` | Keep function boundaries small and cover edge cases with unit tests. |
| User documentation | `docs/en/`, `README.md` | Update documentation when user-visible behavior changes. |
| Change type | Primary location | Guidance |
|-----------------------------------|-------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
| Configuration or startup behavior | `reme/config/`, `reme/application.py`, `reme/reme.py` | Keep the default configuration runnable and avoid breaking existing CLI, HTTP, and MCP entry points. |
| Component capability | `reme/components/` | Reuse `BaseComponent`, the registry, and context objects. |
| Job or Step | `reme/components/job/`, `reme/steps/` | Follow the Job -> Step model in [ReMe Framework](./framework.md), keep request and response schemas clear, and add corresponding tests. |
| Data structure | `reme/schema/`, `reme/enumeration/` | Preserve serialization compatibility and existing frontmatter and wikilink semantics. |
| Utility | `reme/utils/` | Keep function boundaries small and cover edge cases with unit tests. |
| User documentation | `docs/en/`, `README.md` | Update documentation when user-visible behavior changes. |
If a change involves an LLM, embeddings, an external service, file watching, or a background task, also describe its
dependencies, failure behavior, and local validation method.
@ -165,15 +172,16 @@ pytest tests/unit/test_reme_cli.py
If `pre-commit` modifies files automatically, commit those changes and rerun the checks until everything passes.
The current pre-commit configuration includes YAML/TOML/JSON validation, private-key detection, trailing-whitespace checks,
The current pre-commit configuration includes YAML/TOML/JSON validation, private-key detection, trailing-whitespace
checks,
`black`, `flake8`, `pylint`, and `pyroma`. The main formatting rules are:
- `black --line-length=120`
- `flake8 --max-line-length=120`
- `pylint --max-line-length=120`
Some integration tests may require an LLM, embeddings, or external service configuration. If you cannot run them locally,
state why they were skipped and what alternative validation you completed in the PR description.
Some integration tests may require an LLM, embeddings, or external service configuration. If you cannot run them
locally, state why they were skipped and what alternative validation you completed in the PR description.
### 8. Testing Requirements
@ -183,7 +191,8 @@ Add tests according to the risk of the change:
- For a new Step, Job, or component, cover at least the main path and a failure path.
- For changes to shared logic such as indexes, graphs, wikilinks, frontmatter, or file operations, add edge cases.
- For changes to the CLI, services, or configuration parsing, cover the user-visible entry point.
- Documentation-only changes usually do not require new tests, but running `pre-commit run --all-files` is still recommended.
- Documentation-only changes usually do not require new tests, but running `pre-commit run --all-files` is still
recommended.
Place tests according to the existing structure:
@ -213,9 +222,9 @@ Documentation should:
- Bugs and feature requests: [GitHub Issues](https://github.com/agentscope-ai/ReMe/issues)
- Project home: [GitHub Repository](https://github.com/agentscope-ai/ReMe)
- Documentation site: [https://reme.agentscope.io/](https://reme.agentscope.io/)
- Documentation site: [https://reme.agentscope.io](https://reme.agentscope.io)
---
Thank you for contributing to ReMe. Your improvements help make long-term memory for agents more readable, controllable, and
maintainable.
Thank you for contributing to ReMe. Your improvements help make long-term memory for agents more readable, controllable,
and maintainable.

View file

@ -16,13 +16,13 @@ To run and use ReMe first, see [Quick Start](./quick_start.md). For workspace fi
### Capability Boundary
ReMe v4 focuses on long-term memory: it distills conversations and resources into `daily/`, organizes them into `digest/`,
and exposes write, retrieval, and proactive-read capabilities through the CLI, HTTP, and MCP.
ReMe v4 focuses on long-term memory: it distills conversations and resources into `daily/`, organizes them into
`digest/`, and exposes write, retrieval, and proactive-read capabilities through the CLI, HTTP, and MCP.
Single-session context-window management is outside the scope of ReMe v4. This includes compressing the current conversation,
injecting summaries, trimming tool output, or providing an independent `/compact` interface. Those capabilities belong in
the host agent framework. ReMe accepts conversations, resources, and file changes that have already occurred and persists the
information with long-term value.
Single-session context-window management is outside the scope of ReMe v4. This includes compressing the current
conversation, injecting summaries, trimming tool output, or providing an independent `/compact` interface. Those
capabilities belong in the host agent framework. ReMe accepts conversations, resources, and file changes that have
already occurred and persists the information with long-term value.
```mermaid
flowchart LR
@ -38,16 +38,16 @@ flowchart LR
Core layers:
| Layer | Main location | Responsibility |
|---|---|---|
| CLI | `reme/reme.py` | Parse commands; `start` launches the service; other actions call the service through a client. |
| Service | `reme/components/service/` | Register Jobs as HTTP endpoints or MCP tools. |
| Application | `reme/application.py` | Assemble configured objects, start them in dependency order, close them, and invoke Jobs. |
| Job | `reme/components/job/` | Orchestrate Steps and select normal, streaming, background, or scheduled execution. |
| Step | `reme/steps/` | Atomic business operations such as file I/O, retrieval, indexing, and self-evolution. |
| Component | `reme/components/` | Reusable infrastructure such as file_store, file_graph, keyword_index, and agent_wrapper. |
| Schema | `reme/schema/` | Data structures such as `Request`, `Response`, `FileChunk`, `FileNode`, and configuration models. |
| Config | `reme/config/` | Default YAML configuration and command-line override parsing. |
| Layer | Main location | Responsibility |
|-------------|----------------------------|---------------------------------------------------------------------------------------------------|
| CLI | `reme/reme.py` | Parse commands; `start` launches the service; other actions call the service through a client. |
| Service | `reme/components/service/` | Register Jobs as HTTP endpoints or MCP tools. |
| Application | `reme/application.py` | Assemble configured objects, start them in dependency order, close them, and invoke Jobs. |
| Job | `reme/components/job/` | Orchestrate Steps and select normal, streaming, background, or scheduled execution. |
| Step | `reme/steps/` | Atomic business operations such as file I/O, retrieval, indexing, and self-evolution. |
| Component | `reme/components/` | Reusable infrastructure such as file_store, file_graph, keyword_index, and agent_wrapper. |
| Schema | `reme/schema/` | Data structures such as `Request`, `Response`, `FileChunk`, `FileNode`, and configuration models. |
| Config | `reme/config/` | Default YAML configuration and command-line override parsing. |
## 2. Directory Structure
@ -55,11 +55,12 @@ Core layers:
reme/
reme.py # CLI entry point
application.py # Application assembly and lifecycle
plugin.py # installed plugin contract and entry-point loader
config/
default.yaml # default service / jobs / components
config_parser.py # config=, dot notation, and env placeholder parsing
components/
component_registry.py # global registry R
component_registry.py # backend registry and application-local copies
base_component.py # ComponentMixin / BaseComponent / bind dependency declarations
runtime_context.py # context for one Job execution
job/ # BaseJob / StreamJob / BackgroundJob / CronJob
@ -68,18 +69,25 @@ reme/
file_store/ # file-index coordination layer
file_graph/ # wikilink graph
keyword_index/ # BM25 and other keyword indexes
file_chunker/ # Markdown / default text chunking
file_chunker/ # Markdown / JSON / JSONL / generic text chunking
file_catalog/ # change checkpoints
as_llm/, as_embedding/ # model wrappers
agent_wrapper/ # AgentScope / Claude Code wrappers
agent_wrapper/ # AgentScope / Claude Code / Codex wrappers
steps/
base_step.py # BaseStep, Ref, dispatch_steps
common/ # version, help, health_check, demo
common/ # version, help, health_check, status, chat
benchmark/ # LongMemEval / BEAM evaluation steps
cookbook/ # built-in cookbook support steps
file_io/ # read/write/edit/delete/move/frontmatter/daily
index/ # watch/init/update/search/traverse
evolve/ # auto_memory, auto_resource, auto_dream, proactive
transfer/ # upload/download/ingest
channel/ # MCP channel tools
transfer/ # upload/download
plugins/
auto-fin/ # independent example plugin distribution
daily_paper/ # independent paper-research plugin distribution
integrations/
claude_code/ # Claude Code adapter and marketplace
hermes_agent/ # Hermes Agent memory-provider adapter
```
The default workspace directories are defined by `ApplicationConfig`:
@ -87,7 +95,8 @@ The default workspace directories are defined by `ApplicationConfig`:
```text
<workspace_dir>/
metadata/ # persistent file_store, file_graph, keyword_index, file_catalog, and related state
session/ # agent sessions and original conversations
session/ # source conversations used by memory workflows
mem_session/ # generated Agent wrapper sessions and configuration
resource/ # external resources
daily/ # lightly processed memory
digest/ # long-term digest memory
@ -127,22 +136,26 @@ reme search query="memory" backend=mcp
Configuration parsing supports:
| Capability | Source | Description |
|---|---|---|
| Default configuration | `resolve_app_config()` | Load `reme/config/default.yaml` when `config` is not specified. |
| Explicit configuration | `config=<name-or-path>` | Accept a built-in configuration name or a YAML/JSON file path. |
| Dot notation | `parse_dot_notation()` | For example, `service.port=8181`. |
| Environment variables | `_expand_env_vars()` | Support `${VAR}` and `${VAR:-default}`. |
| Value conversion | `_convert_value()` | Convert bool, int, float, JSON list/dict, and null values automatically. |
| Capability | Source | Description |
|------------------------|-------------------------|--------------------------------------------------------------------------|
| Default configuration | `resolve_app_config()` | Load `reme/config/default.yaml` when `config` is not specified. |
| Explicit configuration | `config=<name-or-path>` | Accept a built-in configuration name or a YAML/JSON file path. |
| Dot notation | `parse_dot_notation()` | For example, `service.port=8181`. |
| Environment variables | `_expand_env_vars()` | Support `${VAR}` and `${VAR:-default}`. |
| Value conversion | `_convert_value()` | Convert bool, int, float, JSON list/dict, and null values automatically. |
### 3.2 Service
`BaseService.run_app()` executes in this order:
Set the optional `service.jobs` list to restrict HTTP or MCP exposure to those job names. If omitted, all jobs with
`enable_serve: true` remain eligible; an empty list exposes none. The whitelist does not override `enable_serve: false`.
When the list is configured, a missing, disabled, unsupported, or invalid selected job fails service startup.
```mermaid
flowchart LR
A["Service.build_service(app)"] --> B["read app.context.jobs"]
B --> C{"job.enable_serve == true?"}
B --> C{"enabled and selected by service.jobs?"}
C -->|yes| D["Service.add_job(job)"]
C -->|no| E["skip registration"]
D --> F["Service.start_service(app)"]
@ -153,19 +166,28 @@ flowchart LR
HTTP service behavior:
| Job type | HTTP exposure |
|---|---|
| Non-`StreamJob` with `enable_serve: true` | `POST /<job.name>` returning `Response` JSON. |
| `StreamJob` | `POST /<job.name>` returning `text/event-stream`. |
| `enable_serve: false` | No endpoint is registered. |
| Job type | HTTP exposure |
|-------------------------------------------|---------------------------------------------------|
| Non-`StreamJob` with `enable_serve: true` | `POST /<job.name>` returning `Response` JSON. |
| `StreamJob` | `POST /<job.name>` returning `text/event-stream`. |
| `enable_serve: false` | No endpoint is registered. |
After registering Job endpoints, the HTTP service can also mount the ReMe Studio single-page application. The default is
`service.web_enabled=true`. Builds are resolved from `service.web_static_dir`, `REME_WEB_STATIC_DIR`, the optional
`reme_studio` package installed by the `web` and `core` extras, and source-tree locations such as
`reme_studio/dist-static`. If no `index.html` is found, only the frontend is skipped and the Job API remains available. The
Studio `GET` fallback does not replace existing `POST /<job.name>` routes.
MCP service behavior:
| Job type | MCP exposure |
|---|---|
| Non-`StreamJob` with `enable_serve: true` | Registered as an MCP tool. |
| `StreamJob` | Currently skipped and not registered. |
| `BackgroundJob` | Forces `enable_serve=False` at construction and is never exposed. |
| Job type | MCP exposure |
|-------------------------------------------|-------------------------------------------------------------------|
| Non-`StreamJob` with `enable_serve: true` | Registered as an MCP tool. |
| `StreamJob` | Currently skipped and not registered. |
| `BackgroundJob` | Forces `enable_serve=False` at construction and is never exposed. |
MCP services can inject server-owned arguments with `injected_job_kwargs`; callers cannot override those arguments. Set
`tool_error_on_failure: true` to expose an unsuccessful ReMe `Response` as an MCP tool error.
## 4. Registry and Dependency Injection
@ -191,24 +213,51 @@ The registry key is:
`component_type` comes from a class attribute:
| Type | Class attribute |
|---|---|
| Step | `BaseStep.component_type = ComponentEnum.STEP` |
| Job | `BaseJob.component_type = ComponentEnum.JOB` |
| Service | `BaseService.component_type = ComponentEnum.SERVICE` |
| Type | Class attribute |
|-----------|-----------------------------------------------------------|
| Step | `BaseStep.component_type = ComponentEnum.STEP` |
| Job | `BaseJob.component_type = ComponentEnum.JOB` |
| Service | `BaseService.component_type = ComponentEnum.SERVICE` |
| FileStore | `BaseFileStore.component_type = ComponentEnum.FILE_STORE` |
The same backend name can therefore exist under different component types. For example, `http` can be both a service backend
and a client backend.
The same backend name can therefore exist under different component types. For example, `http` can be both a service
backend and a client backend.
### 4.2 Registration Through Module Imports
`ComponentEnum` provides the built-in identifiers, but installed plugins may declare a new type with a namespaced
string such as `example.reranker`. Custom identifiers use lowercase letters and numbers separated by `.`, `_`, or `-`.
They are configured under `components` and participate in the same dependency ordering and lifecycle as built-ins.
Registration happens when a module is imported. `reme/components/__init__.py` imports component packages, while
`reme/steps/__init__.py` imports `channel/common/evolve/file_io/index/transfer`. Each package's `__init__.py` then imports
its concrete modules, causing `@R.register(...)` to execute.
### 4.2 Built-in and Plugin Registration
After adding a Step file, make sure the package's `__init__.py` imports it. Otherwise, the backend will not appear in the
registry.
Built-in implementations populate the built-in registry through package imports. ReMe freezes that template after
bootstrap, and each `Application` receives a mutable copy. Runtime code resolves backends through the application's
registry rather than changing the process-wide template. ReMe then loads only the installed plugins explicitly named by
`plugins` in the resolved configuration. A plugin exposes its package through the `reme.plugins` Python entry-point
group. The package's `plugin.yaml` has two optional mappings: `backends` maps registration names to
`module:Class` targets, and `application_defaults` contributes a low-priority `ApplicationConfig` fragment. The
entry-point name is the plugin's identity.
Plugins are enabled explicitly through the application config's `plugins` list or a `plugins=[...]` CLI override.
Plugin registration therefore stays local to one application;
duplicate `(component_type, backend)` providers fail during assembly instead of overwriting each other.
The legacy Python `Plugin` descriptor and `reme.configs` entry points remain accepted during migration. Configuration
files can use `extends` to inherit another built-in, legacy plugin, or file-based configuration. See the independently
packaged [Auto Fin](../../plugins/auto-fin/README.md) and [Daily Paper](../../plugins/daily_paper/README.md) plugins.
Plugin packages are managed locally and remain separate from per-application activation:
```bash
reme plugins list
reme plugins install reme-auto-fin
reme plugins install reme-daily-paper
reme plugins show daily-paper
reme plugins validate daily-paper
reme plugins uninstall daily-paper
reme start plugins='["auto-fin","daily-paper"]'
```
These management commands use the current Python interpreter's pip and never run through an HTTP or MCP service.
### 4.3 Component.bind
@ -227,18 +276,18 @@ flowchart LR
Rules for `BaseComponent.bind(name, BaseClass, optional=True)`:
| Scenario | Behavior |
|---|---|
| `name` is empty | Return `None` and skip the dependency. |
| `app_context` exists | Look up `app_context.components[ctype][name]`. |
| Dependency missing and `optional=True` | Resolve to `None`. |
| Dependency missing and `optional=False` | Fail at startup. |
| Standalone mode | A private component can be created with `default_factory`. |
| Scenario | Behavior |
|-----------------------------------------|------------------------------------------------------------|
| `name` is empty | Return `None` and skip the dependency. |
| `app_context` exists | Look up `app_context.components[ctype][name]`. |
| Dependency missing and `optional=True` | Resolve to `None`. |
| Dependency missing and `optional=False` | Fail at startup. |
| Standalone mode | A private component can be created with `default_factory`. |
### 4.4 Step.Ref
Steps do not participate in component topological startup. They are created temporarily for each Job invocation. Steps access
components primarily through `BaseStep.Ref`:
Steps do not participate in component topological startup. They are created temporarily for each Job invocation. Steps
access components primarily through `BaseStep.Ref`:
```python
file_store: BaseFileStore = Ref(BaseFileStore, ComponentEnum.FILE_STORE)
@ -294,11 +343,13 @@ flowchart LR
F --> G["start CronJob"]
```
During shutdown, objects in `_started_components` are closed in reverse order so dependents close before their dependencies.
During shutdown, objects in `_started_components` are closed in reverse order so dependents close before their
dependencies.
## 6. Job Model
A Job is the orchestration unit for an externally callable capability or background task. Jobs are configured under `jobs:`
A Job is the orchestration unit for an externally callable capability or background task. Jobs are configured under
`jobs:`
in `reme/config/default.yaml`.
### 6.1 BaseJob
@ -319,23 +370,23 @@ flowchart LR
Important source behavior:
| Source | Behavior |
|---|---|
| `_start()` | Parse each Step config from YAML into `(step_cls, params)`. |
| `_build_steps()` | Create new Step instances for every call, avoiding state shared across requests. |
| `__call__()` | Create a `RuntimeContext` and execute Steps sequentially. |
| Exception handling | Catch the exception, set `response.success=False`, and set `answer=str(e)`. |
| Source | Behavior |
|--------------------|----------------------------------------------------------------------------------|
| `_start()` | Parse each Step config from YAML into `(step_cls, params)`. |
| `_build_steps()` | Create new Step instances for every call, avoiding state shared across requests. |
| `__call__()` | Create a `RuntimeContext` and execute Steps sequentially. |
| Exception handling | Catch the exception, set `response.success=False`, and set `answer=str(e)`. |
### 6.2 StreamJob
`StreamJob` extends `BaseJob` but returns streaming chunks:
| Behavior | Description |
|---|---|
| Context | Includes `stream_queue`. |
| Behavior | Description |
|-------------|------------------------------------------------------------|
| Context | Includes `stream_queue`. |
| Step output | Call `context.add_stream_string(text, ChunkEnum.CONTENT)`. |
| Exception | Write `ChunkEnum.ERROR`. |
| Completion | Always send a `DONE` chunk. |
| Exception | Write `ChunkEnum.ERROR`. |
| Completion | Always send a `DONE` chunk. |
### 6.3 BackgroundJob
@ -355,8 +406,8 @@ flowchart LR
J --> K["wait close_timeout; cancel on timeout"]
```
The default `BackgroundJob.__call__()` also executes configured Steps in sequence, but it does not swallow exceptions, which
allows the supervisor to restart the task.
The default `BackgroundJob.__call__()` also executes configured Steps in sequence, but it does not swallow exceptions,
which allows the supervisor to restart the task.
### 6.4 CronJob
@ -382,7 +433,9 @@ The current implementation uses `croniter` to calculate the next trigger time. T
```mermaid
flowchart LR
Jobs["default.yaml jobs"] --> BG["background<br/>index_update_loop<br/>resource_watch_loop<br/>digest_watch_loop"]
Jobs --> Base["base<br/>version / help / health_check<br/>search / node_search / traverse / reindex<br/>read / write / edit / delete / move / list / stat<br/>daily_list / daily_reindex / daily_write<br/>auto_memory / auto_resource / auto_dream / proactive"]
Jobs --> Cron["cron<br/>dream_cron<br/>optimize_index_cron"]
Jobs --> Stream["stream<br/>chat"]
Jobs --> Base["base<br/>version / help / health_check / status / app_config<br/>search / node_search / traverse / graph_snapshot / reindex<br/>read / load / read_image / write / save / edit / delete / move / list / stat / frontmatter_*<br/>daily_list / daily_reindex / daily_write<br/>auto_memory / auto_memory_cc / auto_resource / auto_dream / proactive"]
```
## 7. Step Model
@ -406,12 +459,12 @@ flowchart LR
`RuntimeContext` is shared by all Steps within one Job invocation:
| Field | Description |
|---|---|
| `response` | Final `Response(answer, success, metadata)`. |
| `data` | Free-form dictionary containing input parameters and intermediate results. |
| `stream_queue` | Output queue for streaming Jobs. |
| `stop_event` | Stop signal for background Jobs. |
| Field | Description |
|----------------|----------------------------------------------------------------------------|
| `response` | Final `Response(answer, success, metadata)`. |
| `data` | Free-form dictionary containing input parameters and intermediate results. |
| `stream_queue` | Output queue for streaming Jobs. |
| `stop_event` | Stop signal for background Jobs. |
Common Step code:
@ -475,21 +528,22 @@ flowchart LR
Current default components in `reme/config/default.yaml`:
| ComponentEnum | Name | Backend | Description |
|---|---|---|---|
| `service` | singleton | `http` | Default HTTP service. |
| `tokenizer` | `default` | `regex` | BM25 tokenizer. |
| `as_embedding` | `default` | `${EMBEDDING_BACKEND:-openai}` | Embedding model wrapper. |
| `embedding_store` | `default` | `local` | Embedding store depending on `as_embedding: default`. |
| `as_llm` | `default` | `${LLM_BACKEND:-openai}` | LLM model wrapper. |
| `agent_wrapper` | `default` | `agentscope` | AgentScope wrapper. |
| `agent_wrapper` | `claude_code` | `claude_code` | Claude Code wrapper. |
| `file_graph` | `default` | `local` | Wikilink graph. |
| `file_catalog` | `default/resource/digest/dream` | `local` | File-change checkpoints. |
| `file_chunker` | `markdown` | `markdown` | Markdown AST chunking. |
| `file_chunker` | `default` | `default` | Default text chunking, currently supporting `jsonl`. |
| `keyword_index` | `default` | `bm25` | BM25 keyword index. |
| `file_store` | `default` | `local` | Combines file_graph and keyword_index; defaults to `embedding_store: ""`. |
| ComponentEnum | Name | Backend | Description |
|-------------------|---------------------------------|--------------------------------------------------|--------------------------------------------------------------------------------|
| `service` | singleton | `http` | Default HTTP service. |
| `tokenizer` | `default` | `regex` | BM25 tokenizer. |
| `as_embedding` | `default` | Not configured by default; example uses `openai` | Provides the embedding model wrapper after uncommenting the example config. |
| `embedding_store` | `default` | Not configured by default; example uses `local` | Depends on `as_embedding: default` after uncommenting the example config. |
| `as_llm` | `default` | `${LLM_BACKEND:-openai}` | LLM model wrapper. |
| `agent_wrapper` | `default` | `agentscope` | AgentScope wrapper. |
| `agent_wrapper` | `claude_code` | `claude_code` | Claude Code wrapper. |
| `agent_wrapper` | `codex/codex_oauth` | `codex` | Codex wrappers for API-key and OAuth authentication. |
| `file_graph` | `default` | `local` | Wikilink graph. |
| `file_catalog` | `default/resource/digest/dream` | `local` | File-change checkpoints. |
| `file_chunker` | `markdown` | `markdown` | Markdown AST chunking. |
| `file_chunker` | `json/jsonl/default` | `json/jsonl/default` | JSON, JSONL, and generic text chunkers; generic text supports `txt` and `log`. |
| `keyword_index` | `default` | `bm25` | BM25 keyword index. |
| `file_store` | `default` | `local` | Combines file_graph and keyword_index; defaults to `embedding_store: ""`. |
Note that the `search` Step configuration contains `vector_weight`, but `file_store.default.embedding_store` is empty by
default. Vector retrieval is available only when the runtime configuration enables an embedding store.
@ -547,12 +601,12 @@ class MySearchStep(BaseStep):
Common attributes available directly:
| Attribute | Component resolved by default |
|---|---|
| `self.as_llm` | `.model` from `as_llm: default`. |
| Attribute | Component resolved by default |
|----------------------|-------------------------------------|
| `self.as_llm` | `.model` from `as_llm: default`. |
| `self.agent_wrapper` | `agent_wrapper: default`; optional. |
| `self.file_catalog` | `file_catalog: default`; optional. |
| `self.file_store` | `file_store: default`. |
| `self.file_catalog` | `file_catalog: default`; optional. |
| `self.file_store` | `file_store: default`. |
To select a non-default component from Job configuration:
@ -564,13 +618,13 @@ steps:
### 9.4 Step Design Guidance
| Guidance | Reason |
|---|---|
| Read input from `context` and write intermediate results to `context`. | A multi-Step Job passes data through the same context. |
| Write the final result to `context.response`. | Services and clients consume the standard `Response`. |
| Do not store request-scoped state on a Step instance. | A Step is rebuilt for every Job call, and stateless Steps are easier to test. |
| A background loop that supports interruption should check `context.stop_event`. | `BackgroundJob.close()` relies on the stop event for graceful shutdown. |
| Call `add_stream_string()` only from a StreamJob. | A normal Job has no stream queue. |
| Guidance | Reason |
|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| Read input from `context` and write intermediate results to `context`. | A multi-Step Job passes data through the same context. |
| Write the final result to `context.response`. | Services and clients consume the standard `Response`. |
| Do not store request-scoped state on a Step instance. | A Step is rebuilt for every Job call, and stateless Steps are easier to test. |
| A background loop that supports interruption should check `context.stop_event`. | `BackgroundJob.close()` relies on the stop event for graceful shutdown. |
| Call `add_stream_string()` only from a StreamJob. | A normal Job has no stream queue. |
### 9.5 Unit Test Example
@ -593,8 +647,8 @@ async def test_uppercase_step():
## 10. Adding a Job
A Job usually requires no new Python class; configure existing Steps instead. Add a new Job backend only when a new execution
model is required.
A Job usually requires no new Python class; configure existing Steps instead. Add a new Job backend only when a new
execution model is required.
### 10.1 Adding a Normal Request Job
@ -729,11 +783,11 @@ jobs:
Characteristics of a background Job:
| Characteristic | Description |
|---|---|
| Not externally exposed | `BackgroundJob.__init__()` forces `enable_serve=False`. |
| Has a supervisor | Restarts with exponential backoff after an exception by default. |
| Has a stop event | Notifies the loop to exit during close. |
| Characteristic | Description |
|---------------------------------|--------------------------------------------------------------------|
| Not externally exposed | `BackgroundJob.__init__()` forces `enable_serve=False`. |
| Has a supervisor | Restarts with exponential backoff after an exception by default. |
| Has a stop event | Notifies the loop to exit during close. |
| Suitable for watching/consuming | File watching, queue consumption, and periodic long-running loops. |
### 10.5 Adding a Cron Job
@ -760,14 +814,14 @@ An invalid `cron` expression fails at startup.
Most use cases require only a new Step plus a YAML Job. Consider adding `reme/components/job/*.py` only in these cases:
| Requirement | New Job class? |
|---|---|
| Add a business command | No; use `backend: base`. |
| Chain existing steps | No; use `steps:`. |
| Need SSE/streaming output | No; use `backend: stream`. |
| Need a background loop | No; use `backend: background`. |
| Need cron scheduling | No; use `backend: cron`. |
| Need entirely new scheduling, concurrency, or transaction semantics | Yes; add a Job backend. |
| Requirement | New Job class? |
|---------------------------------------------------------------------|--------------------------------|
| Add a business command | No; use `backend: base`. |
| Chain existing steps | No; use `steps:`. |
| Need SSE/streaming output | No; use `backend: stream`. |
| Need a background loop | No; use `backend: background`. |
| Need cron scheduling | No; use `backend: cron`. |
| Need entirely new scheduling, concurrency, or transaction semantics | Yes; add a Job backend. |
Minimal shape of a new Job backend:

View file

@ -6,37 +6,39 @@ ReMe's core idea is **Memory as File, File as Memory**.
<img src="../figure/memory-as-file.svg" alt="ReMe Memory as File model" width="92%">
</p>
**Memory as File**: long-term memory is not hidden in a black-box database. It lives in Markdown files, resource files, and
index snapshots under the workspace. Users and agents can directly read, write, move, and delete those files.
**Memory as File**: long-term memory is not hidden in a black-box database. Its source material and readable memories
live in user-owned files under the workspace. Users and agents can directly read, write, move, and delete those files;
indexes and snapshots under `metadata/` are derived state that can be rebuilt.
**File as Memory**: each file is more than ordinary text. It is an indexable, linkable, and evolvable memory node. ReMe parses
frontmatter, body chunks, and wikilink edges from files and organizes them into retrieval indexes and a graph.
**File as Memory**: each file is more than ordinary text. It is an indexable, linkable, and evolvable memory node. ReMe
parses frontmatter, body chunks, and wikilink edges from files and organizes them into retrieval indexes and a graph.
In other words, files are both a human-readable interface and an operational interface for agents. Directory structure
carries the memory layers, while Markdown syntax expresses content, metadata, and relationships.
## Design Goals
ReMe represents memory as files not merely for convenient storage, but to give long-term memory several essential properties:
ReMe represents memory as files not merely for convenient storage, but to give long-term memory several essential
properties:
| Goal | Meaning |
|---|---|
| Readable | Users can open the workspace directly and read daily notes, digest nodes, and source material like ordinary notes. |
| Editable | Users and agents can correct, extend, move, or delete memory with file operations, without a specialized database client. |
| Traceable | Long-term conclusions in digest can point back to daily, resource, or session sources through `derived_from:: [[...]]`. |
| Portable | The workspace is an ordinary directory. Markdown, JSONL, YAML, and resource files can be backed up, synchronized, versioned, or moved to other tools. |
| Indexable | Although the files are plain text, ReMe parses frontmatter, chunks, and wikilinks to build a retrieval index and file graph. |
| Collaborative | Humans judge and correct; agents organize, link, and retrieve. Both operate on the same files. |
| Goal | Meaning |
|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| Readable | Users can open the workspace directly and read daily notes, digest nodes, and source material like ordinary notes. |
| Editable | Users and agents can correct, extend, move, or delete memory with file operations, without a specialized database client. |
| Traceable | Long-term conclusions in digest can point back to daily, resource, or session files from a Sources section. |
| Portable | The workspace is an ordinary directory. Markdown, JSONL, YAML, and resource files can be backed up, synchronized, versioned, or moved to other tools. |
| Indexable | Although the files are plain text, ReMe parses frontmatter, chunks, and wikilinks to build a retrieval index and file graph. |
| Collaborative | Humans judge and correct; agents organize, link, and retrieve. Both operate on the same files. |
ReMe memory is therefore neither a hidden database record nor a prompt fragment visible only to an LLM. It is first a file
owned by the user and only then indexed by the system for retrieval.
ReMe memory is therefore neither a hidden database record nor a prompt fragment visible only to an LLM. It is first a
file owned by the user and only then indexed by the system for retrieval.
## Memory Layers
A ReMe workspace divides memory into four layers:
```text
raw input -> session/ + resource/
source records -> session/ + resource/
working memory -> daily/
long memory -> digest/
system state -> metadata/
@ -44,19 +46,23 @@ system state -> metadata/
Each layer solves a different problem.
`session/` and `resource/` preserve raw input. Their purpose is to retain the original situation: conversations, agent
sessions, uploaded material, web pages, and reports remain intact as evidence for later verification.
`session/` and `resource/` preserve source records. Files under `resource/` remain unchanged at their original path.
Standard Auto Memory records retain conversation messages while intentionally omitting tool-result and base64 data
blocks; this keeps recalled output and binary payloads from masquerading as user-provided evidence. Generated Agent
runtime state instead lives under `mem_session/`.
`daily/` is the lightly processed layer. It organizes the day's conversations and resources into more readable daily notes:
what happened, which conclusions were reached, which follow-up tasks remain, and where the source material lives. Daily does
not aim for final abstraction; it is closer to a workbench for the day.
`daily/` is the lightly processed layer. It organizes the day's conversations and resources into more readable daily
notes:
what happened, which conclusions were reached, which follow-up tasks remain, and where the source material lives. Daily
does not aim for final abstraction; it is closer to a workbench for the day.
`digest/` is the deeply processed layer. It stores memory nodes that can be reused over time, such as user preferences,
project background, procedural experience, conceptual knowledge, and decision precedents. Digest should not merely copy
daily. It should merge recurring facts, methods, and relationships into more stable descriptions.
`metadata/` is the system index layer. It stores runtime state such as the file catalog, chunk index, and graph snapshots.
Users normally do not edit this content manually. The actual editing surface is `daily/`, `digest/`, and, when necessary,
`metadata/` is the system index layer. It stores runtime state such as the file catalog, chunk index, and graph
snapshots. Users normally do not edit this content manually. The actual editing surface is `daily/`, `digest/`, and,
when necessary,
`resource/`.
These layers let ReMe preserve both the original situation and its abstraction: daily reconstructs what happened, while
@ -73,21 +79,23 @@ The corresponding automatic flows are [Auto Memory](./auto_memory.md), [Auto Res
```text
<workspace_dir>/
├── metadata/ # system index layer; persistent indexes, graph, catalogs; not a manual editing surface
├── session/ # raw input layer; original conversations and agent sessions
├── session/ # source-record layer; source conversations
│ ├── dialog/
│ │ └── <session_id>.jsonl # conversation messages saved by auto_memory
│ ├── agentscope/
│ │ └── <session_id>.jsonl
│ │ └── <session_id>.jsonl # source messages saved by auto_memory
│ └── claude_code/
│ └── <session_id>.jsonl
├── resource/ # raw input layer; original external material
│ └── <session_id>.jsonl # ReMe copy used by auto_memory_cc
├── mem_session/ # generated Agent wrapper sessions/config, not user memory
│ ├── agentscope/
│ ├── claude_config/
│ └── codex/
├── resource/ # source-record layer; original external material
│ ├── <resource>.<ext> # root-level input uses today's date
│ └── YYYY-MM-DD/
│ └── <resource>.<ext>
│ └── <resource>.<ext> # dated input uses the directory date
├── daily/ # lightly processed layer; facts, conversation summaries, and resource interpretations by date
│ ├── YYYY-MM-DD.md # index page for the day
│ └── YYYY-MM-DD/
│ ├── <session_id>.md # daily note distilled from a conversation
│ ├── <resource_stem>.md # daily note distilled from a resource
│ ├── <generated_name>.md # topic-named conversation or resource card
│ └── interests.yaml # proactive interest topics generated by auto_dream
└── digest/ # deeply processed layer; reusable personal facts, procedures, and knowledge nodes
├── personal/
@ -103,17 +111,21 @@ Typical flows:
```text
conversation
-> session/dialog/<session_id>.jsonl
-> daily/YYYY-MM-DD/<session_id>.md
-> daily/YYYY-MM-DD/<generated_name>.md
-> digest/personal | digest/procedure | digest/wiki
external resource
-> resource/YYYY-MM-DD/<resource>.<ext>
-> daily/YYYY-MM-DD/<resource_stem>.md
-> resource/[YYYY-MM-DD/]<resource>.<ext>
-> daily/YYYY-MM-DD/<generated_name>.md
-> digest/wiki | digest/procedure
```
The first two steps focus on recording and organizing; the final step focuses on long-term distillation. `auto_memory` and
`auto_resource` generate daily notes from raw input, and `auto_dream` extracts and integrates digest nodes from daily.
The first two steps focus on recording and organizing; the final step focuses on long-term distillation. `auto_memory`
and
`auto_resource` generate daily notes from source input, and `auto_dream` extracts and integrates digest nodes from
daily. The generated daily filename comes from validated frontmatter `name`; `session_id`, `source_conversation`, and
`source_resource`
provide stable provenance and lookup identity instead of determining the filename.
## Markdown Format
@ -131,9 +143,7 @@ tags: [new energy, solar]
# Conclusions
The solar supply chain consists of [[digest/wiki/polysilicon.md]], wafers, cells, and modules.
upstream:: [[digest/wiki/polysilicon.md]]
[company:: [[digest/wiki/longi.md|LONGi]]]
One major producer is [[digest/wiki/longi.md|LONGi]].
```
### Frontmatter
@ -148,8 +158,8 @@ source_conversation: [[session/dialog/abc.jsonl]]
---
```
The current code recognizes `name` and `description` explicitly. Other fields are preserved as additional metadata. The write
interface merges `name`, `description`, and `metadata` into frontmatter.
The current code recognizes `name` and `description` explicitly. Other fields are preserved as additional metadata. The
write interface merges `name`, `description`, and `metadata` into frontmatter.
Treat frontmatter as a node-level summary and the body as evidence, explanation, and relationships. For example:
@ -163,28 +173,31 @@ confidence: observed
The user repeatedly asks documentation to explain motivation, boundaries, and examples while avoiding marketing language.
derived_from:: [[daily/2026-06-20/session-a.md]]
related:: [[digest/procedure/technical-documentation.md]]
Apply this preference when following [[digest/procedure/technical-documentation.md]].
## Sources
This preference was recorded in [[daily/2026-06-20/documentation-style.md]], which captures the user's repeated guidance.
```
This has three benefits:
1. `name` and `description` serve as lightweight summaries in lists, recall results, and agent decisions.
2. The body can carry fuller facts, conditions, counterexamples, and sources.
3. Typed wikilinks such as `derived_from::` and `related::` can be parsed by the graph and maintained when files move.
3. Ordinary wikilinks can be parsed by the graph and maintained when files move.
Frontmatter is best for stable, short, structured fields; the body is best for explanations meant for people. Do not put long
body text into YAML fields.
Frontmatter is best for stable, short, structured fields; the body is best for explanations meant for people. Do not put
long body text into YAML fields.
### Wikilink
Wikilinks express relationships between files with `[[...]]`:
```text
[[digest/wiki/solar.md]]
[[digest/wiki/solar.md#supply-chain]]
[[digest/wiki/solar.md|solar]]
![[resource/2026-06-01/report.md]]
[[daily/2026-06-20/session.md]]
[[notes/example.md#L9]]
[[notes/example.md#L9-L10]]
[[notes/example.md#L9-L10,L15-L20]]
```
ReMe wikilinks use **literal path semantics**:
@ -196,69 +209,73 @@ ReMe wikilinks use **literal path semantics**:
ReMe does not append `.md` automatically, search by filename, or automatically resolve folder notes. Use complete
workspace-relative paths with their extensions.
Ordinary Markdown links such as `[label](../wiki/example.md)` do not create `FileLink` edges and are not rewritten by
move or retarget operations.
Anchors such as `#L9`, `#L9-L10`, and `#L9-L10,L15-L20` remain ordinary `target_anchor` strings in the graph. The graph
parser does not validate line-anchor syntax, so values such as `#L0`, `#L10-L9`, and `#L9,` are also stored. The `read`
job does not interpret an anchor appended to `path`; use the separate 1-based, inclusive `start_line` and `end_line`
arguments to read a range, for example `read(path="digest/wiki/solar.md", start_line=9, end_line=10)`.
Wikilinks support these behaviors:
```text
body link -> create a FileLink
predicate:: link -> create a FileLink with a relationship name
move a file -> rewrite [[old path]] in inbound edges by default
delete a file -> return remaining inbound edges so references can be cleaned up
search match -> expand inbound and outbound links to provide context
```
Supported relationship forms:
```markdown
industry:: [[digest/wiki/new-energy.md]]
[competitor:: [[digest/wiki/byd.md]]]
```
Parsed result:
```text
FileLink
source_path = current file
target_path = digest/wiki/new-energy.md
predicate = industry
target_path = notes/example.md
target_anchor = L9-L10,L15-L20
```
Older documents containing wrappers such as `related:: [[path]]`,
`- related:: [[path]]`, or `[related:: [[path]]]` remain readable. ReMe ignores the surrounding text and indexes the
inner `[[path]]` as an ordinary link. After upgrading from a version that stored typed links, run `reme reindex`
once to rebuild the derived graph without the removed relationship field.
### Sources and Relationships
The two most important link types in ReMe are source links and conceptual relationship links.
A source link explains where a long-term memory came from:
A Sources section records where a long-term memory came from:
```markdown
derived_from:: [[daily/2026-06-20/session-a.md]]
derived_from:: [[resource/2026-06-20/report.pdf]]
## Sources
The preference was observed in [[daily/2026-06-20/documentation-style.md]], and the supporting report evidence is retained in
[[resource/2026-06-20/report.pdf]].
```
A conceptual relationship link explains which other long-term memories relate to the node:
A conceptual relationship link explains which other long-term memories relate to the node. Weave it into natural prose:
```markdown
related:: [[digest/wiki/solar-supply-chain.md]]
depends_on:: [[digest/procedure/research-report-analysis.md]]
contrasts_with:: [[digest/wiki/central-inverter.md]]
This analysis extends [[digest/wiki/solar-supply-chain.md]], follows
[[digest/procedure/research-report-analysis.md]], and contrasts with
[[digest/wiki/central-inverter.md]].
```
Ordinary body wikilinks also create graph edges, but when the relationship itself has semantic value, prefer
`predicate:: [[path]]`. This makes the meaning of links clearer to search, graph traversal, and later agent integration.
## Human and Agent Editing
Because memory is stored as files, users can edit the workspace directly, while agents can read and write the same files
through ReMe's file tools. Both follow the same conventions:
| Operation | Guidance |
|---|---|
| Add memory | Write to the appropriate directory, use frontmatter for Markdown, and prefer complete workspace-relative wikilinks. |
| Edit a body | Preserve existing sources and important wikilinks. When correcting an old conclusion, explain how the new material changes the previous judgment. |
| Move a file | ReMe's move tool rewrites old paths in inbound edges by default. After a manual move, inspect inbound links again. |
| Operation | Guidance |
|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| Add memory | Write to the appropriate directory, use frontmatter for Markdown, and prefer complete workspace-relative wikilinks. |
| Edit a body | Preserve existing sources and important wikilinks. When correcting an old conclusion, explain how the new material changes the previous judgment. |
| Move a file | ReMe's move tool rewrites old paths in inbound edges by default. After a manual move, inspect inbound links again. |
| Delete a file | Check inbound links first. ReMe's delete tool returns source files that still point to the target, making dangling references easier to clean up. |
| Edit metadata | Use frontmatter for short fields. When the body changes substantially, update `description` as well. |
| Edit metadata | Use frontmatter for short fields. When the body changes substantially, update `description` as well. |
A practical rule is: **an agent may rewrite the wording, but it must not lose evidence edges**. In particular,
`derived_from:: [[...]]` and existing digest-to-digest wikilinks are the basis for traceable and extensible long-term memory.
A practical rule is: **an agent may rewrite the wording, but it must not lose evidence edges**. In particular, Sources
entries and existing digest-to-digest wikilinks are the basis for traceable and extensible long-term memory.
## Path Semantics
@ -266,7 +283,7 @@ All file tools and wikilinks use workspace-relative paths as their basic unit:
```text
digest/wiki/solar.md
daily/2026-06-20/session-a.md
daily/2026-06-20/documentation-style.md
resource/2026-06-20/report.pdf
```
@ -278,16 +295,16 @@ Recommended practices:
1. Include `.md` when linking a Markdown file.
2. Use the complete source path when linking from digest to daily or resource.
3. Rename or move files through ReMe's move tool whenever possible to avoid stale paths.
4. Put external source material under `resource/YYYY-MM-DD/...` and long-term abstractions under `digest/...`. Do not put
raw source material directly into digest.
4. Put external source material under `resource/YYYY-MM-DD/...` and long-term abstractions under `digest/...`. Do not
put raw source material directly into digest.
Explicit path semantics sacrifice a little convenience when writing by hand, but provide predictability, portability, and
automatic maintainability.
Explicit path semantics sacrifice a little convenience when writing by hand, but provide predictability, portability,
and automatic maintainability.
## Memory Chunking
Memory chunking divides a file into retrievable fragments. ReMe does not split Markdown at fixed lengths by default; it tries
to preserve semantic structure.
Memory chunking divides a file into retrievable fragments. ReMe does not split Markdown at fixed lengths by default; it
tries to preserve semantic structure.
This section explains how files become retrieval chunks. For index updates, BM25, vector recall, and link expansion, see
[Memory Search](./memory_search.md).
@ -302,8 +319,8 @@ Document
chunk 1 | chunk 2 | chunk 3 | ...
```
This is simple, but it can cut headings, tables, code blocks, lists, and `[[wikilinks]]` in the middle. After a match, the
agent often sees only an isolated fragment without knowing its section or relationship to other memory nodes.
This is simple, but it can cut headings, tables, code blocks, lists, and `[[wikilinks]]` in the middle. After a match,
the agent often sees only an isolated fragment without knowing its section or relationship to other memory nodes.
ReMe chunking is closer to splitting memory by file structure:
@ -369,5 +386,10 @@ Matched body fragment
This lets the agent see not only an isolated paragraph but also its structural position in the source file.
Non-Markdown files use `DefaultFileChunker` by default. It splits by byte size and preserves a small overlap. For Markdown,
the chunker also avoids cutting `[[wikilinks]]` in the middle.
Non-Markdown files use `DefaultFileChunker` by default. It splits by byte size and preserves a small overlap. For
Markdown, the chunker also avoids cutting `[[wikilinks]]` in the middle.
`DefaultFileChunker` and `MarkdownFileChunker` decode files with their configured `encoding` and normalize platform
newlines to LF before indexing. Their default `invalid_encoding_policy: replace` keeps decodable content searchable
when a source contains invalid bytes, without modifying the source file. Set `invalid_encoding_policy: strict` on a
chunker component to reject such files instead.

View file

@ -1,8 +1,10 @@
# Memory Search
Memory Search is ReMe's memory retrieval entry point. It continuously builds files under `daily/`, `digest/`, and `resource/`
into a searchable chunk index and wikilink graph. At query time, it first recalls the most relevant fragments and then expands
context along the bidirectional links of the files containing those fragments.
Memory Search is ReMe's memory retrieval entry point. The default background loop continuously builds Markdown under
`daily/` and `digest/` into a searchable chunk index and wikilink graph. At query time, it first recalls the most
relevant fragments and then expands context along the bidirectional links of the files containing those fragments.
`reme reindex` has a broader rebuild scope that also scans `resource/` and JSONL; it is intentionally different from the
live watcher.
<p align="center">
<img src="../figure/auto-index-and-memory-search.svg" alt="ReMe Auto Index and Memory Search indexing, recall, fusion, and link expansion" width="92%">
@ -21,24 +23,29 @@ workspace files
## What It Searches
The default `index_update_loop` watches three memory directories:
The default `index_update_loop` watches two memory directories:
- `daily_dir`: daily working memory and session memory cards generated by Auto Memory.
- `digest_dir`: long-term distilled digest nodes.
- `resource_dir`: external resources or imported material.
The default suffixes are `md` and `jsonl`. Markdown uses the `markdown` chunker, which parses frontmatter, heading structure,
and `[[wikilinks]]`. JSONL uses the `default` chunker and creates overlapping chunks by byte size.
The live watcher handles only the `md` suffix. A separate `resource_watch_loop` watches `resource_dir`, and Auto
Resource turns those inputs into daily cards that enter the live index. When `reme reindex` is run manually, its
configuration scans
`daily_dir`, `digest_dir`, and `resource_dir` for `md` and `jsonl`; Markdown uses the `markdown` chunker and JSONL uses
the
`jsonl` chunker.
## How the Index Is Built
### Index Update
The background Job `index_update_loop` maintains the index using configuration from `reme/config/default.yaml`:
```yaml
index_update_loop:
backend: background
watch_dirs: [ daily_dir, digest_dir, resource_dir ]
watch_suffixes: [ md, jsonl ]
watch_dirs: [daily_dir, digest_dir]
watch_suffixes: [md]
steps:
- backend: init_changes_step
monitor_type: file_store
@ -52,9 +59,9 @@ index_update_loop:
`FileNode.st_mtime` values already stored in `file_store`, calculates added, modified, and deleted changes, and passes
`context["changes"]` to `update_index_step`.
While the service is running, `watch_changes_step` takes over. It uses `watchfiles.awatch()` to watch the same directories,
groups file events within a quiet window, and uses `coalesce_changes()` to collapse repeated events for the same path into one
stable batch of changes.
While the service is running, `watch_changes_step` takes over. It uses `watchfiles.awatch()` to watch the same
directories, groups file events within a quiet window, and uses `coalesce_changes()` to collapse repeated events for the
same path into one stable batch of changes.
`update_index_step` performs the actual index writes:
@ -64,9 +71,26 @@ stable batch of changes.
4. For a deleted file, remove its records from `file_store`, `keyword_index`, and `file_graph`.
5. When changes exist, dump state to `metadata/` so it can be restored on the next startup.
The Markdown chunker parses YAML frontmatter, heading structure, and `[[...]]` into `FileNode`, `FileChunk`, and `FileLink`
The Markdown chunker parses YAML frontmatter, heading structure, and wikilinks into `FileNode`, `FileChunk`, and
`FileLink`
objects. For detailed chunking rules, see [Memory as File](./memory_as_file.md#memory-chunking).
### Index Optimization
Both BM25 and the FAISS HNSW vector index use tombstone markers instead of physical removal when deleting nodes; too
many tombstones degrade search performance. An idle-time optimization mechanism is built in—the `optimize_index_cron`
scheduled job compacts tombstones and rebuilds indexes during off-peak hours:
```yaml
optimize_index_cron:
backend: cron
cron: "0 2 * * *"
steps:
- backend: optimize_index_step
```
By default it runs at 2:00 AM daily; adjust the cron expression to customize the schedule.
## What file_store Contains
The default `file_store.default` backend is `local`:
@ -82,15 +106,24 @@ file_store:
It combines three kinds of capability:
| Part | Default state | Purpose |
|---|---|---|
| `file_chunks` | Enabled | Store `FileChunk` text, line numbers, scores, and optional embeddings. |
| `keyword_index.default` | Enabled | BM25 inverted index where chunk ID is the document ID. |
| `file_graph.default` | Enabled | Store `FileNode` objects and wikilink edges. |
| `embedding_store` | Disabled | When enabled, generate embeddings for chunks and support vector recall. |
| Part | Default state | Purpose |
|-------------------------|---------------|-------------------------------------------------------------------------|
| `file_chunks` | Enabled | Store `FileChunk` text, line numbers, scores, and optional embeddings. |
| `keyword_index.default` | Enabled | BM25 inverted index where chunk ID is the document ID. |
| `file_graph.default` | Enabled | Store `FileNode` objects and wikilink edges. |
| `embedding_store` | Disabled | When enabled, generate embeddings for chunks and support vector recall. |
Out of the box, search therefore uses primarily BM25 plus link expansion. After setting `embedding_store: default`,
`SearchStep` runs vector and keyword recall together.
`SearchStep` runs vector and keyword recall together. Additionally, switching the `file_store` `backend` from `local` to
`faiss` upgrades vector retrieval from a linear scan to a FAISS HNSW index, offering faster recall at scale.
The embedding store accepts `health_check_timeout` for its startup probe. A temporary failure skips the current vector
backfill while keeping BM25 available; a later successful provider request resumes the missing-vector backfill
automatically.
Embedded integrations that have already verified a provider can call `resume_embedding(verified=True)`. When changing
the embedding vector space, pass `rebuild=True`; persisted vectors are invalidated before a serial background rebuild,
and vector search remains unavailable until the rebuilt vectors are safely persisted.
## How to Search
@ -104,10 +137,12 @@ search:
query: string
limit: integer
min_score: number
start_date: string
end_date: string
steps:
- backend: search_step
vector_weight: 0.7
candidate_multiplier: 3.0
candidate_multiplier: 5.0
expand_links: true
max_links_per_direction: 10
```
@ -118,11 +153,17 @@ Call it with:
reme search query="recent discussions about indexing" limit=5
```
Use `start_date` and `end_date` for inclusive `YYYY-MM-DD` filtering:
```bash
reme search query="index regression" start_date=2026-06-01 end_date=2026-06-20 limit=10
```
`search_step` executes in this order:
```mermaid
flowchart LR
A["query + limit"] --> B["candidates = limit * candidate_multiplier"]
A["query + limit"] --> B["candidates = min(200, limit * candidate_multiplier)"]
B --> C["file_store.vector_search(...)"]
B --> D["file_store.keyword_search(...)"]
C --> E["RRF fusion"]
@ -133,17 +174,17 @@ flowchart LR
H --> I["Response.answer + metadata"]
```
If only BM25 has results, the BM25 ranking is returned directly. If only vector search has results, the vector ranking is
returned directly. When both have results, they are fused with RRF. RRF does not compare BM25 and cosine scores directly; it
compares ranks in the two result lists:
If only BM25 has results, the BM25 ranking is returned directly. If only vector search has results, the vector ranking
is returned directly. When both have results, they are fused with RRF. RRF does not compare BM25 and cosine scores
directly; it compares ranks in the two result lists:
```text
fused_score = vector_weight / (60 + vector_rank)
+ keyword_weight / (60 + keyword_rank)
```
The default `vector_weight=0.7` gives semantic recall more weight when embeddings are enabled, while keyword search can still
promote chunks with exact term matches.
The default `vector_weight=0.7` gives semantic recall more weight when embeddings are enabled, while keyword search can
still promote chunks with exact term matches.
## How BM25 Works
@ -155,15 +196,18 @@ promote chunks with exact term matches.
- The inverted index records which chunks contain each token and its term frequency within each chunk.
- A query scores only the posting lists matching its tokens and returns the highest-scoring chunk IDs.
When a file changes, `LocalFileStore.upsert()` first removes the BM25 documents corresponding to the file's old `chunk_ids`
When a file changes, `LocalFileStore.upsert()` first removes the BM25 documents corresponding to the file's old
`chunk_ids`
and then adds the new chunk text. Deletion is lazy; the index can later be compacted with optimize.
## Progressive Expansion
"Progressive" in Memory Search does not mean putting the entire repository into one result. Retrieval expands in three layers:
"Progressive" in Memory Search does not mean putting the entire repository into one result. Retrieval expands in three
layers:
1. Chunk recall: return only the `limit` most relevant text fragments.
2. File location: each result includes `path:start_line-end_line`, allowing the caller to read the source precisely with `read`.
2. File location: each result includes `path:start_line-end_line`. Pass the path and line bounds separately as `path`,
`start_line`, and `end_line` when calling `read`; the range is not part of the `path` value.
3. Link neighbors: call `expand_links()` for each matched file and expand at most `max_links_per_direction` outlinks and
inlinks.
@ -175,11 +219,11 @@ matched chunk
-> file_store.get_outlinks(path)
-> file_store.get_inlinks(path)
-> file_store.get_nodes(neighbor_paths)
-> render neighbor path, name, description, predicate, and anchor
-> render neighbor path, name, description, and anchor
```
This keeps search results short while still showing which long-term nodes, resources, or other daily notes a memory connects
to. If a result is worth pursuing, use `read path=...` to open the source or
This keeps search results short while still showing which long-term nodes, resources, or other daily notes a memory
connects to. If a result is worth pursuing, use `read path=...` to open the source or
`traverse path=... depth=2` to continue along the wikilink graph.
## Return Format
@ -193,15 +237,13 @@ to. If a result is worth pursuing, use `read path=...` to open the source or
Typical text structure:
```text
========== daily/2026-06-20/session-a.md:12-28 [score=0.0317 keyword=4.8120] ==========
========== daily/2026-06-20/retrieval-regression.md:12-28 [score=0.0317 keyword=4.8120] ==========
...matched memory fragment...
outlinks (2):
-> digest/indexing.md name="Indexing" description="..."
via predicate=related
inlinks (1):
<- daily/2026-06-19.md name="..."
via plain
```
`counts` reports how many vector and keyword candidates were recalled and how many results were ultimately returned. With
embeddings disabled by default, `vector` is usually `0` and `hybrid` is `false`.
`counts` reports how many vector and keyword candidates were recalled and how many results were ultimately returned.
With embeddings disabled by default, `vector` is usually `0` and `hybrid` is `false`.

View file

@ -0,0 +1,225 @@
# Plugin Management
ReMe plugins are ordinary Python distributions discovered through the `reme.plugins` entry-point group. Installing a
plugin makes it available to the current Python environment; it does not enable the plugin in every ReMe application.
Keep these two operations separate:
```text
reme plugins install ... install a package into the current Python environment
plugins: [auto-fin] enable an installed plugin for one Application
```
Plugin package management is local-only. It does not run through a ReMe HTTP or MCP service and never edits application
configuration files automatically.
A typical plugin workflow has three stages:
1. Install ReMe and the plugin distribution.
2. Configure the plugin's runtime environment as described in the
[ReMe model-configuration guide](../../README.md#optional-model-configuration).
3. Start an Application with the plugin explicitly enabled, for example
`reme start plugins='["auto-fin"]'`.
## List installed plugins
```bash
reme plugins list
```
The table shows the plugin entry-point name, Python distribution, version, and plugin contract:
```text
PLUGIN DISTRIBUTION VERSION FORMAT
-------- ------------- ------- --------
auto-fin reme-auto-fin X.Y.Z manifest
```
`manifest` plugins use the current package-level `plugin.yaml` contract. `legacy` plugins use the compatible Python
descriptor contract.
A manifest separates backend registration from application configuration:
```yaml
backends:
example_step: example_plugin.steps:ExampleStep
application_defaults:
jobs:
example:
backend: base
steps:
- backend: example_step
```
`application_defaults` is a partial `ApplicationConfig`. It is kept below the manifest's `backends` namespace because
backend import declarations are part of plugin discovery and are not application configuration.
Use JSON when another local tool needs structured output:
```bash
reme plugins list --json
```
To compare installed plugins with one application config:
```bash
reme plugins list --config default
```
The optional `ENABLED` column reflects only the `plugins` list resolved from that config. A command-line override used
by another running process is not a global enable state.
## Install a plugin package
Install a published distribution:
```bash
reme plugins install reme-auto-fin
```
Install or upgrade a pinned version:
```bash
reme plugins install 'reme-auto-fin==X.Y.Z'
reme plugins install reme-auto-fin --upgrade
```
Install a local plugin project:
```bash
reme plugins install ./plugins/auto-fin
```
Use editable mode while developing it:
```bash
reme plugins install ./plugins/auto-fin --editable
```
ReMe invokes pip through the same Python interpreter that runs the `reme` command. Pip remains responsible for package
resolution, downloads, dependency changes, and build execution. Install only packages and local projects you trust.
After installation, confirm the discovered plugin name:
```bash
reme plugins list
reme plugins validate auto-fin
```
## Inspect a plugin
```bash
reme plugins show auto-fin
```
For a manifest plugin, the result includes its registered backend names and default Job names. JSON output is also
available:
```bash
reme plugins show auto-fin --json
```
`show` identifies the package contract without constructing a ReMe Application.
## Validate a plugin
Validate an installed plugin:
```bash
reme plugins validate auto-fin
```
Validate a local project before installation:
```bash
reme plugins validate ./plugins/auto-fin
```
Validation checks the entry point, `plugin.yaml`, backend imports and component types, registry collisions, merged
`application_defaults`, and the resulting `ApplicationConfig`. Validation imports plugin backend modules, so run it
only for trusted code.
## Enable a plugin in a service
Installation alone does not load plugin code into an Application. Enable plugins explicitly in configuration:
```yaml
plugins:
- auto-fin
```
Or add them for one service launch:
```bash
reme start plugins='["auto-fin"]'
```
When `config` is omitted, ReMe loads `default.yaml`. The plugin's `application_defaults` are merged below that config,
so explicit config values and CLI overrides win. This mapping is an `ApplicationConfig` fragment, not a separate
configuration schema. The plugin backends are registered only in that Application's local registry.
After the default HTTP service starts, access plugin Jobs through ReMe's CLI client or HTTP:
```bash
reme auto_fin topics="黄金,AI,存储芯片"
```
```bash
curl -s http://127.0.0.1:2333/auto_fin \
-H 'Content-Type: application/json' \
-d '{"topics":"黄金,AI,存储芯片"}'
```
When the application uses an MCP service, service-enabled plugin Jobs appear as MCP tools instead.
Custom application configs must provide the plugin's runtime dependencies, including an `agent_wrapper.default` and
the `search` and `read` Jobs used by Auto Fin.
## Uninstall a plugin
Use the plugin entry-point name, not necessarily the distribution name:
```bash
reme plugins uninstall auto-fin
```
Skip pip's confirmation prompt when needed:
```bash
reme plugins uninstall auto-fin --yes
```
ReMe resolves `auto-fin` to the distribution that provides it, such as `reme-auto-fin`. If one distribution provides
multiple plugin entry points, the command lists the other plugins that will also be removed.
Uninstallation does not rewrite user configuration. Remove the plugin from relevant `plugins` lists yourself;
otherwise the next Application startup fails explicitly because the configured plugin is no longer installed. Restart
already-running ReMe processes after installing, upgrading, or uninstalling packages.
## Troubleshooting
### Plugin is installed but unavailable
Check that the `reme` command and pip package share one Python interpreter:
```bash
reme plugins list
python -c 'import sys; print(sys.executable)'
```
Using `reme plugins install` avoids the most common interpreter mismatch because it runs `python -m pip` with ReMe's
own interpreter.
### Plugin is installed but not loaded
Add its entry-point name to the Application's `plugins` list. ReMe intentionally has no global enable/disable state.
### Startup reports that the plugin is not installed
The active config still enables a missing plugin. Reinstall it or remove the corresponding name from `plugins`.
### Changes are not visible in a running service
Plugin discovery and backend registration happen during Application construction. Restart the service after changing
installed packages.

View file

@ -1,17 +1,17 @@
# Proactive
`proactive` is ReMe's interface for reading proactive memory. It does not reanalyze daily notes or call an LLM. It only reads
the current day's interest topics written by `auto_dream`:
`proactive` is ReMe's interface for reading proactive memory. It does not reanalyze daily notes or call an LLM. It only
reads the current day's interest topics written by `auto_dream`:
```text
daily/<date>/interests.yaml
```
A host agent can use it to learn "what is worth proactive attention today," then decide whether to remind the user, ask a
follow-up question, recommend a next step, or produce a proactive insight.
A host agent can use it to learn "what is worth proactive attention today," then decide whether to remind the user, ask
a follow-up question, recommend a next step, or produce a proactive insight.
`interests.yaml` is generated by the Topics stage of [Auto Dream](./auto_dream.md). `proactive` only reads and exposes the
result.
`interests.yaml` is generated by the Topics stage of [Auto Dream](./auto_dream.md). `proactive` only reads and exposes
the result.
## Configuration
@ -34,10 +34,10 @@ proactive:
Parameters:
| Parameter | Purpose |
|---|---|
| `date` | Date to read in `YYYY-MM-DD` format. When empty, use today in the application's timezone. |
| `include_content` | Whether to return the raw YAML in metadata. Defaults to `true`. |
| Parameter | Purpose |
|-------------------|-------------------------------------------------------------------------------------------|
| `date` | Date to read in `YYYY-MM-DD` format. When empty, use today in the application's timezone. |
| `include_content` | Whether to return the raw YAML in the answer and metadata. Defaults to `true`. |
## Input Contract
@ -63,24 +63,41 @@ Only the `topics` list is parsed into structured results. Every topic requires a
## Return Value
When the file is read successfully, `proactive_step` writes these values to standard response metadata:
When the file is read successfully, `proactive_step` returns `summary` and `topics` in the primary answer. When
`include_content=true`, the answer also contains `content`. The same result fields remain available in standard response
metadata:
| Field | Description |
|---|---|
| `date` | The date actually read. |
| `path` | `daily/<date>/interests.yaml`. |
| `topics` | Parsed topic list. |
| Field | Description |
|-----------|------------------------------------------------------|
| `date` | The date actually read. |
| `path` | `daily/<date>/interests.yaml`. |
| `topics` | Parsed topic list. |
| `content` | Raw YAML; returned only when `include_content=true`. |
| `skipped` | `true` when the file does not exist. |
| `error` | Read or parse error. |
| `summary` | Short summary. |
| `skipped` | `true` when the file does not exist. |
| `error` | Read or parse error. |
| `summary` | Short summary. |
When the file exists and parses successfully, the answer looks like:
When the file exists and parses successfully, the answer is structured data. For example:
```text
Read 3 proactive topic(s) from daily/2026-06-20/interests.yaml
```json
{
"summary": "Read 1 proactive topic(s) from daily/2026-06-20/interests.yaml",
"topics": [
{
"title": "Quality regression in the memory retrieval pipeline",
"reason": "The user has recently made repeated changes to search, node_search, and dream integration.",
"evidence": "daily/2026-06-20/session.md",
"keywords": ["memory search", "auto dream"],
"paths": ["daily/2026-06-20/session.md"]
}
],
"content": "date: 2026-06-20\n..."
}
```
With `include_content=false`, the `content` field is omitted from the answer. Missing-file and read-error answers remain
explicit `Skipped: ...` and `Error: ...` messages, respectively.
A missing file is not an error. The call succeeds with a skipped result:
```text
@ -118,21 +135,21 @@ daily notes
The responsibilities are divided as follows. For the complete Extract, Integrate, Topics, and Finish flow, see
[Auto Dream](./auto_dream.md):
| Module | Responsibility |
|---|---|
| `dream_extract_step` | Extract topic candidates from changed daily inputs. |
| `dream_topics_step` | Deduplicate, select, and write `interests.yaml`. |
| `proactive_step` | Read `interests.yaml` and expose it to the host agent. |
| Module | Responsibility |
|----------------------|--------------------------------------------------------|
| `dream_extract_step` | Extract topic candidates from changed daily inputs. |
| `dream_topics_step` | Deduplicate, select, and write `interests.yaml`. |
| `proactive_step` | Read `interests.yaml` and expose it to the host agent. |
`proactive` does not modify files, update a catalog, or decide whether the user should be interrupted. It only provides the
day's topic material. The caller's product policy determines whether, when, and in what tone to push it to the user.
`proactive` does not modify files, update a catalog, or decide whether the user should be interrupted. It only provides
the day's topic material. The caller's product policy determines whether, when, and in what tone to push it to the user.
## Failure Modes
| Scenario | Behavior |
|---|---|
| `interests.yaml` does not exist | `success=true`, `skipped=true`, `topics=[]`. |
| YAML cannot be read or parsed | `success=false`; the answer contains an error summary. |
| YAML exists but has no valid topics | `success=true`, `topics=[]`. |
| Scenario | Behavior |
|-------------------------------------|--------------------------------------------------------|
| `interests.yaml` does not exist | `success=true`, `skipped=true`, `topics=[]`. |
| YAML cannot be read or parsed | `success=false`; the answer contains an error summary. |
| YAML exists but has no valid topics | `success=true`, `topics=[]`. |
Callers should therefore check `success` first, then `skipped`, and finally whether `topics` is empty.

View file

@ -15,11 +15,17 @@ Install from source:
```bash
git clone https://github.com/agentscope-ai/ReMe.git
cd ReMe
pip install -e ".[core]"
pip install -e reme_studio -e ".[core]"
cd reme_studio
npm ci
npm run build:static
cd ..
```
Installing the `core` extra is recommended. The current code imports the AgentScope wrapper, and self-evolving memory also
depends on it.
The static build step requires Node.js 22.13 or newer and makes Studio available when running ReMe from the source tree.
Installing the `core` extra is recommended. The current code imports the AgentScope wrapper, and self-evolving memory
also depends on it.
To use agent workflows such as `auto_memory`, `auto_resource`, and `auto_dream`, configure an LLM:
@ -51,10 +57,16 @@ reme start service.port=8181
```bash
reme version
reme health_check
reme list
reme help
```
`reme list` lists server actions. Ordinary commands invoke server Jobs over HTTP.
`reme help` lists server actions. Ordinary commands invoke server Jobs over HTTP.
The base `reme-ai` package does not include frontend assets. Install `reme-ai[web]` or `reme-ai[core]`, then open
<http://127.0.0.1:2333/> for ReMe Studio. It uses the same service to
browse, edit, and search the workspace and inspect the digest wikilink graph. Disable it with
`service.web_enabled=false`, or provide a custom build with `service.web_static_dir` / `REME_WEB_STATIC_DIR`. The Job
API still starts if no web build is found.
---
@ -65,7 +77,8 @@ The default workspace is `.reme/` under the current directory. It is created aut
```text
.reme/
├── metadata/ # persistent indexes, graph, catalogs, and related state
├── session/ # agent sessions and original conversations
├── session/ # source conversation records
├── mem_session/ # generated Agent wrapper sessions/config
├── resource/ # external resources
├── daily/ # daily notes
└── digest/ # long-term memory
@ -91,12 +104,13 @@ reme write \
description="Example memory for the quick start" \
content="# Quick Start Demo
ReMe indexes Markdown under the daily, digest, and resource directories.
The default live watcher indexes Markdown under the daily and digest directories.
Related link: [[digest/wiki/search-demo.md]]"
```
`path` is relative to the workspace. A missing suffix is automatically completed with `.md`. For Markdown files, `name` and
`path` is relative to the workspace. A missing suffix is automatically completed with `.md`. For Markdown files, `name`
and
`description` are written to frontmatter.
The background watcher builds the index automatically. You can also rebuild it manually:
@ -117,8 +131,8 @@ Read:
reme read path=digest/wiki/quick-start-demo start_line=1 end_line=20
```
With the default configuration, retrieval is primarily BM25 plus wikilink graph expansion. Vector retrieval is supported by
the code, but the embedding store is disabled by default. For the full retrieval flow, see
With the default configuration, retrieval is primarily BM25 plus wikilink graph expansion. Vector retrieval is supported
by the code, but the embedding store is disabled by default. For the full retrieval flow, see
[Memory Search](./memory_search.md).
---
@ -132,7 +146,13 @@ reme frontmatter_read path=digest/wiki/quick-start-demo
reme frontmatter_update path=digest/wiki/quick-start-demo metadata='{"tags":["demo"]}'
```
The name `list` is used by the CLI to list actions, so the file-listing Job must be called over HTTP:
The file-listing Job can be called directly from the CLI:
```bash
reme list path=digest recursive=true limit=50
```
The equivalent HTTP call is:
```bash
curl -s http://127.0.0.1:2333/list \
@ -161,7 +181,8 @@ reme auto_memory \
memory_hint="Record the user's preference"
```
After placing external material under `resource/YYYY-MM-DD/`, the default background task watches
After placing external material under `resource/YYYY-MM-DD/` or directly under `resource/`, the default background task
watches
`md/txt/json/jsonl/csv/yaml/html`. You can also trigger processing manually:
```bash
@ -175,7 +196,8 @@ reme auto_dream date=2026-06-20
reme proactive date=2026-06-20
```
These flows require a working LLM. Without an LLM configuration, start with basic capabilities such as `write`, `read`, and
These flows require a working LLM. Without an LLM configuration, start with basic capabilities such as `write`, `read`,
and
`search`.
For more detail, see [Auto Memory](./auto_memory.md), [Auto Resource](./auto_resource.md),

339
docs/en/reme-blog.md Normal file
View file

@ -0,0 +1,339 @@
# ReMe: A Personal Knowledge Base That Keeps Growing After Every Conversation
Every day, we talk with AI.
It helps us analyze projects, read papers, and troubleshoot problems. We also tell it about our preferences, plans, and ideas we have not fully worked out yet.
But most of the time, when a conversation ends, its value is locked away in the chat history. The next time we open a new window, the AI may remember a conclusion but not where it came from. It may find an old conversation but fail to connect it with materials we read or decisions we made later.
Useful long-term memory should do more than preserve what once happened. It should keep organizing information, building connections, and bringing past knowledge back into future reasoning when needed.
That is exactly what ReMe sets out to do.
> **ReMe is a local-first, self-evolving personal knowledge base for AI agents. It continuously turns conversations and resources into readable, editable, searchable, and interconnected Markdown memories, while surfacing threads worth following.**
GitHub: [https://github.com/agentscope-ai/ReMe](https://github.com/agentscope-ai/ReMe)
Documentation: [https://reme.agentscope.io](https://reme.agentscope.io)
<p align="center">
<img src="../figure/reme-blog/reme-blog-cover-benchmark.png" alt="ReMe self-evolving personal knowledge base and public benchmark results" width="100%">
</p>
## A Memory Loop That Keeps Growing
<p align="center">
<img src="../figure/reme-blog/reme-blog-architecture.svg" alt="ReMe self-evolving memory loop" width="100%">
</p>
ReMe is not another chatbot, nor does it try to replace the agents you already use. It is a local memory layer that agents such as QwenPaw, OpenClaw, Hermes, and Claude Code can share.
Built around a set of ordinary files, it does four things:
- Auto Memory extracts information worth keeping from conversations;
- Auto Resource turns external materials into traceable memories;
- Auto Dream consolidates daily memories into long-term knowledge;
- Index, Search, and Proactive bring old memories back into new tasks.
Together, they form a `capture → index → consolidate → recall` loop:
- Conversations and external resources are preserved first;
- Valuable information is organized into daily memories;
- Scattered events are consolidated into long-term knowledge nodes;
- Search, knowledge links, and interest discovery bring old memories back into future reasoning.
Most importantly, this loop is centered not on an opaque database, but on files owned by the user. Indexes, graphs, and caches are merely derived state that can always be rebuilt.
## Memory as File: Your Memories Are Your Files
<p align="center">
<img src="../figure/reme-blog/reme-blog-memory-as-file.svg" alt="ReMe Memory as File" width="100%">
</p>
ReMe's core design is called **Memory as File, File as Memory.**
“Memory as File” means long-term memories are not hidden inside a product. They live in Markdown, JSONL, YAML, and original resource files within your workspace. You can open them directly in VS Code, Typora, or Obsidian, and back them up or move them with Git, cloud storage, or your own synchronization setup.
“File as Memory” means each file is more than plain text. With YAML frontmatter, section structure, line ranges, and Wikilinks, it becomes a memory node that can be indexed, connected, and continuously evolved.
For example, a long-term memory about writing preferences might look like this:
```markdown
---
name: "User preference: technical writing style"
description: Prefers stating the problem and outcome first, followed by technical details and examples.
kind: preference
---
The user wants technical articles to have a clear narrative and avoid unnecessary jargon.
When writing an article, refer to [[digest/procedure/Technical content writing process.md]].
## Sources
This preference was observed in [[daily/2026-08-07/content-discussion.md]], which records the user's writing guidance.
```
Months later, even if you have forgotten the conversation, the agent can still read the preference, find the related process, and follow `Sources` back to the original context.
This is also the key difference between ReMe and “black-box memory”: agents can organize memories, but users always retain the right to inspect, correct, move, and delete them.
## Auto Memory: Turning Conversations into a Daily Journal
<p align="center">
<img src="../figure/reme-blog/reme-blog-auto-memory.svg" alt="ReMe Auto Memory turns conversations into daily memories" width="100%">
</p>
A great deal of valuable information does not begin with “please remember this.”
For example, you might say in a conversation:
> “Let's not refactor the login module this week. We can do it after the customer demo. Upgrading dependencies directly caused compatibility issues last time, so let's add regression tests first.”
This short passage contains project status, a time constraint, a lesson from a previous failure, and a next action. Auto Memory extracts these details from the conversation stream and writes them into a daily memory card, while retaining a source conversation record in `session/dialog/`.
```text
session/dialog/project-a.jsonl Source conversation record
daily/2026-08-07/login-refactor-decision.md Content-named memory card
daily/2026-08-07.md Daily index, providing an overview
```
`session_id` remains in the card's frontmatter for stable lookup and provenance; the filename comes from the Agent-generated
topic/event `name`, so it does not have to match the session ID.
The next time the login module comes up, the agent does not need to search through the entire chat history. It can immediately see why the refactor was postponed, what went wrong before, and what should happen next.
It is like having a recorder who is always present—not one that mechanically transcribes every word, but one that organizes what will still matter later.
## Auto Resource: Bringing External Materials into the Same Memory System
<p align="center">
<img src="../figure/reme-blog/reme-blog-auto-resource.svg" alt="ReMe Auto Resource turns external materials into traceable personal memories" width="100%">
</p>
Not all valuable information comes from conversations. Research materials, project documents, meeting notes, archived web pages, and structured data may all become part of a personal knowledge base.
Auto Resource provides a general entry point for external materials. After a resource enters `resource/`, ReMe preserves the original and organizes its topics, key facts, and actionable information into daily cards with `source_resource` links. It currently supports text-based resources including Markdown, plain text, JSON, JSONL, CSV, YAML, and HTML.
In other words, Auto Memory builds personal knowledge from conversations, while Auto Resource builds it from non-conversational materials. Both streams flow into the same daily memory layer, where ReMe indexes, consolidates, and retrieves them together.
### Daily Paper: An Example External-Resource Workflow
Daily Paper is an optional plugin built on this file-based memory system. It collects papers from the weekly and monthly Hugging Face Papers rankings, removes items recommended recently, ranks the remaining papers, selects three, saves their PDFs, and generates Chinese paper notes and a briefing that takes about five minutes to read.
Imagine that you regularly follow research on agent memory. Each morning, instead of receiving only three links, you get three detailed notes already saved locally. The briefing points to the original notes through Wikilinks, and each note links back to its PDF. A month later, when you ask, “What recent methods compress long-term memory?”, those materials are already in the same retrieval system. There is no need to search through browser history again.
Daily Paper demonstrates how Auto Resource can be composed into a concrete workflow, but the external-resource pipeline is not limited to papers.
## Auto Dream: Growing Daily Notes into Connected Long-Term Knowledge
<p align="center">
<img src="../figure/reme-blog/reme-blog-auto-dream.svg" alt="ReMe Auto Dream extracts, classifies, and consolidates long-term knowledge from daily memories while adding Wikilinks" width="100%">
</p>
As daily notes accumulate, a new problem emerges: the information is all there, but it remains scattered across different dates.
Suppose conversations and external materials give you three pieces of information about the same problem:
- The first time a build hung, clearing the cache did not help;
- A project document later confirmed that insufficient Node.js memory was the root cause;
- A third note added that the issue occurs more often in large TypeScript projects.
By default, Auto Dream looks at the two most recent days ending at the target date and sends only daily files changed since
the previous run to extraction. It merges cross-file evidence for the same abstraction and keeps only the strongest reusable
memories within a default cap of five units, then writes them into three categories of long-term memory:
- `Personal`: preferences, conventions, and constraints specific to a user, team, or project;
- `Procedure`: repeatable processes, methods, and troubleshooting guides;
- `Wiki`: general definitions, principles, observations, and knowledge.
For example, the information above would become `digest/procedure/Troubleshooting frozen frontend builds.md`, which records the triggering conditions, diagnostic sequence, failed attempts, solution, and scope of applicability—instead of simply concatenating several daily notes.
When consolidating each memory unit, Auto Dream first searches existing nodes across `personal`, `procedure`, and `wiki`, distinguishing between the “same abstraction” and “related knowledge.” The same abstraction determines how the target node evolves:
- `CREATE`: no equivalent memory exists, so create a new node;
- `CORROBORATE`: the same conclusion appears again, so add its source and strengthen confidence;
- `REFINE`: new material adds conditions, steps, or details;
- `CORRECT`: new information corrects an earlier conclusion.
Related knowledge is written into the body as Wikilinks during the same consolidation process. This is Auto Link. For example, “Troubleshooting frozen frontend builds” can connect general knowledge, team preferences, and original evidence at once:
```markdown
This issue often occurs in [[digest/wiki/Large TypeScript projects.md]]. When resolving it,
follow the “add regression tests first” convention in [[digest/personal/Team change preferences.md]].
## Sources
The root cause and applicable scenarios were documented in
[[daily/2026-08-07/build-debug.md|Build troubleshooting record]].
```
Knowledge evolves and links are created in the same workflow. Relationships are not invisible edges hidden in a graph database; they are readable, editable content in the files themselves. The files can rebuild the graph—the graph never takes control of the files.
## Memory Index: Turning Ordinary Files into a Searchable Memory Network
<p align="center">
<img src="../figure/reme-blog/reme-blog-memory-index.svg" alt="ReMe Memory Index build process" width="100%">
</p>
Markdown is easy for people to read, but if files are merely piled into directories, agents still struggle to find them
quickly. The default live index watches Markdown under `daily/` and `digest/`. A separate resource workflow watches
`resource/` and turns those files into daily cards that enter the same index. For a full rebuild from existing files,
`reme reindex` also scans `resource/` and JSONL.
A Markdown file is parsed into:
- One file node containing file-level information such as its path and frontmatter;
- Multiple semantic chunks split, wherever possible, along the boundaries of headings, paragraphs, lists, and code blocks, while retaining section structure and line numbers;
- Multiple Wikilink edges recording what the file points to and what points back to it.
For retrieval, ReMe can combine three types of signals:
| Retrieval signal | Problem it solves | Example |
|------------------|-------------------|---------|
| BM25 keywords | Exact names, terms, and identifiers must not be missed | “CATL”, “issue #184” |
| Embedding vectors | Semantically similar wording should still match | “build frozen” and “packaging stage not responding” |
| Wikilink graph | Reveal upstream and downstream relationships after finding a node | From “cobalt” to “ternary cathodes” and related research notes |
The default configuration enables BM25 and Wikilink expansion out of the box. Embeddings are optional and participate in vector retrieval only when enabled. Indexes, graphs, and caches are stored in `metadata/`; even if deleted, they can be rebuilt from the user's source files.
## Memory Search: Find the Answer First, Then Expand Relationships Progressively
<p align="center">
<img src="../figure/reme-blog/reme-blog-memory-search.svg" alt="ReMe hybrid search and progressive expansion" width="100%">
</p>
Many RAG systems put all Top-K passages into the context at once. This is simple, but it creates two problems: isolated chunks lack context, while expanding every neighbor's full text quickly consumes tokens.
ReMe's hybrid search lets BM25 and optional vector retrieval produce their own candidates, then fuses the rankings with RRF. Instead of directly comparing BM25 scores with cosine similarities—two different scales—RRF combines where each result appears in the two ranked lists.
After retrieval, information expands progressively in three layers:
1. **Start with the matching passage**: return the most relevant chunk, file path, and line numbers;
2. **Then inspect the relationship directory**: show the file's outgoing and incoming links, including only each neighbor's path, name, description, and anchor rather than loading all of its content immediately;
3. **Finally, go deeper as needed**: the agent decides which relationship is genuinely relevant, then reads the original file or continues traversing the graph.
For example, you ask: “What was the name of the book about attention that Alice recommended last time?”
The first step may find a dinner note that says only, “The title contains the word deep.’” The result also shows that the note links to Alice's personal node and is backlinked by reading notes for *Deep Work*.
The agent does not need to load Alice's entire profile, every reading note, and a whole month of journal entries into its context. It only needs to follow the most relevant link and read once more before answering:
> It was *Deep Work*. Alice recommended it at that dinner, and you later read Chapter 3 and left notes.
This resembles human association: first recall a fragment, then follow the trail to recover the full context.
## Proactive: Discovering Needs You Have Not Yet Put into Words
<p align="center">
<img src="../figure/reme-blog/reme-blog-proactive.svg" alt="ReMe Proactive's two-way memory loop" width="100%">
</p>
At this point, ReMe has two input streams that continuously enrich the knowledge base:
- Auto Memory distills personal context from ongoing conversations;
- Auto Resource adds new knowledge from external materials.
Proactive reverses the direction. From accumulated conversations and materials, it discovers topics you have not yet resolved or may want to pursue, along with information you have not noticed but that closely relates to your recent work. These discoveries can then guide what external knowledge enters the system next.
For example, over the past week you separately mentioned that:
- Search results lack sources;
- Long documents lose section context after chunking;
- You want to compare several agent-memory evaluation methods.
Even though you never explicitly said, “Help me systematically study the explainability of memory retrieval,” Auto Dream can distill an interest topic from these daily memories:
```yaml
title: Evaluating the explainability of memory retrieval
reason: The user has recently focused on source tracing, structure-aware chunking, and memory evaluation.
evidence: daily/2026-08-07/search-discussion.md
keywords:
- memory search
- source attribution
- benchmark
```
In a future beta release, after reading this topic through Proactive, a host agent could ask at an appropriate moment, “Would you like me to turn the retrieval issues we discussed recently into an evaluation plan?” It could also use the topic to initiate a user-authorized research workflow. Users would not need to identify and explicitly specify their interests and scope in advance; external resources related to needs implicit in their conversations could continue flowing into the knowledge base.
There is an important boundary: **ReMe's Proactive feature only reads and exposes interest topics. It does not independently access the internet, send notifications, or rewrite the knowledge base.**
It does not guess your interests from nowhere. It surfaces clues that already appeared in your behavior and conversations but have not yet been explicitly stated.
## Performance: Can It Retrieve Information from Very Long Histories?
ReMe uses LongMemEval and BEAM to evaluate memory across multiple sessions and extremely long conversations. During evaluation, the agent can use ReAct to search and read over multiple rounds, generate an answer, and then receive an LLM-as-judge score.
| Benchmark | Setting | Sample size | Agentic score | Primary capabilities tested |
|-----------|---------|------------:|---------------:|-----------------------------|
| **LongMemEval cleaned-s** | **Overall** | **500 questions** | **89.4%** | Cross-session retrieval, knowledge updates, and temporal reasoning |
| BEAM | 100K context | 20 cases / 400 questions | 66.1% | Ten types of long-context memory tasks |
| BEAM | 1M context | 35 cases / 700 questions | 65.0% | Larger-scale, ultra-long conversation settings |
LongMemEval cleaned-s includes single-session facts, preferences, multi-session reasoning, knowledge updates, temporal reasoning, and other question types. ReMe achieved an overall Agentic score of 89.4% across 500 questions. See the [LongMemEval evaluation guide](../../benchmark/longmemeval/README.md) for the complete workflow and breakdown.
BEAM covers ten categories of tasks, including contradiction resolution, event ordering, information extraction, knowledge updates, multi-session reasoning, preference following, summarization, and temporal reasoning. ReMe scored 66.1% on 20 cases / 400 questions with a 100K context and 65.0% on 35 cases / 700 questions with a 1M context. See the [BEAM evaluation guide](../../benchmark/beam/README.md) for the complete setup.
ReMe also uses $\pi$-Bench to evaluate the potential of multi-session reasoning to improve agent proactivity. The PROC score in $\pi$-Bench evaluates capabilities including directly fulfilling hidden intent, guiding targeted clarification, recovering cross-session preferences, reusing cross-session conventions, inferring cross-task dependencies, and advancing underspecified requests. Across five user personas, ReMe Agent achieved an average PROC score of 0.580, outperforming NanoBot by 2.4% under the same test-model configuration. See the [$\pi$-Bench paper](https://arxiv.org/abs/2605.14678) for details about the benchmark.
## Who Is ReMe For?
### People Who Use Agents Directly
If you want AI to understand you continuously throughout a long-term collaboration, ReMe lets your personal assistant stop starting from scratch. Your preferences, project context, important materials, and past decisions accumulate through ongoing conversations and can be found again when they are genuinely relevant.
Researchers, engineers, analysts, and other knowledge workers all fall into this category. Researchers can connect papers, discussions, and reading notes; engineers can preserve project decisions and cross-session troubleshooting experience; analysts can build an evolving record of events, perspectives, and sources. Their professions differ, but they share the same need: AI that can understand the past, accumulate experience, and recover supporting context for the next task.
### Developers Who Build Agents
If you are building an agent, harness, or AI product, ReMe provides an independent long-term memory layer. Through its CLI, HTTP API, MCP Server, or Python API, you can let multiple agents share the same file-based workspace without reimplementing memory extraction, knowledge organization, hybrid retrieval, and relationship expansion for every application.
Files remain the source of truth, while indexes and caches can be rebuilt at any time. This also makes it easier to determine whether an incorrect retrieval originated in the source material, memory consolidation, or the retrieval pipeline.
Ultimately, ReMe is for users and developers who want AI to do more than “answer this one request”: they want it to understand the past, accumulate experience, and know them better over the course of a long-term collaboration. We want agents to understand you better the more you use them—but that understanding should not live in a black box that you cannot inspect, correct, or take with you.
ReMe's answer is straightforward:
- Memories are files owned by the user;
- Original information preserves what happened, while long-term knowledge preserves the abstraction;
- New conversations and resources keep flowing in, while existing knowledge is continuously supplemented and corrected;
- Every conclusion can be traced to relationships and sources through Wikilinks;
- Indexes and caches serve the files rather than replace them;
- Agents can remember, organize, search, and discover, but users always retain ultimate control.
When these mechanisms come together, a personal knowledge base is no longer a repository you must maintain by hand.
It remembers a little more after every conversation and understands a little more after every new resource. At night, it reorganizes scattered experiences. When a future question arises, it follows the connections between pieces of knowledge and brings back the memory you actually need.
That is what ReMe sets out to do: **make memory not only persistent, but continuously evolving.**
## Integrate ReMe with the Agents You Already Use
ReMe can run as a local memory service accessed through its CLI, HTTP API, or MCP Server, or it can be embedded in a host
process through its Python API. The default HTTP service can also serve ReMe Studio at the same address for browsing,
editing, and searching the workspace and inspecting the digest wikilink graph. Different agents can choose the integration
that best fits their runtime environment and share the same local memory workspace when needed.
| Agent | Recommended integration | Capabilities after integration |
|-------|-------------------------|--------------------------------|
| **DeepSeek Harness** | Install [`@agentscope-ai/reme`](../../typescript/README.md#deepseek-harness) as a DSH profile bundle. | Long-term memory guidance, `reme_search`, automatic capture of completed main-agent turns, and scheduled Auto Dream. |
| **OpenClaw** | Install [`@agentscope-ai/reme`](../../typescript/README.md#openclaw) as the native memory plugin. | Recall before conversational root-agent runs, explicit search, automatic turn capture, and scheduled Auto Dream. |
| **QwenPaw** | Embed ReMe in-process through the Python API. | Reuse the host application's lifecycle and model configuration while keeping memories local and file-based. |
| **Claude Code** | Start the streamable HTTP MCP Service and install [`integrations/claude_code/reme`](../../integrations/claude_code/reme). | MCP memory-recall tools, the `reme-memory` skill, and a Stop hook that automatically records sessions. |
| **Hermes** | Start the HTTP Service and install [`integrations/hermes_agent`](../../integrations/hermes_agent). | Automatically recall relevant memories before model calls and invoke `auto_memory` asynchronously after each conversation turn. |
| **Codex and other CLI-capable agents** | Copy or install [`skills/reme_memory/SKILL.md`](../../skills/reme_memory/SKILL.md). | Search, read, and write memories through the CLI; automatic recording requires the host agent to integrate explicitly with the conversation lifecycle. |
For installation, configuration, and integration demos, see the [README](../../README.md).
## Contributions Welcome
ReMe is open source, and we welcome the community's help in making this self-evolving memory system more complete:
- Integrate more agents and harnesses so different runtime environments can use the same user-owned long-term memory;
- Contribute new Auto Resource sources and workflows so papers, news, and other public materials can continuously enter the knowledge base;
- Improve Auto Memory, Auto Dream, Auto Link, hybrid search, and Proactive so memories are organized more accurately, relationships are clearer, and retrieval is more reliable;
- Add application examples, evaluation tasks, and diagnostic reports to help us understand successes and failures in real long-term use;
- Improve documentation and tests, or share your needs and ideas for personal AI memory through an Issue.
Whether it is a code contribution, a use case, a bug report, or a new memory workflow, every contribution can bring ReMe closer to a truly readable, controllable, and continuously evolving personal knowledge base.
Contribution guide: [https://docs.agentscope.io/reme/latest/en/contribution](https://docs.agentscope.io/reme/latest/en/contribution)

View file

@ -54,20 +54,22 @@ session/
daily/
├── 2026-05-18.md
└── 2026-05-18/
├── 2026-05-18-close.md
├── glencore-q3.md
├── cobalt-policy.md
├── cathode-trend.md
├── cobalt-supply-risk.md
├── glencore-output-update.md
├── drc-cobalt-policy.md
├── high-nickel-cathode-trend.md
└── interests.yaml # generated after auto_dream
```
The corresponding flow is:
- `auto_memory` saves the original conversation to `session/dialog/<session_id>.jsonl`, then asks the agent to write
important facts to `daily/<date>/<session_id>.md`.
- `resource_watch_loop` watches text-file changes under `resource/` and triggers `auto_resource_step` to write a
same-named daily note.
- `daily_create` maintains `daily/<date>.md` as the index page for that day.
- `auto_memory` saves a filtered source conversation record to `session/dialog/<session_id>.jsonl`, then asks the agent to write
important facts to a topic-named `daily/<date>/<generated_name>.md`. The note keeps `session_id` and
`source_conversation` in frontmatter for stable lookup and provenance.
- `resource_watch_loop` watches text-file changes under `resource/` and triggers `auto_resource_step` to write a daily note
with `source_resource`. The agent suggests a content-based filename, which the system sanitizes and de-duplicates; it is
not guaranteed to match the resource filename.
- Auto Memory, Auto Resource, and Auto Dream refresh `daily/<date>.md` after writing.
### Day 1 evening: Auto Dream writes to Digest
@ -81,8 +83,8 @@ reme auto_dream date=2026-05-18
```text
dream_extract_step
scan daily/2026-05-18.md and changed files under daily/2026-05-18/
output units and topics
scan the daily window from 2026-05-17 through 2026-05-18 by default
output at most 5 units plus topics from changed files
dream_integrate_step
recall existing digest nodes with node_search for each unit
decide CREATE / CORROBORATE / REFINE / CORRECT
@ -110,17 +112,19 @@ name: Cobalt
description: A key raw material for lithium-battery cathodes, with production concentrated in the DRC
---
downstream_product:: [[digest/wiki/ternary-cathodes.md]]
producer:: [[digest/wiki/glencore.md]]
source_event:: [[daily/2026-05-18/2026-05-18-close.md]]
# Cobalt
Used by [[digest/wiki/ternary-cathodes.md]]; a major producer is [[digest/wiki/glencore.md]].
## Supply
Glencore's third-quarter cobalt output fell 18% year over year. Continue monitoring how tighter supply affects prices.
## Policy risk
Changes to mining-rights policy in the DRC may affect KFM mine operations and should be tracked together with CMOC.
## Sources
The production decline and policy risk were recorded in [[daily/2026-05-18/cobalt-supply-risk.md]].
```
Note that wikilinks use literal path semantics. Prefer complete workspace-relative paths with the `.md` extension. ReMe
@ -185,10 +189,10 @@ The result shape is:
Glencore's third-quarter cobalt output fell 18% year over year...
outlinks:
-> digest/wiki/ternary-cathodes.md name="Ternary Cathodes" via predicate=downstream_product
-> digest/wiki/glencore.md name="Glencore" via predicate=producer
-> digest/wiki/ternary-cathodes.md name="Ternary Cathodes"
-> digest/wiki/glencore.md name="Glencore"
inlinks:
<- digest/wiki/ternary-cathodes.md name="Ternary Cathodes" via predicate=upstream_material
<- digest/wiki/ternary-cathodes.md name="Ternary Cathodes"
========== digest/wiki/ternary-cathodes.md:5-18 [score=0.0139 keyword=3.2017] ==========
...
@ -233,7 +237,7 @@ topics:
reason: The user repeatedly mentioned KFM and cobalt-price risk today
keywords: [cobalt, DRC, CMOC, KFM]
paths:
- daily/2026-05-18/2026-05-18-close.md
- daily/2026-05-18/cobalt-supply-risk.md
```
Call:
@ -301,11 +305,10 @@ name: TypeScript project build OOM diagnostic path
description: When a build stalls and memory grows, check the type-checking process first
---
source_event:: [[daily/2026-03-10/build-oom-2026-03-10.md]]
related_preference:: [[digest/personal/code-style.md]]
# TypeScript Project Build OOM Diagnostic Path
Apply [[digest/personal/code-style.md]] while following this runbook.
## Symptoms
The build stalls near the end. CPU usage is low, but memory keeps growing.
@ -317,6 +320,10 @@ The build stalls near the end. CPU usage is low, but memory keeps growing.
## Known ineffective paths
- Deleting `.cache` alone did not resolve the issue on 2026-03-10.
- Upgrading the terser plugin did not resolve the issue on 2026-03-10.
## Sources
The failed attempts and successful memory adjustment were recorded in [[daily/2026-03-10/build-oom-2026-03-10.md]].
```
Example `digest/personal/code-style.md`:
@ -369,7 +376,7 @@ and upgrading the minification plugin did not help last time.
- `digest/procedure/` stores both "how to do it" and "which paths failed," letting the agent reuse diagnostic experience.
- `digest/personal/` stores user preferences so the agent can follow the same engineering style across sessions.
- The original conversation remains under `session/dialog/`; daily records stay traceable, and digest is only the
- The source conversation record remains under `session/dialog/`; daily records stay traceable, and digest is only the
long-term distilled result.
## Scenario 3: A Personal Second Brain
@ -411,13 +418,14 @@ name: Alice
description: A friend of the user who often recommends reading material
---
recommended_book:: [[digest/wiki/deep-work.md]]
source_event:: [[daily/2026-04-20/lunch-with-alice.md]]
# Alice
## Reading recommendations
At lunch on 2026-04-20, Alice recommended a book about attention and deep work.
At lunch on 2026-04-20, Alice recommended [[digest/wiki/deep-work.md]], a book about attention and deep work.
## Sources
The recommendation was recorded in [[daily/2026-04-20/lunch-with-alice.md]].
```
### An associative recall
@ -439,7 +447,7 @@ Matches:
```text
digest/personal/alice.md
outlinks:
-> digest/wiki/deep-work.md via predicate=recommended_book
-> digest/wiki/deep-work.md
daily/2026-04-20/lunch-with-alice.md
```

View file

@ -1,129 +1,117 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img" aria-labelledby="title desc">
<title id="title">ReMe auto dream and proactive flow</title>
<desc id="desc">A left-to-right flow from changed daily notes to digest integration, interest topic writing, catalog checkpointing, and proactive reads.</desc>
<defs>
<style>
.bg { fill: #fffdf8; }
.title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.step-num { font: 700 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; }
.step-title { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.step-subtitle { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.chip-title { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.chip-text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; }
.note { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; }
.panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; rx: 18; ry: 18; stroke-linecap: round; stroke-linejoin: round; }
.chip { fill: #f8fbff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; }
.badge { fill: #44546a; }
.arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); }
.soft-arrow { stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); }
.line { stroke: #a3adbd; stroke-width: 1.25; stroke-linecap: round; }
</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img"
aria-labelledby="title desc">
<title id="title">ReMe auto dream and proactive flow</title>
<desc id="desc">A left-to-right flow from a recent changed-daily window to digest integration, interest topic
writing, catalog checkpointing, and proactive reads.
</desc>
<defs>
<style>.bg { fill: #fffdf8; } .title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .step-num { font: 700 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; } .step-title { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .step-subtitle { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .chip-title { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .chip-text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; } .note { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; } .panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; rx: 18; ry: 18; stroke-linecap: round; stroke-linejoin: round; } .chip { fill: #f8fbff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; } .badge { fill: #44546a; } .arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); } .soft-arrow { stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); } .line { stroke: #a3adbd; stroke-width: 1.25; stroke-linecap: round; }</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto"
markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<rect class="bg" x="0" y="0" width="1200" height="640"/>
<text class="title" x="600" y="54" text-anchor="middle">Auto Dream and Proactive</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">Scan changed daily memory, integrate reusable units into digest, then expose proactive topics.</text>
<rect class="bg" x="0" y="0" width="1200" height="640"/>
<text class="title" x="600" y="54" text-anchor="middle">Auto Dream and Proactive</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">Scan a recent daily window, integrate a compact set of reusable units, then expose proactive topics.</text>
<rect class="panel" x="38" y="132" width="196" height="300"/>
<circle class="badge" cx="72" cy="170" r="15"/>
<text class="step-num" x="72" y="174" text-anchor="middle">1</text>
<text class="step-title" x="100" y="176">Extract</text>
<text class="step-subtitle" x="66" y="206">dream_extract_step</text>
<rect class="chip" x="66" y="232" width="140" height="44"/>
<text class="chip-title" x="136" y="251" text-anchor="middle">refresh index</text>
<text class="chip-text" x="136" y="269" text-anchor="middle">daily/&lt;date&gt;.md</text>
<rect class="chip" x="66" y="296" width="140" height="44"/>
<text class="chip-title" x="136" y="315" text-anchor="middle">compare catalog</text>
<text class="chip-text" x="136" y="333" text-anchor="middle">changed daily</text>
<rect class="chip" x="66" y="360" width="140" height="44"/>
<text class="chip-title" x="136" y="379" text-anchor="middle">LLM extract</text>
<text class="chip-text" x="136" y="397" text-anchor="middle">units + topics</text>
<rect class="panel" x="38" y="132" width="196" height="300"/>
<circle class="badge" cx="72" cy="170" r="15"/>
<text class="step-num" x="72" y="174" text-anchor="middle">1</text>
<text class="step-title" x="100" y="176">Extract</text>
<text class="step-subtitle" x="66" y="206">dream_extract_step</text>
<rect class="chip" x="66" y="232" width="140" height="44"/>
<text class="chip-title" x="136" y="251" text-anchor="middle">refresh index</text>
<text class="chip-text" x="136" y="269" text-anchor="middle">recent 2 days</text>
<rect class="chip" x="66" y="296" width="140" height="44"/>
<text class="chip-title" x="136" y="315" text-anchor="middle">compare catalog</text>
<text class="chip-text" x="136" y="333" text-anchor="middle">changed daily</text>
<rect class="chip" x="66" y="360" width="140" height="44"/>
<text class="chip-title" x="136" y="379" text-anchor="middle">LLM extract</text>
<text class="chip-text" x="136" y="397" text-anchor="middle">≤ 5 units + topics</text>
<rect class="panel" x="270" y="132" width="196" height="300"/>
<circle class="badge" cx="304" cy="170" r="15"/>
<text class="step-num" x="304" y="174" text-anchor="middle">2</text>
<text class="step-title" x="332" y="176">Integrate</text>
<text class="step-subtitle" x="298" y="206">dream_integrate_step</text>
<rect class="chip" x="298" y="232" width="140" height="44"/>
<text class="chip-title" x="368" y="251" text-anchor="middle">node_search</text>
<text class="chip-text" x="368" y="269" text-anchor="middle">recall digest</text>
<rect class="chip" x="298" y="296" width="140" height="44"/>
<text class="chip-title" x="368" y="315" text-anchor="middle">auto link</text>
<text class="chip-text" x="368" y="333" text-anchor="middle">dedup + links</text>
<rect class="chip" x="298" y="360" width="140" height="44"/>
<text class="chip-title" x="368" y="379" text-anchor="middle">write digest</text>
<text class="chip-text" x="368" y="397" text-anchor="middle">create / update</text>
<rect class="panel" x="270" y="132" width="196" height="300"/>
<circle class="badge" cx="304" cy="170" r="15"/>
<text class="step-num" x="304" y="174" text-anchor="middle">2</text>
<text class="step-title" x="332" y="176">Integrate</text>
<text class="step-subtitle" x="298" y="206">dream_integrate_step</text>
<rect class="chip" x="298" y="232" width="140" height="44"/>
<text class="chip-title" x="368" y="251" text-anchor="middle">node_search</text>
<text class="chip-text" x="368" y="269" text-anchor="middle">recall digest</text>
<rect class="chip" x="298" y="296" width="140" height="44"/>
<text class="chip-title" x="368" y="315" text-anchor="middle">auto link</text>
<text class="chip-text" x="368" y="333" text-anchor="middle">dedup + links</text>
<rect class="chip" x="298" y="360" width="140" height="44"/>
<text class="chip-title" x="368" y="379" text-anchor="middle">write digest</text>
<text class="chip-text" x="368" y="397" text-anchor="middle">create / update</text>
<rect class="panel" x="502" y="132" width="196" height="300"/>
<circle class="badge" cx="536" cy="170" r="15"/>
<text class="step-num" x="536" y="174" text-anchor="middle">3</text>
<text class="step-title" x="564" y="176">Topics</text>
<text class="step-subtitle" x="530" y="206">dream_topics_step</text>
<rect class="chip" x="530" y="232" width="140" height="44"/>
<text class="chip-title" x="600" y="251" text-anchor="middle">merge topics</text>
<text class="chip-text" x="600" y="269" text-anchor="middle">same day kept</text>
<rect class="chip" x="530" y="296" width="140" height="44"/>
<text class="chip-title" x="600" y="315" text-anchor="middle">avoid repeats</text>
<text class="chip-text" x="600" y="333" text-anchor="middle">last 7 days</text>
<rect class="chip" x="530" y="360" width="140" height="44"/>
<text class="chip-title" x="600" y="379" text-anchor="middle">write YAML</text>
<text class="chip-text" x="600" y="397" text-anchor="middle">interests.yaml</text>
<rect class="panel" x="502" y="132" width="196" height="300"/>
<circle class="badge" cx="536" cy="170" r="15"/>
<text class="step-num" x="536" y="174" text-anchor="middle">3</text>
<text class="step-title" x="564" y="176">Topics</text>
<text class="step-subtitle" x="530" y="206">dream_topics_step</text>
<rect class="chip" x="530" y="232" width="140" height="44"/>
<text class="chip-title" x="600" y="251" text-anchor="middle">merge topics</text>
<text class="chip-text" x="600" y="269" text-anchor="middle">same day kept</text>
<rect class="chip" x="530" y="296" width="140" height="44"/>
<text class="chip-title" x="600" y="315" text-anchor="middle">avoid repeats</text>
<text class="chip-text" x="600" y="333" text-anchor="middle">last 7 days</text>
<rect class="chip" x="530" y="360" width="140" height="44"/>
<text class="chip-title" x="600" y="379" text-anchor="middle">write YAML</text>
<text class="chip-text" x="600" y="397" text-anchor="middle">interests.yaml</text>
<rect class="panel" x="734" y="132" width="196" height="300"/>
<circle class="badge" cx="768" cy="170" r="15"/>
<text class="step-num" x="768" y="174" text-anchor="middle">4</text>
<text class="step-title" x="796" y="176">Finish</text>
<text class="step-subtitle" x="762" y="206">dream_finish_step</text>
<rect class="chip" x="762" y="232" width="140" height="44"/>
<text class="chip-title" x="832" y="251" text-anchor="middle">checkpoint</text>
<text class="chip-text" x="832" y="269" text-anchor="middle">skip failures</text>
<rect class="chip" x="762" y="296" width="140" height="44"/>
<text class="chip-title" x="832" y="315" text-anchor="middle">persist catalog</text>
<text class="chip-text" x="832" y="333" text-anchor="middle">file_catalog</text>
<rect class="chip" x="762" y="360" width="140" height="44"/>
<text class="chip-title" x="832" y="379" text-anchor="middle">return summary</text>
<text class="chip-text" x="832" y="397" text-anchor="middle">counts + errors</text>
<rect class="panel" x="734" y="132" width="196" height="300"/>
<circle class="badge" cx="768" cy="170" r="15"/>
<text class="step-num" x="768" y="174" text-anchor="middle">4</text>
<text class="step-title" x="796" y="176">Finish</text>
<text class="step-subtitle" x="762" y="206">dream_finish_step</text>
<rect class="chip" x="762" y="232" width="140" height="44"/>
<text class="chip-title" x="832" y="251" text-anchor="middle">checkpoint</text>
<text class="chip-text" x="832" y="269" text-anchor="middle">skip failures</text>
<rect class="chip" x="762" y="296" width="140" height="44"/>
<text class="chip-title" x="832" y="315" text-anchor="middle">persist catalog</text>
<text class="chip-text" x="832" y="333" text-anchor="middle">file_catalog</text>
<rect class="chip" x="762" y="360" width="140" height="44"/>
<text class="chip-title" x="832" y="379" text-anchor="middle">return summary</text>
<text class="chip-text" x="832" y="397" text-anchor="middle">counts + errors</text>
<rect class="panel" x="966" y="132" width="196" height="300"/>
<circle class="badge" cx="1000" cy="170" r="15"/>
<text class="step-num" x="1000" y="174" text-anchor="middle">5</text>
<text class="step-title" x="1028" y="176">Proactive</text>
<text class="step-subtitle" x="994" y="206">proactive_step</text>
<rect class="chip" x="994" y="232" width="140" height="44"/>
<text class="chip-title" x="1064" y="251" text-anchor="middle">read YAML</text>
<text class="chip-text" x="1064" y="269" text-anchor="middle">interests.yaml</text>
<rect class="chip" x="994" y="296" width="140" height="44"/>
<text class="chip-title" x="1064" y="315" text-anchor="middle">return topics</text>
<text class="chip-text" x="1064" y="333" text-anchor="middle">metadata</text>
<rect class="chip" x="994" y="360" width="140" height="44"/>
<text class="chip-title" x="1064" y="379" text-anchor="middle">agent decides</text>
<text class="chip-text" x="1064" y="397" text-anchor="middle">whether to notify</text>
<rect class="panel" x="966" y="132" width="196" height="300"/>
<circle class="badge" cx="1000" cy="170" r="15"/>
<text class="step-num" x="1000" y="174" text-anchor="middle">5</text>
<text class="step-title" x="1028" y="176">Proactive</text>
<text class="step-subtitle" x="994" y="206">proactive_step</text>
<rect class="chip" x="994" y="232" width="140" height="44"/>
<text class="chip-title" x="1064" y="251" text-anchor="middle">read YAML</text>
<text class="chip-text" x="1064" y="269" text-anchor="middle">interests.yaml</text>
<rect class="chip" x="994" y="296" width="140" height="44"/>
<text class="chip-title" x="1064" y="315" text-anchor="middle">return topics</text>
<text class="chip-text" x="1064" y="333" text-anchor="middle">metadata</text>
<rect class="chip" x="994" y="360" width="140" height="44"/>
<text class="chip-title" x="1064" y="379" text-anchor="middle">agent decides</text>
<text class="chip-text" x="1064" y="397" text-anchor="middle">whether to notify</text>
<path class="arrow" d="M234 282 H270"/>
<path class="arrow" d="M466 282 H502"/>
<path class="arrow" d="M698 282 H734"/>
<path class="arrow" d="M930 282 H966"/>
<path class="arrow" d="M234 282 H270"/>
<path class="arrow" d="M466 282 H502"/>
<path class="arrow" d="M698 282 H734"/>
<path class="arrow" d="M930 282 H966"/>
<rect class="panel" x="80" y="502" width="1040" height="82"/>
<text class="note" x="126" y="532">Inputs</text>
<text class="chip-text" x="126" y="554">daily/&lt;date&gt;.md and daily/&lt;date&gt;/**/*.md</text>
<line class="line" x1="358" y1="518" x2="358" y2="566"/>
<text class="chip-title" x="402" y="532">Long-term memory</text>
<text class="chip-text" x="402" y="554">digest/procedure, digest/personal, digest/wiki</text>
<line class="line" x1="704" y1="518" x2="704" y2="566"/>
<text class="chip-title" x="748" y="532">Proactive material</text>
<text class="chip-text" x="748" y="554">daily/&lt;date&gt;/interests.yaml</text>
<line class="line" x1="942" y1="518" x2="942" y2="566"/>
<text class="chip-title" x="972" y="532">Boundary</text>
<text class="chip-text" x="972" y="554">Read only; caller decides</text>
<rect class="panel" x="80" y="502" width="1040" height="82"/>
<text class="note" x="126" y="532">Inputs</text>
<text class="chip-text" x="126" y="554">recent scan_days; changed files</text>
<line class="line" x1="358" y1="518" x2="358" y2="566"/>
<text class="chip-title" x="402" y="532">Long-term memory</text>
<text class="chip-text" x="402" y="554">digest/{procedure, personal, wiki}</text>
<line class="line" x1="704" y1="518" x2="704" y2="566"/>
<text class="chip-title" x="748" y="532">Proactive material</text>
<text class="chip-text" x="748" y="554">daily/&lt;date&gt;/interests.yaml</text>
<line class="line" x1="942" y1="518" x2="942" y2="566"/>
<text class="chip-title" x="972" y="532">Boundary</text>
<text class="chip-text" x="972" y="554" style="font-size:11px">Read-only; caller decides</text>
<path class="soft-arrow" d="M1064 432 C1064 476 600 472 600 432"/>
<text class="note" x="834" y="474" text-anchor="middle">proactive reads interests.yaml after auto_dream writes it</text>
<path class="soft-arrow" d="M1064 432 C1064 476 600 472 600 432"/>
<text class="note" x="834" y="474" text-anchor="middle">proactive reads interests.yaml after auto_dream writes it</text>
</svg>

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -1,98 +1,86 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img" aria-labelledby="title desc">
<title id="title">ReMe auto index and memory search flow</title>
<desc id="desc">A concise left-to-right flow from automatic workspace indexing to memory search and progressive link expansion.</desc>
<defs>
<style>
.bg { fill: #fffdf8; }
.title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.step-num { font: 700 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; }
.step-title { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.step-subtitle { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.chip-title { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.chip-text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; }
.note { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; }
.panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; rx: 18; ry: 18; stroke-linecap: round; stroke-linejoin: round; }
.chip { fill: #f8fbff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; }
.badge { fill: #44546a; }
.arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); }
.soft-arrow { stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); }
.line { stroke: #a3adbd; stroke-width: 1.25; stroke-linecap: round; }
</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img"
aria-labelledby="title desc">
<title id="title">ReMe auto index and memory search flow</title>
<desc id="desc">A concise left-to-right flow from live daily and digest Markdown indexing to memory search and
progressive link expansion.
</desc>
<defs>
<style>.bg { fill: #fffdf8; } .title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .step-num { font: 700 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; } .step-title { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .step-subtitle { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .chip-title { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .chip-text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; } .note { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; } .panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; rx: 18; ry: 18; stroke-linecap: round; stroke-linejoin: round; } .chip { fill: #f8fbff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; } .badge { fill: #44546a; } .arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); } .soft-arrow { stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); } .line { stroke: #a3adbd; stroke-width: 1.25; stroke-linecap: round; }</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto"
markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<rect class="bg" x="0" y="0" width="1200" height="640"/>
<text class="title" x="600" y="54" text-anchor="middle">Auto Index and Memory Search</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">Keep workspace indexes fresh, recall relevant chunks, then expand nearby wikilink context.</text>
<rect class="bg" x="0" y="0" width="1200" height="640"/>
<text class="title" x="600" y="54" text-anchor="middle">Auto Index and Memory Search</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">Keep workspace indexes fresh, recall relevant chunks, then expand nearby wikilink context.</text>
<rect class="panel" x="64" y="150" width="232" height="260"/>
<circle class="badge" cx="100" cy="188" r="15"/>
<text class="step-num" x="100" y="192" text-anchor="middle">1</text>
<text class="step-title" x="128" y="194">Watch memory</text>
<text class="step-subtitle" x="92" y="224">index_update_loop</text>
<rect class="chip" x="92" y="250" width="176" height="44"/>
<text class="chip-title" x="180" y="269" text-anchor="middle">daily / digest / resource</text>
<text class="chip-text" x="180" y="287" text-anchor="middle">md and jsonl files</text>
<rect class="chip" x="92" y="314" width="176" height="44"/>
<text class="chip-title" x="180" y="333" text-anchor="middle">init + watch changes</text>
<text class="chip-text" x="180" y="351" text-anchor="middle">added / modified / deleted</text>
<rect class="panel" x="64" y="150" width="232" height="260"/>
<circle class="badge" cx="100" cy="188" r="15"/>
<text class="step-num" x="100" y="192" text-anchor="middle">1</text>
<text class="step-title" x="128" y="194">Watch memory</text>
<text class="step-subtitle" x="92" y="224">index_update_loop</text>
<rect class="chip" x="92" y="250" width="176" height="44"/>
<text class="chip-title" x="180" y="269" text-anchor="middle">daily / digest</text>
<text class="chip-text" x="180" y="287" text-anchor="middle">Markdown live watch</text>
<rect class="chip" x="92" y="314" width="176" height="44"/>
<text class="chip-title" x="180" y="333" text-anchor="middle">init + watch changes</text>
<text class="chip-text" x="180" y="351" text-anchor="middle">added / modified / deleted</text>
<rect class="panel" x="348" y="150" width="232" height="260"/>
<circle class="badge" cx="384" cy="188" r="15"/>
<text class="step-num" x="384" y="192" text-anchor="middle">2</text>
<text class="step-title" x="412" y="194">Build index</text>
<text class="step-subtitle" x="376" y="224">update_index_step</text>
<rect class="chip" x="376" y="250" width="176" height="44"/>
<text class="chip-title" x="464" y="269" text-anchor="middle">chunk file</text>
<text class="chip-text" x="464" y="287" text-anchor="middle">FileNode + FileChunk[]</text>
<rect class="chip" x="376" y="314" width="176" height="44"/>
<text class="chip-title" x="464" y="333" text-anchor="middle">store structures</text>
<text class="chip-text" x="464" y="351" text-anchor="middle">BM25 + graph + chunks</text>
<rect class="panel" x="348" y="150" width="232" height="260"/>
<circle class="badge" cx="384" cy="188" r="15"/>
<text class="step-num" x="384" y="192" text-anchor="middle">2</text>
<text class="step-title" x="412" y="194">Build index</text>
<text class="step-subtitle" x="376" y="224">update_index_step</text>
<rect class="chip" x="376" y="250" width="176" height="44"/>
<text class="chip-title" x="464" y="269" text-anchor="middle">chunk file</text>
<text class="chip-text" x="464" y="287" text-anchor="middle">FileNode + FileChunk[]</text>
<rect class="chip" x="376" y="314" width="176" height="44"/>
<text class="chip-title" x="464" y="333" text-anchor="middle">store structures</text>
<text class="chip-text" x="464" y="351" text-anchor="middle">BM25 + graph + chunks</text>
<rect class="panel" x="632" y="150" width="232" height="260"/>
<circle class="badge" cx="668" cy="188" r="15"/>
<text class="step-num" x="668" y="192" text-anchor="middle">3</text>
<text class="step-title" x="696" y="194">Recall chunks</text>
<text class="step-subtitle" x="660" y="224">search_step</text>
<rect class="chip" x="660" y="250" width="176" height="44"/>
<text class="chip-title" x="748" y="269" text-anchor="middle">BM25 search</text>
<text class="chip-text" x="748" y="287" text-anchor="middle">keyword-ranked chunks</text>
<rect class="chip" x="660" y="314" width="176" height="44"/>
<text class="chip-title" x="748" y="333" text-anchor="middle">optional vector search</text>
<text class="chip-text" x="748" y="351" text-anchor="middle">RRF fusion when enabled</text>
<rect class="panel" x="632" y="150" width="232" height="260"/>
<circle class="badge" cx="668" cy="188" r="15"/>
<text class="step-num" x="668" y="192" text-anchor="middle">3</text>
<text class="step-title" x="696" y="194">Recall chunks</text>
<text class="step-subtitle" x="660" y="224">search_step</text>
<rect class="chip" x="660" y="250" width="176" height="44"/>
<text class="chip-title" x="748" y="269" text-anchor="middle">BM25 search</text>
<text class="chip-text" x="748" y="287" text-anchor="middle">keyword-ranked chunks</text>
<rect class="chip" x="660" y="314" width="176" height="44"/>
<text class="chip-title" x="748" y="333" text-anchor="middle">optional vector search</text>
<text class="chip-text" x="748" y="351" text-anchor="middle">RRF fusion when enabled</text>
<rect class="panel" x="916" y="150" width="220" height="260"/>
<circle class="badge" cx="952" cy="188" r="15"/>
<text class="step-num" x="952" y="192" text-anchor="middle">4</text>
<text class="step-title" x="980" y="194">Expand context</text>
<text class="step-subtitle" x="944" y="224">expand_links</text>
<rect class="chip" x="944" y="250" width="164" height="44"/>
<text class="chip-title" x="1026" y="269" text-anchor="middle">top chunks</text>
<text class="chip-text" x="1026" y="287" text-anchor="middle">path + line range</text>
<rect class="chip" x="944" y="314" width="164" height="44"/>
<text class="chip-title" x="1026" y="333" text-anchor="middle">outlinks + inlinks</text>
<text class="chip-text" x="1026" y="351" text-anchor="middle">name, description, via</text>
<rect class="panel" x="916" y="150" width="220" height="260"/>
<circle class="badge" cx="952" cy="188" r="15"/>
<text class="step-num" x="952" y="192" text-anchor="middle">4</text>
<text class="step-title" x="980" y="194">Expand context</text>
<text class="step-subtitle" x="944" y="224">expand_links</text>
<rect class="chip" x="944" y="250" width="164" height="44"/>
<text class="chip-title" x="1026" y="269" text-anchor="middle">top chunks</text>
<text class="chip-text" x="1026" y="287" text-anchor="middle">path + line range</text>
<rect class="chip" x="944" y="314" width="164" height="44"/>
<text class="chip-title" x="1026" y="333" text-anchor="middle">outlinks + inlinks</text>
<text class="chip-text" x="1026" y="351" text-anchor="middle">name, description, via</text>
<path class="arrow" d="M296 280 H348"/>
<path class="arrow" d="M580 280 H632"/>
<path class="arrow" d="M864 280 H916"/>
<path class="arrow" d="M296 280 H348"/>
<path class="arrow" d="M580 280 H632"/>
<path class="arrow" d="M864 280 H916"/>
<rect class="panel" x="142" y="484" width="916" height="76"/>
<text class="note" x="190" y="514">Default path</text>
<text class="chip-text" x="190" y="536">BM25 first, vector optional.</text>
<line class="line" x1="410" y1="500" x2="410" y2="544"/>
<text class="chip-title" x="454" y="514">BM25 is enabled by default</text>
<text class="chip-text" x="454" y="536">embedding_store is empty unless configured.</text>
<line class="line" x1="720" y1="500" x2="720" y2="544"/>
<text class="chip-title" x="764" y="514">Results stay compact first</text>
<text class="chip-text" x="764" y="536">Use read or traverse for deeper expansion.</text>
<rect class="panel" x="142" y="484" width="916" height="76"/>
<text class="note" x="190" y="514">Rebuild scope</text>
<text class="chip-text" x="190" y="536">reindex adds resource + JSONL.</text>
<line class="line" x1="410" y1="500" x2="410" y2="544"/>
<text class="chip-title" x="454" y="514">BM25 is enabled by default</text>
<text class="chip-text" x="454" y="536">Recall cap: 200; embeddings opt-in.</text>
<line class="line" x1="720" y1="500" x2="720" y2="544"/>
<text class="chip-title" x="764" y="514">Results stay compact first</text>
<text class="chip-text" x="764" y="536">Use read or traverse for deeper expansion.</text>
<path class="soft-arrow" d="M1026 410 C1026 456 748 452 748 410"/>
<text class="note" x="888" y="454" text-anchor="middle">link expansion is contextual, not a full-workspace dump</text>
<path class="soft-arrow" d="M1026 410 C1026 456 748 452 748 410"/>
<text class="note" x="888" y="454" text-anchor="middle">link expansion is contextual, not a full-workspace dump</text>
</svg>

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -1,91 +1,79 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img" aria-labelledby="title desc">
<title id="title">ReMe auto memory and auto resource flow</title>
<desc id="desc">A concise flow where auto memory and auto resource both create daily cards, which are indexed in the daily memory layer and then used by dream and search.</desc>
<defs>
<style>
.bg { fill: #fffdf8; }
.title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.step-num { font: 700 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; }
.step-title { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.step-subtitle { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.chip-title { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.chip-text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; }
.note { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; }
.panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; rx: 18; ry: 18; stroke-linecap: round; stroke-linejoin: round; }
.chip { fill: #f8fbff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; }
.badge { fill: #44546a; }
.arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); }
.soft-arrow { stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); }
.line { stroke: #a3adbd; stroke-width: 1.25; stroke-linecap: round; }
</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img"
aria-labelledby="title desc">
<title id="title">ReMe auto memory and auto resource flow</title>
<desc id="desc">A concise flow where auto memory and auto resource both create daily cards, which are indexed in the
daily memory layer and then used by dream and search.
</desc>
<defs>
<style>.bg { fill: #fffdf8; } .title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .step-num { font: 700 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; } .step-title { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .step-subtitle { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .chip-title { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .chip-text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; } .note { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; } .panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; rx: 18; ry: 18; stroke-linecap: round; stroke-linejoin: round; } .chip { fill: #f8fbff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; } .badge { fill: #44546a; } .arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); } .soft-arrow { stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); } .line { stroke: #a3adbd; stroke-width: 1.25; stroke-linecap: round; }</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto"
markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<rect class="bg" x="0" y="0" width="1200" height="640"/>
<text class="title" x="600" y="54" text-anchor="middle">Auto Memory &amp; Auto Resource</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">Two ingestion paths create daily cards, then share one daily index and downstream memory flow.</text>
<rect class="bg" x="0" y="0" width="1200" height="640"/>
<text class="title" x="600" y="54" text-anchor="middle">Auto Memory &amp; Auto Resource</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">Two ingestion paths create daily cards, then share one daily index and downstream memory flow.</text>
<rect class="panel" x="84" y="132" width="300" height="300"/>
<circle class="badge" cx="118" cy="170" r="15"/>
<text class="step-num" x="118" y="174" text-anchor="middle">1</text>
<text class="step-title" x="146" y="176">Inputs</text>
<text class="step-subtitle" x="112" y="206">memory + resource</text>
<rect class="chip" x="112" y="234" width="244" height="54"/>
<text class="chip-title" x="234" y="255" text-anchor="middle">Auto Memory</text>
<text class="chip-text" x="234" y="273" text-anchor="middle">conversation -> session daily card</text>
<rect class="chip" x="112" y="312" width="244" height="54"/>
<text class="chip-title" x="234" y="333" text-anchor="middle">Auto Resource</text>
<text class="chip-text" x="234" y="351" text-anchor="middle">dated file -> resource daily card</text>
<rect class="chip" x="112" y="388" width="244" height="28"/>
<text class="chip-text" x="234" y="407" text-anchor="middle">original sources remain verifiable</text>
<rect class="panel" x="84" y="132" width="300" height="300"/>
<circle class="badge" cx="118" cy="170" r="15"/>
<text class="step-num" x="118" y="174" text-anchor="middle">1</text>
<text class="step-title" x="146" y="176">Inputs</text>
<text class="step-subtitle" x="112" y="206">memory + resource</text>
<rect class="chip" x="112" y="234" width="244" height="54"/>
<text class="chip-title" x="234" y="255" text-anchor="middle">Auto Memory</text>
<text class="chip-text" x="234" y="273" text-anchor="middle">conversation -> topic-named card</text>
<rect class="chip" x="112" y="312" width="244" height="54"/>
<text class="chip-title" x="234" y="333" text-anchor="middle">Auto Resource</text>
<text class="chip-text" x="234" y="351" text-anchor="middle">source file -> topic-named card</text>
<rect class="chip" x="112" y="388" width="244" height="28"/>
<text class="chip-text" x="234" y="407" text-anchor="middle">original sources remain verifiable</text>
<rect class="panel" x="450" y="132" width="300" height="300"/>
<circle class="badge" cx="484" cy="170" r="15"/>
<text class="step-num" x="484" y="174" text-anchor="middle">2</text>
<text class="step-title" x="512" y="176">Daily Memory Layer</text>
<text class="step-subtitle" x="478" y="206">daily/&lt;date&gt;/</text>
<rect class="chip" x="478" y="234" width="244" height="54"/>
<text class="chip-title" x="600" y="255" text-anchor="middle">daily cards</text>
<text class="chip-text" x="600" y="273" text-anchor="middle">facts, context, actions</text>
<rect class="chip" x="478" y="312" width="244" height="54"/>
<text class="chip-title" x="600" y="333" text-anchor="middle">day index</text>
<text class="chip-text" x="600" y="351" text-anchor="middle">daily/&lt;date&gt;.md links cards</text>
<rect class="chip" x="478" y="388" width="244" height="28"/>
<text class="chip-text" x="600" y="407" text-anchor="middle">one readable daily memory stream</text>
<rect class="panel" x="450" y="132" width="300" height="300"/>
<circle class="badge" cx="484" cy="170" r="15"/>
<text class="step-num" x="484" y="174" text-anchor="middle">2</text>
<text class="step-title" x="512" y="176">Daily Memory Layer</text>
<text class="step-subtitle" x="478" y="206">daily/&lt;date&gt;/</text>
<rect class="chip" x="478" y="234" width="244" height="54"/>
<text class="chip-title" x="600" y="255" text-anchor="middle">daily cards</text>
<text class="chip-text" x="600" y="273" text-anchor="middle">facts, context, actions</text>
<rect class="chip" x="478" y="312" width="244" height="54"/>
<text class="chip-title" x="600" y="333" text-anchor="middle">day index</text>
<text class="chip-text" x="600" y="351" text-anchor="middle">daily/&lt;date&gt;.md links cards</text>
<rect class="chip" x="478" y="388" width="244" height="28"/>
<text class="chip-text" x="600" y="407" text-anchor="middle">one readable daily memory stream</text>
<rect class="panel" x="816" y="132" width="300" height="300"/>
<circle class="badge" cx="850" cy="170" r="15"/>
<text class="step-num" x="850" y="174" text-anchor="middle">3</text>
<text class="step-title" x="878" y="176">Downstream</text>
<text class="step-subtitle" x="844" y="206">dream + search</text>
<rect class="chip" x="844" y="234" width="244" height="54"/>
<text class="chip-title" x="966" y="255" text-anchor="middle">Auto Dream</text>
<text class="chip-text" x="966" y="273" text-anchor="middle">daily material -> digest memory</text>
<rect class="chip" x="844" y="312" width="244" height="54"/>
<text class="chip-title" x="966" y="333" text-anchor="middle">Memory Search</text>
<text class="chip-text" x="966" y="351" text-anchor="middle">retrieve daily and digest nodes</text>
<rect class="chip" x="844" y="388" width="244" height="28"/>
<text class="chip-text" x="966" y="407" text-anchor="middle">daily is the shared ingestion layer</text>
<rect class="panel" x="816" y="132" width="300" height="300"/>
<circle class="badge" cx="850" cy="170" r="15"/>
<text class="step-num" x="850" y="174" text-anchor="middle">3</text>
<text class="step-title" x="878" y="176">Downstream</text>
<text class="step-subtitle" x="844" y="206">dream + search</text>
<rect class="chip" x="844" y="234" width="244" height="54"/>
<text class="chip-title" x="966" y="255" text-anchor="middle">Auto Dream</text>
<text class="chip-text" x="966" y="273" text-anchor="middle">daily material -> digest memory</text>
<rect class="chip" x="844" y="312" width="244" height="54"/>
<text class="chip-title" x="966" y="333" text-anchor="middle">Memory Search</text>
<text class="chip-text" x="966" y="351" text-anchor="middle">retrieve daily and digest nodes</text>
<rect class="chip" x="844" y="388" width="244" height="28"/>
<text class="chip-text" x="966" y="407" text-anchor="middle">daily is the shared ingestion layer</text>
<path class="arrow" d="M384 282 H450"/>
<path class="arrow" d="M750 282 H816"/>
<path class="arrow" d="M384 282 H450"/>
<path class="arrow" d="M750 282 H816"/>
<rect class="panel" x="126" y="502" width="948" height="82"/>
<text class="note" x="176" y="532">Conversation source</text>
<text class="chip-text" x="176" y="554">session/dialog/&lt;session_id&gt;.jsonl</text>
<line class="line" x1="454" y1="518" x2="454" y2="566"/>
<text class="chip-title" x="498" y="532">Resource source</text>
<text class="chip-text" x="498" y="554">resource/&lt;date&gt;/&lt;resource_file&gt;</text>
<line class="line" x1="796" y1="518" x2="796" y2="566"/>
<text class="chip-title" x="838" y="532">Daily output</text>
<text class="chip-text" x="838" y="554">daily cards plus daily/&lt;date&gt;.md</text>
<rect class="panel" x="126" y="502" width="948" height="82"/>
<text class="note" x="176" y="532">Conversation source</text>
<text class="chip-text" x="176" y="554">session/dialog/&lt;session_id&gt;.jsonl</text>
<line class="line" x1="454" y1="518" x2="454" y2="566"/>
<text class="chip-title" x="498" y="532">Resource source</text>
<text class="chip-text" x="498" y="554">resource/[&lt;date&gt;/]&lt;resource_file&gt;</text>
<line class="line" x1="796" y1="518" x2="796" y2="566"/>
<text class="chip-title" x="838" y="532">Daily output</text>
<text class="chip-text" x="838" y="554">daily/&lt;date&gt;/&lt;name&gt;.md</text>
<path class="soft-arrow" d="M966 432 C966 474 600 474 600 432"/>
<text class="note" x="782" y="478" text-anchor="middle">downstream flows read the same daily memory layer</text>
<path class="soft-arrow" d="M966 432 C966 474 600 474 600 432"/>
<text class="note" x="782" y="478" text-anchor="middle">downstream flows read the same daily memory layer</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -1,97 +1,83 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="520" viewBox="0 0 1200 520" role="img" aria-labelledby="title desc">
<title id="title">ReMe design philosophy</title>
<desc id="desc">A simple diagram showing ReMe's design philosophy: file-native memory, progressive abstraction, and explicit relationship graph for agents.</desc>
<defs>
<style>
.bg { fill: #ffffff; }
.title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.panel-blue { fill: #eef7ff; stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.panel-green { fill: #f0fbf5; stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.panel-yellow { fill: #fff7e5; stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.label { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.small { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.tiny { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; }
.chip { fill: #ffffff; stroke: #1f2430; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; }
.chip-text { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); }
.soft-arrow { stroke: #a3adbd; stroke-width: 1.25; fill: none; stroke-dasharray: 6 6; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); }
.node { fill: #ffffff; stroke: #1f2430; stroke-width: 1.6; }
.link { stroke: #a3adbd; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.file-line { stroke: #7f8b9d; stroke-width: 1.45; stroke-linecap: round; }
</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="520" viewBox="0 0 1200 520" role="img"
aria-labelledby="title desc">
<title id="title">ReMe design philosophy</title>
<desc id="desc">A simple diagram showing ReMe's design philosophy: file-native memory, progressive abstraction, and
explicit relationship graph for agents.
</desc>
<defs>
<style>.bg { fill: #ffffff; } .title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; } .panel-blue { fill: #eef7ff; stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; } .panel-green { fill: #f0fbf5; stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; } .panel-yellow { fill: #fff7e5; stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; } .label { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .small { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .tiny { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; } .chip { fill: #ffffff; stroke: #1f2430; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; } .chip-text { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); } .soft-arrow { stroke: #a3adbd; stroke-width: 1.25; fill: none; stroke-dasharray: 6 6; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); } .node { fill: #ffffff; stroke: #1f2430; stroke-width: 1.6; } .link { stroke: #a3adbd; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; } .file-line { stroke: #7f8b9d; stroke-width: 1.45; stroke-linecap: round; }</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto"
markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<rect class="bg" x="0" y="0" width="1200" height="520"/>
<text class="title" x="600" y="54" text-anchor="middle">ReMe Design Philosophy</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">File-native memory, progressive abstraction, and explicit links for reusable agent context.</text>
<rect class="bg" x="0" y="0" width="1200" height="520"/>
<text class="title" x="600" y="54" text-anchor="middle">ReMe Design Philosophy</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">File-native memory, progressive abstraction, and explicit links for reusable agent context.</text>
<rect class="panel-blue" x="64" y="134" width="304" height="220" rx="14"/>
<text class="label" x="216" y="170" text-anchor="middle">Foundation: Memory as File</text>
<text class="small" x="216" y="196" text-anchor="middle">Readable, editable knowledge assets</text>
<g transform="translate(126 224)">
<rect class="panel" x="0" y="0" width="120" height="82" rx="8"/>
<path class="file-line" d="M22 26 H98"/>
<path class="file-line" d="M22 43 H86"/>
<path class="file-line" d="M22 60 H72"/>
<rect class="chip" x="136" y="6" width="96" height="28" rx="8"/>
<text class="chip-text" x="184" y="25" text-anchor="middle">Markdown</text>
<rect class="chip" x="136" y="48" width="96" height="28" rx="8"/>
<text class="chip-text" x="184" y="67" text-anchor="middle">YAML/JSONL</text>
</g>
<text class="tiny" x="216" y="332" text-anchor="middle">Human-readable. Agent-operable.</text>
<rect class="panel-blue" x="64" y="134" width="304" height="220" rx="14"/>
<text class="label" x="216" y="170" text-anchor="middle">Foundation: Memory as File</text>
<text class="small" x="216" y="196" text-anchor="middle">Readable, editable knowledge assets</text>
<g transform="translate(126 224)">
<rect class="panel" x="0" y="0" width="120" height="82" rx="8"/>
<path class="file-line" d="M22 26 H98"/>
<path class="file-line" d="M22 43 H86"/>
<path class="file-line" d="M22 60 H72"/>
<rect class="chip" x="136" y="6" width="96" height="28" rx="8"/>
<text class="chip-text" x="184" y="25" text-anchor="middle">Markdown</text>
<rect class="chip" x="136" y="48" width="96" height="28" rx="8"/>
<text class="chip-text" x="184" y="67" text-anchor="middle">YAML/JSONL</text>
</g>
<text class="tiny" x="216" y="332" text-anchor="middle">Human-readable. Agent-operable.</text>
<rect class="panel-yellow" x="448" y="134" width="304" height="220" rx="14"/>
<text class="label" x="600" y="170" text-anchor="middle">Core: Progressive Abstraction</text>
<text class="small" x="600" y="196" text-anchor="middle">From raw material to durable memory</text>
<g transform="translate(472 235)">
<rect class="chip" x="0" y="0" width="66" height="42" rx="10"/>
<text class="chip-text" x="33" y="26" text-anchor="middle">Raw</text>
<rect class="chip" x="95" y="0" width="66" height="42" rx="10"/>
<text class="chip-text" x="128" y="26" text-anchor="middle">Daily</text>
<rect class="chip" x="190" y="0" width="66" height="42" rx="10"/>
<text class="chip-text" x="223" y="26" text-anchor="middle">Digest</text>
<path class="arrow" d="M68 21 H89"/>
<path class="arrow" d="M163 21 H184"/>
</g>
<text class="tiny" x="600" y="310" text-anchor="middle">session/ + resource/ -> daily/ -> digest/</text>
<text class="tiny" x="600" y="332" text-anchor="middle">Continuous consolidation, not one-shot summary.</text>
<rect class="panel-yellow" x="448" y="134" width="304" height="220" rx="14"/>
<text class="label" x="600" y="170" text-anchor="middle">Core: Progressive Abstraction</text>
<text class="small" x="600" y="196" text-anchor="middle">From raw material to durable memory</text>
<g transform="translate(472 235)">
<rect class="chip" x="0" y="0" width="66" height="42" rx="10"/>
<text class="chip-text" x="33" y="26" text-anchor="middle">Raw</text>
<rect class="chip" x="95" y="0" width="66" height="42" rx="10"/>
<text class="chip-text" x="128" y="26" text-anchor="middle">Daily</text>
<rect class="chip" x="190" y="0" width="66" height="42" rx="10"/>
<text class="chip-text" x="223" y="26" text-anchor="middle">Digest</text>
<path class="arrow" d="M68 21 H89"/>
<path class="arrow" d="M163 21 H184"/>
</g>
<text class="tiny" x="600" y="310" text-anchor="middle">session/resource -> daily -> digest</text>
<text class="tiny" x="600" y="332" text-anchor="middle">Continuous consolidation; not one-shot.</text>
<rect class="panel-green" x="832" y="134" width="304" height="220" rx="14"/>
<text class="label" x="984" y="170" text-anchor="middle">Innovation: Link Graph</text>
<text class="small" x="984" y="196" text-anchor="middle">Explicit links expand context</text>
<g transform="translate(930 207) scale(0.74)">
<path class="link" d="M74 40 L26 90"/>
<path class="link" d="M74 40 L130 84"/>
<path class="link" d="M26 90 L130 84"/>
<path class="link" d="M74 40 L152 26"/>
<circle class="node" cx="74" cy="40" r="24"/>
<circle class="node" cx="26" cy="90" r="20"/>
<circle class="node" cx="130" cy="84" r="20"/>
<circle class="node" cx="152" cy="26" r="18"/>
<text class="chip-text" x="74" y="38" text-anchor="middle">[[ ]]</text>
<text class="tiny" x="74" y="55" text-anchor="middle">Node</text>
<text class="tiny" x="26" y="95" text-anchor="middle">Src</text>
<text class="tiny" x="130" y="89" text-anchor="middle">Risk</text>
<text class="tiny" x="152" y="31" text-anchor="middle">Dep</text>
</g>
<text class="tiny" x="984" y="318" text-anchor="middle">Beyond similarity: source, cause, dependency, risk.</text>
<text class="tiny" x="984" y="338" text-anchor="middle">Wikilinks guide expansion across nodes.</text>
<rect class="panel-green" x="832" y="134" width="304" height="220" rx="14"/>
<text class="label" x="984" y="170" text-anchor="middle">Innovation: Link Graph</text>
<text class="small" x="984" y="196" text-anchor="middle">Explicit links expand context</text>
<g transform="translate(930 207) scale(0.74)">
<path class="link" d="M74 40 L26 90"/>
<path class="link" d="M74 40 L130 84"/>
<path class="link" d="M26 90 L130 84"/>
<path class="link" d="M74 40 L152 26"/>
<circle class="node" cx="74" cy="40" r="24"/>
<circle class="node" cx="26" cy="90" r="20"/>
<circle class="node" cx="130" cy="84" r="20"/>
<circle class="node" cx="152" cy="26" r="18"/>
<text class="chip-text" x="74" y="38" text-anchor="middle">[[ ]]</text>
<text class="tiny" x="74" y="55" text-anchor="middle">Node</text>
<text class="tiny" x="26" y="95" text-anchor="middle">Src</text>
<text class="tiny" x="130" y="89" text-anchor="middle">Risk</text>
<text class="tiny" x="152" y="31" text-anchor="middle">Dep</text>
</g>
<text class="tiny" x="984" y="318" text-anchor="middle">Source · cause · dependency · risk</text>
<text class="tiny" x="984" y="338" text-anchor="middle">Wikilinks guide expansion across nodes.</text>
<path class="arrow" d="M368 244 H448"/>
<path class="arrow" d="M752 244 H832"/>
<path class="arrow" d="M368 244 H448"/>
<path class="arrow" d="M752 244 H832"/>
<rect class="panel" x="220" y="414" width="760" height="56" rx="14"/>
<text class="small" x="600" y="438" text-anchor="middle">One shared file tree acts as both memory store and operating surface.</text>
<text class="chip-text" x="600" y="460" text-anchor="middle">capture -> refine -> link -> retrieve -> expand</text>
<path class="soft-arrow" d="M216 354 C216 392 428 392 454 414"/>
<path class="soft-arrow" d="M600 354 V414"/>
<path class="soft-arrow" d="M984 354 C984 392 772 392 746 414"/>
<rect class="panel" x="220" y="414" width="760" height="56" rx="14"/>
<text class="small" x="600" y="438" text-anchor="middle">One shared file tree acts as both memory store and operating surface.</text>
<text class="chip-text" x="600" y="460" text-anchor="middle">capture -> refine -> link -> retrieve -> expand</text>
<path class="soft-arrow" d="M216 354 C216 392 428 392 454 414"/>
<path class="soft-arrow" d="M600 354 V414"/>
<path class="soft-arrow" d="M984 354 C984 392 772 392 746 414"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -1,158 +1,149 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="920" viewBox="0 0 1200 920" role="img" aria-labelledby="title desc">
<title id="title">ReMe framework structure</title>
<desc id="desc">An architectural map of ReMe from file-backed workspace storage through knowledge kernel, workflows, application wiring, and external service surfaces.</desc>
<defs>
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#fbfaf6"/>
<stop offset="0.55" stop-color="#f5f7fb"/>
<stop offset="1" stop-color="#eef7f4"/>
</linearGradient>
<linearGradient id="serviceGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#d9f3ec"/>
<stop offset="1" stop-color="#dcecf7"/>
</linearGradient>
<linearGradient id="appGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#e5e8fb"/>
<stop offset="1" stop-color="#f1e4f5"/>
</linearGradient>
<linearGradient id="jobGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#fae4d8"/>
<stop offset="1" stop-color="#f4eccd"/>
</linearGradient>
<linearGradient id="kernelGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#d9f1f4"/>
<stop offset="1" stop-color="#e1e8f7"/>
</linearGradient>
<linearGradient id="workspaceGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#f0e5d3"/>
<stop offset="1" stop-color="#e3efd9"/>
</linearGradient>
<filter id="softShadow" x="-8%" y="-18%" width="116%" height="145%">
<feDropShadow dx="0" dy="16" stdDeviation="16" flood-color="#1c2b3a" flood-opacity="0.13"/>
</filter>
<filter id="nodeShadow" x="-15%" y="-30%" width="130%" height="170%">
<feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="#1c2b3a" flood-opacity="0.12"/>
</filter>
<style>
.bg { fill: url(#paper); }
.grain { fill: none; stroke: #d9ded8; stroke-width: 1; opacity: 0.38; }
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; }
.title { font-size: 30px; font-weight: 800; fill: #172033; }
.subtitle { font-size: 14px; fill: #667489; }
.layer-title { font-size: 18px; font-weight: 800; fill: #263548; }
.layer-note { font-size: 13px; fill: rgba(38,53,72,0.72); }
.section-label { font-size: 11px; font-weight: 800; fill: #647187; letter-spacing: 1.8px; }
.node-title { font-size: 14px; font-weight: 700; fill: #1d2838; }
.node-text { font-size: 12px; fill: #667489; }
.micro { font-size: 11px; fill: #728096; }
.plate { filter: url(#softShadow); }
.plate-bg { fill: #ffffff; stroke: rgba(52, 65, 84, 0.12); stroke-width: 1; }
.side { opacity: 0.18; }
.node { fill: #ffffff; stroke: rgba(90, 105, 128, 0.22); stroke-width: 1; filter: url(#nodeShadow); }
</style>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="920" viewBox="0 0 1200 920" role="img"
aria-labelledby="title desc">
<title id="title">ReMe framework structure</title>
<desc id="desc">An architectural map of ReMe from file-backed workspace storage through knowledge kernel, workflows,
application wiring, and external service surfaces.
</desc>
<defs>
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#fbfaf6"/>
<stop offset="0.55" stop-color="#f5f7fb"/>
<stop offset="1" stop-color="#eef7f4"/>
</linearGradient>
<linearGradient id="serviceGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#d9f3ec"/>
<stop offset="1" stop-color="#dcecf7"/>
</linearGradient>
<linearGradient id="appGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#e5e8fb"/>
<stop offset="1" stop-color="#f1e4f5"/>
</linearGradient>
<linearGradient id="jobGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#fae4d8"/>
<stop offset="1" stop-color="#f4eccd"/>
</linearGradient>
<linearGradient id="kernelGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#d9f1f4"/>
<stop offset="1" stop-color="#e1e8f7"/>
</linearGradient>
<linearGradient id="workspaceGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#f0e5d3"/>
<stop offset="1" stop-color="#e3efd9"/>
</linearGradient>
<filter id="softShadow" x="-8%" y="-18%" width="116%" height="145%">
<feDropShadow dx="0" dy="16" stdDeviation="16" flood-color="#1c2b3a" flood-opacity="0.13"/>
</filter>
<filter id="nodeShadow" x="-15%" y="-30%" width="130%" height="170%">
<feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="#1c2b3a" flood-opacity="0.12"/>
</filter>
<style>.bg { fill: url(#paper); } .grain { fill: none; stroke: #d9ded8; stroke-width: 1; opacity: 0.38; } text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; } .title { font-size: 30px; font-weight: 800; fill: #172033; } .subtitle { font-size: 14px; fill: #667489; } .layer-title { font-size: 18px; font-weight: 800; fill: #263548; } .layer-note { font-size: 13px; fill: rgba(38,53,72,0.72); } .section-label { font-size: 11px; font-weight: 800; fill: #647187; letter-spacing: 1.8px; } .node-title { font-size: 14px; font-weight: 700; fill: #1d2838; } .node-text { font-size: 12px; fill: #667489; } .micro { font-size: 11px; fill: #728096; } .plate { filter: url(#softShadow); } .plate-bg { fill: #ffffff; stroke: rgba(52, 65, 84, 0.12); stroke-width: 1; } .side { opacity: 0.18; } .node { fill: #ffffff; stroke: rgba(90, 105, 128, 0.22); stroke-width: 1; filter: url(#nodeShadow); }</style>
</defs>
<rect class="bg" width="1200" height="920"/>
<path class="grain" d="M98 138 C246 104 337 155 475 121 C616 86 752 124 890 96 C1019 70 1082 98 1136 126"/>
<path class="grain" d="M64 768 C218 720 374 790 514 742 C660 692 812 740 966 706 C1066 684 1118 708 1146 732"/>
<rect class="bg" width="1200" height="920"/>
<path class="grain" d="M98 138 C246 104 337 155 475 121 C616 86 752 124 890 96 C1019 70 1082 98 1136 126"/>
<path class="grain" d="M64 768 C218 720 374 790 514 742 C660 692 812 740 966 706 C1066 684 1118 708 1146 732"/>
<text class="title" x="600" y="56" text-anchor="middle">ReMe Framework Structure</text>
<text class="subtitle" x="600" y="83" text-anchor="middle">File-backed memory, searchable knowledge kernel, composable jobs, app wiring, and public service surfaces.</text>
<text class="title" x="600" y="56" text-anchor="middle">ReMe Framework Structure</text>
<text class="subtitle" x="600" y="83" text-anchor="middle">File-backed memory, searchable knowledge kernel, composable jobs, app wiring, and public service surfaces.</text>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M126 196 L1074 196 L1042 226 L158 226 Z" fill="#83cbbb"/>
<rect class="plate-bg" x="126" y="128" width="948" height="96" rx="18"/>
<rect x="126" y="128" width="280" height="96" rx="18" fill="url(#serviceGrad)"/>
<text class="layer-title" x="178" y="166">Service</text>
<text class="layer-note" x="178" y="188">Public interfaces</text>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M126 196 L1074 196 L1042 226 L158 226 Z" fill="#83cbbb"/>
<rect class="plate-bg" x="126" y="128" width="948" height="96" rx="18"/>
<rect x="126" y="128" width="280" height="96" rx="18" fill="url(#serviceGrad)"/>
<text class="layer-title" x="178" y="166">Service</text>
<text class="layer-note" x="178" y="188">Public interfaces</text>
<rect class="node" x="436" y="151" width="138" height="50" rx="13"/>
<text class="node-title" x="505" y="172" text-anchor="middle">HTTP API</text>
<text class="node-text" x="505" y="190" text-anchor="middle">server routes</text>
<rect class="node" x="626" y="151" width="138" height="50" rx="13"/>
<text class="node-title" x="695" y="172" text-anchor="middle">MCP Tools</text>
<text class="node-text" x="695" y="190" text-anchor="middle">agent actions</text>
<rect class="node" x="816" y="151" width="138" height="50" rx="13"/>
<text class="node-title" x="885" y="172" text-anchor="middle">CLI Client</text>
<text class="node-text" x="885" y="190" text-anchor="middle">local access</text>
</g>
<rect class="node" x="436" y="151" width="138" height="50" rx="13"/>
<text class="node-title" x="505" y="172" text-anchor="middle">HTTP API</text>
<text class="node-text" x="505" y="190" text-anchor="middle">API + Studio</text>
<rect class="node" x="626" y="151" width="138" height="50" rx="13"/>
<text class="node-title" x="695" y="172" text-anchor="middle">MCP Tools</text>
<text class="node-text" x="695" y="190" text-anchor="middle">agent actions</text>
<rect class="node" x="816" y="151" width="138" height="50" rx="13"/>
<text class="node-title" x="885" y="172" text-anchor="middle">CLI Client</text>
<text class="node-text" x="885" y="190" text-anchor="middle">service calls</text>
</g>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M94 336 L1106 336 L1074 366 L126 366 Z" fill="#aab5ec"/>
<rect class="plate-bg" x="94" y="264" width="1012" height="100" rx="18"/>
<rect x="94" y="264" width="280" height="100" rx="18" fill="url(#appGrad)"/>
<text class="layer-title" x="146" y="303">Application</text>
<text class="layer-note" x="146" y="325">Config, wiring, lifecycle</text>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M94 336 L1106 336 L1074 366 L126 366 Z" fill="#aab5ec"/>
<rect class="plate-bg" x="94" y="264" width="1012" height="100" rx="18"/>
<rect x="94" y="264" width="280" height="100" rx="18" fill="url(#appGrad)"/>
<text class="layer-title" x="146" y="303">Application</text>
<text class="layer-note" x="146" y="325">Config, wiring, lifecycle</text>
<rect class="node" x="424" y="290" width="122" height="48" rx="12"/>
<text class="node-title" x="485" y="319" text-anchor="middle">Context</text>
<rect class="node" x="574" y="290" width="122" height="48" rx="12"/>
<text class="node-title" x="635" y="319" text-anchor="middle">Wiring</text>
<rect class="node" x="724" y="290" width="122" height="48" rx="12"/>
<text class="node-title" x="785" y="319" text-anchor="middle">Lifecycle</text>
<rect class="node" x="874" y="290" width="122" height="48" rx="12"/>
<text class="node-title" x="935" y="319" text-anchor="middle">Job APIs</text>
</g>
<rect class="node" x="424" y="290" width="122" height="48" rx="12"/>
<text class="node-title" x="485" y="319" text-anchor="middle">Context</text>
<rect class="node" x="574" y="290" width="122" height="48" rx="12"/>
<text class="node-title" x="635" y="319" text-anchor="middle">Wiring</text>
<rect class="node" x="724" y="290" width="122" height="48" rx="12"/>
<text class="node-title" x="785" y="319" text-anchor="middle">Lifecycle</text>
<rect class="node" x="874" y="290" width="122" height="48" rx="12"/>
<text class="node-title" x="935" y="319" text-anchor="middle">Job APIs</text>
</g>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M126 500 L1074 500 L1042 530 L158 530 Z" fill="#e9ad8f"/>
<rect class="plate-bg" x="126" y="404" width="948" height="124" rx="18"/>
<rect x="126" y="404" width="280" height="124" rx="18" fill="url(#jobGrad)"/>
<text class="layer-title" x="178" y="448">Steps / Jobs</text>
<text class="layer-note" x="178" y="470">Composable workflows</text>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M126 500 L1074 500 L1042 530 L158 530 Z" fill="#e9ad8f"/>
<rect class="plate-bg" x="126" y="404" width="948" height="124" rx="18"/>
<rect x="126" y="404" width="280" height="124" rx="18" fill="url(#jobGrad)"/>
<text class="layer-title" x="178" y="448">Steps / Jobs</text>
<text class="layer-note" x="178" y="470">Composable workflows</text>
<rect class="node" x="424" y="428" width="232" height="76" rx="14"/>
<text class="node-title" x="448" y="456">Jobs</text>
<text class="node-text" x="448" y="479">base · stream · background · cron</text>
<rect class="node" x="704" y="428" width="300" height="76" rx="14"/>
<text class="node-title" x="728" y="456">Step Modules</text>
<text class="node-text" x="728" y="479">file_io · index · evolve · transfer · channel · common</text>
</g>
<rect class="node" x="424" y="428" width="232" height="76" rx="14"/>
<text class="node-title" x="448" y="456">Jobs</text>
<text class="node-text" x="448" y="479">base · stream · background · cron</text>
<rect class="node" x="704" y="428" width="300" height="76" rx="14"/>
<text class="node-title" x="728" y="456">Step Modules</text>
<text class="node-text" x="728" y="479">file_io · index · evolve · common</text>
<text class="node-text" x="728" y="497">transfer · cookbook · benchmark</text>
</g>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M94 674 L1106 674 L1074 704 L126 704 Z" fill="#8fcbd5"/>
<rect class="plate-bg" x="94" y="568" width="1012" height="134" rx="18"/>
<rect x="94" y="568" width="280" height="134" rx="18" fill="url(#kernelGrad)"/>
<text class="layer-title" x="146" y="615">Knowledge Kernel</text>
<text class="layer-note" x="146" y="637">Index, watch, schema</text>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M94 674 L1106 674 L1074 704 L126 704 Z" fill="#8fcbd5"/>
<rect class="plate-bg" x="94" y="568" width="1012" height="134" rx="18"/>
<rect x="94" y="568" width="280" height="134" rx="18" fill="url(#kernelGrad)"/>
<text class="layer-title" x="146" y="615">Knowledge Kernel</text>
<text class="layer-note" x="146" y="637">Index, watch, schema</text>
<rect class="node" x="424" y="594" width="228" height="82" rx="14"/>
<text class="node-title" x="448" y="623">Index Stores</text>
<text class="node-text" x="448" y="646">file_store · keyword_index</text>
<text class="node-text" x="448" y="664">embedding_store · file_graph</text>
<rect class="node" x="698" y="594" width="172" height="82" rx="14"/>
<text class="node-title" x="722" y="623">File Watcher</text>
<text class="node-text" x="722" y="646">scanner · chunker</text>
<text class="node-text" x="722" y="664">catalog</text>
<rect class="node" x="916" y="594" width="142" height="82" rx="14"/>
<text class="node-title" x="940" y="623">Memory</text>
<text class="node-text" x="940" y="646">FileNode</text>
<text class="node-text" x="940" y="664">FileChunk · FileLink</text>
</g>
<rect class="node" x="424" y="594" width="228" height="82" rx="14"/>
<text class="node-title" x="448" y="623">Index Stores</text>
<text class="node-text" x="448" y="646">file_store · keyword_index</text>
<text class="node-text" x="448" y="664">embedding_store · file_graph</text>
<rect class="node" x="698" y="594" width="172" height="82" rx="14"/>
<text class="node-title" x="722" y="623">File Watcher</text>
<text class="node-text" x="722" y="646">scanner · chunker</text>
<text class="node-text" x="722" y="664">catalog</text>
<rect class="node" x="916" y="594" width="142" height="82" rx="14"/>
<text class="node-title" x="940" y="623">Memory</text>
<text class="node-text" x="940" y="646">FileNode</text>
<text class="node-text" x="940" y="664">FileChunk · FileLink</text>
</g>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M126 818 L1074 818 L1042 848 L158 848 Z" fill="#d0b98f"/>
<rect class="plate-bg" x="126" y="738" width="948" height="108" rx="18"/>
<rect x="126" y="738" width="280" height="108" rx="18" fill="url(#workspaceGrad)"/>
<text class="layer-title" x="178" y="777">Workspace Layout</text>
<text class="layer-note" x="178" y="799">File-backed memory</text>
<g class="plate" transform="translate(0,0)">
<path class="side" d="M126 818 L1074 818 L1042 848 L158 848 Z" fill="#d0b98f"/>
<rect class="plate-bg" x="126" y="738" width="948" height="108" rx="18"/>
<rect x="126" y="738" width="280" height="108" rx="18" fill="url(#workspaceGrad)"/>
<text class="layer-title" x="178" y="777">Workspace Layout</text>
<text class="layer-note" x="178" y="799">File-backed memory</text>
<rect class="node" x="424" y="767" width="126" height="50" rx="13"/>
<text class="node-title" x="487" y="788" text-anchor="middle">daily/</text>
<text class="node-text" x="487" y="806" text-anchor="middle">working notes</text>
<rect class="node" x="584" y="767" width="126" height="50" rx="13"/>
<text class="node-title" x="647" y="788" text-anchor="middle">digest/</text>
<text class="node-text" x="647" y="806" text-anchor="middle">long-term</text>
<rect class="node" x="744" y="767" width="126" height="50" rx="13"/>
<text class="node-title" x="807" y="788" text-anchor="middle">resource/</text>
<text class="node-text" x="807" y="806" text-anchor="middle">resources</text>
<rect class="node" x="904" y="767" width="126" height="50" rx="13"/>
<text class="node-title" x="967" y="788" text-anchor="middle">metadata/</text>
<text class="node-text" x="967" y="806" text-anchor="middle">state</text>
</g>
<rect class="node" x="400" y="767" width="160" height="50" rx="13"/>
<text class="node-title" x="480" y="788" text-anchor="middle">session directories</text>
<text class="node-text" x="480" y="806" text-anchor="middle">sources · agent runtime</text>
<rect class="node" x="572" y="767" width="100" height="50" rx="13"/>
<text class="node-title" x="622" y="788" text-anchor="middle">daily/</text>
<text class="node-text" x="622" y="806" text-anchor="middle">working</text>
<rect class="node" x="684" y="767" width="100" height="50" rx="13"/>
<text class="node-title" x="734" y="788" text-anchor="middle">digest/</text>
<text class="node-text" x="734" y="806" text-anchor="middle">long-term</text>
<rect class="node" x="796" y="767" width="100" height="50" rx="13"/>
<text class="node-title" x="846" y="788" text-anchor="middle">resource/</text>
<text class="node-text" x="846" y="806" text-anchor="middle">sources</text>
<rect class="node" x="908" y="767" width="122" height="50" rx="13"/>
<text class="node-title" x="969" y="788" text-anchor="middle">metadata/</text>
<text class="node-text" x="969" y="806" text-anchor="middle">derived state</text>
</g>
<text class="section-label" x="92" y="118">EXTERNAL SURFACES</text>
<text class="section-label" x="92" y="734">PERSISTENT MEMORY BASE</text>
<text class="micro" x="600" y="878" text-anchor="middle">Structure reads bottom-up during boot and top-down during use.</text>
<text class="micro" x="600" y="896" text-anchor="middle">Workspace files feed the kernel; workflows compose operations; application wiring exposes stable service entry points.</text>
<text class="section-label" x="92" y="118">EXTERNAL SURFACES</text>
<text class="section-label" x="92" y="734">PERSISTENT MEMORY BASE</text>
<text class="micro" x="600" y="878" text-anchor="middle">Structure reads bottom-up during boot and top-down during use.</text>
<text class="micro" x="600" y="896" text-anchor="middle">Workspace files feed the kernel; workflows compose operations; application wiring exposes stable service entry points.</text>
</svg>

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,110 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="670" viewBox="0 0 1200 670" role="img" aria-labelledby="title desc">
<title id="title">ReMe memory as file model</title>
<desc id="desc">A diagram showing ReMe workspace files as both a human readable memory interface and an agent operable memory graph, flowing from raw input to daily notes, digest nodes, and metadata indexes.</desc>
<defs>
<style>
.bg { fill: #fffdf8; }
.title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.section-title { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.section-subtitle { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.chip-title { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.chip-text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; }
.note { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; }
.label { font: 700 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; }
.panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; rx: 18; ry: 18; stroke-linecap: round; stroke-linejoin: round; }
.chip { fill: #f8fbff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; }
.badge { fill: #44546a; }
.soft { fill: #eef7ff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; }
.arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); }
.soft-arrow { stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); }
.line { stroke: #a3adbd; stroke-width: 1.25; stroke-linecap: round; }
</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="670" viewBox="0 0 1200 670" role="img"
aria-labelledby="title desc">
<title id="title">ReMe memory as file model</title>
<desc id="desc">A diagram showing ReMe workspace files as both a human readable memory interface and an agent
operable memory graph, flowing from raw input to daily notes, digest nodes, and metadata indexes.
</desc>
<defs>
<style>.bg { fill: #fffdf8; } .title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .section-title { font: 700 18px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; fill: #1f2430; } .section-subtitle { font: 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; fill: #556276; } .chip-title { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .chip-text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; } .note { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; } .label { font: 700 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #ffffff; } .panel { fill: #ffffff; stroke: #1f2430; stroke-width: 2.2; rx: 18; ry: 18; stroke-linecap: round; stroke-linejoin: round; } .chip { fill: #f8fbff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 5; } .badge { fill: #44546a; } .soft { fill: #eef7ff; stroke: #1f2430; stroke-width: 1.6; rx: 11; ry: 11; stroke-linecap: round; stroke-linejoin: round; } .arrow { stroke: #7f8b9d; stroke-width: 1.45; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); } .soft-arrow { stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; fill: none; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); } .line { stroke: #a3adbd; stroke-width: 1.25; stroke-linecap: round; }</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto"
markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<rect class="bg" x="0" y="0" width="1200" height="670"/>
<text class="title" x="600" y="54" text-anchor="middle">Memory as File</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">Workspace files are the readable memory surface and the operable graph/index substrate.</text>
<rect class="bg" x="0" y="0" width="1200" height="670"/>
<text class="title" x="600" y="54" text-anchor="middle">Memory as File</text>
<text class="subtitle" x="600" y="80" text-anchor="middle">Workspace files are the readable memory surface and the operable graph/index substrate.</text>
<rect class="panel" x="72" y="124" width="280" height="168"/>
<rect class="badge" x="102" y="150" width="94" height="24" rx="12" ry="12"/>
<text class="label" x="149" y="167" text-anchor="middle">Human</text>
<text class="section-title" x="102" y="202">Read and edit files</text>
<text class="section-subtitle" x="102" y="226">Markdown, YAML, JSONL, resources</text>
<rect class="chip" x="102" y="248" width="220" height="28"/>
<text class="chip-text" x="212" y="267" text-anchor="middle">open, revise, move, delete</text>
<rect class="panel" x="72" y="124" width="280" height="168"/>
<rect class="badge" x="102" y="150" width="94" height="24" rx="12" ry="12"/>
<text class="label" x="149" y="167" text-anchor="middle">Human</text>
<text class="section-title" x="102" y="202">Read and edit files</text>
<text class="section-subtitle" x="102" y="226" style="font-size:11px">Markdown · YAML · JSONL · resources</text>
<rect class="chip" x="102" y="248" width="220" height="28"/>
<text class="chip-text" x="212" y="267" text-anchor="middle">open, revise, move, delete</text>
<rect class="panel" x="460" y="120" width="280" height="176"/>
<text class="section-title" x="600" y="152" text-anchor="middle">Workspace directory</text>
<text class="section-subtitle" x="600" y="176" text-anchor="middle">the shared memory interface</text>
<rect class="soft" x="504" y="204" width="192" height="30"/>
<text class="chip-title" x="600" y="224" text-anchor="middle">Memory as File</text>
<rect class="soft" x="504" y="248" width="192" height="30"/>
<text class="chip-title" x="600" y="268" text-anchor="middle">File as Memory</text>
<rect class="panel" x="460" y="120" width="280" height="176"/>
<text class="section-title" x="600" y="152" text-anchor="middle">Workspace directory</text>
<text class="section-subtitle" x="600" y="176" text-anchor="middle">the shared memory interface</text>
<rect class="soft" x="504" y="204" width="192" height="30"/>
<text class="chip-title" x="600" y="224" text-anchor="middle">Memory as File</text>
<rect class="soft" x="504" y="248" width="192" height="30"/>
<text class="chip-title" x="600" y="268" text-anchor="middle">File as Memory</text>
<rect class="panel" x="848" y="124" width="280" height="168"/>
<rect class="badge" x="878" y="150" width="94" height="24" rx="12" ry="12"/>
<text class="label" x="925" y="167" text-anchor="middle">Agent</text>
<text class="section-title" x="878" y="202">Parse and operate graph</text>
<text class="section-subtitle" x="878" y="226">frontmatter, chunks, wikilinks</text>
<rect class="chip" x="878" y="248" width="220" height="28"/>
<text class="chip-text" x="988" y="267" text-anchor="middle">search, link, rewrite, index</text>
<rect class="panel" x="848" y="124" width="280" height="168"/>
<rect class="badge" x="878" y="150" width="94" height="24" rx="12" ry="12"/>
<text class="label" x="925" y="167" text-anchor="middle">Agent</text>
<text class="section-title" x="878" y="202">Parse and operate graph</text>
<text class="section-subtitle" x="878" y="226">frontmatter, chunks, wikilinks</text>
<rect class="chip" x="878" y="248" width="220" height="28"/>
<text class="chip-text" x="988" y="267" text-anchor="middle">search, link, rewrite, index</text>
<path class="arrow" d="M352 208 H460"/>
<path class="arrow" d="M740 208 H848"/>
<path class="soft-arrow" d="M848 250 C740 326 460 326 352 250"/>
<text class="note" x="600" y="336" text-anchor="middle">people and agents see the same file tree, so edits and evidence links stay inspectable</text>
<path class="arrow" d="M352 208 H460"/>
<path class="arrow" d="M740 208 H848"/>
<path class="soft-arrow" d="M848 250 C740 326 460 326 352 250"/>
<text class="note" x="600" y="336" text-anchor="middle">people and agents see the same file tree, so edits and evidence links stay inspectable</text>
<rect class="panel" x="54" y="398" width="224" height="144"/>
<circle class="badge" cx="88" cy="432" r="15"/>
<text class="label" x="88" y="436" text-anchor="middle">1</text>
<text class="section-title" x="116" y="438">Raw input</text>
<text class="section-subtitle" x="82" y="468">keep the original scene</text>
<rect class="chip" x="82" y="494" width="168" height="28"/>
<text class="chip-text" x="166" y="513" text-anchor="middle">session/ + resource/</text>
<rect class="panel" x="54" y="398" width="224" height="144"/>
<circle class="badge" cx="88" cy="432" r="15"/>
<text class="label" x="88" y="436" text-anchor="middle">1</text>
<text class="section-title" x="116" y="438">Raw input</text>
<text class="section-subtitle" x="82" y="468">keep the original scene</text>
<rect class="chip" x="82" y="494" width="168" height="28"/>
<text class="chip-text" x="166" y="513" text-anchor="middle">session/ + resource/</text>
<rect class="panel" x="326" y="398" width="224" height="144"/>
<circle class="badge" cx="360" cy="432" r="15"/>
<text class="label" x="360" y="436" text-anchor="middle">2</text>
<text class="section-title" x="388" y="438">Daily</text>
<text class="section-subtitle" x="354" y="468">shallow working memory</text>
<rect class="chip" x="354" y="494" width="168" height="28"/>
<text class="chip-text" x="438" y="513" text-anchor="middle">daily/YYYY-MM-DD/*.md</text>
<rect class="panel" x="326" y="398" width="224" height="144"/>
<circle class="badge" cx="360" cy="432" r="15"/>
<text class="label" x="360" y="436" text-anchor="middle">2</text>
<text class="section-title" x="388" y="438">Daily</text>
<text class="section-subtitle" x="354" y="468">shallow working memory</text>
<rect class="chip" x="354" y="494" width="168" height="28"/>
<text class="chip-text" x="438" y="513" text-anchor="middle">daily/&lt;date&gt;/&lt;name&gt;.md</text>
<rect class="panel" x="598" y="398" width="224" height="144"/>
<circle class="badge" cx="632" cy="432" r="15"/>
<text class="label" x="632" y="436" text-anchor="middle">3</text>
<text class="section-title" x="660" y="438">Digest</text>
<text class="section-subtitle" x="626" y="468">long-term reusable nodes</text>
<rect class="chip" x="626" y="494" width="168" height="28"/>
<text class="chip-text" x="710" y="513" text-anchor="middle">personal / procedure / wiki</text>
<rect class="panel" x="598" y="398" width="224" height="144"/>
<circle class="badge" cx="632" cy="432" r="15"/>
<text class="label" x="632" y="436" text-anchor="middle">3</text>
<text class="section-title" x="660" y="438">Digest</text>
<text class="section-subtitle" x="626" y="468">long-term reusable nodes</text>
<rect class="chip" x="626" y="494" width="168" height="28"/>
<text class="chip-text" x="710" y="513" text-anchor="middle">personal / procedure / wiki</text>
<rect class="panel" x="870" y="398" width="224" height="144"/>
<circle class="badge" cx="904" cy="432" r="15"/>
<text class="label" x="904" y="436" text-anchor="middle">4</text>
<text class="section-title" x="932" y="438">Metadata</text>
<text class="section-subtitle" x="898" y="468">system state and indexes</text>
<rect class="chip" x="898" y="494" width="168" height="28"/>
<text class="chip-text" x="982" y="513" text-anchor="middle">catalog + chunks + links</text>
<rect class="panel" x="870" y="398" width="224" height="144"/>
<circle class="badge" cx="904" cy="432" r="15"/>
<text class="label" x="904" y="436" text-anchor="middle">4</text>
<text class="section-title" x="932" y="438">Metadata</text>
<text class="section-subtitle" x="898" y="468">system state and indexes</text>
<rect class="chip" x="898" y="494" width="168" height="28"/>
<text class="chip-text" x="982" y="513" text-anchor="middle">catalog + chunks + links</text>
<path class="arrow" d="M278 470 H326"/>
<path class="arrow" d="M550 470 H598"/>
<path class="arrow" d="M822 470 H870"/>
<path class="soft-arrow" d="M982 398 C982 356 710 356 710 398"/>
<path class="soft-arrow" d="M982 542 C982 592 438 592 438 542"/>
<path class="arrow" d="M278 470 H326"/>
<path class="arrow" d="M550 470 H598"/>
<path class="arrow" d="M822 470 H870"/>
<path class="soft-arrow" d="M982 398 C982 356 710 356 710 398"/>
<path class="soft-arrow" d="M982 542 C982 592 438 592 438 542"/>
<rect class="panel" x="126" y="594" width="948" height="46"/>
<text class="note" x="176" y="622">Stable paths</text>
<line class="line" x1="308" y1="606" x2="308" y2="628"/>
<text class="chip-text" x="360" y="622">workspace-relative wikilinks</text>
<line class="line" x1="548" y1="606" x2="548" y2="628"/>
<text class="chip-text" x="600" y="622">derived_from evidence edges</text>
<line class="line" x1="814" y1="606" x2="814" y2="628"/>
<text class="chip-text" x="862" y="622">search expands structure and links</text>
<rect class="panel" x="126" y="594" width="948" height="46"/>
<text class="note" x="176" y="622">Stable paths</text>
<line class="line" x1="308" y1="606" x2="308" y2="628"/>
<text class="chip-text" x="360" y="622">workspace-relative wikilinks</text>
<line class="line" x1="548" y1="606" x2="548" y2="628"/>
<text class="chip-text" x="600" y="622">derived_from evidence edges</text>
<line class="line" x1="814" y1="606" x2="814" y2="628"/>
<text class="chip-text" x="862" y="622">search expands linked context</text>
</svg>

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 385 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

View file

@ -0,0 +1,80 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img"
aria-labelledby="title desc">
<title id="title">Auto Resource</title>
<desc id="desc">A general workflow that imports external information, interprets it with traceable sources, and
integrates it into a user-owned personal knowledge base.
</desc>
<defs>
<style>.bg{fill:#fffdf8}.title{font:800 30px Arial,sans-serif;fill:#1f2430}.sub{font:14px Arial,sans-serif;fill:#667085}.panel{fill:#fff;stroke:#1f2430;stroke-width:2}.head{font:700 18px Arial,sans-serif;fill:#1f2430}.text{font:13px Arial,sans-serif;fill:#5e6a7c}.small{font:12px Arial,sans-serif;fill:#667085}.label{font:700 13px Arial,sans-serif;fill:#1f2430}.path{font:700 13px Arial,sans-serif;fill:#e66f11}.chip{fill:#f8fbff;stroke:#1f2430;stroke-width:1.3;stroke-dasharray:6 5}.orange{fill:#fff3e7}.blue{fill:#eef7ff}.green{fill:#eef9f4}.arrow{fill:none;stroke:#8793a3;stroke-width:2.5;marker-end:url(#a)}</style>
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
<path d="M0 0v9l9-4.5z" fill="#8793a3"/>
</marker>
</defs>
<rect class="bg" width="1200" height="640"/>
<text text-anchor="middle"><tspan class="title" x="600" y="50">Auto Resource</tspan><tspan class="sub" x="600" y="77">Bring external information into a traceable, user-owned knowledge base</tspan></text>
<text x="1080" y="51" style="font:800 25px Arial,sans-serif;fill:#19bfc3">Re</text>
<text x="1111" y="51" style="font:800 25px Arial,sans-serif;fill:#3274d9">Me</text>
<rect class="panel" x="46" y="112" width="304" height="458" rx="18"/>
<circle cx="92" cy="159" r="24" fill="#ff963d"/>
<path d="M80 149h21l7 7v19H80z" fill="#fff"/>
<path d="M101 149v8h8" fill="none" stroke="#ff963d" stroke-width="2"/>
<text class="head" x="128" y="155">External Information</text>
<text class="text" x="128" y="179" style="font-size:13px">Originals remain verifiable</text>
<rect class="chip orange" x="76" y="214" width="244" height="68" rx="11"/>
<text class="label" x="198" y="240" text-anchor="middle">Documents · notes · web archives</text>
<text class="small" x="198" y="263" text-anchor="middle">research · reports · structured data</text>
<rect class="chip" x="76" y="302" width="244" height="68" rx="11"/>
<text class="label" x="198" y="328" text-anchor="middle">md · txt · json · jsonl</text>
<text class="small" x="198" y="351" text-anchor="middle">csv · yaml · html</text>
<rect class="chip" x="76" y="390" width="244" height="68" rx="11"/>
<text class="label" x="198" y="416" text-anchor="middle">Create · update · delete</text>
<text class="small" x="198" y="439" text-anchor="middle" style="font-size:12px">Changes stay visible and recoverable</text>
<text class="path" x="198" y="505" text-anchor="middle">resource/</text>
<text class="text" x="198" y="532" text-anchor="middle">Original files remain user-owned</text>
<path class="arrow" d="M350 341h62"/>
<rect class="panel" x="412" y="112" width="376" height="458" rx="18"/>
<circle cx="458" cy="159" r="24" fill="#5b91dc"/>
<path d="M445 149h26v22h-26zM451 143h14v6" fill="#fff" stroke="#fff" stroke-width="3" stroke-linejoin="round"/>
<text class="head" x="494" y="155">Auto Resource Ingestion</text>
<text class="text" x="494" y="179">Parse, interpret, and preserve provenance</text>
<rect class="chip blue" x="442" y="214" width="316" height="68" rx="11"/>
<text class="label" x="468" y="240">1 Parse content and structure</text>
<text class="small" x="468" y="263">Read text, sections, fields, and metadata</text>
<rect class="chip orange" x="442" y="302" width="316" height="68" rx="11"/>
<text class="label" x="468" y="328">2 Interpret reusable information</text>
<text class="small" x="468" y="351" style="font-size:12px">Extract themes, facts, and actionable details</text>
<rect class="chip green" x="442" y="390" width="316" height="68" rx="11"/>
<text class="label" x="468" y="416">3 Attach traceable provenance</text>
<text class="small" x="468" y="439" style="font-size:12px">Link each memory with source_resource</text>
<text class="path" x="600" y="505" text-anchor="middle">daily/</text>
<text class="text" x="600" y="532" text-anchor="middle">Write readable resource memory</text>
<path class="arrow" d="M788 341h62"/>
<rect class="panel" x="850" y="112" width="304" height="458" rx="18"/>
<circle cx="896" cy="159" r="24" fill="#ff963d"/>
<path d="M883 171c7-19 18-26 27-27-1 12-6 25-27 27z" fill="#fff"/>
<path d="M896 151v23" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
<text class="head" x="932" y="155">Knowledge Base</text>
<text class="text" x="932" y="179" style="font-size:13px">One memory system per source</text>
<rect class="chip blue" x="880" y="214" width="244" height="68" rx="11"/>
<text class="label" x="1002" y="240" text-anchor="middle">daily/</text>
<text class="small" x="1002" y="263" text-anchor="middle" style="font-size:12px">Readable, traceable resource memory</text>
<rect class="chip green" x="880" y="302" width="244" height="68" rx="11"/>
<text class="label" x="1002" y="328" text-anchor="middle">digest/</text>
<text class="small" x="1002" y="351" text-anchor="middle" style="font-size:12px">Long-term memory via Auto Dream</text>
<rect class="chip" x="880" y="390" width="244" height="68" rx="11"/>
<text class="label" x="1002" y="416" text-anchor="middle">metadata/</text>
<text class="small" x="1002" y="439" text-anchor="middle" style="font-size:12px">Rebuildable index and link graph</text>
<text class="path" x="1002" y="505" text-anchor="middle">Search · link · reuse</text>
<text class="text" x="1002" y="532" text-anchor="middle">External knowledge returns to future tasks</text>
<text class="small" x="600" y="610" text-anchor="middle">User files stay authoritative · derived indexes remain rebuildable</text>
</svg>

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -0,0 +1,44 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="1680" height="973"
viewBox="0 0 1680 973" role="img" aria-labelledby="title desc">
<title id="title">ReMe Hero and Public Benchmark Results</title>
<desc id="desc">ReMe self-evolving personal knowledge base hero with LongMemEval 89.4%, BEAM 100K 66.1%, and BEAM 1M
65.0% results.
</desc>
<defs>
<linearGradient id="glass" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#171b25" stop-opacity="0.96"/>
<stop offset="0.58" stop-color="#202530" stop-opacity="0.94"/>
<stop offset="1" stop-color="#252b36" stop-opacity="0.92"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-30%" width="140%" height="180%">
<feDropShadow dx="0" dy="-8" stdDeviation="16" flood-color="#000" flood-opacity="0.16"/>
</filter>
<style>.sans{font-family:Arial,sans-serif} .eyebrow{font-size:17px;font-weight:750;letter-spacing:2px;fill:#ffad61} .metric{font-size:49px;font-weight:900;letter-spacing:-1.5px;fill:#fff} .label{font-size:17px;font-weight:720;fill:#fff} .small{font-size:14px;font-weight:520;fill:#d0d5dd}</style>
</defs>
<image xlink:href="reme-blog-cover.png" x="0" y="0" width="1680" height="973" preserveAspectRatio="xMidYMid slice"/>
<rect x="0" y="711" width="1680" height="262" fill="url(#glass)" filter="url(#shadow)"/>
<g class="sans">
<text class="eyebrow" x="84" y="765">PUBLIC BENCHMARK RESULTS</text>
<text class="small" x="84" y="798">Agentic scores across multi-session and long-context settings</text>
<line x1="84" y1="829" x2="1596" y2="829" stroke="#fff" stroke-opacity="0.17"/>
<text class="metric" x="84" y="898">89.4%</text>
<text class="label" x="277" y="875">LongMemEval cleaned-s</text>
<text class="small" x="277" y="903">500 questions · cross-session retrieval</text>
<line x1="590" y1="852" x2="590" y2="923" stroke="#fff" stroke-opacity="0.18"/>
<text class="metric" x="650" y="898">66.1%</text>
<text class="label" x="833" y="875">BEAM 100K</text>
<text class="small" x="833" y="903">20 cases · 400 questions</text>
<line x1="1080" y1="852" x2="1080" y2="923" stroke="#fff" stroke-opacity="0.18"/>
<text class="metric" x="1140" y="898">65.0%</text>
<text class="label" x="1323" y="875">BEAM 1M</text>
<text class="small" x="1323" y="903">35 cases · 700 questions</text>
<text x="1596" y="950" text-anchor="end" style="font-size:12px;font-weight:520;fill:#98a2b3">Scores from different benchmarks are not compared directly</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 382 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,204 +1,184 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img" aria-labelledby="title desc">
<title id="title">ReMe overview</title>
<desc id="desc">A hand-drawn style overview of ReMe, showing Auto Memory plus Auto Resource, Auto Dream plus Proactive, Auto Index plus Memory Search, and Memory as File.</desc>
<defs>
<style>
.bg { fill: #fffdf8; }
.ink { stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.thin { stroke-width: 1.6; }
.dash { stroke-dasharray: 8 7; }
.title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; }
.head { font: 700 17px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.label { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; }
.text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; }
.tiny { font: 11px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; }
.box { fill: #ffffff; }
.blue { fill: #eef7ff; }
.green { fill: #f0fbf5; }
.yellow { fill: #fff7e5; }
.pink { fill: #fff2f6; }
.violet { fill: #f4f1ff; }
.mint { fill: #eefafa; }
.peach { fill: #fff2ea; }
.paper { fill: #f8fbff; }
.tab { fill: #ffffff; }
.step { stroke-dasharray: 6 5; }
.arrow { fill: none; stroke: #7f8b9d; stroke-width: 1.45; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); }
.soft-arrow { fill: none; stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); }
</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img"
aria-labelledby="title desc">
<title id="title">ReMe overview</title>
<desc id="desc">A hand-drawn style overview of ReMe, showing Auto Memory plus Auto Resource, Auto Dream plus
Proactive, Auto Index plus Memory Search, and Memory as File.
</desc>
<defs>
<style>.bg { fill: #fffdf8; } .ink { stroke: #1f2430; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; } .thin { stroke-width: 1.6; } .dash { stroke-dasharray: 8 7; } .title { font: 700 30px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .subtitle { font: 14px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #556276; } .head { font: 700 17px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .label { font: 700 13px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1f2430; } .text { font: 12px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #4f5c6f; } .tiny { font: 11px "Comic Sans MS", "Bradley Hand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #5e6a7c; } .box { fill: #ffffff; } .blue { fill: #eef7ff; } .green { fill: #f0fbf5; } .yellow { fill: #fff7e5; } .pink { fill: #fff2f6; } .violet { fill: #f4f1ff; } .mint { fill: #eefafa; } .peach { fill: #fff2ea; } .paper { fill: #f8fbff; } .tab { fill: #ffffff; } .step { stroke-dasharray: 6 5; } .arrow { fill: none; stroke: #7f8b9d; stroke-width: 1.45; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow); } .soft-arrow { fill: none; stroke: #a3adbd; stroke-width: 1.25; stroke-dasharray: 6 6; stroke-linecap: round; stroke-linejoin: round; marker-end: url(#arrow-soft); }</style>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#7f8b9d"/>
</marker>
<marker id="arrow-soft" markerWidth="6" markerHeight="6" refX="5" refY="2" orient="auto"
markerUnits="strokeWidth">
<path d="M0,0 L0,4 L5,2 z" fill="#a3adbd"/>
</marker>
</defs>
<rect class="bg" x="0" y="0" width="1200" height="640"/>
<text class="title" x="600" y="44" text-anchor="middle">ReMe</text>
<text class="subtitle" x="600" y="68" text-anchor="middle">A file-native memory loop: capture, consolidate, link, search, and proactively surface what matters.</text>
<rect class="bg" x="0" y="0" width="1200" height="640"/>
<text class="title" x="600" y="44" text-anchor="middle">ReMe</text>
<text class="subtitle" x="600" y="68" text-anchor="middle">A file-native memory loop: capture, consolidate, link, search, and proactively surface what matters.</text>
<!-- Top workflow -->
<g transform="translate(40 120)">
<rect class="blue ink thin" x="0" y="0" width="300" height="260" rx="18"/>
<text class="head" x="150" y="31" text-anchor="middle">Auto Memory + Auto Resource</text>
<text class="text" x="150" y="56" text-anchor="middle">Capture sessions and resources into daily cards.</text>
<!-- Top workflow -->
<g transform="translate(40 120)">
<rect class="blue ink thin" x="0" y="0" width="300" height="260" rx="18"/>
<text class="head" x="150" y="31" text-anchor="middle">Auto Memory + Auto Resource</text>
<text class="text" x="150" y="56" text-anchor="middle" style="font-size:12px">Capture sessions and resources into daily cards.</text>
<g transform="translate(24 78)">
<rect class="blue ink thin dash" x="0" y="0" width="252" height="64" rx="13"/>
<text class="label" x="54" y="27" text-anchor="middle">Auto</text>
<text class="label" x="54" y="45" text-anchor="middle">Memory</text>
<g transform="translate(24 78)">
<rect class="blue ink thin dash" x="0" y="0" width="252" height="64" rx="13"/>
<text class="label" x="54" y="27" text-anchor="middle">Auto</text>
<text class="label" x="54" y="45" text-anchor="middle">Memory</text>
<rect class="box ink thin step" x="100" y="12" width="62" height="40" rx="9"/>
<text class="label" x="131" y="29" text-anchor="middle">Capture</text>
<text class="tiny" x="131" y="44" text-anchor="middle">chat</text>
<rect class="box ink thin step" x="100" y="12" width="62" height="40" rx="9"/>
<text class="label" x="131" y="29" text-anchor="middle">Capture</text>
<text class="tiny" x="131" y="44" text-anchor="middle">chat</text>
<rect class="yellow ink thin step" x="178" y="12" width="56" height="40" rx="9"/>
<text class="label" x="206" y="29" text-anchor="middle">Write</text>
<text class="tiny" x="206" y="44" text-anchor="middle">daily</text>
<rect class="yellow ink thin step" x="178" y="12" width="56" height="40" rx="9"/>
<text class="label" x="206" y="29" text-anchor="middle">Write</text>
<text class="tiny" x="206" y="44" text-anchor="middle">daily</text>
<path class="arrow" d="M164 32 H176"/>
<path class="arrow" d="M164 32 H176"/>
</g>
<g transform="translate(24 162)">
<rect class="green ink thin dash" x="0" y="0" width="252" height="64" rx="13"/>
<text class="label" x="54" y="27" text-anchor="middle">Auto</text>
<text class="label" x="54" y="45" text-anchor="middle">Resource</text>
<rect class="box ink thin step" x="100" y="12" width="62" height="40" rx="9"/>
<text class="label" x="131" y="29" text-anchor="middle">Read</text>
<text class="tiny" x="131" y="44" text-anchor="middle">source</text>
<rect class="yellow ink thin step" x="178" y="12" width="56" height="40" rx="9"/>
<text class="label" x="206" y="29" text-anchor="middle">Write</text>
<text class="tiny" x="206" y="44" text-anchor="middle">daily</text>
<path class="arrow" d="M164 32 H176"/>
</g>
</g>
<g transform="translate(24 162)">
<rect class="green ink thin dash" x="0" y="0" width="252" height="64" rx="13"/>
<text class="label" x="54" y="27" text-anchor="middle">Auto</text>
<text class="label" x="54" y="45" text-anchor="middle">Resource</text>
<g transform="translate(380 120)">
<rect class="yellow ink thin" x="0" y="0" width="320" height="260" rx="18"/>
<text class="head" x="160" y="32" text-anchor="middle">Auto Dream + Proactive</text>
<text class="text" x="160" y="58" text-anchor="middle" style="font-size:12px">Consolidate notes, link knowledge, surface interests.</text>
<rect class="box ink thin step" x="100" y="12" width="62" height="40" rx="9"/>
<text class="label" x="131" y="29" text-anchor="middle">Read</text>
<text class="tiny" x="131" y="44" text-anchor="middle">source</text>
<rect class="box ink thin step" x="38" y="84" width="112" height="56" rx="12"/>
<text class="label" x="94" y="108" text-anchor="middle">Extract</text>
<text class="tiny" x="94" y="126" text-anchor="middle">changed files</text>
<rect class="yellow ink thin step" x="178" y="12" width="56" height="40" rx="9"/>
<text class="label" x="206" y="29" text-anchor="middle">Write</text>
<text class="tiny" x="206" y="44" text-anchor="middle">daily</text>
<rect class="peach ink thin step" x="170" y="84" width="112" height="56" rx="12"/>
<text class="label" x="226" y="108" text-anchor="middle">Auto Link</text>
<text class="tiny" x="226" y="126" text-anchor="middle">dedupe + edges</text>
<path class="arrow" d="M164 32 H176"/>
</g>
</g>
<rect class="pink ink thin step" x="38" y="168" width="112" height="56" rx="12"/>
<text class="label" x="94" y="192" text-anchor="middle">Integrate</text>
<text class="tiny" x="94" y="210" text-anchor="middle">write digest</text>
<g transform="translate(380 120)">
<rect class="yellow ink thin" x="0" y="0" width="320" height="260" rx="18"/>
<text class="head" x="160" y="32" text-anchor="middle">Auto Dream + Proactive</text>
<text class="text" x="160" y="58" text-anchor="middle">Consolidate notes, link knowledge, surface interests.</text>
<rect class="mint ink thin step" x="170" y="168" width="112" height="56" rx="12"/>
<text class="label" x="226" y="192" text-anchor="middle">Proactive</text>
<text class="tiny" x="226" y="210" text-anchor="middle">interests.yaml</text>
<rect class="box ink thin step" x="38" y="84" width="112" height="56" rx="12"/>
<text class="label" x="94" y="108" text-anchor="middle">Extract</text>
<text class="tiny" x="94" y="126" text-anchor="middle">changed files</text>
<rect class="peach ink thin step" x="170" y="84" width="112" height="56" rx="12"/>
<text class="label" x="226" y="108" text-anchor="middle">Auto Link</text>
<text class="tiny" x="226" y="126" text-anchor="middle">dedupe + edges</text>
<rect class="pink ink thin step" x="38" y="168" width="112" height="56" rx="12"/>
<text class="label" x="94" y="192" text-anchor="middle">Integrate</text>
<text class="tiny" x="94" y="210" text-anchor="middle">write digest</text>
<rect class="mint ink thin step" x="170" y="168" width="112" height="56" rx="12"/>
<text class="label" x="226" y="192" text-anchor="middle">Proactive</text>
<text class="tiny" x="226" y="210" text-anchor="middle">interests.yaml</text>
<path class="arrow" d="M152 112 H168"/>
<path class="arrow" d="M226 142 V166"/>
<path class="arrow" d="M168 196 H152"/>
<path class="arrow" d="M94 166 V142"/>
</g>
<g transform="translate(730 120)">
<rect class="violet ink thin" x="0" y="0" width="430" height="260" rx="18"/>
<text class="head" x="215" y="31" text-anchor="middle">Auto Index + Memory Search</text>
<text class="text" x="215" y="56" text-anchor="middle">Keep indexes fresh, recall chunks, then follow links.</text>
<g transform="translate(24 86)">
<rect class="blue ink thin dash" x="0" y="0" width="178" height="136" rx="14"/>
<text class="label" x="89" y="24" text-anchor="middle">Auto Index</text>
<rect class="box ink thin step" x="18" y="46" width="64" height="58" rx="10"/>
<text class="label" x="50" y="70" text-anchor="middle">Watch</text>
<text class="tiny" x="50" y="88" text-anchor="middle">changes</text>
<rect class="yellow ink thin step" x="98" y="46" width="64" height="58" rx="10"/>
<text class="label" x="130" y="70" text-anchor="middle">Build</text>
<text class="tiny" x="130" y="88" text-anchor="middle">index</text>
<path class="arrow" d="M84 75 H96"/>
<path class="arrow" d="M152 112 H168"/>
<path class="arrow" d="M226 142 V166"/>
<path class="arrow" d="M168 196 H152"/>
<path class="arrow" d="M94 166 V142"/>
</g>
<g transform="translate(228 86)">
<rect class="green ink thin dash" x="0" y="0" width="178" height="136" rx="14"/>
<text class="label" x="89" y="24" text-anchor="middle">Memory Search</text>
<g transform="translate(730 120)">
<rect class="violet ink thin" x="0" y="0" width="430" height="260" rx="18"/>
<text class="head" x="215" y="31" text-anchor="middle">Auto Index + Memory Search</text>
<text class="text" x="215" y="56" text-anchor="middle" style="font-size:12px">Keep indexes fresh, recall chunks, then follow links.</text>
<rect class="box ink thin step" x="18" y="46" width="64" height="58" rx="10"/>
<text class="label" x="50" y="70" text-anchor="middle">Recall</text>
<text class="tiny" x="50" y="88" text-anchor="middle">BM25 + vec</text>
<g transform="translate(24 86)">
<rect class="blue ink thin dash" x="0" y="0" width="178" height="136" rx="14"/>
<text class="label" x="89" y="24" text-anchor="middle">Auto Index</text>
<rect class="mint ink thin step" x="98" y="46" width="64" height="58" rx="10"/>
<text class="label" x="130" y="70" text-anchor="middle">Expand</text>
<text class="tiny" x="130" y="88" text-anchor="middle">links</text>
<rect class="box ink thin step" x="18" y="46" width="64" height="58" rx="10"/>
<text class="label" x="50" y="70" text-anchor="middle">Watch</text>
<text class="tiny" x="50" y="88" text-anchor="middle">changes</text>
<path class="arrow" d="M84 75 H96"/>
<rect class="yellow ink thin step" x="98" y="46" width="64" height="58" rx="10"/>
<text class="label" x="130" y="70" text-anchor="middle">Build</text>
<text class="tiny" x="130" y="88" text-anchor="middle">index</text>
<path class="arrow" d="M84 75 H96"/>
</g>
<g transform="translate(228 86)">
<rect class="green ink thin dash" x="0" y="0" width="178" height="136" rx="14"/>
<text class="label" x="89" y="24" text-anchor="middle">Memory Search</text>
<rect class="box ink thin step" x="18" y="46" width="64" height="58" rx="10"/>
<text class="label" x="50" y="70" text-anchor="middle">Recall</text>
<text class="tiny" x="50" y="88" text-anchor="middle" style="font-size:10px">BM25 + vec</text>
<rect class="mint ink thin step" x="98" y="46" width="64" height="58" rx="10"/>
<text class="label" x="130" y="70" text-anchor="middle">Expand</text>
<text class="tiny" x="130" y="88" text-anchor="middle">links</text>
<path class="arrow" d="M84 75 H96"/>
</g>
<path class="arrow" d="M204 161 H226"/>
</g>
<path class="arrow" d="M204 161 H226"/>
</g>
<path class="arrow" d="M340 272 H380"/>
<path class="arrow" d="M340 272 H380"/>
<!-- Bottom foundation -->
<rect class="box ink" x="40" y="408" width="1120" height="202" rx="24"/>
<text class="head" x="600" y="442" text-anchor="middle">Memory as File</text>
<text class="text" x="600" y="466" text-anchor="middle">Every memory is readable, editable, indexable, linkable, and auditable as files.</text>
<!-- Bottom foundation -->
<rect class="box ink" x="40" y="408" width="1120" height="202" rx="24"/>
<text class="head" x="600" y="442" text-anchor="middle">Memory as File</text>
<text class="text" x="600" y="466" text-anchor="middle">Every memory is readable, editable, indexable, linkable, and auditable as files.</text>
<g transform="translate(94 500)">
<rect class="paper ink thin" x="10" y="6" width="62" height="52"/>
<rect class="paper ink thin" x="5" y="3" width="62" height="52"/>
<rect class="paper ink thin" x="0" y="0" width="62" height="52"/>
<rect class="tab ink thin" x="-12" y="14" width="44" height="22"/>
<text class="label" x="10" y="30" text-anchor="middle">.jsonl</text>
<line class="ink thin" x1="26" y1="28" x2="50" y2="28"/>
<line class="ink thin" x1="22" y1="42" x2="52" y2="42"/>
<text class="label" x="106" y="19">session/</text>
<text class="tiny" x="106" y="39">source conversations</text>
</g>
<g transform="translate(94 500)">
<rect class="paper ink thin" x="10" y="6" width="62" height="52"/>
<rect class="paper ink thin" x="5" y="3" width="62" height="52"/>
<rect class="paper ink thin" x="0" y="0" width="62" height="52"/>
<rect class="tab ink thin" x="-12" y="14" width="44" height="22"/>
<text class="label" x="10" y="30" text-anchor="middle">.json</text>
<line class="ink thin" x1="26" y1="28" x2="50" y2="28"/>
<line class="ink thin" x1="22" y1="42" x2="52" y2="42"/>
<text class="label" x="106" y="19">session/</text>
<text class="tiny" x="106" y="39">raw session logs</text>
</g>
<g transform="translate(344 500)">
<rect class="paper ink thin" x="10" y="6" width="62" height="52"/>
<rect class="paper ink thin" x="5" y="3" width="62" height="52"/>
<rect class="paper ink thin" x="0" y="0" width="62" height="52"/>
<rect class="tab ink thin" x="-8" y="14" width="34" height="22"/>
<text class="label" x="9" y="30" text-anchor="middle">.src</text>
<line class="ink thin" x1="26" y1="28" x2="50" y2="28"/>
<line class="ink thin" x1="22" y1="42" x2="52" y2="42"/>
<text class="label" x="106" y="19">resource/</text>
<text class="tiny" x="106" y="39">original source material</text>
</g>
<g transform="translate(344 500)">
<rect class="paper ink thin" x="10" y="6" width="62" height="52"/>
<rect class="paper ink thin" x="5" y="3" width="62" height="52"/>
<rect class="paper ink thin" x="0" y="0" width="62" height="52"/>
<rect class="tab ink thin" x="-8" y="14" width="34" height="22"/>
<text class="label" x="9" y="30" text-anchor="middle">.md</text>
<line class="ink thin" x1="26" y1="28" x2="50" y2="28"/>
<line class="ink thin" x1="22" y1="42" x2="52" y2="42"/>
<text class="label" x="106" y="19">resource/</text>
<text class="tiny" x="106" y="39">raw material with source</text>
</g>
<g transform="translate(604 500)">
<rect class="paper ink thin" x="10" y="6" width="62" height="52"/>
<rect class="paper ink thin" x="5" y="3" width="62" height="52"/>
<rect class="paper ink thin" x="0" y="0" width="62" height="52"/>
<rect class="tab ink thin" x="-8" y="14" width="34" height="22"/>
<text class="label" x="9" y="30" text-anchor="middle">.md</text>
<line class="ink thin" x1="26" y1="28" x2="50" y2="28"/>
<line class="ink thin" x1="22" y1="42" x2="52" y2="42"/>
<text class="label" x="106" y="19">daily/</text>
<text class="tiny" x="106" y="39">working memory cards</text>
</g>
<g transform="translate(604 500)">
<rect class="paper ink thin" x="10" y="6" width="62" height="52"/>
<rect class="paper ink thin" x="5" y="3" width="62" height="52"/>
<rect class="paper ink thin" x="0" y="0" width="62" height="52"/>
<rect class="tab ink thin" x="-8" y="14" width="34" height="22"/>
<text class="label" x="9" y="30" text-anchor="middle">.md</text>
<line class="ink thin" x1="26" y1="28" x2="50" y2="28"/>
<line class="ink thin" x1="22" y1="42" x2="52" y2="42"/>
<text class="label" x="106" y="19">daily/</text>
<text class="tiny" x="106" y="39">working memory cards</text>
</g>
<g transform="translate(850 500)">
<rect class="paper ink thin" x="10" y="6" width="62" height="52"/>
<rect class="paper ink thin" x="5" y="3" width="62" height="52"/>
<rect class="paper ink thin" x="0" y="0" width="62" height="52"/>
<rect class="tab ink thin" x="-8" y="14" width="34" height="22"/>
<text class="label" x="9" y="30" text-anchor="middle">.md</text>
<line class="ink thin" x1="26" y1="28" x2="50" y2="28"/>
<line class="ink thin" x1="22" y1="42" x2="52" y2="42"/>
<text class="label" x="106" y="19">digest/</text>
<text class="tiny" x="106" y="39">long-term knowledge nodes</text>
</g>
<g transform="translate(850 500)">
<rect class="paper ink thin" x="10" y="6" width="62" height="52"/>
<rect class="paper ink thin" x="5" y="3" width="62" height="52"/>
<rect class="paper ink thin" x="0" y="0" width="62" height="52"/>
<rect class="tab ink thin" x="-8" y="14" width="34" height="22"/>
<text class="label" x="9" y="30" text-anchor="middle">.md</text>
<line class="ink thin" x1="26" y1="28" x2="50" y2="28"/>
<line class="ink thin" x1="22" y1="42" x2="52" y2="42"/>
<text class="label" x="106" y="19">digest/</text>
<text class="tiny" x="106" y="39">long-term knowledge nodes</text>
</g>
<path class="soft-arrow" d="M190 380 V408"/>
<path class="soft-arrow" d="M540 380 V408"/>
<path class="soft-arrow" d="M945 380 V408"/>
<path class="soft-arrow" d="M190 380 V408"/>
<path class="soft-arrow" d="M540 380 V408"/>
<path class="soft-arrow" d="M945 380 V408"/>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show more