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
53 changes: 53 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:
- "context-graph/actions-graph/**"
- "context-graph/agent-context-graph/**"
- "context-graph/sessions-graph/**"
- "context-graph/resources-graph/**"
- "context-graph/eval/**"
- ".github/workflows/tests.yaml"
pull_request:
Expand All @@ -29,6 +30,7 @@ on:
- "context-graph/actions-graph/**"
- "context-graph/agent-context-graph/**"
- "context-graph/sessions-graph/**"
- "context-graph/resources-graph/**"
- "context-graph/eval/**"
- ".github/workflows/tests.yaml"

Expand All @@ -46,6 +48,7 @@ jobs:
skills-graph: ${{ steps.filter.outputs.skills-graph }}
actions-graph: ${{ steps.filter.outputs.actions-graph }}
sessions-graph: ${{ steps.filter.outputs.sessions-graph }}
resources-graph: ${{ steps.filter.outputs.resources-graph }}
context-graph-eval: ${{ steps.filter.outputs.context-graph-eval }}
steps:
- name: Checkout code
Expand Down Expand Up @@ -100,6 +103,12 @@ jobs:
- 'hygm/**'
- 'memgraph-toolbox/**'
- '.github/workflows/tests.yaml'
resources-graph:
- 'context-graph/resources-graph/**'
- 'context-graph/sessions-graph/**'
- 'context-graph/agent-context-graph/**'
- 'memgraph-toolbox/**'
- '.github/workflows/tests.yaml'
context-graph-eval:
- 'context-graph/eval/**'
- 'context-graph/actions-graph/**'
Expand Down Expand Up @@ -416,6 +425,50 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: uv run --package skills-graph --extra test --extra agent-context-graph pytest tests/ -v

test-resources-graph:
needs: changes
if: ${{ needs.changes.outputs.resources-graph == 'true' }}
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.12", "3.13"]
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}

- name: Start Memgraph container
run: |
docker run -d -p 7687:7687 --name memgraph memgraph/memgraph-mage:latest --schema-info-enabled=True --telemetry-enabled=false

- name: Wait for Memgraph to be ready
run: |
for i in $(seq 1 30); do
if echo "RETURN 1;" | docker exec -i memgraph mgconsole --host 127.0.0.1 --port 7687 >/dev/null 2>&1; then
echo "Memgraph is ready to serve queries"
exit 0
fi
echo "Waiting for Memgraph (attempt $i)..."
sleep 2
done
echo "Memgraph did not become ready in time" >&2
docker logs memgraph || true
exit 1

- name: Install uv
run: python -m pip install uv

- name: Run tests
working-directory: context-graph/resources-graph
env:
# Only the one live Sweep test uses it; the rest replay recorded GitHub responses.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: uv run --package resources-graph --extra test --extra agent-context-graph pytest tests/ -v

test-actions-graph:
needs: changes
if: ${{ needs.changes.outputs.actions-graph == 'true' }}
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Cursor, OpenCode, Antigravity CLI, Grok Build) into a queryable Memgraph graph.
| `actions-graph` | Records tool calls/results/messages/subagent activity as `(:Action)`/`(:Agent)` nodes — observability, not memory. |
| `skills-graph` | Tracks Agent-Skills-spec `(:Skill)` usage per session. |
| `sessions-graph` | Owns `(:User)`/`(:Session)`, durable `(:Memory)` writes/recall, and session reconciliation into `(:Episode)` + extracted entities. |
| `resources-graph` | Remembers public GitHub resources the agent touched: hooks record private `(:Touch)` nodes, the out-of-band `resources-graph sweep` stores shared `(:Resource)`s, and the `resource` tool serves them from memory. |

Everything joins on a shared, idempotently-`MERGE`d `(:Session {session_id})`
node; only `sessions-graph` owns `(:User)` and `HAD_SESSION`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class HookConfig:
ontology_path: str | None = None
#: ``[ontology] derive``: "extend" (the default) or "off"; see sessions-graph's ontology module.
ontology_derive: str | None = None
#: ``[github] token``: overrides the ``gh`` login resources-graph's Sweep otherwise fetches with.
github_token: str | None = None


def load_config() -> HookConfig:
Expand Down Expand Up @@ -222,6 +224,7 @@ def write_config(
embedding_model: str | None = None,
ontology_path: str | None = None,
ontology_derive: str | None = None,
github_token: str | None = None,
) -> Path:
"""Write or update the config file. Returns the path written to.

Expand All @@ -244,6 +247,7 @@ def write_config(
final_embedding_model = embedding_model if embedding_model is not None else existing.embedding_model
final_ontology_path = ontology_path if ontology_path is not None else existing.ontology_path
final_ontology_derive = ontology_derive if ontology_derive is not None else existing.ontology_derive
final_github_token = github_token if github_token is not None else existing.github_token

content = _render_config(
user_id=final_user_id or "",
Expand All @@ -258,6 +262,7 @@ def write_config(
recall_settings=existing.recall_settings,
ontology_path=final_ontology_path,
ontology_derive=final_ontology_derive,
github_token=final_github_token,
)

path = config_file()
Expand All @@ -283,15 +288,15 @@ def write_full_config(
) -> Path:
"""Write a complete config file with all sections (used by bootstrap).

