Skip to content

PRD: resources-graph — remember public GitHub resources the agent explored and serve them from memory #464

Description

@antejavor

Spec from Map: Resources component. Vocabulary: context-graph/resources-graph/CONTEXT.md (Resource, Resource Kind, Address, Touch, Listing, Comment, Unresolved Touch, Sweep, Provenance, Cache Read, Nudge).

Problem Statement

A coding agent working in a harness keeps re-reading the same public GitHub material — issues, pull requests, repositories, often hundreds at a time ("go over the 500 open issues on the Memgraph repo"). Context Graph remembers the session, but not the thing that was read: today a GitHub read survives only as a ToolResult Action (often a model-written summary or truncated shell output) and, after reconciliation, as Chunks/Entities scoped to that session. Nothing gives a GitHub issue a durable identity. So when the same user — or a teammate — asks the agent to go over those issues again, every one is fetched again: slow, rate-limited (60 unauthenticated REST calls an hour), and invisible to memory. There is also no record of which resources an agent explored and why, so they can't be connected to each other or to the sessions that used them.

Solution

A new Context Graph component, resources-graph, that remembers public external resources an agent explored — GitHub first — as shared, canonical Resources, and serves them back from memory.

  • Whenever the agent fetches a GitHub resource itself (WebFetch, gh, curl, a GitHub MCP tool) or a GitHub link appears in a prompt, a hook records a lightweight, private Touch: just the Address, how it arrived (FETCHED / PROMPTED), and when. Hooks never call GitHub.
  • An out-of-band Sweep later turns pending Touches into Resources: it fetches the canonical public content (issue/PR with all comments, repository with README, every member of a listing up to what the agent asked for), keys it on GitHub's stable node_id, and links resources that reference each other. Anything it can't resolve stays as an Unresolved Touch with a reason.
  • The model gets a resource(address) tool. Asking it for an issue, PR, repository or listing it has seen before returns the stored content with fetched_at and source updated_at — facts, so the model judges staleness itself. A narrower listing ("open bugs") can be answered from a broader fully expanded listing ("all open issues"). Every such read is itself a Touch with outcome hit / subsumed / miss.
  • Where the harness allows, a non-blocking Nudge tells the model, just before it fetches something already in memory, that resource has it. The fetch always proceeds.
  • Resources belong to no user and are shared; Touches stay private to the user whose session made them.

User Stories

  1. As a developer using a coding agent, I want issues the agent already read to be served from memory, so that re-reading 500 Memgraph issues doesn't re-fetch them all.
  2. As a developer, I want the agent to remember GitHub resources across sessions, so that work started yesterday continues without re-reading.
  3. As a developer on a team, I want a public issue my teammate's agent already fetched to be available to my agent, so that we don't each pay to fetch it.
  4. As a developer, I want my own Touches — what my agent explored and when — to stay private to me, so that sharing public content never leaks my activity.
  5. As a developer, I want a link I paste into a prompt to be remembered as PROMPTED, so that resources handed to the agent are captured as well as ones it found itself.
  6. As a developer, I want resources the agent fetched on its own to be recorded as FETCHED, so that I can see what the model decided to explore.
  7. As a developer, I want a gh issue list the agent ran to be remembered as one Listing with its filters, so that the same question can be answered again from memory.
  8. As a developer, I want listing members stored at full depth (body and all comments), so that a later question about any one of them doesn't need GitHub.
  9. As a developer, I want the Sweep to fetch only as many listing members as the agent asked for, so that memory holds what was asked or explored, not more.
  10. As a developer, I want a narrower listing (e.g. open issues labelled bug) answered from a broader fully expanded one, so that filtering variants of the same question don't re-fetch.
  11. As a developer, I want a truncated listing never to answer a broader or differently filtered question, so that the model is never served an incomplete set as if it were complete.
  12. As a developer, I want free-text searches matched exactly only, so that the model never gets a guessed search result.
  13. As a developer, I want pull requests stored with review comments and changed-file paths (but not diffs), so that PR discussions are memorable without storing code.
  14. As a developer, I want repositories stored with their description and README, so that "what is this repo" is answerable from memory.
  15. As the model, I want a resource(address) tool that accepts the same forms I fetch with (URL, owner/repo#n, gh arguments), so that I don't have to translate addresses.
  16. As the model, I want resource to return fetched_at and the source updated_at, so that I can decide whether memory is fresh enough for my task.
  17. As the model, I want a listing returned as a paged index (number, title, state, labels, updated_at, comment count), so that I can pick which members to read in full without flooding my context.
  18. As the model, I want to read listing members one at a time at full depth, so that I only load what I need.
  19. As the model, I want a subsumed answer labelled with the listing it was derived from and when that was fetched, so that I know how it was produced.
  20. As the model, I want a clear miss when something isn't in memory, so that I fall back to fetching it.
  21. As the model, I want a short hint at session start that resource exists, so that I check memory before fetching GitHub.
  22. As the model, I want a non-blocking nudge just before I fetch something already in memory, so that I can choose memory instead; my fetch is never blocked.
  23. As a developer, I want a real re-fetch (the model deciding memory was too stale) to make the next Sweep refresh that Resource, so that freshness follows actual need, with no background crawling.
  24. As a developer, I want a Cache Read never to trigger a refresh, so that reading memory doesn't cost GitHub calls.
  25. As a developer, I want resources that reference each other (#123 mentions, PRs closing issues) to be connected when both are in memory, so that related work can be traversed.
  26. As a developer, I want a reference to something not yet in memory to be kept and connected automatically once that resource arrives, so that links aren't lost because of fetch order.
  27. As a developer, I want each Touch linked to the Action and Agent (subagent) that caused it, so that I can see which tool call and which agent explored a resource.
  28. As a developer, I want a Touch whose resource couldn't be fetched kept with a reason (not found, not public, rate-limited), so that failures are visible rather than silently dropped.
  29. As a security-conscious developer, I want only public resources ever stored, enforced by checking visibility, so that private repository content never enters shared memory.
  30. As a developer, I want the Sweep's optional GitHub token read only from the Context Graph config file, so that it behaves the same in hooks, CLIs and my shell.
  31. As a developer, I want the Sweep to work without a token for small reads, and to record rate-limited Touches rather than fail, so that setup isn't a barrier.
  32. As a developer, I want to run the Sweep as a CLI command (e.g. after a session, or on a schedule), so that GitHub calls never slow a hook down.
  33. As a developer, I want hooks to record Touches with no network access, so that the agent's tool calls are never delayed by memory.
  34. As a developer, I want /issues/N that is really a pull request stored as a PullRequest, so that the kind is always right.
  35. As a developer, I want a renamed or transferred repository not to duplicate Resources, so that identity follows GitHub's node_id, not the URL.
  36. As a developer, I want comments stored as their own nodes with author and time, so that later chunking and per-comment freshness are possible.
  37. As a developer, I want to enable resources-graph with the same --connector mechanism and doctor checks as the other components, so that setup is uniform.
  38. As a developer, I want the resource tool served by agent-context-graph mcp like recall, so that every supported harness gets it.
  39. As a maintainer, I want the address parsing behind a per-platform adapter, so that webpages and PDFs can follow GitHub without redesigning the component.
  40. As a maintainer, I want every Cache Read's outcome recorded, so that the separate served-and-used analysis can measure whether the cache helps.
  41. As a developer with several harnesses, I want Touches captured consistently across Claude Code, Codex, Copilot CLI, Cursor, OpenCode and Grok Build, so that memory doesn't depend on which harness I used.

Implementation Decisions

Package and placement. A new workspace member resources-graph in the Context Graph family (connector → Touches, core store, Sweep CLI, read Tool). Joins the family on the shared (:Session {session_id}); it never owns (:User) or HAD_SESSION. No imports of sibling component packages — links to Actions/Agents are soft joins in Cypher. agent-context-graph gains resources-graph as a connector name (runner wiring and doctor), the same way it optionally wires the other components.

Graph model (validated by the graph-model prototype on all 686 open memgraph/memgraph issues):

(:User)-[:HAD_SESSION]->(:Session)<-[:IN_SESSION]-(:Touch)-[:TOUCHED]->(:Resource | :Listing)
(:Resource:Repository)-[:HAS_ISSUE|HAS_PULL_REQUEST]->(:Resource:Issue|PullRequest)-[:HAS_COMMENT]->(:Comment)
(:Listing)-[:LISTS_FROM]->(:Repository)    (:Listing)-[:HAS_MEMBER]->(:Issue|PullRequest)
(:Resource)-[:REFERENCES]->(:Resource)     (:PullRequest)-[:CLOSES]->(:Issue)
(:Touch)-[...]->(:Action), (:Touch)-[...]->(:Agent)   -- soft joins drawn by the Sweep
  • Resource identity is the GitHub GraphQL node_id. The normalised Address (github:owner/repo#n — issues and PRs share numbering) is a mutable, indexed lookup property, never identity. Every Resource carries fetched_at and source updated_at.
  • Issue: title, body, state, author, labels, assignees, milestone, created/updated/closed times, comment count. PullRequest: the same plus merged, base ref, changed-file paths — no diff. Repository: description, README, pushed time.
  • Comment: its own node per comment (review comments carry the file path), captured with its item, never touched on its own, not a Resource.
  • Labels, authors, assignees and milestone are properties, not nodes, for now.
  • Listing: shared, belongs to no user, not a Resource. Keyed by the normalised Address (repo + kind + sorted structured filters, or exact free-text query); holds limit, total count, member count, fully_expanded, fetched_at. A refetch replaces its members.
  • Touch: Address key, address kind, limit, provenance FETCHED | PROMPTED, served_from_memory, outcome (hit | subsumed | miss — Cache Reads only), status pending | resolved | unresolved, unresolved reason (not_found | not_public | rate_limited), tool_use_id, time. One Touch per listing, none per member.
  • References: REFERENCES (from cross-referenced events) and CLOSES (PR closing references) are drawn only between stored Resources. References to anything else are kept on the item as URLs and become edges when the other end is stored. No stub nodes, no reference following.

Address parser (pure, per-platform adapter; GitHub first). Input: a tool call (tool name + input — WebFetch URL, gh issue|pr view|list, gh api repos/..., gh repo view, curl to api.github.com, GitHub MCP tool arguments) or prompt text. Output: zero or more Addresses (kind repo | item | listing, key, limit). Listing filters normalised: state defaults to open, labels lowercased and sorted, author/assignee/milestone lowercased. limit is not part of the key — it decides expansion. Web ?q= and --search are free-text. gh calls with no -R/--repo need the working directory's remote; if it can't be determined, no Touch.

Listing matcher (pure). Exact match on key, served when the cached Listing is fully expanded or the asked limit fits its member count. Subsumption only from a fully expanded cached Listing of the same repo and kind, with no free text on either side: cached state is all or equal; cached labels are a subset of the asked labels; cached author/assignee/milestone unset or equal. The asked filters are then applied locally to stored members.

Resource store. One module owning all Cypher: record a pending Touch; serve a Cache Read and record it as a Touch (served from memory, with outcome, linked to what served it); list pending Touches; upsert Repository / Issue / PullRequest with Comments, references and backfill; upsert Listing with member replacement; resolve / unresolve Touches. Every read is scoped to the asking user and fails closed when the identity is unknown. All values are parameterised.

GitHub source. GraphQL via the optional token from the config file only (never env vars, per the hook config rule). Items are resolved with repository.issueOrPullRequest(number) — never by resource(url:), which types a PR addressed as /issues/N as Issue (verified in the prototype). Every fetched node's repository must be visibility == PUBLIC, else Unresolved not_public. Listings page through the repository connection (or search for free text), ordered by updated_at, up to the asked limit. Without a token, REST fallback within the 60-calls-an-hour budget; exhaustion yields Unresolved rate_limited, retried by a later Sweep.

Sweep. A CLI command, never run inside a hook. Processes pending real-fetch Touches: resolve → fetch → upsert → mark resolved, or record an Unresolved reason. Revalidation is touch-driven only: a real re-fetch of a stored Resource makes the Sweep fetch it again (conditional request for items, since=<fetched_at> for listings). Cache Reads never schedule refresh. No TTL, no background crawl. Links Touches to (:Action {tool_use_id}) and to the subagent (:Agent) when present; actions-graph already stores tool_use_id on tool Actions — add an index there if the join needs it.

Connector. A GraphConnector handling tool-start events (FETCHED Touches, with tool_use_id and agent name) and user prompt message events (PROMPTED Touches). Address parsing only; no network; writes are idempotent per event.

resource tool. Registered under the agent_context_graph.tools entry point, served by agent-context-graph mcp, identity and connection from the config file, never from arguments. Input: one Address in any form the parser accepts (plus paging for listings). Output: for an item or repository, content + comments + fetched_at + updated_at; for a listing, a paged index with fetched_at, member count and — when subsumed — the source listing; for a miss, a clear "not in memory". Session hint: one line telling the model to check resource before fetching GitHub issues/PRs/repos. The tool never calls GitHub.

Nudge. agent-context-graph gains a PreToolUse response path (today only SessionStart returns additionalContext). On a fetch-like tool start, the resources-graph side parses the Address and does one indexed lookup; on a hit it contributes one line ("

is in memory, fetched , updated_at — resource returns it"). Never denies or modifies the call, never calls GitHub, and any failure yields no nudge. Harnesses that can't add context to a pre-tool hook get the session hint only.

Known GitHub behaviour accepted. Reactions don't bump updated_at (verified), so stored reaction counts may be stale; label changes and review comments do bump it.

Testing Decisions

  • Good tests assert external behaviour — what the graph looks like after a scenario, what resource returns, which Addresses a tool call yields — never the literal Cypher sent or internal call sequences (repo testing policy: real Memgraph over mocks).
  • Address parser and Listing matcher: plain unit tests, table-driven over real-world inputs — gh argv in all its flag spellings, web and API URLs, owner/repo#n, prompt text with mixed links; subsumption truth tables including truncated and free-text listings.
  • Resource store, Sweep, Connector, resource tool: end-to-end against a real Memgraph via scripts/dev-memgraph.sh test resources-graph, asserting graph shape and tool output. Scenarios include: listing touched then served (hit), narrower listing subsumed, truncated listing not subsuming, item hit / miss, cross-user sharing with private Touches, a second user unable to see the first user's Touches, reference backfill in either fetch order, Unresolved reasons, refresh only after a real re-fetch, Touch→Action join against a real actions-graph Action.
  • Sweep's GitHub calls: replayed from recorded GraphQL responses captured from public memgraph/memgraph data, plus one live test against memgraph/memgraph gated by a skip marker (same pattern as requires_openai_key).
  • Nudge and connector event mapping: adapter-level mapping tests, like the existing hook-output and adapter tests.
  • Prior art: actions-graph and sessions-graph test_e2e.py (real Memgraph), agent-context-graph test_tools.py (tool protocol) and the per-harness adapter tests, eval's skip-marker-gated live tests. Prototype reference (throwaway, not to be merged): branch prototype/github-resource-graph-model.

Out of Scope

  • Private or authenticated resources (no private repos, no org-internal content).
  • Non-GitHub platforms (webpages, PDFs, pasted text) — the parser's adapter shape anticipates them, but they are not built here.
  • Feeding Resource content into Chunks/Entities and recall.
  • Code as a Resource (diffs, source files, local clones).
  • Retention/eviction of large resource sets; behaviour under Action decay/compression.
  • Labels and GitHub users as nodes.
  • Served/used analytics — tracked in track and analyse memory served to and used by the model; this PRD only records Cache Read outcomes it needs.
  • Blocking or redirecting the model's fetches.

Further Notes

  • Measured in the prototype: all 686 open memgraph/memgraph issues with 875 comments ≈ 1.6 MB of text, Sweep ≈ 63 s with a token. A second user's identical listing was a hit; --label bug was served by subsumption (30 rows).
  • Unverified: whether an issue keeps its node_id after transfer to another repository — check during implementation; it affects Resource identity.
  • Which harnesses can add context to a pre-tool hook without denying the call decides where the Nudge works — verify per adapter during implementation.
  • Decisions with detail: hook payloads, identity and freshness, capture model, placement and ownership, cache hit, graph model.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageMaintainer needs to evaluate this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions