You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A specced, harness-general graph model covering the whole Context Graph family — agent-context-graph's Event Protocol, actions-graph's collection-tier schema (session → agent nesting → tool-call sequences), skills-graph's Skill/Procedure separation, and sessions-graph's Memory/Episode schema — establishing one coherent model and a clear, verified collection-tier/memory-tier boundary across all four components. Every mechanism justified against what real harnesses (Claude Code, Codex) actually provide, not aspirational fields (the pattern that produced dead PARENT_OF/FORKED_FROM shouldn't repeat elsewhere).
End state: the complete node/relationship model, Event Protocol fields, and population rules per component — ready to implement.
Notes
Domain: context-graph in memgraph/ai-toolkit. See context-graph/CONTEXT-MAP.md and each component's CONTEXT.md (gitignored, local-only — sessions-graph, actions-graph, skills-graph, agent-context-graph all have one; keep them updated as this map resolves things, the last map (#259) initially missed three of the four and had to backfill).
Skills every session should consult: /grilling + /domain-modeling for HITL tickets; a /research subagent for research tickets.
Standing preferences for this effort:
Model only what's actually extractable from real harness data (verified against primary docs) or explicitly justified as an inference rule with a stated mechanism — never an aspirational field nobody populates. This is the whole reason this map exists: actions-graph's PARENT_OF (nested actions) and Session.parent_session_id/FORKED_FROM (forked sessions) both exist in-schema today but are never populated by any real adapter — verified by reading claude_code.py's adapter and the Event Protocol directly.
"General" means grounded in at least two real harnesses (Claude Code, Codex), not designed against one and hoped to generalize.
Prior art / findings already grounded (from direct doc research, not assumption):
Both Claude Code (docs) and Codex (docs) use the identical field nametool_use_id to correlate a tool call's before/after events. This primitive is genuinely general and already correctly modeled (ToolCall/ToolResult.tool_use_id).
Both harnesses have the identical gap: subagent events carry agent_id/agent_type, but neither harness gives an explicit field linking that back to the specific parent tool_use_id that spawned the subagent. Any parent-linking must be inferred (e.g. temporally), not read from a field, on either harness.
Codex's docs state explicitly that subagent hooks report the parent'ssession_id, not a distinct one — a working assumption for Claude Code too until checked.
Neither harness's session-resume/fork signal (source: "resume"/"fork" on Claude Code; source: "resume"/"clear"/"compact" on Codex) carries the actual previous/parent session's ID in the payload. FORKED_FROM may not be populatable from hook data on any harness — a real scope question, not a modeling detail.
Claude Code has a PostToolBatch event ("after a full batch of parallel tool calls resolves") implying tool calls can run in parallel/batched — this breaks a naive "exactly one tool call is ever open at a time" assumption that a temporal subagent-parent-inference rule might otherwise rely on.
agent-context-graph's Event Protocol (ToolStartEvent/ToolEndEvent) has no field at all to carry a parent/nesting link, even if one were inferred — any inference rule needs a place to put its answer, which doesn't exist yet.
Research: does the Claude Agent SDK / OpenAI Agents SDK give real parent-tool-call correlation, unlike command-hooks? — Not a clean "command-hooks bad, SDKs good" split. As currently wired, all four adapters (Claude Code, Codex, Claude Agent SDK, OpenAI Agents SDK) need the same general inference rule — none of their actually-used API surfaces carries a real parent-tool-call link. But one real, scoped upgrade path exists: Claude Agent SDK's message-stream API (query()/receive_response(), not the hooks={...} API claude.py currently uses) carries a genuine parent_tool_use_id field, verified empirically via a live probe. OpenAI Agents SDK has no equivalent path anywhere in its public surface. Spun out the "build it or not" decision as its own ticket, Grilling: should the Claude Agent SDK adapter consume the message-stream API for a real parent_tool_use_id? #283, since it's a real asymmetry question separate from designing the general rule.
Grilling: formalize the subagent-nesting parent-inference rule — Three-tier rule: (1) filter open tool calls (anywhere in the session, any nesting depth) by agent-spawning tool name; (2) if multiple open, disambiguate by matching SubagentStart.agent_type against each candidate's subagent-type input; (3) genuine ambiguity (same type, parallel) returns an honest null, never a guess. Verified explicitly against the real payload schema that agent_id ("agent_abc123def456", independent of tool_use_id) doesn't rescue tier 3 — it IDs the subagent, not its parent call. Explicit requirement, not optional: this is doc-derived, not observed — must be verified end-to-end against a live dev Memgraph instance with real Claude Code and Codex sessions before implementation is trusted.
Grilling: first-class Agent node, or populated PARENT_OF? — First-class (:Agent {agent_id, agent_type, started_at, ended_at, last_assistant_message, ...}) node. SubagentStart/SubagentStop stop being separate Action nodes, folding into one entity's lifecycle properties instead of two loose events in the flat timeline — same precedent as the Episode reversal (Grilling: what does continuous episodic-memory organization mean? #261). (:Session)-[:HAS_AGENT]->(:Agent) unconditionally (keeps a tier-3-ambiguous Agent reachable); (:Action)-[:SPAWNED]->(:Agent) only when Grilling: formalize the subagent-nesting parent-inference rule (including parallel/batch tool calls) #277 actually resolves a parent; (:Agent)-[:HAS_ACTION]->(:Action) for the agent's own tool calls, recursing naturally for further nesting. FOLLOWED_BY scoped per-container (session's own chain, each Agent's own separate chain), not one flat chain. Agent.agent_id is the unique key. Correction, 2026-08-14 (surfaced during implementation): PARENT_OF/parent_action_id was NOT dead as originally checked — actions-graph has a second, independent integration surface (actions_graph.hooks.ActionTracker, a direct Claude Agent SDK hooks={...} integration bypassing agent-context-graph) that actively populates it for real subagent nesting, tested in test_hooks.py. The original check only looked at the agent-context-graph/connector.py path. Resolution: migrate hooks.py onto the same Agent node model too, rather than freezing it as legacy or deleting it. The unrelated ToolResult→ToolCall use of parent_action_id/PARENT_OF (in both connector.py and hooks.py) is a different concept, stays as-is.
Grilling: keep, drop, or find another path for FORKED_FROM? — Drop FORKED_FROM/Session.parent_session_id entirely, same treatment as PARENT_OF. Verified against the exact, complete SessionStart field table (raw .md source, not a truncated fetch): no field anywhere carries a parent/previous session id, for any source value including fork. Also clarified resume vs. fork aren't the same kind of event — resume re-fires SessionStart for the same session_id (never a linkage question at all); only fork creates a genuinely new session_id, and that's exactly the case missing a parent-id field.
Grilling: should collection-tier vs. memory-tier be enforced in code, or stay a documented convention? — The map's synthesis ticket. Don't build a separate enforcement mechanism (no marker base class, no lint rule) — make Grilling: retention/compression policy for raw collection-tier data #267's confirmation-gated deletion mechanism (not yet implemented) the actual enforcement point, once built, via an explicit allowlist of collection-tier types (Action, Agent) rather than a denylist. Everything not on that allowlist is structurally protected by construction, with zero extra machinery. A marker class was rejected as symbolic-only (visible but not protective without an enforcement point attached); a lint rule was rejected as disproportionate to how small and stable this set of node types actually is.
This closes out the map's core destination — every structural ticket (#277 inference rule, #278 Agent node, #279 FORKED_FROM, #280 Skill Usage attachment, #281 Episode/nesting interaction, #282 enforcement) is now resolved. Only #283 (the Claude Agent SDK message-stream upgrade-path question) remains open — a real but genuinely separable decision, not required for the core model to be implementable.
Not yet specified
(empty — every fog item so far has graduated straight into a ticket; see the map's child issues.)
Out of scope
unstructured2graph/entity-extraction's own graph shape (Chunk/entity nodes) — unaffected by this map, not revisited.
The actual Procedure-mining algorithm mechanics (sequence-mining threshold, content-mining pipeline) — already decided in #262, tracked separately at #272. This map only covers where Procedure sits relative to Skill, not how mining works.
Destination
A specced, harness-general graph model covering the whole Context Graph family —
agent-context-graph's Event Protocol,actions-graph's collection-tier schema (session → agent nesting → tool-call sequences),skills-graph's Skill/Procedure separation, andsessions-graph's Memory/Episode schema — establishing one coherent model and a clear, verified collection-tier/memory-tier boundary across all four components. Every mechanism justified against what real harnesses (Claude Code, Codex) actually provide, not aspirational fields (the pattern that produced deadPARENT_OF/FORKED_FROMshouldn't repeat elsewhere).End state: the complete node/relationship model, Event Protocol fields, and population rules per component — ready to implement.
Notes
Domain:
context-graphinmemgraph/ai-toolkit. Seecontext-graph/CONTEXT-MAP.mdand each component'sCONTEXT.md(gitignored, local-only —sessions-graph,actions-graph,skills-graph,agent-context-graphall have one; keep them updated as this map resolves things, the last map (#259) initially missed three of the four and had to backfill).Skills every session should consult:
/grilling+/domain-modelingfor HITL tickets; a/researchsubagent for research tickets.Standing preferences for this effort:
actions-graph'sPARENT_OF(nested actions) andSession.parent_session_id/FORKED_FROM(forked sessions) both exist in-schema today but are never populated by any real adapter — verified by readingclaude_code.py's adapter and the Event Protocol directly.Prior art / findings already grounded (from direct doc research, not assumption):
tool_use_idto correlate a tool call's before/after events. This primitive is genuinely general and already correctly modeled (ToolCall/ToolResult.tool_use_id).agent_id/agent_type, but neither harness gives an explicit field linking that back to the specific parenttool_use_idthat spawned the subagent. Any parent-linking must be inferred (e.g. temporally), not read from a field, on either harness.session_id, not a distinct one — a working assumption for Claude Code too until checked.source: "resume"/"fork"on Claude Code;source: "resume"/"clear"/"compact"on Codex) carries the actual previous/parent session's ID in the payload.FORKED_FROMmay not be populatable from hook data on any harness — a real scope question, not a modeling detail.PostToolBatchevent ("after a full batch of parallel tool calls resolves") implying tool calls can run in parallel/batched — this breaks a naive "exactly one tool call is ever open at a time" assumption that a temporal subagent-parent-inference rule might otherwise rely on.agent-context-graph's Event Protocol (ToolStartEvent/ToolEndEvent) has no field at all to carry a parent/nesting link, even if one were inferred — any inference rule needs a place to put its answer, which doesn't exist yet.ToolStartEvent/ToolEndEvent/MessageEventhave noagent_idfield at all") was partially wrong.ToolStartEvent/ToolEndEventalready carry anagent_name: str | Nonefield inevents.py— it was simply never populated byclaude_code.py's adapter for tool events (only forAgentStartEvent/AgentEndEventconstruction fromSubagentStart/SubagentStop).MessageEventgenuinely had no such field. Fixed by populating the existing field frompayload.get("agent_id")inclaude_code.py(PreToolUse/PostToolUse/PostToolUseFailure/UserPromptSubmit/UserPromptExpansion/PermissionRequest), and addingagent_nametoMessageEvent. Codex's adapter has noSubagentStart/SubagentStop/agent_idhandling at all today (only 6 of Codex's documented hook types are wired up) — left unchanged, a pre-existing gap outside this map's scope. Bothactions-graph's(:Agent)-[:HAS_ACTION]->(:Action)(Grilling: does a subagent instance become a first-class Agent node, or stay a flat Action pair with a populated PARENT_OF? #278) andskills-graph's(:Agent)-[:USED_SKILL]->(:Skill)(Grilling: does Skill Usage need to attach to the specific nested Agent, not just the flat Session? #280) now have the field they need.Decisions so far
query()/receive_response(), not thehooks={...}APIclaude.pycurrently uses) carries a genuineparent_tool_use_idfield, verified empirically via a live probe. OpenAI Agents SDK has no equivalent path anywhere in its public surface. Spun out the "build it or not" decision as its own ticket, Grilling: should the Claude Agent SDK adapter consume the message-stream API for a real parent_tool_use_id? #283, since it's a real asymmetry question separate from designing the general rule.SubagentStart.agent_typeagainst each candidate's subagent-type input; (3) genuine ambiguity (same type, parallel) returns an honest null, never a guess. Verified explicitly against the real payload schema thatagent_id("agent_abc123def456", independent oftool_use_id) doesn't rescue tier 3 — it IDs the subagent, not its parent call. Explicit requirement, not optional: this is doc-derived, not observed — must be verified end-to-end against a live dev Memgraph instance with real Claude Code and Codex sessions before implementation is trusted.(:Agent {agent_id, agent_type, started_at, ended_at, last_assistant_message, ...})node.SubagentStart/SubagentStopstop being separateActionnodes, folding into one entity's lifecycle properties instead of two loose events in the flat timeline — same precedent as the Episode reversal (Grilling: what does continuous episodic-memory organization mean? #261).(:Session)-[:HAS_AGENT]->(:Agent)unconditionally (keeps a tier-3-ambiguous Agent reachable);(:Action)-[:SPAWNED]->(:Agent)only when Grilling: formalize the subagent-nesting parent-inference rule (including parallel/batch tool calls) #277 actually resolves a parent;(:Agent)-[:HAS_ACTION]->(:Action)for the agent's own tool calls, recursing naturally for further nesting.FOLLOWED_BYscoped per-container (session's own chain, each Agent's own separate chain), not one flat chain.Agent.agent_idis the unique key.Correction, 2026-08-14 (surfaced during implementation):
PARENT_OF/parent_action_idwas NOT dead as originally checked —actions-graphhas a second, independent integration surface (actions_graph.hooks.ActionTracker, a direct Claude Agent SDKhooks={...}integration bypassingagent-context-graph) that actively populates it for real subagent nesting, tested intest_hooks.py. The original check only looked at theagent-context-graph/connector.pypath. Resolution: migratehooks.pyonto the sameAgentnode model too, rather than freezing it as legacy or deleting it. The unrelatedToolResult→ToolCalluse ofparent_action_id/PARENT_OF(in bothconnector.pyandhooks.py) is a different concept, stays as-is.FORKED_FROM/Session.parent_session_identirely, same treatment asPARENT_OF. Verified against the exact, completeSessionStartfield table (raw.mdsource, not a truncated fetch): no field anywhere carries a parent/previous session id, for anysourcevalue includingfork. Also clarifiedresumevs.forkaren't the same kind of event —resumere-firesSessionStartfor the same session_id (never a linkage question at all); onlyforkcreates a genuinely new session_id, and that's exactly the case missing a parent-id field.USED_SKILLattaches to the specificAgentwhen usage happened inside a subagent, not just the flatSession— same relationship type either way, mirroringHAS_ACTION's existing either-container pattern from Grilling: does a subagent instance become a first-class Agent node, or stay a flat Action pair with a populated PARENT_OF? #278.Skill Surfacinggets the same treatment. Surfaced the shared Event Protocolagent_idgap (now in Notes above) rather than treating it as new fog specific to skills-graph.get_session_actions()(actions-graph/core.py:594) does a direct single-hopHAS_ACTIONmatch, not a recursive traversal. Once Grilling: does a subagent instance become a first-class Agent node, or stay a flat Action pair with a populated PARENT_OF? #278 lands, a subagent's tool calls move under(:Agent)-[:HAS_ACTION]->(:Action)— this query would silently stop seeing them, meaning reconciliation quietly loses all subagent activity (no entities extracted, no mention in the Episode). Real regression risk, not a no-op. Output shape stays the same (one flat Episode narrative, Chunks linking back to whichever Action produced them at any depth) — only the action-gathering step needs to traverse the full tree recursively. Recorded as a concrete implementation requirement for Grilling: does a subagent instance become a first-class Agent node, or stay a flat Action pair with a populated PARENT_OF? #278.Action,Agent) rather than a denylist. Everything not on that allowlist is structurally protected by construction, with zero extra machinery. A marker class was rejected as symbolic-only (visible but not protective without an enforcement point attached); a lint rule was rejected as disproportionate to how small and stable this set of node types actually is.This closes out the map's core destination — every structural ticket (#277 inference rule, #278 Agent node, #279 FORKED_FROM, #280 Skill Usage attachment, #281 Episode/nesting interaction, #282 enforcement) is now resolved. Only #283 (the Claude Agent SDK message-stream upgrade-path question) remains open — a real but genuinely separable decision, not required for the core model to be implementable.
Not yet specified
(empty — every fog item so far has graduated straight into a ticket; see the map's child issues.)
Out of scope
unstructured2graph/entity-extraction's own graph shape (Chunk/entity nodes) — unaffected by this map, not revisited.Procedure-mining algorithm mechanics (sequence-mining threshold, content-mining pipeline) — already decided in #262, tracked separately at #272. This map only covers whereProceduresits relative toSkill, not how mining works.