Overwrites every section except ``[reconcile]``, ``[recall]`` and ``[ontology]``: unlike identity/Memgraph/LLM
Overwrites every section except ``[reconcile]``, ``[recall]``, ``[ontology]`` and ``[github]``: unlike identity/Memgraph/LLM
settings, ``auto_reconcile`` has no legitimate ambient-env source for
``bootstrap`` to capture (nobody has ``SESSIONS_GRAPH_AUTO_RECONCILE``
exported for an unrelated reason the way they might already have
``OPENAI_API_KEY``/`MEMGRAPH_PASSWORD` set) — it is only ever set via
``config set reconcile.auto_reconcile``. Re-running bootstrap must not
silently revert it to off, so ``auto_reconcile`` is preserved from the
existing file unless explicitly given here. ``[recall]`` and ``[ontology]``
are preserved the same way: they are only ever set via ``config set`` or by
existing file unless explicitly given here. ``[recall]``, ``[ontology]`` and
``[github]`` are preserved the same way: they are only ever set via ``config set`` or by
editing the file.
"""
global _cached_config
Expand All @@ -312,6 +317,7 @@ def write_full_config(
recall_settings=existing.recall_settings,
ontology_path=existing.ontology_path,
ontology_derive=existing.ontology_derive,
github_token=existing.github_token,
)

path = config_file()
Expand Down Expand Up @@ -352,6 +358,7 @@ def _read_config_file() -> HookConfig:
reconcile = sections.get("reconcile", {})
recall = sections.get("recall", {})
ontology = sections.get("ontology", {})
github = sections.get("github", {})
auto_reconcile_raw = reconcile.get("auto_reconcile")

return HookConfig(
Expand All @@ -367,6 +374,7 @@ def _read_config_file() -> HookConfig:
recall_settings={key: value for key, value in recall.items() if key != "embedding_model"},
ontology_path=ontology.get("path") or None,
ontology_derive=ontology.get("derive") or None,
github_token=github.get("token") or None,
)


Expand Down Expand Up @@ -413,14 +421,15 @@ def _render_config(
recall_settings: dict[str, str] | None = None,
ontology_path: str | None = None,
ontology_derive: str | None = None,
github_token: str | None = None,
) -> str:
"""Render the full config file content.

The ``[reconcile]`` section is omitted entirely when ``auto_reconcile`` is
``None`` (never configured), so a fresh read of the file resolves it back
to ``None`` rather than a concrete ``false`` — see
:func:`resolve_auto_reconcile` for why that distinction matters.
``[recall]`` and ``[ontology]`` are likewise omitted while they hold nothing.
``[recall]``, ``[ontology]`` and ``[github]`` are likewise omitted while they hold nothing.
"""
lines = [
"# Context Graph hook configuration",
Expand Down Expand Up @@ -448,6 +457,8 @@ def _render_config(
ontology = {key: value for key, value in (("path", ontology_path), ("derive", ontology_derive)) if value}
if ontology:
lines += ["", "[ontology]", *(f'{key} = "{value}"' for key, value in ontology.items())]
if github_token:
lines += ["", "[github]", f'token = "{github_token}"']
lines.append("")
return "\n".join(lines)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ def _config(argv: list[str]) -> int:
"recall.embedding_model": "embedding_model",
"ontology.path": "ontology_path",
"ontology.derive": "ontology_derive",
"github.token": "github_token",
}
_SECRET_KEYS = {"memgraph.password", "llm.openai_api_key", "llm.anthropic_api_key"}
_SECRET_KEYS = {"memgraph.password", "llm.openai_api_key", "llm.anthropic_api_key", "github.token"}
_BOOL_KEYS = {"reconcile.auto_reconcile"}

if not argv or argv[0] in {"-h", "--help"}:
Expand Down Expand Up @@ -137,6 +138,7 @@ def _config(argv: list[str]) -> int:
print("ontology.derive = unset (defaults to extend)")
else:
print(f"ontology.derive = {config.ontology_derive!r}")
print(f"github.token = {'***' if config.github_token else 'unset'}")
return 0

if action == "set":
Expand Down Expand Up @@ -585,6 +587,33 @@ def _check_connector(connector_name: str) -> _CheckResult:
if driver is not None:
driver.close()

if normalized == "resources_graph":
try:
from agent_context_graph.adapters._identity import load_config, resolve_memgraph_env
from resources_graph import ResourcesGraph

env = resolve_memgraph_env()
graph = ResourcesGraph(
url=env["MEMGRAPH_URL"],
username=env["MEMGRAPH_USER"],
password=env["MEMGRAPH_PASSWORD"],
database=env["MEMGRAPH_DATABASE"],
)
version = _package_version("resources-graph")
# Touches are recorded without a token; the Sweep falls back to the user's gh login.
token = "set" if load_config().github_token else "unset (the Sweep uses the gh login)"
return {
"name": "connector:resources-graph",
"ok": True,
"detail": f"installed={version}; memgraph=reachable; github.token {token}",
}
except Exception as exc:
return {"name": "connector:resources-graph", "ok": False, "detail": f"{type(exc).__name__}: {exc}"}
finally:
driver = getattr(getattr(locals().get("graph", None), "_db", None), "driver", None)
if driver is not None:
driver.close()

else:
return {"name": f"connector:{connector_name}", "ok": False, "detail": "unsupported connector"}

Expand Down Expand Up @@ -696,6 +725,8 @@ def _connector_requirement(connector: str) -> str | None:
return "actions-graph[agent-context-graph]"
if normalized == "sessions-graph":
return "sessions-graph[agent-context-graph]"
if normalized == "resources-graph":
return "resources-graph[agent-context-graph]"
return None


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def load_payload(stream: Any | None = None) -> dict[str, Any]:
def create_link(connector_names: Iterable[str] = (), *, memgraph_env: dict[str, str] | None = None) -> AgentLink:
"""Create an AgentLink with optional connectors named by CLI/config.

