memgraph-context-graph 0.12.2__tar.gz → 0.12.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/CONTEXT.md +8 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/PKG-INFO +1 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/runtime-setup.md +5 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/sdk-memory.md +4 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/pyproject.toml +1 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/cli.py +14 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/connectors.py +1 -1
- memgraph_context_graph-0.12.3/src/memgraph_context_graph/graph_version.py +327 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/hooks/runner.py +35 -4
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/mcp_server.py +18 -3
- memgraph_context_graph-0.12.3/src/memgraph_context_graph/refused_events.py +317 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/sdk/__init__.py +32 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/session.py +16 -3
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/setup/cli.py +4 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/setup/tool.py +2 -2
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/setup/update.py +24 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/tools.py +9 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/conftest.py +11 -0
- memgraph_context_graph-0.12.3/tests/test_graph_version.py +249 -0
- memgraph_context_graph-0.12.3/tests/test_refused_events.py +335 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_session.py +9 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_setup.py +2 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/.gitignore +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/LICENSE +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/README.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/adr/0003-config-path-override.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/adr/0007-spawned-processes-read-the-config-file-themselves.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/command-hooks.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/configuration.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/extending.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/memory.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/docs/research/2026-10-codex-memory.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/examples/sdk_claude_agent.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/examples/sdk_openai_agents.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/_identity.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/_spec.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/agent_mcp.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/agent_memory.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/antigravity_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/claude_code.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/claude_code_memory.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/codex.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/codex_memory.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/copilot_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/cursor.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/grok.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/adapters/opencode.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/capture_log.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/capture_policy.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/config_schema.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/events.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/forget.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/hooks/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/hooks/cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/hooks/runtime_plugin.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/link.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/memory_backend.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/protocols.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/sdk/claude.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/sdk/openai.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/setup/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/setup/agents.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/setup/engine.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/setup/memgraph.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/setup/prompts.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/setup/uninstall.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/spaces.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/terminal.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/fake_agent_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_agent_memory_wiring.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_antigravity_cli_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_capture_log.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_capture_policy.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_claude_code_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_claude_code_memory.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_codex_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_codex_memory.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_config_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_connectors.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_context_before_tool.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_copilot_cli_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_cursor_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_dev_scripts.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_events.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_grok_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_hook_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_identity.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_link.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_opencode_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_plugin_hooks.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_runtime_plugin.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_setup_docker.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_spaces.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_terminal.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/tests/test_tools.py +0 -0
|
@@ -78,6 +78,14 @@ _Avoid_: MCP tool (the protocol it is served over, not the thing), Graph Connect
|
|
|
78
78
|
`ConnectorSpec` (`connectors.py`): a graph component's name, its graph class, how to build its **Graph Connector** from **Hook Configuration**, and an optional `doctor` check. Published via the `memgraph_context_graph.connectors` entry-point group; the hook runner, schema setup and `doctor` find connectors only through it. The hub keeps just what it needs before a component is installed: each installable connector's requirement (`INSTALLABLE`) and the default set.
|
|
79
79
|
_Avoid_: Component (the package), Graph Connector (the object a registration builds)
|
|
80
80
|
|
|
81
|
+
**Graph Epoch**:
|
|
82
|
+
The version of one component's shape in the graph, an integer that only a migration raises; every graph is at epoch 1 until one does. The graph states per component the oldest epoch that may write and read it, `(:ContextGraphSchema {component, epoch, min_writer_epoch, min_reader_epoch})`, and every client checks that before writing or reading (`graph_version.py`): a client older than the graph stops, never corrupts. No node means no constraint. Clients record themselves as `(:ContextGraphClient)`, so a migration can see who still writes an old shape. Not a package version: a release that leaves the graph's shape alone keeps the epoch.
|
|
83
|
+
_Avoid_: schema version, package version, migration (raises an epoch; isn't one)
|
|
84
|
+
|
|
85
|
+
**Refused-Event Spool**:
|
|
86
|
+
Where a connector's events go while the graph refuses it (**Graph Epoch**): a local, 0600, size-capped JSONL file beside the capture-failure log (`refused_events.py`), holding each event as the capture policy left it, original timestamp included. `update` replays it into the connectors the graph allows again. Its line format is versioned and read by later releases, so a change keeps a reader for every earlier format.
|
|
87
|
+
_Avoid_: queue, buffer, outbox (nothing sends it on its own; `update` replays it)
|
|
88
|
+
|
|
81
89
|
## Relationships
|
|
82
90
|
|
|
83
91
|
- **Memgraph Context Graph** belongs to broader **Context Graph** family.
|
|
@@ -76,6 +76,11 @@ and constraints. It then runs `doctor`'s checks. Name runtimes to refresh only t
|
|
|
76
76
|
and the graph's data are untouched, and running it again changes nothing.
|
|
77
77
|
Running agent sessions keep the hooks they loaded: restart them. Outside the uv tool (a virtualenv, say) the
|
|
78
78
|
package step only warns, and you upgrade it yourself first.
|
|
79
|
+
Once a newer Context Graph has reshaped your graph, an older install stops writing (and, if needed, reading) the
|
|
80
|
+
parts it no longer knows instead of corrupting them: the session-start context, the capture-failure log and
|
|
81
|
+
`doctor` say so, and `update` brings it level. What the agents did meanwhile isn't lost: it is kept, redacted
|
|
82
|
+
like any capture, in `refused-events.jsonl` beside the capture-failure log (readable only by you, at most 64 MB),
|
|
83
|
+
and `update` adds it to the graph with its original times.
|
|
79
84
|
|
|
80
85
|
**Identity and connection.** setup writes `~/.config/context-graph/config.toml`; hooks read their configuration from that file at runtime (see [Configuration](configuration.md) — env vars are **not** read at hook time). To change your identity later:
|
|
81
86
|
|
|
@@ -31,6 +31,10 @@ from memgraph_context_graph.sdk import ContextGraphMemory
|
|
|
31
31
|
memory = ContextGraphMemory(user_id="alice")
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
A long-running app keeps working across a Context Graph upgrade elsewhere. About every ten minutes it checks whether
|
|
35
|
+
a newer Context Graph has reshaped the graph. If it has, the app stops writing the parts it no longer knows, keeps
|
|
36
|
+
those events on the machine, and `memgraph-context-graph update` adds them once this install is upgraded.
|
|
37
|
+
|
|
34
38
|
## Claude Agent SDK
|
|
35
39
|
|
|
36
40
|
The SDK's hook callbacks get Claude Code's own hook payloads, so the `claude-code` rules translate them, under `source_sdk = "claude-agent-sdk"`. Python callbacks get no `SessionStart` or `SessionEnd`, which changes two things:
|
{memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.3}/src/memgraph_context_graph/cli.py
RENAMED
|
@@ -405,6 +405,7 @@ def doctor_checks(runtimes: Sequence[str], connectors: Sequence[str]) -> list[_C
|
|
|
405
405
|
# produce one, the config check already says why, and the rest would only repeat it.
|
|
406
406
|
if _edition_resolves():
|
|
407
407
|
checks.append(_check_memgraph())
|
|
408
|
+
checks.append(_check_graph_version(connectors))
|
|
408
409
|
for connector in connectors:
|
|
409
410
|
checks.append(_check_connector(connector))
|
|
410
411
|
if any(normalize_connector_name(connector) == "sessions-graph" for connector in connectors):
|
|
@@ -503,6 +504,19 @@ def _check_memgraph() -> _CheckResult:
|
|
|
503
504
|
_close(db)
|
|
504
505
|
|
|
505
506
|
|
|
507
|
+
def _check_graph_version(connectors: Sequence[str]) -> _CheckResult:
|
|
508
|
+
"""Whether a newer Context Graph has reshaped the graph past what this install can write or read."""
|
|
509
|
+
from memgraph_context_graph import graph_version
|
|
510
|
+
|
|
511
|
+
rules = graph_version.check_configured(fresh=True)
|
|
512
|
+
components = [graph_version.HUB, *(normalize_connector_name(connector) for connector in connectors)]
|
|
513
|
+
refused = [rules.refuse_write(c) for c in components if not rules.may_write(c)]
|
|
514
|
+
refused += [rules.refuse_read(c) for c in components if not rules.may_read(c)]
|
|
515
|
+
if refused:
|
|
516
|
+
return {"name": "graph version", "ok": False, "detail": "; ".join(dict.fromkeys(map(str, refused)))}
|
|
517
|
+
return {"name": "graph version", "ok": True, "detail": "this install can write and read the graph"}
|
|
518
|
+
|
|
519
|
+
|
|
506
520
|
def _close(db: Any) -> None:
|
|
507
521
|
if db is not None:
|
|
508
522
|
db.close()
|
|
@@ -60,7 +60,7 @@ class ConnectorSpec:
|
|
|
60
60
|
INSTALLABLE: dict[str, str] = {
|
|
61
61
|
"skills-graph": "skills-graph[memgraph-context-graph]>=0.4.0",
|
|
62
62
|
"actions-graph": "actions-graph[memgraph-context-graph]>=0.7.1",
|
|
63
|
-
"sessions-graph": "sessions-graph[memgraph-context-graph]>=0.12.
|
|
63
|
+
"sessions-graph": "sessions-graph[memgraph-context-graph]>=0.12.3",
|
|
64
64
|
"resources-graph": "resources-graph[memgraph-context-graph]>=0.3.0",
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Whether this client may write or read a graph that a newer Context Graph may have reshaped.
|
|
2
|
+
|
|
3
|
+
A later release may migrate a component's part of the graph to a new shape.
|
|
4
|
+
Clients installed before it keep running: on other machines, against a shared
|
|
5
|
+
Enterprise server, or as a harness's long-lived MCP server. They would write
|
|
6
|
+
the old shape into the new graph, or read it and find nothing. So the graph
|
|
7
|
+
says, per component, the oldest shape a client must know to write or read it:
|
|
8
|
+
|
|
9
|
+
(:ContextGraphSchema {component, epoch, min_writer_epoch, min_reader_epoch})
|
|
10
|
+
|
|
11
|
+
and every client checks that before it writes or reads. This rule is the one
|
|
12
|
+
part of graph versioning frozen into every installed client, so it stays this
|
|
13
|
+
small. The migrations, their ledger and ``epoch`` itself belong to the release
|
|
14
|
+
that writes these nodes. A component with no node has no constraint, so every
|
|
15
|
+
graph written before this module allows everything.
|
|
16
|
+
|
|
17
|
+
``memgraph-context-graph`` is the component of the shared Session Node, which
|
|
18
|
+
every connector writes through, so a constraint on it holds for all of them.
|
|
19
|
+
|
|
20
|
+
A client also records itself on each check as
|
|
21
|
+
``(:ContextGraphClient {client_id, version, baseline_epoch, epochs, last_seen})``.
|
|
22
|
+
Before a migration removes an old shape, it can then see whether clients that
|
|
23
|
+
still write that shape are about.
|
|
24
|
+
|
|
25
|
+
Checks are cached per graph for :data:`CHECK_INTERVAL` in a state file beside
|
|
26
|
+
the capture-failure log, so a hook pays for one at most every few minutes. A
|
|
27
|
+
check that can't reach the graph allows everything and isn't cached: the
|
|
28
|
+
connectors then fail and log on their own.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import contextlib
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import uuid
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from datetime import datetime, timedelta, timezone
|
|
39
|
+
from typing import TYPE_CHECKING, Any
|
|
40
|
+
|
|
41
|
+
from memgraph_context_graph import capture_log
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from collections.abc import Callable, Mapping
|
|
45
|
+
from contextlib import AbstractContextManager
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
#: The component that owns the Session Node; a constraint on it covers every connector.
|
|
49
|
+
HUB = "memgraph-context-graph"
|
|
50
|
+
#: The shape every component is at in a graph no migration has touched.
|
|
51
|
+
BASELINE_EPOCH = 1
|
|
52
|
+
#: The epoch of each component's shape this release writes and reads, where it differs from the baseline.
|
|
53
|
+
EPOCHS: dict[str, int] = {}
|
|
54
|
+
#: How long a check holds before the graph is asked again.
|
|
55
|
+
CHECK_INTERVAL = timedelta(minutes=10)
|
|
56
|
+
|
|
57
|
+
UPDATE_COMMAND = "memgraph-context-graph update"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class GraphTooNewError(Exception):
|
|
61
|
+
"""The graph's shape for a component is newer than this client can safely write or read."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def known_epoch(component: str) -> int:
|
|
65
|
+
"""The epoch of *component*'s shape this release writes and reads."""
|
|
66
|
+
return EPOCHS.get(_normalize(component), BASELINE_EPOCH)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class Rules:
|
|
71
|
+
"""What the graph asks of its clients: per component, the oldest epoch that may write and read it."""
|
|
72
|
+
|
|
73
|
+
min_writer: Mapping[str, int] = field(default_factory=dict)
|
|
74
|
+
min_reader: Mapping[str, int] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
def may_write(self, component: str) -> bool:
|
|
77
|
+
"""Whether this client may write *component*'s part of the graph."""
|
|
78
|
+
return not self._newer(self.min_writer, component)
|
|
79
|
+
|
|
80
|
+
def may_read(self, component: str) -> bool:
|
|
81
|
+
"""Whether this client may read *component*'s part of the graph."""
|
|
82
|
+
return not self._newer(self.min_reader, component)
|
|
83
|
+
|
|
84
|
+
def refuse_write(self, component: str) -> GraphTooNewError:
|
|
85
|
+
"""The error to report for a write :meth:`may_write` refuses."""
|
|
86
|
+
return GraphTooNewError(_refusal("write", self._newer(self.min_writer, component)))
|
|
87
|
+
|
|
88
|
+
def refuse_read(self, component: str) -> GraphTooNewError:
|
|
89
|
+
"""The error to report for a read :meth:`may_read` refuses."""
|
|
90
|
+
return GraphTooNewError(_refusal("read", self._newer(self.min_reader, component)))
|
|
91
|
+
|
|
92
|
+
def notice(self) -> str | None:
|
|
93
|
+
"""One line for the model's session-start context while anything is refused, else None."""
|
|
94
|
+
writes = sorted(c for c, epoch in self.min_writer.items() if epoch > known_epoch(c))
|
|
95
|
+
reads = sorted(c for c, epoch in self.min_reader.items() if epoch > known_epoch(c))
|
|
96
|
+
if not writes and not reads:
|
|
97
|
+
return None
|
|
98
|
+
paused = ", ".join(dict.fromkeys(writes + reads))
|
|
99
|
+
# Refused writes are spooled for replay (refused_events); refused reads have nothing to keep.
|
|
100
|
+
kept = " What this session does is kept on this machine and added after the update." if writes else ""
|
|
101
|
+
return (
|
|
102
|
+
f"Context Graph is paused for {paused}: a newer Context Graph upgraded this graph, and the "
|
|
103
|
+
f"version installed here can't safely use it.{kept} Ask the user to run `{UPDATE_COMMAND}`."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _newer(self, required: Mapping[str, int], component: str) -> list[str]:
|
|
107
|
+
"""The components among *component* and the hub whose required epoch is above what this client knows."""
|
|
108
|
+
return [c for c in dict.fromkeys((HUB, _normalize(component))) if required.get(c, 0) > known_epoch(c)]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _refusal(action: str, components: list[str]) -> str:
|
|
112
|
+
return (
|
|
113
|
+
f"this graph needs a newer Context Graph to {action} {', '.join(components)} than the one installed "
|
|
114
|
+
f"here; run {UPDATE_COMMAND}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def setup(db: Any) -> None:
|
|
119
|
+
"""Create the constraints and indexes the check and the client record use; safe to repeat."""
|
|
120
|
+
db.query("CREATE CONSTRAINT ON (s:ContextGraphSchema) ASSERT s.component IS UNIQUE;")
|
|
121
|
+
db.query("CREATE INDEX ON :ContextGraphSchema;")
|
|
122
|
+
db.query("CREATE CONSTRAINT ON (c:ContextGraphClient) ASSERT c.client_id IS UNIQUE;")
|
|
123
|
+
db.query("CREATE INDEX ON :ContextGraphClient(client_id);")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def setup_at(memgraph_env: dict[str, str]) -> None:
|
|
127
|
+
""":func:`setup` on the graph a resolved Memgraph environment (``resolve_memgraph_env``) names."""
|
|
128
|
+
with _connect(memgraph_env) as db:
|
|
129
|
+
setup(db)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check(
|
|
133
|
+
connect: Callable[[], AbstractContextManager[Any]],
|
|
134
|
+
*,
|
|
135
|
+
url: str,
|
|
136
|
+
database: str = "",
|
|
137
|
+
fresh: bool = False,
|
|
138
|
+
strict: bool = False,
|
|
139
|
+
now: datetime | None = None,
|
|
140
|
+
) -> Rules:
|
|
141
|
+
"""The graph's rules for this client, from the cache while it holds, else from the graph.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
connect: Opens a client for the graph, called only when the cache doesn't hold.
|
|
145
|
+
url: The graph's Bolt URL; with *database*, what the cache keys on.
|
|
146
|
+
database: The Memgraph database, empty for the default one.
|
|
147
|
+
fresh: Ask the graph even while the cache holds.
|
|
148
|
+
strict: Raise when the graph can't be asked, instead of allowing everything.
|
|
149
|
+
now: The current time, for tests.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The rules; no rules (everything allowed) when the graph can't be asked and not *strict*.
|
|
153
|
+
"""
|
|
154
|
+
now = now or datetime.now(timezone.utc)
|
|
155
|
+
state = _load_state()
|
|
156
|
+
key = _graph_key(url, database)
|
|
157
|
+
cached = state["graphs"].get(key)
|
|
158
|
+
if not fresh and cached is not None and _holds(cached, now):
|
|
159
|
+
return _rules_from(cached.get("rules"))
|
|
160
|
+
try:
|
|
161
|
+
with connect() as db:
|
|
162
|
+
rules = read_rules(db)
|
|
163
|
+
# A client that can read but not write the graph (a shared space) still gets its rules.
|
|
164
|
+
with contextlib.suppress(Exception):
|
|
165
|
+
_record_client(db, state["client_id"], now)
|
|
166
|
+
except Exception:
|
|
167
|
+
if strict:
|
|
168
|
+
raise
|
|
169
|
+
return Rules()
|
|
170
|
+
state["graphs"][key] = {"checked_at": now.isoformat(), "rules": _rules_to(rules)}
|
|
171
|
+
_save_state(state)
|
|
172
|
+
return rules
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def cached_rules(url: str, database: str = "") -> Rules:
|
|
176
|
+
"""The rules this client last read from the graph, without asking it; none when it never has."""
|
|
177
|
+
cached = _load_state()["graphs"].get(_graph_key(url, database))
|
|
178
|
+
return _rules_from(cached.get("rules")) if cached is not None else Rules()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def check_configured(*, fresh: bool = False) -> Rules:
|
|
182
|
+
""":func:`check` for the graph the config file names; no rules when the config can't name one."""
|
|
183
|
+
from memgraph_context_graph.adapters._identity import resolve_memgraph_env
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
env = resolve_memgraph_env()
|
|
187
|
+
except Exception:
|
|
188
|
+
return Rules()
|
|
189
|
+
return check_at(env, fresh=fresh)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def check_at(memgraph_env: dict[str, str], *, fresh: bool = False, strict: bool = False) -> Rules:
|
|
193
|
+
""":func:`check` for the graph a resolved Memgraph environment (``resolve_memgraph_env``) names."""
|
|
194
|
+
return check(
|
|
195
|
+
lambda: _connect(memgraph_env),
|
|
196
|
+
url=memgraph_env["MEMGRAPH_URL"],
|
|
197
|
+
database=memgraph_env["MEMGRAPH_DATABASE"],
|
|
198
|
+
fresh=fresh,
|
|
199
|
+
strict=strict,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _connect(memgraph_env: dict[str, str]) -> AbstractContextManager[Any]:
|
|
204
|
+
from memgraph_context_graph.hooks.runner import memgraph_kwargs
|
|
205
|
+
from memgraph_toolbox.api.memgraph import Memgraph
|
|
206
|
+
|
|
207
|
+
return contextlib.closing(Memgraph(**memgraph_kwargs(memgraph_env)))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def configured_notice() -> str | None:
|
|
211
|
+
"""The session-start notice for the graph the config file names, from the cache alone; never raises."""
|
|
212
|
+
from memgraph_context_graph.adapters._identity import resolve_memgraph_env
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
env = resolve_memgraph_env()
|
|
216
|
+
return cached_rules(env["MEMGRAPH_URL"], env["MEMGRAPH_DATABASE"]).notice()
|
|
217
|
+
except Exception:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def read_rules(db: Any) -> Rules:
|
|
222
|
+
"""The graph's rules, read from its ``(:ContextGraphSchema)`` nodes."""
|
|
223
|
+
rows = db.query(
|
|
224
|
+
"MATCH (s:ContextGraphSchema) "
|
|
225
|
+
"RETURN s.component AS component, s.min_writer_epoch AS min_writer, s.min_reader_epoch AS min_reader"
|
|
226
|
+
)
|
|
227
|
+
min_writer: dict[str, int] = {}
|
|
228
|
+
min_reader: dict[str, int] = {}
|
|
229
|
+
for row in rows:
|
|
230
|
+
component = row.get("component")
|
|
231
|
+
if not isinstance(component, str):
|
|
232
|
+
continue
|
|
233
|
+
if isinstance(row.get("min_writer"), int):
|
|
234
|
+
min_writer[_normalize(component)] = row["min_writer"]
|
|
235
|
+
if isinstance(row.get("min_reader"), int):
|
|
236
|
+
min_reader[_normalize(component)] = row["min_reader"]
|
|
237
|
+
return Rules(min_writer, min_reader)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _client_version() -> str:
|
|
241
|
+
from memgraph_context_graph import __version__
|
|
242
|
+
|
|
243
|
+
return __version__
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _record_client(db: Any, client_id: str, now: datetime) -> None:
|
|
247
|
+
from memgraph_toolbox.api.memgraph import merge_with_retry
|
|
248
|
+
|
|
249
|
+
merge_with_retry(
|
|
250
|
+
db,
|
|
251
|
+
"""
|
|
252
|
+
MERGE (c:ContextGraphClient {client_id: $client_id})
|
|
253
|
+
SET c.version = $version, c.baseline_epoch = $baseline, c.epochs = $epochs, c.last_seen = $now
|
|
254
|
+
""",
|
|
255
|
+
params={
|
|
256
|
+
"client_id": client_id,
|
|
257
|
+
"version": _client_version(),
|
|
258
|
+
"baseline": BASELINE_EPOCH,
|
|
259
|
+
"epochs": dict(EPOCHS),
|
|
260
|
+
"now": now.isoformat(),
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _holds(cached: dict[str, Any], now: datetime) -> bool:
|
|
266
|
+
try:
|
|
267
|
+
checked_at = datetime.fromisoformat(cached["checked_at"])
|
|
268
|
+
except (KeyError, TypeError, ValueError):
|
|
269
|
+
return False
|
|
270
|
+
return timedelta(0) <= now - checked_at < CHECK_INTERVAL
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _rules_to(rules: Rules) -> dict[str, dict[str, int]]:
|
|
274
|
+
return {"min_writer": dict(rules.min_writer), "min_reader": dict(rules.min_reader)}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _rules_from(raw: Any) -> Rules:
|
|
278
|
+
if not isinstance(raw, dict):
|
|
279
|
+
return Rules()
|
|
280
|
+
|
|
281
|
+
def epochs(name: str) -> dict[str, int]:
|
|
282
|
+
value = raw.get(name)
|
|
283
|
+
if not isinstance(value, dict):
|
|
284
|
+
return {}
|
|
285
|
+
return {str(c): e for c, e in value.items() if isinstance(e, int)}
|
|
286
|
+
|
|
287
|
+
return Rules(epochs("min_writer"), epochs("min_reader"))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _graph_key(url: str, database: str) -> str:
|
|
291
|
+
return f"{url}#{database}"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _normalize(component: str) -> str:
|
|
295
|
+
from memgraph_context_graph.tools import normalize_connector_name
|
|
296
|
+
|
|
297
|
+
return normalize_connector_name(component)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def state_path() -> Path:
|
|
301
|
+
"""The cache file: beside the capture-failure log, so a config chosen with ``CONTEXT_GRAPH_CONFIG`` gets its own."""
|
|
302
|
+
log = capture_log.path()
|
|
303
|
+
return log.with_name(log.name.removesuffix("capture-failures.jsonl") + "graph-version.json")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _load_state() -> dict[str, Any]:
|
|
307
|
+
try:
|
|
308
|
+
state = json.loads(state_path().read_text(encoding="utf-8"))
|
|
309
|
+
except (OSError, ValueError):
|
|
310
|
+
state = {}
|
|
311
|
+
if not isinstance(state, dict):
|
|
312
|
+
state = {}
|
|
313
|
+
if not isinstance(state.get("client_id"), str):
|
|
314
|
+
state["client_id"] = uuid.uuid4().hex
|
|
315
|
+
if not isinstance(state.get("graphs"), dict):
|
|
316
|
+
state["graphs"] = {}
|
|
317
|
+
return state
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _save_state(state: dict[str, Any]) -> None:
|
|
321
|
+
"""Write the cache whole, through a rename, so a hook running at the same moment never reads half of it."""
|
|
322
|
+
target = state_path()
|
|
323
|
+
with contextlib.suppress(OSError):
|
|
324
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
325
|
+
temporary = target.with_name(f"{target.name}.{os.getpid()}.tmp")
|
|
326
|
+
temporary.write_text(json.dumps(state), encoding="utf-8")
|
|
327
|
+
os.replace(temporary, target)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import contextlib
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
8
9
|
import sys
|
|
@@ -10,7 +11,7 @@ import threading
|
|
|
10
11
|
import time
|
|
11
12
|
from typing import TYPE_CHECKING, Any, NoReturn, TypedDict
|
|
12
13
|
|
|
13
|
-
from memgraph_context_graph import capture_log
|
|
14
|
+
from memgraph_context_graph import capture_log, graph_version, refused_events
|
|
14
15
|
from memgraph_context_graph.capture_policy import CapturePolicy, policy_from_config
|
|
15
16
|
from memgraph_context_graph.connectors import (
|
|
16
17
|
INSTALLABLE,
|
|
@@ -68,6 +69,7 @@ def create_link(
|
|
|
68
69
|
memgraph_env: dict[str, str] | None = None,
|
|
69
70
|
on_error: ConnectorFailureHandler | None = None,
|
|
70
71
|
policy: CapturePolicy | None = None,
|
|
72
|
+
spool_refused: bool = True,
|
|
71
73
|
) -> AgentLink:
|
|
72
74
|
"""Create an AgentLink with optional connectors named by CLI/config.
|
|
73
75
|
|
|
@@ -79,6 +81,13 @@ def create_link(
|
|
|
79
81
|
Memgraph raises, since no connector could run. Without it each connector
|
|
80
82
|
connects on its own, as library callers always have.
|
|
81
83
|
|
|
84
|
+
With *memgraph_env*, a connector whose part of the graph a newer Context
|
|
85
|
+
Graph has reshaped (:mod:`memgraph_context_graph.graph_version`) is not
|
|
86
|
+
added: it fails like one that can't be set up, with a
|
|
87
|
+
:class:`~memgraph_context_graph.graph_version.GraphTooNewError`, and
|
|
88
|
+
its events are spooled for ``update`` to replay
|
|
89
|
+
(:mod:`memgraph_context_graph.refused_events`).
|
|
90
|
+
|
|
82
91
|
Args:
|
|
83
92
|
connector_names: Connectors to enable, by distribution name.
|
|
84
93
|
memgraph_env: Resolved Memgraph settings (``resolve_memgraph_env``).
|
|
@@ -86,6 +95,8 @@ def create_link(
|
|
|
86
95
|
event, is reported here and the others still run. Without it the
|
|
87
96
|
first failure raises.
|
|
88
97
|
policy: The capture policy every event passes before a connector sees it.
|
|
98
|
+
spool_refused: Keep refused connectors' events for replay; off for the
|
|
99
|
+
replay itself, which keeps them on its own terms.
|
|
89
100
|
|
|
90
101
|
Raises:
|
|
91
102
|
ValueError: if a connector name is not supported, or Memgraph can't be reached.
|
|
@@ -98,13 +109,28 @@ def create_link(
|
|
|
98
109
|
|
|
99
110
|
link = AgentLink(on_error=report if on_error is not None else None, policy=policy)
|
|
100
111
|
shared = _hook_memgraph(memgraph_env) if names and memgraph_env is not None else None
|
|
112
|
+
rules = (
|
|
113
|
+
graph_version.check(
|
|
114
|
+
lambda: contextlib.nullcontext(shared),
|
|
115
|
+
url=memgraph_env["MEMGRAPH_URL"],
|
|
116
|
+
database=memgraph_env["MEMGRAPH_DATABASE"],
|
|
117
|
+
)
|
|
118
|
+
if shared is not None and memgraph_env is not None
|
|
119
|
+
else graph_version.Rules()
|
|
120
|
+
)
|
|
121
|
+
refused: list[str] = []
|
|
101
122
|
for name in names:
|
|
102
123
|
try:
|
|
124
|
+
if not rules.may_write(name):
|
|
125
|
+
refused.append(name)
|
|
126
|
+
raise rules.refuse_write(name)
|
|
103
127
|
link.add_connector(_build_connector(name, memgraph_env, memgraph=shared))
|
|
104
128
|
except Exception as exc:
|
|
105
129
|
if on_error is None:
|
|
106
130
|
raise
|
|
107
131
|
on_error(name, None, exc)
|
|
132
|
+
if refused and spool_refused:
|
|
133
|
+
link.add_connector(refused_events.Spool(refused))
|
|
108
134
|
return link
|
|
109
135
|
|
|
110
136
|
|
|
@@ -420,11 +446,17 @@ def session_start_text(connector_names: list[str], payload: dict[str, Any] | Non
|
|
|
420
446
|
its index of the user's memory files, standing in for the harness's own
|
|
421
447
|
memory it replaces (#484). Each runtime wraps the text in the shape its
|
|
422
448
|
session-start hook output takes (``RuntimeSpec.session_context``).
|
|
449
|
+
|
|
450
|
+
While a newer Context Graph's graph refuses this client, a notice comes first
|
|
451
|
+
(:func:`~memgraph_context_graph.graph_version.configured_notice`), so the
|
|
452
|
+
model can tell the user to update.
|
|
423
453
|
"""
|
|
424
454
|
from memgraph_context_graph.tools import session_hints
|
|
425
455
|
|
|
456
|
+
notice = graph_version.configured_notice()
|
|
457
|
+
lines = [notice] if notice else []
|
|
426
458
|
try:
|
|
427
|
-
|
|
459
|
+
lines += session_hints(connector_names, payload=_with_cwd(payload or {}))
|
|
428
460
|
except Exception as exc: # A tool that fails to load must not fail the hook; the model just isn't told.
|
|
429
461
|
capture_log.record_failure(
|
|
430
462
|
"respond",
|
|
@@ -433,8 +465,7 @@ def session_start_text(connector_names: list[str], payload: dict[str, Any] | Non
|
|
|
433
465
|
hook_event="SessionStart",
|
|
434
466
|
session_id=_payload_session_id(payload or {}),
|
|
435
467
|
)
|
|
436
|
-
|
|
437
|
-
return "\n".join(hints) or None
|
|
468
|
+
return "\n".join(lines) or None
|
|
438
469
|
|
|
439
470
|
|
|
440
471
|
def _with_cwd(payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -20,7 +20,9 @@ from __future__ import annotations
|
|
|
20
20
|
import os
|
|
21
21
|
from typing import TYPE_CHECKING, Any
|
|
22
22
|
|
|
23
|
-
from memgraph_context_graph
|
|
23
|
+
from memgraph_context_graph import graph_version
|
|
24
|
+
from memgraph_context_graph.graph_version import GraphTooNewError
|
|
25
|
+
from memgraph_context_graph.tools import Tool, ToolError, is_available, load_tools, writes_graph
|
|
24
26
|
|
|
25
27
|
if TYPE_CHECKING:
|
|
26
28
|
from mcp.server.lowlevel import Server
|
|
@@ -67,8 +69,8 @@ def build_server(tools: dict[str, Tool] | None = None, *, instructions: str | No
|
|
|
67
69
|
return _error(f"Unknown tool: {name}")
|
|
68
70
|
try:
|
|
69
71
|
# Tools query Memgraph synchronously; a thread keeps the server responsive.
|
|
70
|
-
result = await to_thread.run_sync(tool
|
|
71
|
-
except ToolError as exc:
|
|
72
|
+
result = await to_thread.run_sync(_call_checked, tool, arguments or {}, config)
|
|
73
|
+
except (ToolError, GraphTooNewError) as exc:
|
|
72
74
|
return _error(str(exc))
|
|
73
75
|
return types.CallToolResult(content=[types.TextContent(type="text", text=result.text)])
|
|
74
76
|
|
|
@@ -87,6 +89,19 @@ def build_server(tools: dict[str, Tool] | None = None, *, instructions: str | No
|
|
|
87
89
|
return Server(SERVER_NAME, instructions=instructions, on_list_tools=on_list_tools, on_call_tool=on_call_tool)
|
|
88
90
|
|
|
89
91
|
|
|
92
|
+
def _call_checked(tool: Tool, arguments: dict[str, Any], config: Any) -> Any:
|
|
93
|
+
"""Call *tool* unless a newer Context Graph reshaped the graph it reads, or writes (``writes_graph``).
|
|
94
|
+
|
|
95
|
+
The server outlives package upgrades, so it checks on each call rather than once at start; the check is cached.
|
|
96
|
+
"""
|
|
97
|
+
rules = graph_version.check_configured()
|
|
98
|
+
if not rules.may_read(tool.connector):
|
|
99
|
+
raise rules.refuse_read(tool.connector)
|
|
100
|
+
if writes_graph(tool) and not rules.may_write(tool.connector):
|
|
101
|
+
raise rules.refuse_write(tool.connector)
|
|
102
|
+
return tool.call(arguments, config)
|
|
103
|
+
|
|
104
|
+
|
|
90
105
|
def session_instructions(tools: dict[str, Tool]) -> str | None:
|
|
91
106
|
"""The session-start text for the directory the harness started this server in."""
|
|
92
107
|
from memgraph_context_graph.hooks.runner import session_start_text
|