Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .agents/skills/grag/references/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

Use the graph's existing vocabulary. Common choices are Task (status and acceptance),
Decision (choice, reason and rejected alternative), Insight (established finding),
Question (unresolved issue), and Concept/Integration. They are conventions, not built-in
Question (unresolved issue), and Concept/Integration. They are conventions, not required
schema. Check unfamiliar schema before writes; define only missing types and
endpoint pairs. Topic search needs no schema preflight.
endpoint pairs. With no memory labels yet, `define_schema` with only `preset="memory"`
adds Decision/Insight/Task/Question (title, body, status, scope) without changing
existing tables. Topic search needs no schema preflight.

## Preserve the result of an investigation

Expand Down
6 changes: 4 additions & 2 deletions .claude/skills/grag/references/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

Use the graph's existing vocabulary. Common choices are Task (status and acceptance),
Decision (choice, reason and rejected alternative), Insight (established finding),
Question (unresolved issue), and Concept/Integration. They are conventions, not built-in
Question (unresolved issue), and Concept/Integration. They are conventions, not required
schema. Check unfamiliar schema before writes; define only missing types and
endpoint pairs. Topic search needs no schema preflight.
endpoint pairs. With no memory labels yet, `define_schema` with only `preset="memory"`
adds Decision/Insight/Task/Question (title, body, status, scope) without changing
existing tables. Topic search needs no schema preflight.

## Preserve the result of an investigation

Expand Down
6 changes: 4 additions & 2 deletions .cursor/skills/grag/references/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

Use the graph's existing vocabulary. Common choices are Task (status and acceptance),
Decision (choice, reason and rejected alternative), Insight (established finding),
Question (unresolved issue), and Concept/Integration. They are conventions, not built-in
Question (unresolved issue), and Concept/Integration. They are conventions, not required
schema. Check unfamiliar schema before writes; define only missing types and
endpoint pairs. Topic search needs no schema preflight.
endpoint pairs. With no memory labels yet, `define_schema` with only `preset="memory"`
adds Decision/Insight/Task/Question (title, body, status, scope) without changing
existing tables. Topic search needs no schema preflight.

## Preserve the result of an investigation

Expand Down
3 changes: 2 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ The core workflow is **ingest, retrieve and remember**:

The agent harness owns planning, model execution and tool selection. Authored
memory uses the project's vocabulary; grag does not require a task engine,
ontology or schema preset. Embeddings and the browser views are optional ways to
ontology or schema preset. An optional, versioned memory preset gives a new graph
a shared starting point. Embeddings and the browser views are optional ways to
use the same local graph.

Single-owner access, atomic writes, edit guards, backup and recovery support the
Expand Down
3 changes: 2 additions & 1 deletion docs/guides/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ decision, correction or handoff. You can also explicitly ask it to remember.

Reuse the project's labels and records; check unfamiliar schema before writes.
Topic search needs no schema preflight. `Task`, `Decision`, `Insight` and `Question`
are conventions, not a required schema.
are conventions, not a required schema; the optional [memory
preset](../reference/schema.md#optional-memory-preset) creates them for you.

| Intent | Existing MCP tools |
|---|---|
Expand Down
6 changes: 4 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ command syntax supported by your installed version.
|---|---|
| Connect | `init`, `relocate` |
| Index | `ingest-code`, `ingest` |
| Save and retrieve | `remember`, `inspect`, `retire`, `search`, `context` |
| Save and retrieve | `remember`, `inspect`, `retire`, `search`, `context`, `memory adopt` |
| Run a server | `serve`, `mcp`, `start`, `restart`, `stop` |
| Diagnose | `status`, `doctor` |
| Preserve data | `export`, `import`, `recover` |
Expand Down Expand Up @@ -40,7 +40,8 @@ These commands use the same database resolution and owner routing as ingestion:
| `search/context --tokens N` | `2000` | Estimated retrieval budget; this CLI default overrides `GRAG_TOKEN_BUDGET` for these commands. |
| `search/context --hops N` | `1` | Graph-expansion depth. |
| `search/context/inspect --freshness MODE` | `allow_stale` | `allow_stale`, `wait` or `require`; uses the request's default freshness timeout. |
| `remember/search/context/inspect/retire --json` | off | Emits compact machine-readable JSON instead of the human summary (`inspect` otherwise uses indented JSON). |
| `memory adopt` | — | Since 0.13.0: adopts the optional, versioned [memory preset](schema.md#optional-memory-preset); additive and repeatable. Conflicts are reported and left unchanged. `--allow-similar` creates preset tables beside near-duplicates. |
| `remember/search/context/inspect/retire/memory adopt --json` | off | Emits compact machine-readable JSON instead of the human summary (`inspect` otherwise uses indented JSON). |

See the [everyday memory walkthrough](../guides/memory.md#cli-shortcuts) for guarded
corrections, retry semantics and the difference between retraction and deletion.
Expand All @@ -61,6 +62,7 @@ corrections, retry semantics and the difference between retraction and deletion.
| `init --port PORT` | saved, otherwise derived per-project | Port written into generated MCP/shared-server configuration. New defaults derive from the database path (41000–49151); choose an explicit port if occupied. |
| `init --ingest` | off | Also runs `ingest-code` on the resolved project root immediately. |
| `init --ingest-if-empty` | off | Since 0.10.0: checks the selected local graph and sets up/indexes the checkout only when absent, empty or containing only the verified init marker. Other content skips setup and indexing. Mutually exclusive with `--ingest`; refuses remote servers and `--remove`. |
| `init --memory-preset` | off | Since 0.13.0: also runs `memory adopt` on the selected local database after setup. Refuses remote servers and `--remove`. |
| `init --global-skill` | off | Since 0.10.0: installs only the user-level skill bundle so it can be invoked in new repos. Supports `--client`, `--dry-run` and `--remove`; does not open a database or configure MCP. Claude honors `CLAUDE_CONFIG_DIR` for its personal skill location. |
| `init --remove` | off | Undoes init: removes the grag MCP entry and the CLAUDE.md block. |
| `init --url` | off | Writes direct HTTP URL transport instead of stdio plus auto-serve; the shared server must already be running. |
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Any MCP client gets these 10 tools:
| tool | purpose |
|---|---|
| `describe_schema` | compact schema: tables, property types, primary keys and directed endpoints. Optional full detail and revision-based reuse. Call before writing Cypher. |
| `define_schema` | create node/rel tables (LLM designs the graph for a domain); grag 0.10.0 validates reserved names and publishes the batch atomically |
| `define_schema` | create node/rel tables (LLM designs the graph for a domain); grag 0.10.0 validates reserved names and publishes the batch atomically; since 0.13.0, `preset="memory"` alone adopts the optional [memory preset](schema.md#optional-memory-preset) |
| `upsert_nodes` / `upsert_edges` | atomic MERGE batches; optional retry IDs and revision checks; `_source` provenance automatic |
| `cypher_query` | read-only Cypher; errors come back with correction hints |
| `search_knowledge` | hybrid BM25 + vector seeds → RRF fusion → per-label diversity cap → k-hop expansion → cited, token-budgeted context |
Expand Down
25 changes: 25 additions & 0 deletions docs/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ Use the project's existing labels and properties. Task, Decision, Insight and
Question are useful conventions, not a required schema.
{ .grag-lead }

## Optional memory preset

**New in 0.13.0:** `grag memory adopt`, `grag init --memory-preset`, or
`define_schema` with only `preset="memory"` adopts version 1 of the memory
preset: `Decision`, `Insight`, `Task` and `Question` node tables, each keyed by a
STRING `id` with STRING `title`, `body`, `status` and `scope` properties.

Adoption is additive and can be repeated:

- Missing tables are created and registered as searchable.
- Existing tables gain missing properties. The table's text search index is
rebuilt to include new text properties.
- Nothing is dropped, renamed or retyped. A different primary key, a property
with another type, a relationship table with a preset name, or a near-duplicate
table (`Decisions`) is reported as a conflict and left unchanged. Pass
`allow_similar=true` (`--allow-similar`) to create a preset table beside a
near-duplicate.
- The adopted version is recorded in grag's internal metadata. A later grag
version adds its changes on the next adoption and refuses to run against a
database adopted by a newer version.

The response lists created tables, added properties and conflicts. Export does not
carry the recorded version; re-adopting after an import is safe. A server that
predates presets is refused before any request is sent.

## Reuse before defining

`describe_schema` shows the current vocabulary. `define_schema` rejects a new
Expand Down
2 changes: 1 addition & 1 deletion src/grag/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ def health() -> dict:
"status": ("shutting_down" if registry.closing or (shutdown and shutdown["state"] != "open")
else "reopen_required" if runtime and runtime["writer_state"] != "ready" else "ok"),
"version": grag.__version__,
"capabilities": {"ingestion_scope": 2, "snapshot_format": 2, "passive_diagnostics": 1},
"capabilities": {"ingestion_scope": 2, "snapshot_format": 2, "passive_diagnostics": 1, "memory_preset": 1},
"database_id": identity,
"server_id": database_identity(server_target),
"pid": os.getpid(),
Expand Down
6 changes: 4 additions & 2 deletions src/grag/assets/skill/references/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

Use the graph's existing vocabulary. Common choices are Task (status and acceptance),
Decision (choice, reason and rejected alternative), Insight (established finding),
Question (unresolved issue), and Concept/Integration. They are conventions, not built-in
Question (unresolved issue), and Concept/Integration. They are conventions, not required
schema. Check unfamiliar schema before writes; define only missing types and
endpoint pairs. Topic search needs no schema preflight.
endpoint pairs. With no memory labels yet, `define_schema` with only `preset="memory"`
adds Decision/Insight/Task/Question (title, body, status, scope) without changing
existing tables. Topic search needs no schema preflight.

## Preserve the result of an investigation

Expand Down
31 changes: 29 additions & 2 deletions src/grag/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ def main(argv: list[str] | None = None) -> int:
retire.add_argument("--reason", help="reason for retiring this memory")
retire.add_argument("--operation-id", help="durable retry ID for this exact retirement")
retire.add_argument("--json", action="store_true")
memory = sub.add_parser("memory", help="manage the optional, versioned memory schema preset")
memory_sub = memory.add_subparsers(dest="memory_cmd", required=True)
adopt = memory_sub.add_parser(
"adopt",
help="add the memory preset (Decision/Insight/Task/Question with title, body, status, scope); "
"additive and repeatable: existing tables are never dropped or retyped, conflicts are reported",
)
adopt.add_argument("--allow-similar", action="store_true", help="create preset tables even when a near-duplicate table exists")
adopt.add_argument("--json", action="store_true")
search = sub.add_parser("search", help="retrieve knowledge from the selected graph")
search.add_argument("query")
search.add_argument("--label", action="append", dest="labels")
Expand Down Expand Up @@ -429,6 +438,10 @@ def main(argv: list[str] | None = None) -> int:
"--ingest-if-empty", action="store_true",
help="index the checkout only when its graph is empty or contains only the init verification record",
)
init.add_argument(
"--memory-preset", action="store_true",
help="also adopt the optional memory schema preset (same as 'grag memory adopt')",
)
init.add_argument(
"--global-skill", action="store_true",
help="install only the user-level grag skill so it is available in new repos; no database or MCP registration",
Expand Down Expand Up @@ -645,7 +658,7 @@ def main(argv: list[str] | None = None) -> int:
except GragError as exc:
print(str(exc), file=sys.stderr)
return 1
elif args.cmd in ("ingest", "ingest-code", "remember", "inspect", "retire", "search", "context"):
elif args.cmd in ("ingest", "ingest-code", "remember", "inspect", "retire", "memory", "search", "context"):
from pydantic import ValidationError

from grag.cli_graph import graph_command
Expand Down Expand Up @@ -838,6 +851,8 @@ def _init_command(args: argparse.Namespace, cfg: GragConfig) -> int:
project_root = find_root()
if args.ingest_if_empty and (args.server_url or cfg.server_url or args.remove):
raise ProjectConfigError("--ingest-if-empty needs a local checkout database and cannot be combined with --remove or a remote server.")
if args.memory_preset and (args.server_url or cfg.server_url or args.remove):
raise ProjectConfigError("--memory-preset needs a local checkout database and cannot be combined with --remove or a remote server; use 'grag --server-url <url> memory adopt' for a remote graph.")
if args.client == "codex" and not args.no_mcp and not args.remove:
raise ProjectConfigError("Codex MCP registration is not managed by init. Use --client codex --no-mcp for local setup/CLI access, or --global-skill for the skill alone.")
clients = (
Expand Down Expand Up @@ -944,6 +959,8 @@ def _init_command(args: argparse.Namespace, cfg: GragConfig) -> int:

if args.dry_run:
preview_ops(ops)
if args.memory_preset:
print(f"Would adopt the memory preset in {db_path}")
return 0

if args.ingest_if_empty:
Expand All @@ -968,6 +985,16 @@ def _init_command(args: argparse.Namespace, cfg: GragConfig) -> int:
elif mcp_ops:
print("Client connection unverified (--no-verify). Rerun init to verify it.")

if args.memory_preset:
from grag.cli_graph import preset_lines
from grag.client import GraphClient
from grag.core.types import DefineSchemaRequest

cfg.db_path = db_path
with GraphClient(cfg) as client:
result = client.call("define_schema", DefineSchemaRequest(preset="memory"))
print("\n" + "\n".join(preset_lines(result["preset"], client.target)))

if args.ingest_if_empty:
import json

Expand Down Expand Up @@ -1015,7 +1042,7 @@ def _global_skill_command(args: argparse.Namespace) -> int:
from grag.project import apply_ops, plan_global_skill_ops, preview_ops
from grag.project_files import ProjectConfigError

if any((args.db, args.db_dir, args.port, args.ingest, args.ingest_if_empty,
if any((args.db, args.db_dir, args.port, args.ingest, args.ingest_if_empty, args.memory_preset,
args.server_url, args.server_db, args.url, args.no_mcp,
args.no_claude_md, args.no_skill, args.no_verify)):
raise ProjectConfigError("--global-skill supports --client, --dry-run and --remove only; it does not configure a project or database.")
Expand Down
21 changes: 21 additions & 0 deletions src/grag/cli_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ def _print_mutation(result: dict, client: GraphClient, args: argparse.Namespace,
print(f"Warning: {warning}")


def preset_lines(report: dict, target: str) -> list[str]:
"""Human summary of one preset adoption."""
before = report.get("previous_version")
lines = [f"Memory preset v{report['version']} in {target}"
+ (f" (was v{before})." if before not in (None, report["version"]) else ".")]
if report.get("created"):
lines.append(f"Created: {', '.join(report['created'])}")
if report.get("added"):
lines.append(f"Added properties: {', '.join(report['added'])}")
if not report.get("created") and not report.get("added"):
lines.append("No changes needed.")
lines.extend(f"Conflict (unchanged): {conflict}" for conflict in report.get("conflicts", []))
return lines


def graph_command(args: argparse.Namespace, cfg: GragConfig) -> int:
if args.cmd == "ingest":
from grag.ingest.loaders import ingest_paths
Expand Down Expand Up @@ -114,6 +129,12 @@ def graph_command(args: argparse.Namespace, cfg: GragConfig) -> int:
with GraphClient(cfg) as client:
if args.cmd == "inspect":
print(json.dumps(_inspect(client, args), ensure_ascii=False, indent=None if args.json else 2))
elif args.cmd == "memory":
result = client.call("define_schema", DefineSchemaRequest(preset="memory", allow_similar=args.allow_similar))
if args.json:
print(json.dumps(result["preset"], ensure_ascii=False))
else:
print("\n".join(preset_lines(result["preset"], client.target)))
elif args.cmd == "retire":
label, key, _ = _node_schema(client, args.node_id)
result = client.call("upsert_nodes", UpsertNodesRequest(nodes=[UpsertNode(
Expand Down
16 changes: 14 additions & 2 deletions src/grag/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from grag.config import GragConfig
from grag.core.errors import ConfigurationError, GragError
from grag.core.limits import validate_request
from grag.core.types import UpsertNodesRequest
from grag.core.types import DefineSchemaRequest, UpsertNodesRequest

_ROUTES = {
"describe_schema": ("GET", "/api/schema"),
Expand Down Expand Up @@ -159,6 +159,12 @@ def call(self, operation: str, req: BaseModel | None = None) -> dict:
"The running owner does not support the current ingestion scope policy.",
hint="Restart that server with the updated grag installation, then retry. No ingestion request was sent.",
)
preset = isinstance(req, DefineSchemaRequest) and req.preset is not None
if preset and self.capabilities.get("memory_preset", 0) < 1:
raise ConfigurationError(
"The running owner does not support schema presets.",
hint="Restart that server with the updated grag installation, then retry. No schema request was sent.",
)
method, path = _ROUTES[operation]
payload = req.model_dump(mode="json") if req is not None else {}
if isinstance(req, UpsertNodesRequest):
Expand All @@ -168,11 +174,17 @@ def call(self, operation: str, req: BaseModel | None = None) -> dict:
for node, item in zip(req.nodes, payload["nodes"], strict=True):
if node.evidence is not None:
item["evidence"] = node.evidence.model_dump(mode="json", exclude_unset=True)
return self._request(
result = self._request(
method,
path,
**({"json": payload} if req is not None else {}),
)
if preset and not result.get("preset"):
raise GragError(
"The server returned no preset report.",
hint="Inspect the schema with describe_schema before retrying; the server may predate presets.",
)
return result

def __exit__(
self,
Expand Down
2 changes: 1 addition & 1 deletion src/grag/core/evidence.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Explicit memory lifecycle and opt-in, transactional authored revision history.

No preset schema or background expiry worker. Untracked graphs keep working;
No required schema or background expiry worker. Untracked graphs keep working;
expiry is evaluated at read time and never deletes evidence. History begins at
adoption, with a baseline for an existing node, not invented earlier revisions.
"""
Expand Down
8 changes: 8 additions & 0 deletions src/grag/core/mutate.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,14 @@ def _similar_error(name: str, existing: str, kind: str) -> SchemaError:
def define_schema(
engine: Engine, config: GragConfig, req: DefineSchemaRequest, *, detail: SchemaDetail = "full",
) -> SchemaDocument:
if req.preset is not None:
from grag.core.presets import adopt_preset
from grag.core.schema import build_schema_document

report = adopt_preset(engine, req.preset, allow_similar=req.allow_similar)
doc = build_schema_document(engine, config, detail=detail)
doc.preset = report
return doc
# Validate every name before the first DDL, and publish tables + registry
# together. Joining an ingest transaction does not imply a savepoint.
with engine.atomic_writes():
Expand Down
Loading
Loading