Runtime-agnostic: connectors (skills-graph, actions-graph, sessions-graph)
Runtime-agnostic: connectors (skills-graph, actions-graph, sessions-graph, resources-graph)
are graph components, not tied to any particular runtime adapter.
"""
link = AgentLink()
Expand All @@ -55,6 +55,8 @@ def create_link(connector_names: Iterable[str] = (), *, memgraph_env: dict[str,
_add_actions_graph_connector(link, memgraph_env)
elif normalized == "sessions_graph":
_add_sessions_graph_connector(link, memgraph_env)
elif normalized == "resources_graph":
_add_resources_graph_connector(link, memgraph_env)
else:
msg = f"Unsupported connector: {connector_name}"
raise ValueError(msg)
Expand All @@ -65,6 +67,7 @@ def create_link(connector_names: Iterable[str] = (), *, memgraph_env: dict[str,
"skills_graph": ("skills_graph", "SkillGraph"),
"actions_graph": ("actions_graph", "ActionsGraph"),
"sessions_graph": ("sessions_graph", "SessionsGraph"),
"resources_graph": ("resources_graph", "ResourcesGraph"),
}


Expand Down Expand Up @@ -113,7 +116,7 @@ def run_hook(plugin: RuntimeCLIPlugin, argv: Sequence[str] | None = None) -> int
"--connector",
action="append",
default=None,
help="Graph connector to enable. Currently supported: skills-graph, actions-graph, sessions-graph.",
help="Graph connector to enable. Currently supported: skills-graph, actions-graph, sessions-graph, resources-graph.",
)
parser.add_argument(
"--session-id",
Expand Down Expand Up @@ -233,6 +236,18 @@ def _add_actions_graph_connector(link: AgentLink, memgraph_env: dict[str, str] |
link.add_connector(ActionsGraphConnector(graph))


def _add_resources_graph_connector(link: AgentLink, memgraph_env: dict[str, str] | None = None) -> None:
try:
from resources_graph import ResourcesGraph
from resources_graph.connector import ResourcesGraphConnector
except ImportError as exc:
msg = "resources-graph is required for the resources-graph connector"
raise ImportError(msg) from exc

kwargs = _memgraph_kwargs(memgraph_env)
link.add_connector(ResourcesGraphConnector(ResourcesGraph(**kwargs)))


# A closed key set (never `memgraph`) so `Component(**kwargs)` below can't be
# read as colliding with that parameter's `Memgraph | None` type, unlike a
# plain `dict[str, str]`.
Expand Down
18 changes: 18 additions & 0 deletions context-graph/agent-context-graph/tests/test_config_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,21 @@ def test_ontology_is_unset_by_default(config_dir, capsys):
out = capsys.readouterr().out
assert "ontology.path = unset" in out
assert "ontology.derive = unset (defaults to extend)" in out


def test_config_set_github_token_is_secret_and_survives_bootstrap(config_dir, capsys):
assert top_level_main(["config", "set", "github.token", "ghp_example"]) == 0
assert "Wrote github.token = ***" in capsys.readouterr().out
_identity.write_full_config(user_id="alice")
_identity._reset_cache()

assert _identity.load_config().github_token == "ghp_example"
assert top_level_main(["config", "show"]) == 0
shown = capsys.readouterr().out
assert "github.token = ***" in shown and "ghp_example" not in shown


def test_github_token_is_unset_by_default(config_dir, capsys):
assert _identity.load_config().github_token is None
assert top_level_main(["config", "show"]) == 0
assert "github.token = unset" in capsys.readouterr().out
21 changes: 21 additions & 0 deletions context-graph/resources-graph/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Memgraph

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading